diff options
557 files changed, 13472 insertions, 7541 deletions
diff --git a/apct-tests/perftests/core/src/android/libcore/regression/ExpensiveObjectsPerfTest.java b/apct-tests/perftests/core/src/android/libcore/regression/ExpensiveObjectsPerfTest.java index ecbfc7169945..10ec2bfcb49a 100644 --- a/apct-tests/perftests/core/src/android/libcore/regression/ExpensiveObjectsPerfTest.java +++ b/apct-tests/perftests/core/src/android/libcore/regression/ExpensiveObjectsPerfTest.java @@ -75,8 +75,19 @@ public class ExpensiveObjectsPerfTest { @Test(timeout = 900000) public void timeNewCollator() { BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + int i = 0; while (state.keepRunning()) { Collator.getInstance(Locale.US); + + if (++i % 1000 == 0) { + state.pauseTiming(); + // GC and finalize occasionally to avoid GC for alloc and/or + // blocking on finalization during benchmark time. + // See: b/394961590 + System.gc(); + System.runFinalization(); + state.resumeTiming(); + } } } @@ -84,8 +95,19 @@ public class ExpensiveObjectsPerfTest { public void timeClonedCollator() { Collator c = Collator.getInstance(Locale.US); BenchmarkState state = mPerfStatusReporter.getBenchmarkState(); + int i = 0; while (state.keepRunning()) { c.clone(); + + if (++i % 1000 == 0) { + state.pauseTiming(); + // GC and finalize occasionally to avoid GC for alloc and/or + // blocking on finalization during benchmark time. + // See: b/394961590 + System.gc(); + System.runFinalization(); + state.resumeTiming(); + } } } 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/cmds/svc/src/com/android/commands/svc/OWNERS b/cmds/svc/src/com/android/commands/svc/OWNERS index d5a5d7b3b858..a901dfdeddfb 100644 --- a/cmds/svc/src/com/android/commands/svc/OWNERS +++ b/cmds/svc/src/com/android/commands/svc/OWNERS @@ -1,2 +1,2 @@ # Bug component: 48448 -per-file NfcCommand.java = file:platform/packages/apps/Nfc:/OWNERS +per-file NfcCommand.java = file:platform/packages/modules/Nfc:/OWNERS diff --git a/core/api/current.txt b/core/api/current.txt index d4ed533cad9e..e1c26adb2275 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 { @@ -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 { @@ -25039,8 +25056,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 +25067,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); diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 76cce7439454..03607d45eabb 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 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/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl index 6fedcbe50f30..b9255ecaf1b6 100644 --- a/core/java/android/app/INotificationManager.aidl +++ b/core/java/android/app/INotificationManager.aidl @@ -126,6 +126,7 @@ interface INotificationManager boolean areChannelsBypassingDnd(); ParceledListSlice getNotificationChannelsBypassingDnd(String pkg, int uid); ParceledListSlice getPackagesBypassingDnd(int userId); + List<String> getPackagesWithAnyChannels(int userId); boolean isPackagePaused(String pkg); void deleteNotificationHistoryItem(String pkg, int uid, long postedTime); boolean isPermissionFixed(String pkg, int userId); 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/notification.aconfig b/core/java/android/app/notification.aconfig index a10b6ff39a37..9d8ab03982e6 100644 --- a/core/java/android/app/notification.aconfig +++ b/core/java/android/app/notification.aconfig @@ -308,6 +308,16 @@ flag { } flag { + name: "nm_binder_perf_get_apps_with_channels" + namespace: "systemui" + description: "Use a single binder call to get the set of apps with channels for a user" + bug: "362981561" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "no_sbnholder" namespace: "systemui" description: "removes sbnholder from NLS" diff --git a/core/java/android/appwidget/flags.aconfig b/core/java/android/appwidget/flags.aconfig index 9914ba2b020a..58bf4ef8100b 100644 --- a/core/java/android/appwidget/flags.aconfig +++ b/core/java/android/appwidget/flags.aconfig @@ -97,16 +97,6 @@ flag { } flag { - name: "check_remote_views_uri_permission" - namespace: "app_widgets" - description: "Check that the widget provider has permissions to access any URIs within its RemoteViews" - bug: "369137473" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { name: "engagement_metrics" namespace: "app_widgets" description: "Enable collection of widget engagement metrics" diff --git a/core/java/android/content/AttributionSource.java b/core/java/android/content/AttributionSource.java index ffed5366d31b..ebf522b66c4e 100644 --- a/core/java/android/content/AttributionSource.java +++ b/core/java/android/content/AttributionSource.java @@ -34,6 +34,7 @@ import android.os.UserHandle; import android.permission.PermissionManager; import android.permission.flags.Flags; import android.util.ArraySet; +import android.util.Log; import com.android.internal.annotations.Immutable; @@ -90,6 +91,7 @@ import java.util.Set; */ @Immutable public final class AttributionSource implements Parcelable { + private static final String TAG = "AttributionSource"; private static final String DESCRIPTOR = "android.content.AttributionSource"; private static final Binder sDefaultToken = new Binder(DESCRIPTOR); @@ -273,6 +275,13 @@ public final class AttributionSource implements Parcelable { final AttributionSource globalSource = ActivityThread.currentAttributionSource(); if (globalSource != null) { + if (Flags.enforceDefaultDeviceIdInMyAttributionSource() + && globalSource.getDeviceId() != Context.DEVICE_ID_DEFAULT) { + Log.w(TAG, + "Avoid using myAttributionSource() to fetch an attributionSource with a " + + "non-default device Id"); + return globalSource.withDeviceId(Context.DEVICE_ID_DEFAULT); + } return globalSource; } diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 8378695fd7a7..3391e79b2ae4 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -3334,7 +3334,7 @@ public abstract class Context { * this case, only one of these can be returned directly by the function; * which of these that is returned is arbitrarily decided by the system. * - * <p>If you know the Intent your are registering for is sticky, you can + * <p>If you know the Intent you are registering for is sticky, you can * supply null for your <var>receiver</var>. In this case, no receiver is * registered -- the function simply returns the sticky Intent that * matches <var>filter</var>. In the case of multiple matches, the same diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index 0312ad7a739a..038756148a32 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -8181,7 +8181,7 @@ public class Intent implements Parcelable, Cloneable { /** * Create an intent from a URI. This URI may encode the action, * category, and other intent fields, if it was returned by - * {@link #toUri}. If the Intent was not generate by toUri(), its data + * {@link #toUri}. If the Intent was not generated by toUri(), its data * will be the entire URI and its action will be ACTION_VIEW. * * <p>The URI given here must not be relative -- that is, it must include 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/hardware/contexthub/HubEndpointSession.java b/core/java/android/hardware/contexthub/HubEndpointSession.java index dd6e52f51df0..ca59be8fcc65 100644 --- a/core/java/android/hardware/contexthub/HubEndpointSession.java +++ b/core/java/android/hardware/contexthub/HubEndpointSession.java @@ -27,6 +27,7 @@ import android.hardware.location.ContextHubTransactionHelper; import android.hardware.location.IContextHubTransactionCallback; import android.util.CloseGuard; +import java.util.Objects; import java.util.concurrent.atomic.AtomicBoolean; /** @@ -160,6 +161,32 @@ public class HubEndpointSession implements AutoCloseable { return stringBuilder.toString(); } + @Override + public boolean equals(@Nullable Object object) { + if (object == this) { + return true; + } + + boolean isEqual = false; + if (object instanceof HubEndpointSession other) { + isEqual = (other.getId() == mId); + if (mServiceDescriptor != null) { + isEqual &= mServiceDescriptor.equals(other.getServiceDescriptor()); + } else { + isEqual &= (other.getServiceDescriptor() == null); + } + isEqual &= + mInitiator.equals(other.mInitiator) && mDestination.equals(other.mDestination); + } + + return isEqual; + } + + @Override + public int hashCode() { + return Objects.hash(mId, mServiceDescriptor, mInitiator, mDestination); + } + /** @hide */ protected void finalize() throws Throwable { try { diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java index c00f31db1a38..33bf4a29ecc6 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -982,8 +982,8 @@ public class UserManager { /** * Specifies if a user is disallowed from adding new users. This can only be set by device * owners or profile owners on the main user. The default value is <code>false</code>. - * <p> When the device is an organization-owned device provisioned with a managed profile, - * this restriction will be set as a base restriction which cannot be removed by any admin. + * <p> When the device is an organization-owned device, this restriction will be set as + * a base restriction which cannot be removed by any admin. * * <p>Holders of the permission * {@link android.Manifest.permission#MANAGE_DEVICE_POLICY_MODIFY_USERS} @@ -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/permission/flags.aconfig b/core/java/android/permission/flags.aconfig index daa5584462ba..d469a2f985fa 100644 --- a/core/java/android/permission/flags.aconfig +++ b/core/java/android/permission/flags.aconfig @@ -507,3 +507,10 @@ flag { description: "Use IoThread handler for AppOpsService background/IO work." bug: "394380603" } + +flag { + name: "enforce_default_device_id_in_my_attribution_source" + namespace: "permissions" + description: "Force AttributionSource.myAttributionSource() to return a default device id" + bug: "343121936" +} 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/flags.aconfig b/core/java/android/security/flags.aconfig index 792e6ff52d01..3a3ea189b227 100644 --- a/core/java/android/security/flags.aconfig +++ b/core/java/android/security/flags.aconfig @@ -126,6 +126,16 @@ flag { } flag { + name: "internal_log_event_listener" + namespace: "hardware_backed_security" + description: "Use internal callback to gather SecurityMonitor logs." + bug: "389732143" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "protect_device_config_flags" namespace: "psap_ai" description: "Feature flag to limit adb shell to allowlisted flags" 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/text/Layout.java b/core/java/android/text/Layout.java index 323d83b92143..2fa56137a8a0 100644 --- a/core/java/android/text/Layout.java +++ b/core/java/android/text/Layout.java @@ -672,7 +672,7 @@ public abstract class Layout { // LINT.IfChange(hct_darken) var lab = new double[3]; ColorUtils.colorToLAB(color, lab); - return lab[0] < 50.0; + return lab[0] <= 50.0; // LINT.ThenChange(/libs/hwui/hwui/DrawTextFunctor.h:hct_darken) } @@ -1021,6 +1021,12 @@ public abstract class Layout { return; } + if (!mSpannedText || mSpanColors == null) { + if (mPaint.getAlpha() == 0) { + return; + } + } + var padding = Math.max(HIGH_CONTRAST_TEXT_BORDER_WIDTH_MIN_PX, mPaint.getTextSize() * HIGH_CONTRAST_TEXT_BORDER_WIDTH_FACTOR); var cornerRadius = mPaint.density * HIGH_CONTRAST_TEXT_BACKGROUND_CORNER_RADIUS_DP; diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 80b4f2caabbb..de3e45b8ebde 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -538,6 +538,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<>(); @@ -1342,6 +1347,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) { @@ -13562,4 +13572,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/window/DesktopModeFlags.java b/core/java/android/window/DesktopModeFlags.java index 1ce5df7cd137..d43469fa76ca 100644 --- a/core/java/android/window/DesktopModeFlags.java +++ b/core/java/android/window/DesktopModeFlags.java @@ -57,6 +57,8 @@ public enum DesktopModeFlags { true), ENABLE_DESKTOP_CLOSE_SHORTCUT_BUGFIX(Flags::enableDesktopCloseShortcutBugfix, false), ENABLE_DESKTOP_COMPAT_UI_VISIBILITY_STATUS(Flags::enableCompatUiVisibilityStatus, true), + ENABLE_DESKTOP_INDICATOR_IN_SEPARATE_THREAD_BUGFIX( + Flags::enableDesktopIndicatorInSeparateThreadBugfix, false), ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX( Flags::enableDesktopRecentsTransitionsCornersBugfix, false), ENABLE_DESKTOP_SKIP_COMPAT_UI_EDUCATION_IN_DESKTOP_MODE_BUGFIX( diff --git a/core/java/android/window/TaskFragmentOperation.java b/core/java/android/window/TaskFragmentOperation.java index 9d0ea547e734..1fd79ccf9e3f 100644 --- a/core/java/android/window/TaskFragmentOperation.java +++ b/core/java/android/window/TaskFragmentOperation.java @@ -90,26 +90,6 @@ public final class TaskFragmentOperation implements Parcelable { public static final int OP_TYPE_SET_ISOLATED_NAVIGATION = 11; /** - * Reorders the TaskFragment to be the bottom-most in the Task. Note that this op will bring the - * TaskFragment to the bottom of the Task below all the other Activities and TaskFragments. - * - * This is only allowed for system organizers. See - * {@link com.android.server.wm.TaskFragmentOrganizerController#registerOrganizer( - * ITaskFragmentOrganizer, boolean)} - */ - public static final int OP_TYPE_REORDER_TO_BOTTOM_OF_TASK = 12; - - /** - * Reorders the TaskFragment to be the top-most in the Task. Note that this op will bring the - * TaskFragment to the top of the Task above all the other Activities and TaskFragments. - * - * This is only allowed for system organizers. See - * {@link com.android.server.wm.TaskFragmentOrganizerController#registerOrganizer( - * ITaskFragmentOrganizer, boolean)} - */ - public static final int OP_TYPE_REORDER_TO_TOP_OF_TASK = 13; - - /** * Creates a decor surface in the parent Task of the TaskFragment. The created decor surface * will be provided in {@link TaskFragmentTransaction#TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED} * event callback. If a decor surface already exists in the parent Task, the current @@ -118,27 +98,17 @@ public final class TaskFragmentOperation implements Parcelable { * * The decor surface can be used to draw the divider between TaskFragments or other decorations. */ - public static final int OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE = 14; + public static final int OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE = 12; /** * Removes the decor surface in the parent Task of the TaskFragment. */ - public static final int OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE = 15; + public static final int OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE = 13; /** * Applies dimming on the parent Task which could cross two TaskFragments. */ - public static final int OP_TYPE_SET_DIM_ON_TASK = 16; - - /** - * Sets this TaskFragment to move to bottom of the Task if any of the activities below it is - * launched in a mode requiring clear top. - * - * This is only allowed for system organizers. See - * {@link com.android.server.wm.TaskFragmentOrganizerController#registerOrganizer( - * ITaskFragmentOrganizer, boolean)} - */ - public static final int OP_TYPE_SET_MOVE_TO_BOTTOM_IF_CLEAR_WHEN_LAUNCH = 17; + public static final int OP_TYPE_SET_DIM_ON_TASK = 14; /** * Sets whether the decor surface will be boosted. When not boosted, the decor surface is placed @@ -147,7 +117,7 @@ public final class TaskFragmentOperation implements Parcelable { * surface is placed above all the non-boosted windows in the Task, the content of these * non-boosted windows will be hidden and inputs are disabled. */ - public static final int OP_TYPE_SET_DECOR_SURFACE_BOOSTED = 18; + public static final int OP_TYPE_SET_DECOR_SURFACE_BOOSTED = 15; /** * Sets the TaskFragment to be pinned. @@ -159,7 +129,44 @@ public final class TaskFragmentOperation implements Parcelable { * <p> * See {@link #OP_TYPE_REORDER_TO_FRONT} on how to reorder a pinned TaskFragment to the top. */ - public static final int OP_TYPE_SET_PINNED = 19; + public static final int OP_TYPE_SET_PINNED = 16; + + /** + * The start index of the privileged operations. Only system organizers are allowed to use + * operations with index greater than or equal to this value. + */ + public static final int PRIVILEGED_OP_START = 1000; + + /** + * Reorders the TaskFragment to be the bottom-most in the Task. Note that this op will bring the + * TaskFragment to the bottom of the Task below all the other Activities and TaskFragments. + * + * This is only allowed for system organizers. See + * {@link com.android.server.wm.TaskFragmentOrganizerController#registerOrganizer( + * ITaskFragmentOrganizer, boolean)} + */ + public static final int OP_TYPE_PRIVILEGED_REORDER_TO_BOTTOM_OF_TASK = PRIVILEGED_OP_START + 1; + + /** + * Reorders the TaskFragment to be the top-most in the Task. Note that this op will bring the + * TaskFragment to the top of the Task above all the other Activities and TaskFragments. + * + * This is only allowed for system organizers. See + * {@link com.android.server.wm.TaskFragmentOrganizerController#registerOrganizer( + * ITaskFragmentOrganizer, boolean)} + */ + public static final int OP_TYPE_PRIVILEGED_REORDER_TO_TOP_OF_TASK = PRIVILEGED_OP_START + 2; + + /** + * Sets this TaskFragment to move to bottom of the Task if any of the activities below it is + * launched in a mode requiring clear top. + * + * This is only allowed for system organizers. See + * {@link com.android.server.wm.TaskFragmentOrganizerController#registerOrganizer( + * ITaskFragmentOrganizer, boolean)} + */ + public static final int OP_TYPE_PRIVILEGED_SET_MOVE_TO_BOTTOM_IF_CLEAR_WHEN_LAUNCH = + PRIVILEGED_OP_START + 3; /** * Sets whether this TaskFragment can affect system UI flags such as the status bar. Default @@ -169,7 +176,8 @@ public final class TaskFragmentOperation implements Parcelable { * {@link com.android.server.wm.TaskFragmentOrganizerController#registerOrganizer( * ITaskFragmentOrganizer, boolean)} */ - public static final int OP_TYPE_SET_CAN_AFFECT_SYSTEM_UI_FLAGS = 20; + public static final int OP_TYPE_PRIVILEGED_SET_CAN_AFFECT_SYSTEM_UI_FLAGS = + PRIVILEGED_OP_START + 4; @IntDef(prefix = { "OP_TYPE_" }, value = { OP_TYPE_UNKNOWN, @@ -185,15 +193,15 @@ public final class TaskFragmentOperation implements Parcelable { OP_TYPE_SET_RELATIVE_BOUNDS, OP_TYPE_REORDER_TO_FRONT, OP_TYPE_SET_ISOLATED_NAVIGATION, - OP_TYPE_REORDER_TO_BOTTOM_OF_TASK, - OP_TYPE_REORDER_TO_TOP_OF_TASK, OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE, OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE, OP_TYPE_SET_DIM_ON_TASK, - OP_TYPE_SET_MOVE_TO_BOTTOM_IF_CLEAR_WHEN_LAUNCH, OP_TYPE_SET_DECOR_SURFACE_BOOSTED, OP_TYPE_SET_PINNED, - OP_TYPE_SET_CAN_AFFECT_SYSTEM_UI_FLAGS, + OP_TYPE_PRIVILEGED_REORDER_TO_BOTTOM_OF_TASK, + OP_TYPE_PRIVILEGED_REORDER_TO_TOP_OF_TASK, + OP_TYPE_PRIVILEGED_SET_MOVE_TO_BOTTOM_IF_CLEAR_WHEN_LAUNCH, + OP_TYPE_PRIVILEGED_SET_CAN_AFFECT_SYSTEM_UI_FLAGS, }) @Retention(RetentionPolicy.SOURCE) public @interface OperationType {} 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 b805ac560b8d..e358540afe6b 100644 --- a/core/java/android/window/flags/lse_desktop_experience.aconfig +++ b/core/java/android/window/flags/lse_desktop_experience.aconfig @@ -165,6 +165,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." @@ -623,6 +633,13 @@ flag { } flag { + name: "enable_multi_display_split" + namespace: "lse_desktop_experience" + description: "Enables split screen on multiple displays at the same time" + bug: "395943397" +} + +flag { name: "exclude_caption_from_app_bounds" namespace: "lse_desktop_experience" description: "Whether caption insets are excluded from app bounds in freeform" @@ -706,3 +723,13 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "enable_desktop_indicator_in_separate_thread_bugfix" + namespace: "lse_desktop_experience" + description: "Enables running visual indicator view operations in ShellDesktopThread." + bug: "366413536" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/com/android/internal/jank/Cuj.java b/core/java/com/android/internal/jank/Cuj.java index 9085bbec949f..41e2ca9cdfad 100644 --- a/core/java/com/android/internal/jank/Cuj.java +++ b/core/java/com/android/internal/jank/Cuj.java @@ -816,7 +816,7 @@ public class Cuj { case CUJ_LAUNCHER_WORK_UTILITY_VIEW_SHRINK: return "LAUNCHER_WORK_UTILITY_VIEW_SHRINK"; case CUJ_DEFAULT_TASK_TO_TASK_ANIMATION: - return "CUJ_DEFAULT_TASK_TO_TASK_ANIMATION"; + return "DEFAULT_TASK_TO_TASK_ANIMATION"; } return "UNKNOWN"; } 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/protolog/IProtoLogConfigurationService.aidl b/core/java/com/android/internal/protolog/IProtoLogConfigurationService.aidl index 9b60f49d1446..a6f0b306f4bf 100644 --- a/core/java/com/android/internal/protolog/IProtoLogConfigurationService.aidl +++ b/core/java/com/android/internal/protolog/IProtoLogConfigurationService.aidl @@ -40,12 +40,12 @@ import com.android.internal.protolog.IProtoLogClient; * * {@hide} */ -oneway interface IProtoLogConfigurationService { - interface IRegisterClientArgs { - String[] getGroups(); - boolean[] getGroupsDefaultLogcatStatus(); - String getViewerConfigFile(); +interface IProtoLogConfigurationService { + parcelable RegisterClientArgs { + String[] groups; + boolean[] groupsDefaultLogcatStatus; + String viewerConfigFile; } - void registerClient(IProtoLogClient client, IRegisterClientArgs args); + oneway void registerClient(IProtoLogClient client, in RegisterClientArgs args); }
\ No newline at end of file diff --git a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java index d8cf258e23ba..93be3b02a12a 100644 --- a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java +++ b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java @@ -62,7 +62,7 @@ import android.util.Slog; import android.util.proto.ProtoOutputStream; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.protolog.ProtoLogConfigurationServiceImpl.RegisterClientArgs; +import com.android.internal.protolog.IProtoLogConfigurationService.RegisterClientArgs; import com.android.internal.protolog.common.ILogger; import com.android.internal.protolog.common.IProtoLog; import com.android.internal.protolog.common.IProtoLogGroup; @@ -164,11 +164,15 @@ public abstract class PerfettoProtoLogImpl extends IProtoLogClient.Stub implemen try { var args = createConfigurationServiceRegisterClientArgs(); - final var groupArgs = mLogGroups.values().stream() - .map(group -> new RegisterClientArgs - .GroupConfig(group.name(), group.isLogToLogcat())) - .toArray(RegisterClientArgs.GroupConfig[]::new); - args.setGroups(groupArgs); + args.groups = new String[mLogGroups.size()]; + args.groupsDefaultLogcatStatus = new boolean[mLogGroups.size()]; + + var groups = mLogGroups.values().stream().toList(); + for (var i = 0; i < groups.size(); i++) { + var group = groups.get(i); + args.groups[i] = group.name(); + args.groupsDefaultLogcatStatus[i] = group.isLogToLogcat(); + } mConfigurationService.registerClient(this, args); } catch (RemoteException e) { diff --git a/core/java/com/android/internal/protolog/ProcessedPerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/ProcessedPerfettoProtoLogImpl.java index 1f9df3cc842a..d9c54d695a87 100644 --- a/core/java/com/android/internal/protolog/ProcessedPerfettoProtoLogImpl.java +++ b/core/java/com/android/internal/protolog/ProcessedPerfettoProtoLogImpl.java @@ -22,7 +22,7 @@ import android.os.ServiceManager; import android.util.Log; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.protolog.ProtoLogConfigurationServiceImpl.RegisterClientArgs; +import com.android.internal.protolog.IProtoLogConfigurationService.RegisterClientArgs; import com.android.internal.protolog.common.ILogger; import com.android.internal.protolog.common.IProtoLogGroup; @@ -104,8 +104,9 @@ public class ProcessedPerfettoProtoLogImpl extends PerfettoProtoLogImpl { @NonNull @Override protected RegisterClientArgs createConfigurationServiceRegisterClientArgs() { - return new RegisterClientArgs() - .setViewerConfigFile(mViewerConfigFilePath); + var args = new RegisterClientArgs(); + args.viewerConfigFile = mViewerConfigFilePath; + return args; } /** diff --git a/core/java/com/android/internal/protolog/ProtoLogConfigurationServiceImpl.java b/core/java/com/android/internal/protolog/ProtoLogConfigurationServiceImpl.java index 23f7c2a3d987..f83359dddfcc 100644 --- a/core/java/com/android/internal/protolog/ProtoLogConfigurationServiceImpl.java +++ b/core/java/com/android/internal/protolog/ProtoLogConfigurationServiceImpl.java @@ -134,74 +134,6 @@ public class ProtoLogConfigurationServiceImpl extends IProtoLogConfigurationServ mDataSource = datasource; } - public static class RegisterClientArgs extends IRegisterClientArgs.Stub { - /** - * The viewer config file to be registered for this client ProtoLog process. - */ - @Nullable - private String mViewerConfigFile = null; - /** - * The list of all groups that this client protolog process supports and might trace. - */ - @NonNull - private String[] mGroups = new String[0]; - /** - * The default logcat status of the ProtoLog client. True is logging to logcat, false - * otherwise. The indices should match the indices in {@link mGroups}. - */ - @NonNull - private boolean[] mLogcatStatus = new boolean[0]; - - public record GroupConfig(@NonNull String group, boolean logToLogcat) {} - - /** - * Specify groups to register with this client that will be used for protologging in this - * process. - * @param groups to register with this client. - * @return self - */ - public RegisterClientArgs setGroups(GroupConfig... groups) { - mGroups = new String[groups.length]; - mLogcatStatus = new boolean[groups.length]; - - for (int i = 0; i < groups.length; i++) { - mGroups[i] = groups[i].group; - mLogcatStatus[i] = groups[i].logToLogcat; - } - - return this; - } - - /** - * Set the viewer config file that the logs in this process are using. - * @param viewerConfigFile The file path of the viewer config. - * @return self - */ - public RegisterClientArgs setViewerConfigFile(@NonNull String viewerConfigFile) { - mViewerConfigFile = viewerConfigFile; - - return this; - } - - @Override - @NonNull - public String[] getGroups() { - return mGroups; - } - - @Override - @NonNull - public boolean[] getGroupsDefaultLogcatStatus() { - return mLogcatStatus; - } - - @Nullable - @Override - public String getViewerConfigFile() { - return mViewerConfigFile; - } - } - @FunctionalInterface public interface ViewerConfigFileTracer { /** @@ -216,16 +148,16 @@ public class ProtoLogConfigurationServiceImpl extends IProtoLogConfigurationServ } @Override - public void registerClient(@NonNull IProtoLogClient client, @NonNull IRegisterClientArgs args) + public void registerClient(@NonNull IProtoLogClient client, @NonNull RegisterClientArgs args) throws RemoteException { client.asBinder().linkToDeath(() -> onClientBinderDeath(client), /* flags */ 0); - final String viewerConfigFile = args.getViewerConfigFile(); + final String viewerConfigFile = args.viewerConfigFile; if (viewerConfigFile != null) { registerViewerConfigFile(client, viewerConfigFile); } - registerGroups(client, args.getGroups(), args.getGroupsDefaultLogcatStatus()); + registerGroups(client, args.groups, args.groupsDefaultLogcatStatus); } @Override diff --git a/core/java/com/android/internal/protolog/UnprocessedPerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/UnprocessedPerfettoProtoLogImpl.java index ebb07a04270d..39b01fb59d9e 100644 --- a/core/java/com/android/internal/protolog/UnprocessedPerfettoProtoLogImpl.java +++ b/core/java/com/android/internal/protolog/UnprocessedPerfettoProtoLogImpl.java @@ -19,7 +19,7 @@ package com.android.internal.protolog; import android.annotation.NonNull; import android.os.ServiceManager; -import com.android.internal.protolog.ProtoLogConfigurationServiceImpl.RegisterClientArgs; +import com.android.internal.protolog.IProtoLogConfigurationService.RegisterClientArgs; import com.android.internal.protolog.common.IProtoLogGroup; public class UnprocessedPerfettoProtoLogImpl extends PerfettoProtoLogImpl { diff --git a/core/java/com/android/internal/security/TEST_MAPPING b/core/java/com/android/internal/security/TEST_MAPPING index 5bd9d2e4512d..be50bebf0831 100644 --- a/core/java/com/android/internal/security/TEST_MAPPING +++ b/core/java/com/android/internal/security/TEST_MAPPING @@ -6,10 +6,6 @@ { "name": "UpdatableSystemFontTest", "file_patterns": ["VerityUtils\\.java"] - }, - { - "name": "CtsApkVerityInstallHostTestCases", - "file_patterns": ["VerityUtils\\.java"] } ], "postsubmit": [ 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/NotificationProgressBar.java b/core/java/com/android/internal/widget/NotificationProgressBar.java index 905d4dd547f3..c0fe0d125e44 100644 --- a/core/java/com/android/internal/widget/NotificationProgressBar.java +++ b/core/java/com/android/internal/widget/NotificationProgressBar.java @@ -64,6 +64,7 @@ public final class NotificationProgressBar extends ProgressBar implements NotificationProgressDrawable.BoundsChangeListener { private static final String TAG = "NotificationProgressBar"; private static final boolean DEBUG = false; + private static final float FADED_OPACITY = 0.5f; private NotificationProgressDrawable mNotificationProgressDrawable; private final Rect mProgressDrawableBounds = new Rect(); @@ -851,12 +852,12 @@ public final class NotificationProgressBar extends ProgressBar implements } /** - * Get a color with an opacity that's 50% of the input color. + * Get a color that's the input color with opacity updated to FADED_OPACITY. */ @ColorInt static int getFadedColor(@ColorInt int color) { return Color.argb( - (int) (Color.alpha(color) * 0.5f + 0.5f), + (int) (Color.alpha(color) * FADED_OPACITY + 0.5f), Color.red(color), Color.green(color), Color.blue(color)); @@ -1200,7 +1201,7 @@ public final class NotificationProgressBar extends ProgressBar implements * <p> * <pre> * When mFaded is set to true, a combination of the following is done to the segment: - * 1. The drawing color is mColor with opacity updated to 50%. + * 1. The drawing color is mColor with opacity updated to FADED_OPACITY. * 2. The gap between faded and non-faded segments is: * - the segment-segment gap, when there is no tracker icon * - 0, when there is tracker icon diff --git a/core/java/com/android/internal/widget/remotecompose/OWNERS b/core/java/com/android/internal/widget/remotecompose/OWNERS index 54facab0c3f4..e163474bccb9 100644 --- a/core/java/com/android/internal/widget/remotecompose/OWNERS +++ b/core/java/com/android/internal/widget/remotecompose/OWNERS @@ -5,4 +5,3 @@ sihua@google.com sunnygoyal@google.com oscarad@google.com pinyaoting@google.com -zakcohen@google.com 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 dea1caf76a5c..b8503da2c09b 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java +++ b/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java @@ -35,6 +35,7 @@ import com.android.internal.widget.remotecompose.core.operations.layout.LoopOper import com.android.internal.widget.remotecompose.core.operations.layout.RootLayoutComponent; import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.ComponentModifiers; import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.ModifierOperation; +import com.android.internal.widget.remotecompose.core.operations.utilities.IntMap; 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.Serializable; @@ -64,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.0f; + static final float BUILD = 0.2f; @NonNull ArrayList<Operation> mOperations = new ArrayList<>(); @@ -99,6 +100,8 @@ public class CoreDocument implements Serializable { private int mLastId = 1; // last component id when inflating the file + private IntMap<Object> mDocProperties; + /** Returns a version number that is monotonically increasing. */ public static int getDocumentApiLevel() { return DOCUMENT_API_LEVEL; @@ -407,10 +410,31 @@ public class CoreDocument implements Serializable { @Override public void serialize(MapSerializer serializer) { - serializer.add("type", "CoreDocument"); - serializer.add("width", mWidth); - serializer.add("height", mHeight); - serializer.add("operations", mOperations); + serializer + .add("type", "CoreDocument") + .add("width", mWidth) + .add("height", mHeight) + .add("operations", mOperations); + } + + /** + * Set the properties of the document + * + * @param properties the properties to set + */ + public void setProperties(IntMap<Object> properties) { + mDocProperties = properties; + } + + /** + * @param key the key + * @return the value associated with the key + */ + public Object getProperty(short key) { + if (mDocProperties == null) { + return null; + } + return mDocProperties.get(key); } // ============== Haptic support ================== @@ -718,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/PaintContext.java b/core/java/com/android/internal/widget/remotecompose/core/PaintContext.java index a5b669d6aecf..1b0b9d783dff 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/PaintContext.java +++ b/core/java/com/android/internal/widget/remotecompose/core/PaintContext.java @@ -207,6 +207,13 @@ public abstract class PaintContext { public abstract void restorePaint(); /** + * Replace the current paint with the PaintBundle + * + * @param paint + */ + public abstract void replacePaint(PaintBundle paint); + + /** * draw a round rect * * @param left left coordinate of the rectangle 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 c6ef1d3c457d..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; @@ -191,6 +193,11 @@ public class RemoteComposeBuffer { // Supported operations on the buffer /////////////////////////////////////////////////////////////////////////////////////////////// + /** Insert a header */ + public void addHeader(short[] tags, Object[] values) { + Header.apply(mBuffer, tags, values); + } + /** * Insert a header * @@ -219,6 +226,28 @@ public class RemoteComposeBuffer { * @param width the width of the document in pixels * @param height the height of the document in pixels * @param contentDescription content description of the document + * @param capabilities bitmask indicating needed capabilities (unused for now) + */ + public void addHeader( + int width, + int height, + @Nullable String contentDescription, + float density, + long capabilities) { + Header.apply(mBuffer, width, height, density, capabilities); + int contentDescriptionId = 0; + if (contentDescription != null) { + contentDescriptionId = addText(contentDescription); + RootContentDescription.apply(mBuffer, contentDescriptionId); + } + } + + /** + * Insert a header + * + * @param width the width of the document in pixels + * @param height the height of the document in pixels + * @param contentDescription content description of the document */ public void header(int width, int height, @Nullable String contentDescription) { header(width, height, contentDescription, 1f, 0); @@ -1860,7 +1889,7 @@ public class RemoteComposeBuffer { } /** Add a component end tag */ - public void addComponentEnd() { + public void addContainerEnd() { ContainerEnd.apply(mBuffer); } @@ -2204,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 * @@ -2400,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/RemoteContext.java b/core/java/com/android/internal/widget/remotecompose/core/RemoteContext.java index 36e4ec1ff303..622f0c8d12b7 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/RemoteContext.java +++ b/core/java/com/android/internal/widget/remotecompose/core/RemoteContext.java @@ -26,6 +26,7 @@ import com.android.internal.widget.remotecompose.core.operations.layout.Componen import com.android.internal.widget.remotecompose.core.operations.utilities.ArrayAccess; import com.android.internal.widget.remotecompose.core.operations.utilities.CollectionsAccess; import com.android.internal.widget.remotecompose.core.operations.utilities.DataMap; +import com.android.internal.widget.remotecompose.core.operations.utilities.IntMap; import java.time.LocalDateTime; import java.time.OffsetDateTime; @@ -392,6 +393,7 @@ public abstract class RemoteContext { * @param width original width of the document when created * @param height original height of the document when created * @param capabilities bitmask of capabilities used in the document (TBD) + * @param properties properties of the document (TBD) */ public void header( int majorVersion, @@ -399,13 +401,15 @@ public abstract class RemoteContext { int patchVersion, int width, int height, - long capabilities) { + long capabilities, + IntMap<Object> properties) { mRemoteComposeState.setWindowWidth(width); mRemoteComposeState.setWindowHeight(height); mDocument.setVersion(majorVersion, minorVersion, patchVersion); mDocument.setWidth(width); mDocument.setHeight(height); mDocument.setRequiredCapabilities(capabilities); + mDocument.setProperties(properties); } /** diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/ColorExpression.java b/core/java/com/android/internal/widget/remotecompose/core/operations/ColorExpression.java index a593241888df..d5af7914607a 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/ColorExpression.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/ColorExpression.java @@ -27,6 +27,8 @@ 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.documentation.DocumentedOperation; +import com.android.internal.widget.remotecompose.core.serialize.MapSerializer; +import com.android.internal.widget.remotecompose.core.serialize.Serializable; import java.util.List; @@ -34,7 +36,7 @@ import java.util.List; * Operation to Colors Color modes mMode = 0 two colors and a tween mMode = 1 color1 is a colorID. * mMode = 2 color2 is a colorID. mMode = 3 color1 & color2 are ids mMode = 4 H S V mode */ -public class ColorExpression extends Operation implements VariableSupport { +public class ColorExpression extends Operation implements VariableSupport, Serializable { private static final int OP_CODE = Operations.COLOR_EXPRESSIONS; private static final String CLASS_NAME = "ColorExpression"; public int mId; @@ -502,4 +504,36 @@ public class ColorExpression extends Operation implements VariableSupport { public String deepToString(@NonNull String indent) { return indent + toString(); } + + @Override + public void serialize(MapSerializer serializer) { + serializer.add("type", CLASS_NAME).add("id", mId); + switch (mMode) { + case COLOR_COLOR_INTERPOLATE: + case ID_COLOR_INTERPOLATE: + case COLOR_ID_INTERPOLATE: + case ID_ID_INTERPOLATE: + serializer.add("mode", "TWEEN"); + serializer.add("startColor", mColor1, mOutColor1); + serializer.add("endColor", mColor2, mOutColor2); + serializer.add("startColor", mTween, mOutTween); + break; + case HSV_MODE: + serializer.add("mode", "HSV"); + serializer.add("hue", mHue, mOutHue); + serializer.add("sat", mSat, mOutSat); + serializer.add("val", mValue, mOutValue); + break; + case ARGB_MODE: + case IDARGB_MODE: + serializer.add("mode", "ARGB"); + serializer.add("a", mArgbAlpha, mOutArgbAlpha); + serializer.add("r", mArgbRed, mOutArgbRed); + serializer.add("g", mArgbGreen, mOutArgbGreen); + serializer.add("b", mArgbBlue, mOutArgbBlue); + break; + default: + serializer.add("mode", "NONE"); + } + } } diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/DataListFloat.java b/core/java/com/android/internal/widget/remotecompose/core/operations/DataListFloat.java index 7a72b109b2a8..fb3abdbb0626 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/DataListFloat.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/DataListFloat.java @@ -28,11 +28,13 @@ import com.android.internal.widget.remotecompose.core.WireBuffer; import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder; import com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation; import com.android.internal.widget.remotecompose.core.operations.utilities.ArrayAccess; +import com.android.internal.widget.remotecompose.core.serialize.MapSerializer; +import com.android.internal.widget.remotecompose.core.serialize.Serializable; import java.util.Arrays; import java.util.List; -public class DataListFloat extends Operation implements VariableSupport, ArrayAccess { +public class DataListFloat extends Operation implements VariableSupport, ArrayAccess, Serializable { private static final int OP_CODE = Operations.FLOAT_LIST; private static final String CLASS_NAME = "IdListData"; private final int mId; @@ -145,4 +147,9 @@ public class DataListFloat extends Operation implements VariableSupport, ArrayAc public int getLength() { return mValues.length; } + + @Override + public void serialize(MapSerializer serializer) { + serializer.add("type", CLASS_NAME).add("id", mId).add("values", List.of(mValues)); + } } diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/DataListIds.java b/core/java/com/android/internal/widget/remotecompose/core/operations/DataListIds.java index 7e29620ec104..58fd74fdcf3e 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/DataListIds.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/DataListIds.java @@ -29,11 +29,13 @@ import com.android.internal.widget.remotecompose.core.WireBuffer; import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder; import com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation; import com.android.internal.widget.remotecompose.core.operations.utilities.ArrayAccess; +import com.android.internal.widget.remotecompose.core.serialize.MapSerializer; +import com.android.internal.widget.remotecompose.core.serialize.Serializable; import java.util.Arrays; import java.util.List; -public class DataListIds extends Operation implements VariableSupport, ArrayAccess { +public class DataListIds extends Operation implements VariableSupport, ArrayAccess, Serializable { private static final int OP_CODE = Operations.ID_LIST; private static final String CLASS_NAME = "IdListData"; private final int mId; @@ -147,4 +149,9 @@ public class DataListIds extends Operation implements VariableSupport, ArrayAcce public int getIntValue(int index) { return 0; } + + @Override + public void serialize(MapSerializer serializer) { + serializer.add("type", CLASS_NAME).add("id", mId).add("ids", List.of(mIds)); + } } diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBase2.java b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBase2.java index 6df4b91cc405..5dbaf29b8cc0 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBase2.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBase2.java @@ -121,6 +121,6 @@ public abstract class DrawBase2 extends PaintOperation implements VariableSuppor } protected MapSerializer serialize(MapSerializer serializer, String v1Name, String v2Name) { - return serializer.add(v1Name, mV1, mValue1).add(v2Name, mV2, mValue2); + return serializer.add(v1Name, mValue1, mV1).add(v2Name, mValue2, mV2); } } diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBase3.java b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBase3.java index 16ead454d84f..238f10e794d0 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBase3.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBase3.java @@ -126,8 +126,8 @@ public abstract class DrawBase3 extends PaintOperation implements VariableSuppor protected MapSerializer serialize( MapSerializer serializer, String v1Name, String v2Name, String v3Name) { return serializer - .add(v1Name, mV1, mValue1) - .add(v2Name, mV2, mValue2) - .add(v3Name, mV3, mValue3); + .add(v1Name, mValue1, mV1) + .add(v2Name, mValue2, mV2) + .add(v3Name, mValue3, mV3); } } diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBase4.java b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBase4.java index 0733b833be11..ca34e01f1725 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBase4.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBase4.java @@ -158,9 +158,9 @@ public abstract class DrawBase4 extends PaintOperation implements VariableSuppor protected MapSerializer serialize( MapSerializer serializer, String x1Name, String y1Name, String x2Name, String y2Name) { return serializer - .add(x1Name, mX1, mX1Value) - .add(y1Name, mY1, mY1Value) - .add(x2Name, mX2, mX2Value) - .add(y2Name, mY2, mY2Value); + .add(x1Name, mX1Value, mX1) + .add(y1Name, mY1Value, mY1) + .add(x2Name, mX2Value, mX2) + .add(y2Name, mY2Value, mY2); } } diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBase6.java b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBase6.java index 75b87c04e810..ca0584d0ee73 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBase6.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawBase6.java @@ -173,11 +173,11 @@ public abstract class DrawBase6 extends PaintOperation implements VariableSuppor String v5Name, String v6Name) { return serializer - .add(v1Name, mV1, mValue1) - .add(v2Name, mV2, mValue2) - .add(v3Name, mV3, mValue3) - .add(v4Name, mV4, mValue4) - .add(v5Name, mV5, mValue5) - .add(v6Name, mV6, mValue6); + .add(v1Name, mValue1, mV1) + .add(v2Name, mValue2, mV2) + .add(v3Name, mValue3, mV3) + .add(v4Name, mValue4, mV4) + .add(v5Name, mValue5, mV5) + .add(v6Name, mValue6, mV6); } } 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/DrawTextAnchored.java b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawTextAnchored.java index 5d0c43723ea1..92469d1834ba 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawTextAnchored.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawTextAnchored.java @@ -26,11 +26,13 @@ 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.documentation.DocumentedOperation; +import com.android.internal.widget.remotecompose.core.serialize.MapSerializer; +import com.android.internal.widget.remotecompose.core.serialize.Serializable; import java.util.List; /** Draw Text in Anchored to a point */ -public class DrawTextAnchored extends PaintOperation implements VariableSupport { +public class DrawTextAnchored extends PaintOperation implements VariableSupport, Serializable { private static final int OP_CODE = Operations.DRAW_TEXT_ANCHOR; private static final String CLASS_NAME = "DrawTextAnchored"; int mTextID; @@ -238,4 +240,16 @@ public class DrawTextAnchored extends PaintOperation implements VariableSupport float y = Float.isNaN(mOutPanY) ? mOutY : mOutY + getVerticalOffset(); context.drawTextRun(mTextID, 0, -1, 0, 1, x, y, (mFlags & ANCHOR_TEXT_RTL) == 1); } + + @Override + public void serialize(MapSerializer serializer) { + serializer + .add("type", CLASS_NAME) + .add("textId", mTextID) + .add("x", mX, mOutX) + .add("y", mY, mOutY) + .add("panX", mPanX, mOutPanX) + .add("panY", mPanY, mOutPanY) + .add("flags", mFlags); + } } diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawTextOnPath.java b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawTextOnPath.java index 3ab4a87c614c..1f7910ede4b5 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawTextOnPath.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawTextOnPath.java @@ -26,11 +26,13 @@ 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.documentation.DocumentedOperation; +import com.android.internal.widget.remotecompose.core.serialize.MapSerializer; +import com.android.internal.widget.remotecompose.core.serialize.Serializable; import java.util.List; /** Draw text along a path. */ -public class DrawTextOnPath extends PaintOperation implements VariableSupport { +public class DrawTextOnPath extends PaintOperation implements VariableSupport, Serializable { private static final int OP_CODE = Operations.DRAW_TEXT_ON_PATH; private static final String CLASS_NAME = "DrawTextOnPath"; int mPathId; @@ -153,4 +155,14 @@ public class DrawTextOnPath extends PaintOperation implements VariableSupport { public void paint(@NonNull PaintContext context) { context.drawTextOnPath(mTextId, mPathId, mOutHOffset, mOutVOffset); } + + @Override + public void serialize(MapSerializer serializer) { + serializer + .add("type", CLASS_NAME) + .add("pathId", mPathId) + .add("textId", mTextId) + .add("vOffset", mVOffset, mOutVOffset) + .add("hOffset", mHOffset, mOutHOffset); + } } diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/FloatConstant.java b/core/java/com/android/internal/widget/remotecompose/core/operations/FloatConstant.java index e04e691c312c..7f3c3ed6bcff 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/FloatConstant.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/FloatConstant.java @@ -25,30 +25,32 @@ import com.android.internal.widget.remotecompose.core.RemoteContext; import com.android.internal.widget.remotecompose.core.WireBuffer; import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder; import com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation; +import com.android.internal.widget.remotecompose.core.serialize.MapSerializer; +import com.android.internal.widget.remotecompose.core.serialize.Serializable; import java.util.List; -/** Operation to deal with Text data */ -public class FloatConstant extends Operation { +/** Used to represent a float */ +public class FloatConstant extends Operation implements Serializable { private static final int OP_CODE = Operations.DATA_FLOAT; private static final String CLASS_NAME = "FloatConstant"; - public int mTextId; + public int mId; public float mValue; - public FloatConstant(int textId, float value) { - this.mTextId = textId; + public FloatConstant(int id, float value) { + this.mId = id; this.mValue = value; } @Override public void write(@NonNull WireBuffer buffer) { - apply(buffer, mTextId, mValue); + apply(buffer, mId, mValue); } @NonNull @Override public String toString() { - return "FloatConstant[" + mTextId + "] = " + mValue; + return "FloatConstant[" + mId + "] = " + mValue; } /** @@ -90,10 +92,10 @@ public class FloatConstant extends Operation { * @param operations the list of operations that will be added to */ public static void read(@NonNull WireBuffer buffer, @NonNull List<Operation> operations) { - int textId = buffer.readInt(); + int id = buffer.readInt(); float value = buffer.readFloat(); - operations.add(new FloatConstant(textId, value)); + operations.add(new FloatConstant(id, value)); } /** @@ -110,7 +112,7 @@ public class FloatConstant extends Operation { @Override public void apply(@NonNull RemoteContext context) { - context.loadFloat(mTextId, mValue); + context.loadFloat(mId, mValue); } @NonNull @@ -118,4 +120,9 @@ public class FloatConstant extends Operation { public String deepToString(@NonNull String indent) { return indent + toString(); } + + @Override + public void serialize(MapSerializer serializer) { + serializer.add("type", CLASS_NAME).add("id", mId).add("value", mValue); + } } diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/FloatExpression.java b/core/java/com/android/internal/widget/remotecompose/core/operations/FloatExpression.java index e09745aa8546..c1fa898ec619 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/FloatExpression.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/FloatExpression.java @@ -34,6 +34,9 @@ import com.android.internal.widget.remotecompose.core.operations.utilities.Anima import com.android.internal.widget.remotecompose.core.operations.utilities.NanMap; import com.android.internal.widget.remotecompose.core.operations.utilities.easing.FloatAnimation; import com.android.internal.widget.remotecompose.core.operations.utilities.easing.SpringStopEngine; +import com.android.internal.widget.remotecompose.core.serialize.MapSerializer; +import com.android.internal.widget.remotecompose.core.serialize.Serializable; +import com.android.internal.widget.remotecompose.core.serialize.SerializeTags; import java.util.List; @@ -42,7 +45,7 @@ import java.util.List; * like injecting the width of the component int draw rect As well as supporting generalized * animation floats. The floats represent a RPN style calculator */ -public class FloatExpression extends Operation implements VariableSupport { +public class FloatExpression extends Operation implements VariableSupport, Serializable { private static final int OP_CODE = Operations.ANIMATED_FLOAT; private static final String CLASS_NAME = "FloatExpression"; public int mId; @@ -336,4 +339,14 @@ public class FloatExpression extends Operation implements VariableSupport { public String deepToString(@NonNull String indent) { return indent + toString(); } + + @Override + public void serialize(MapSerializer serializer) { + serializer + .addTags(SerializeTags.EXPRESSION) + .add("type", CLASS_NAME) + .add("id", mId) + .addFloatExpressionSrc("srcValues", mSrcValue) + .add("animation", mFloatAnimation); + } } diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/Header.java b/core/java/com/android/internal/widget/remotecompose/core/operations/Header.java index 98c2745380d7..3d6316b67e8f 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/Header.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/Header.java @@ -30,7 +30,11 @@ import com.android.internal.widget.remotecompose.core.RemoteComposeOperation; import com.android.internal.widget.remotecompose.core.RemoteContext; 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.utilities.IntMap; +import java.io.DataInputStream; +import java.io.IOException; +import java.io.InputStream; import java.util.List; /** @@ -42,16 +46,75 @@ import java.util.List; public class Header extends Operation implements RemoteComposeOperation { private static final int OP_CODE = Operations.HEADER; private static final String CLASS_NAME = "Header"; + private static final int MAGIC_NUMBER = 0x048C0000; // to uniquly identify the protocol int mMajorVersion; int mMinorVersion; int mPatchVersion; - int mWidth; - int mHeight; + int mWidth = 256; + int mHeight = 256; - float mDensity; - long mCapabilities; + float mDensity = 3; + long mCapabilities = 0; + private IntMap<Object> mProperties; + + /** + * Get a property on the header + * + * @param property the property to get + * @return the value of the property + */ + public Object get(short property) { + return mProperties.get(property); + } + + /** the width of the document */ + public static final short DOC_WIDTH = 5; + + /** The height of the document */ + public static final short DOC_HEIGHT = 6; + + /** The density at generation */ + public static final short DOC_DENSITY_AT_GENERATION = 7; + + /** The desired FPS for the document */ + public static final short DOC_DESIRED_FPS = 8; + + /** The description of the contents of the document */ + public static final short DOC_CONTENT_DESCRIPTION = 9; + + /** The source of the document */ + public static final short DOC_SOURCE = 11; + + /** The object is an integer */ + private static final short DATA_TYPE_INT = 0; + + /** The object is an float */ + private static final short DATA_TYPE_FLOAT = 1; + + /** The object is an LONG */ + private static final short DATA_TYPE_LONG = 2; + + /** The object is an UTF-8 encoded string */ + private static final short DATA_TYPE_STRING = 3; + + private static final short[] KEYS = { + DOC_WIDTH, + DOC_HEIGHT, + DOC_DENSITY_AT_GENERATION, + DOC_DESIRED_FPS, + DOC_CONTENT_DESCRIPTION, + DOC_SOURCE + }; + private static final String[] KEY_NAMES = { + "DOC_WIDTH", + "DOC_HEIGHT", + "DOC_DENSITY_AT_GENERATION", + "DOC_DESIRED_FPS", + "DOC_CONTENT_DESCRIPTION", + "DOC_SOURCE" + }; /** * It encodes the version of the document (following semantic versioning) as well as the @@ -82,6 +145,60 @@ public class Header extends Operation implements RemoteComposeOperation { this.mCapabilities = capabilities; } + /** + * @param majorVersion the major version of the RemoteCompose document API + * @param minorVersion the minor version of the RemoteCompose document API + * @param patchVersion the patch version of the RemoteCompose document API + * @param properties the properties of the document + */ + public Header(int majorVersion, int minorVersion, int patchVersion, IntMap<Object> properties) { + this.mMajorVersion = majorVersion; + this.mMinorVersion = minorVersion; + this.mPatchVersion = patchVersion; + if (properties != null) { + this.mProperties = properties; + this.mWidth = getInt(DOC_WIDTH, 256); + this.mHeight = getInt(DOC_HEIGHT, 256); + this.mDensity = getFloat(DOC_DENSITY_AT_GENERATION, 0); + } + } + + private int getInt(int key, int defaultValue) { + Integer value = (Integer) mProperties.get(key); + if (value != null) { + return value; + } else { + return defaultValue; + } + } + + private long getLong(int key, long defaultValue) { + Long value = (Long) mProperties.get(key); + if (value != null) { + return value; + } else { + return defaultValue; + } + } + + private float getFloat(int key, float defaultValue) { + Float value = (Float) mProperties.get(key); + if (value != null) { + return value; + } else { + return defaultValue; + } + } + + private String getString(int key, String defaultValue) { + String value = (String) mProperties.get(key); + if (value != null) { + return value; + } else { + return defaultValue; + } + } + @Override public void write(@NonNull WireBuffer buffer) { apply(buffer, mWidth, mHeight, mDensity, mCapabilities); @@ -90,6 +207,16 @@ public class Header extends Operation implements RemoteComposeOperation { @NonNull @Override public String toString() { + String prop = ""; + if (mProperties != null) { + for (int i = 0; i < KEYS.length; i++) { + Object p = mProperties.get(KEYS[i]); + if (p != null) { + prop += "\n " + KEY_NAMES[i] + " " + p.toString(); + } + } + return "HEADER v" + mMajorVersion + "." + mMinorVersion + "." + mPatchVersion + prop; + } return "HEADER v" + mMajorVersion + "." @@ -102,12 +229,20 @@ public class Header extends Operation implements RemoteComposeOperation { + mHeight + " [" + mCapabilities - + "]"; + + "]" + + prop; } @Override public void apply(@NonNull RemoteContext context) { - context.header(mMajorVersion, mMinorVersion, mPatchVersion, mWidth, mHeight, mCapabilities); + context.header( + mMajorVersion, + mMinorVersion, + mPatchVersion, + mWidth, + mHeight, + mCapabilities, + mProperties); } @NonNull @@ -157,22 +292,44 @@ public class Header extends Operation implements RemoteComposeOperation { } /** - * Read this operation and add it to the list of operations + * Apply the header to the wire buffer * - * @param buffer the buffer to read - * @param operations the list of operations that will be added to + * @param buffer */ - public static void read(@NonNull WireBuffer buffer, @NonNull List<Operation> operations) { - int majorVersion = buffer.readInt(); - int minorVersion = buffer.readInt(); - int patchVersion = buffer.readInt(); - int width = buffer.readInt(); - int height = buffer.readInt(); - // float density = buffer.readFloat(); - float density = 1f; - long capabilities = buffer.readLong(); - Header header = - new Header( + public static void apply(@NonNull WireBuffer buffer, short[] type, Object[] value) { + buffer.start(OP_CODE); + buffer.writeInt(MAJOR_VERSION | MAGIC_NUMBER); // major version number of the protocol + buffer.writeInt(MINOR_VERSION); // minor version number of the protocol + buffer.writeInt(PATCH_VERSION); // patch version number of the protocol + buffer.writeInt(type.length); + writeMap(buffer, type, value); + } + + /** + * @param is the stream to read from + * @return the header + * @throws IOException if there is an error reading the header + */ + public static Header readDirect(InputStream is) throws IOException { + DataInputStream stream = new DataInputStream(is); + try { + + int type = stream.readByte(); + + if (type != OP_CODE) { + throw new IOException("Invalid header " + type + " != " + OP_CODE); + } + int majorVersion = stream.readInt(); + int minorVersion = stream.readInt(); + int patchVersion = stream.readInt(); + + if (majorVersion < 0x10000) { + int width = stream.readInt(); + int height = stream.readInt(); + // float density = is.read(); + float density = 1f; + long capabilities = stream.readLong(); + return new Header( majorVersion, minorVersion, patchVersion, @@ -180,7 +337,173 @@ public class Header extends Operation implements RemoteComposeOperation { height, density, capabilities); - operations.add(header); + } + + if ((majorVersion & 0xFFFF0000) != MAGIC_NUMBER) { + throw new IOException( + "Invalid header MAGIC_NUMBER " + + (majorVersion & 0xFFFF0000) + + " != " + + MAGIC_NUMBER); + } + majorVersion &= 0xFFFF; + int len = stream.readInt(); + short[] types = new short[len]; + Object[] values = new Object[len]; + readMap(stream, types, values); + IntMap<Object> map = new IntMap<>(); + for (int i = 0; i < len; i++) { + map.put(types[i], values[i]); + } + return new Header(majorVersion, minorVersion, patchVersion, map); + + } finally { + stream.close(); + } + } + + /** + * Read this operation and add it to the list of operations + * + * @param stream the buffer to read + * @param types the list of types that will be populated + * @param values the list of values that will be populated + */ + private static void readMap(DataInputStream stream, short[] types, Object[] values) + throws IOException { + for (int i = 0; i < types.length; i++) { + short tag = (short) stream.readShort(); + int itemLen = stream.readShort(); + int dataType = tag >> 10; + types[i] = (short) (tag & 0x3F); + Object value; + switch (dataType) { + case DATA_TYPE_INT: + values[i] = stream.readInt(); + break; + case DATA_TYPE_FLOAT: + values[i] = stream.readFloat(); + break; + case DATA_TYPE_LONG: + values[i] = stream.readLong(); + break; + case DATA_TYPE_STRING: + int slen = stream.readInt(); + byte[] data = new byte[slen]; + stream.readFully(data); + values[i] = new String(data); + break; + } + } + } + + /** + * 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) { + int majorVersion = buffer.readInt(); + int minorVersion = buffer.readInt(); + int patchVersion = buffer.readInt(); + if (majorVersion < 0x10000) { + int width = buffer.readInt(); + int height = buffer.readInt(); + // float density = buffer.readFloat(); + float density = 1f; + long capabilities = buffer.readLong(); + Header header = + new Header( + majorVersion, + minorVersion, + patchVersion, + width, + height, + density, + capabilities); + operations.add(header); + } else { + majorVersion &= 0xFFFF; + int length = buffer.readInt(); + short[] types = new short[length]; + Object[] values = new Object[length]; + readMap(buffer, types, values); + IntMap<Object> map = new IntMap<>(); + for (int i = 0; i < length; i++) { + map.put(types[i], values[i]); + } + Header header = new Header(majorVersion, minorVersion, patchVersion, map); + operations.add(header); + } + } + + /** + * Read this operation and add it to the list of operations + * + * @param buffer the buffer to read + * @param types the list of types that will be populated + * @param values the list of values that will be populated + */ + private static void readMap(@NonNull WireBuffer buffer, short[] types, Object[] values) { + for (int i = 0; i < types.length; i++) { + short tag = (short) buffer.readShort(); + int itemLen = buffer.readShort(); + int dataType = tag >> 10; + types[i] = (short) (tag & 0x3F); + Object value; + switch (dataType) { + case DATA_TYPE_INT: + values[i] = buffer.readInt(); + break; + case DATA_TYPE_FLOAT: + values[i] = buffer.readFloat(); + break; + case DATA_TYPE_LONG: + values[i] = buffer.readLong(); + break; + case DATA_TYPE_STRING: + values[i] = buffer.readUTF8(); + break; + } + } + } + + /** + * Write the map of values to the buffer + * + * @param buffer the buffer to read + * @param types the list of types that will be written + * @param values the list of values that will be written + */ + private static void writeMap(@NonNull WireBuffer buffer, short[] types, Object[] values) { + for (int i = 0; i < types.length; i++) { + short tag = types[i]; + if (values[i] instanceof String) { + tag = (short) (tag | (DATA_TYPE_STRING << 10)); + buffer.writeShort(tag); + String str = (String) values[i]; + byte[] data = str.getBytes(); + buffer.writeShort((data.length + 4)); + buffer.writeBuffer(data); + } else if (values[i] instanceof Integer) { + tag = (short) (tag | (DATA_TYPE_INT << 10)); + buffer.writeShort(tag); + buffer.writeShort(4); + buffer.writeInt((Integer) values[i]); + } else if (values[i] instanceof Float) { + tag = (short) (tag | (DATA_TYPE_FLOAT << 10)); + buffer.writeShort(tag); + buffer.writeShort(4); + + buffer.writeFloat((float) values[i]); + } else if (values[i] instanceof Long) { + tag = (short) (tag | (DATA_TYPE_LONG << 10)); + buffer.writeShort(tag); + buffer.writeShort(8); + buffer.writeLong((Long) values[i]); + } + } } /** diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/IntegerExpression.java b/core/java/com/android/internal/widget/remotecompose/core/operations/IntegerExpression.java index f04f30dc62fb..2a5260c0c9b1 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/IntegerExpression.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/IntegerExpression.java @@ -29,6 +29,9 @@ import com.android.internal.widget.remotecompose.core.WireBuffer; import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder; import com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation; import com.android.internal.widget.remotecompose.core.operations.utilities.IntegerExpressionEvaluator; +import com.android.internal.widget.remotecompose.core.serialize.MapSerializer; +import com.android.internal.widget.remotecompose.core.serialize.Serializable; +import com.android.internal.widget.remotecompose.core.serialize.SerializeTags; import java.util.Arrays; import java.util.List; @@ -38,7 +41,7 @@ import java.util.List; * like injecting the width of the component int draw rect As well as supporting generalized * animation floats. The floats represent a RPN style calculator */ -public class IntegerExpression extends Operation implements VariableSupport { +public class IntegerExpression extends Operation implements VariableSupport, Serializable { private static final int OP_CODE = Operations.INTEGER_EXPRESSION; private static final String CLASS_NAME = "IntegerExpression"; public int mId; @@ -225,4 +228,14 @@ public class IntegerExpression extends Operation implements VariableSupport { public static boolean isId(int mask, int i, int value) { return ((1 << i) & mask) != 0 && value < IntegerExpressionEvaluator.OFFSET; } + + @Override + public void serialize(MapSerializer serializer) { + serializer + .addTags(SerializeTags.EXPRESSION) + .add("type", CLASS_NAME) + .add("id", mId) + .add("mask", mId) + .addIntExpressionSrc("srcValues", mSrcValue, mMask); + } } diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/NamedVariable.java b/core/java/com/android/internal/widget/remotecompose/core/operations/NamedVariable.java index dde632e0c346..96628fd51225 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/NamedVariable.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/NamedVariable.java @@ -26,11 +26,13 @@ import com.android.internal.widget.remotecompose.core.RemoteContext; import com.android.internal.widget.remotecompose.core.WireBuffer; import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder; import com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation; +import com.android.internal.widget.remotecompose.core.serialize.MapSerializer; +import com.android.internal.widget.remotecompose.core.serialize.Serializable; import java.util.List; /** Operation to deal with Text data */ -public class NamedVariable extends Operation { +public class NamedVariable extends Operation implements Serializable { private static final int OP_CODE = Operations.NAMED_VARIABLE; private static final String CLASS_NAME = "NamedVariable"; public final int mVarId; @@ -135,4 +137,28 @@ public class NamedVariable extends Operation { public String deepToString(@NonNull String indent) { return indent + toString(); } + + @Override + public void serialize(MapSerializer serializer) { + serializer + .add("type", CLASS_NAME) + .add("varId", mVarId) + .add("varName", mVarName) + .add("varType", typeToString()); + } + + private String typeToString() { + switch (mVarType) { + case COLOR_TYPE: + return "COLOR_TYPE"; + case FLOAT_TYPE: + return "FLOAT_TYPE"; + case STRING_TYPE: + return "STRING_TYPE"; + case IMAGE_TYPE: + return "IMAGE_TYPE"; + default: + return "INVALID_TYPE"; + } + } } diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/PaintData.java b/core/java/com/android/internal/widget/remotecompose/core/operations/PaintData.java index f756b76b86c3..8389aa707ee6 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/PaintData.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/PaintData.java @@ -29,11 +29,13 @@ 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.paint.PaintBundle; +import com.android.internal.widget.remotecompose.core.serialize.MapSerializer; +import com.android.internal.widget.remotecompose.core.serialize.Serializable; import java.util.List; /** Paint data operation */ -public class PaintData extends PaintOperation implements VariableSupport { +public class PaintData extends PaintOperation implements VariableSupport, Serializable { private static final int OP_CODE = Operations.PAINT_VALUES; private static final String CLASS_NAME = "PaintData"; @NonNull public PaintBundle mPaintData = new PaintBundle(); @@ -126,4 +128,9 @@ public class PaintData extends PaintOperation implements VariableSupport { public void paint(@NonNull PaintContext context) { context.applyPaint(mPaintData); } + + @Override + public void serialize(MapSerializer serializer) { + serializer.add("type", CLASS_NAME).add("paintBundle", mPaintData); + } } diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/PathAppend.java b/core/java/com/android/internal/widget/remotecompose/core/operations/PathAppend.java index e7cce03f0c4b..8f353ce4a26b 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/PathAppend.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/PathAppend.java @@ -30,11 +30,13 @@ 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.documentation.DocumentedOperation; +import com.android.internal.widget.remotecompose.core.serialize.MapSerializer; +import com.android.internal.widget.remotecompose.core.serialize.Serializable; import java.util.Arrays; import java.util.List; -public class PathAppend extends PaintOperation implements VariableSupport { +public class PathAppend extends PaintOperation implements VariableSupport, Serializable { private static final int OP_CODE = Operations.PATH_ADD; private static final String CLASS_NAME = "PathAppend"; int mInstanceId; @@ -253,4 +255,12 @@ public class PathAppend extends PaintOperation implements VariableSupport { } return str.toString(); } + + @Override + public void serialize(MapSerializer serializer) { + serializer + .add("type", CLASS_NAME) + .add("id", mInstanceId) + .add("path", pathString(mFloatPath)); + } } diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/PathCreate.java b/core/java/com/android/internal/widget/remotecompose/core/operations/PathCreate.java index 1f76639b1b1f..7aa3390b53ee 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/PathCreate.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/PathCreate.java @@ -29,11 +29,13 @@ 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.documentation.DocumentedOperation; +import com.android.internal.widget.remotecompose.core.serialize.MapSerializer; +import com.android.internal.widget.remotecompose.core.serialize.Serializable; import java.util.Arrays; import java.util.List; -public class PathCreate extends PaintOperation implements VariableSupport { +public class PathCreate extends PaintOperation implements VariableSupport, Serializable { private static final int OP_CODE = Operations.PATH_CREATE; private static final String CLASS_NAME = "PathCreate"; int mInstanceId; @@ -237,4 +239,12 @@ public class PathCreate extends PaintOperation implements VariableSupport { public void apply(@NonNull RemoteContext context) { context.loadPathData(mInstanceId, mOutputPath); } + + @Override + public void serialize(MapSerializer serializer) { + serializer + .add("type", CLASS_NAME) + .add("id", mInstanceId) + .add("path", pathString(mFloatPath)); + } } diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/PathTween.java b/core/java/com/android/internal/widget/remotecompose/core/operations/PathTween.java index 65adfeabefa6..c5add57d4dd0 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/PathTween.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/PathTween.java @@ -29,11 +29,13 @@ 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.documentation.DocumentedOperation; +import com.android.internal.widget.remotecompose.core.serialize.MapSerializer; +import com.android.internal.widget.remotecompose.core.serialize.Serializable; import java.util.List; /** Operation to deal with Path data */ -public class PathTween extends PaintOperation implements VariableSupport { +public class PathTween extends PaintOperation implements VariableSupport, Serializable { private static final int OP_CODE = Operations.PATH_TWEEN; private static final String CLASS_NAME = "PathTween"; public int mOutId; @@ -155,4 +157,14 @@ public class PathTween extends PaintOperation implements VariableSupport { public void paint(PaintContext context) { context.tweenPath(mOutId, mPathId1, mPathId2, mTweenOut); } + + @Override + public void serialize(MapSerializer serializer) { + serializer + .add("type", CLASS_NAME) + .add("outId", mOutId) + .add("pathId1", mPathId1) + .add("pathId2", mPathId2) + .add("tween", mTween, mTweenOut); + } } diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/RootContentDescription.java b/core/java/com/android/internal/widget/remotecompose/core/operations/RootContentDescription.java index c1ddf63264fa..a6a8a810d2ad 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/RootContentDescription.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/RootContentDescription.java @@ -25,12 +25,14 @@ import com.android.internal.widget.remotecompose.core.WireBuffer; import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder; import com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation; import com.android.internal.widget.remotecompose.core.semantics.AccessibleComponent; +import com.android.internal.widget.remotecompose.core.serialize.MapSerializer; +import com.android.internal.widget.remotecompose.core.serialize.Serializable; import java.util.List; /** Describe a content description for the document */ public class RootContentDescription extends Operation - implements RemoteComposeOperation, AccessibleComponent { + implements RemoteComposeOperation, AccessibleComponent, Serializable { private static final int OP_CODE = Operations.ROOT_CONTENT_DESCRIPTION; private static final String CLASS_NAME = "RootContentDescription"; int mContentDescription; @@ -128,4 +130,9 @@ public class RootContentDescription extends Operation .description("Content description of root") .field(DocumentedOperation.INT, "id", "id of Int"); } + + @Override + public void serialize(MapSerializer serializer) { + serializer.add("type", CLASS_NAME).add("contentDescriptionId", mContentDescription); + } } diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/ShaderData.java b/core/java/com/android/internal/widget/remotecompose/core/operations/ShaderData.java index 46a32905b96c..5f6162b68e9e 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/ShaderData.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/ShaderData.java @@ -32,6 +32,8 @@ 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.documentation.DocumentedOperation; +import com.android.internal.widget.remotecompose.core.serialize.MapSerializer; +import com.android.internal.widget.remotecompose.core.serialize.Serializable; import java.util.Arrays; import java.util.HashMap; @@ -41,7 +43,7 @@ import java.util.List; * Operation to deal with bitmap data On getting an Image during a draw call the bitmap is * compressed and saved in playback the image is decompressed */ -public class ShaderData extends Operation implements VariableSupport { +public class ShaderData extends Operation implements VariableSupport, Serializable { private static final int OP_CODE = Operations.DATA_SHADER; private static final String CLASS_NAME = "ShaderData"; int mShaderTextId; // the actual text of a shader @@ -384,4 +386,15 @@ public class ShaderData extends Operation implements VariableSupport { public void enable(boolean shaderValid) { mShaderValid = shaderValid; } + + @Override + public void serialize(MapSerializer serializer) { + serializer + .add("type", CLASS_NAME) + .add("shaderTextId", mShaderTextId) + .add("shaderID", mShaderID) + .add("uniformRawFloatMap", mUniformRawFloatMap) + .add("uniformFloatMap", mUniformFloatMap) + .add("uniformBitmapMap", mUniformBitmapMap); + } } diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/TextAttribute.java b/core/java/com/android/internal/widget/remotecompose/core/operations/TextAttribute.java index 45cced3f8b45..3e72995de9db 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/TextAttribute.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/TextAttribute.java @@ -27,11 +27,13 @@ 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.serialize.MapSerializer; +import com.android.internal.widget.remotecompose.core.serialize.Serializable; import java.util.List; /** Operation to Measure Text data */ -public class TextAttribute extends PaintOperation { +public class TextAttribute extends PaintOperation implements Serializable { private static final int OP_CODE = Operations.ATTRIBUTE_TEXT; private static final String CLASS_NAME = "TextMeasure"; public int mId; @@ -167,4 +169,34 @@ public class TextAttribute extends PaintOperation { break; } } + + @Override + public void serialize(MapSerializer serializer) { + serializer + .add("type", CLASS_NAME) + .add("id", mId) + .add("textId", mTextId) + .add("measureType", typeToString()); + } + + private String typeToString() { + switch (mType) { + case MEASURE_WIDTH: + return "MEASURE_WIDTH"; + case MEASURE_HEIGHT: + return "MEASURE_HEIGHT"; + case MEASURE_LEFT: + return "MEASURE_LEFT"; + case MEASURE_RIGHT: + return "MEASURE_RIGHT"; + case MEASURE_TOP: + return "MEASURE_TOP"; + case MEASURE_BOTTOM: + return "MEASURE_BOTTOM"; + case TEXT_LENGTH: + return "TEXT_LENGTH"; + default: + return "INVALID_TYPE"; + } + } } diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/TextData.java b/core/java/com/android/internal/widget/remotecompose/core/operations/TextData.java index 5788d8f4da64..419e6d074479 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/TextData.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/TextData.java @@ -27,11 +27,13 @@ import com.android.internal.widget.remotecompose.core.WireBuffer; import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder; import com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation; 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.Serializable; import java.util.List; /** Operation to deal with Text data */ -public class TextData extends Operation implements SerializableToString { +public class TextData extends Operation implements SerializableToString, Serializable { private static final int OP_CODE = Operations.DATA_TEXT; private static final String CLASS_NAME = "TextData"; public final int mTextId; @@ -131,4 +133,9 @@ public class TextData extends Operation implements SerializableToString { private String getSerializedName() { return "DATA_TEXT"; } + + @Override + public void serialize(MapSerializer serializer) { + serializer.add("type", CLASS_NAME).add("textId", mTextId).add("text", mText); + } } diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/TextFromFloat.java b/core/java/com/android/internal/widget/remotecompose/core/operations/TextFromFloat.java index cc0ff025f09b..6b2f49be76f0 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/TextFromFloat.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/TextFromFloat.java @@ -28,6 +28,8 @@ import com.android.internal.widget.remotecompose.core.WireBuffer; import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder; import com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation; import com.android.internal.widget.remotecompose.core.operations.utilities.StringUtils; +import com.android.internal.widget.remotecompose.core.serialize.MapSerializer; +import com.android.internal.widget.remotecompose.core.serialize.Serializable; import java.util.List; @@ -36,7 +38,7 @@ import java.util.List; * [command][textID][before,after][flags] before and after define number of digits before and after * the decimal point */ -public class TextFromFloat extends Operation implements VariableSupport { +public class TextFromFloat extends Operation implements VariableSupport, Serializable { private static final int OP_CODE = Operations.TEXT_FROM_FLOAT; private static final String CLASS_NAME = "TextFromFloat"; public int mTextId; @@ -209,4 +211,15 @@ public class TextFromFloat extends Operation implements VariableSupport { public String deepToString(@NonNull String indent) { return indent + toString(); } + + @Override + public void serialize(MapSerializer serializer) { + serializer + .add("type", CLASS_NAME) + .add("textId", mTextId) + .add("value", mValue, mOutValue) + .add("digitsBefore", mDigitsBefore) + .add("digitsAfter", mDigitsAfter) + .add("flags", mFlags); + } } diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/TextLookup.java b/core/java/com/android/internal/widget/remotecompose/core/operations/TextLookup.java index dceb8b67ec3a..e8865c26db12 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/TextLookup.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/TextLookup.java @@ -26,6 +26,8 @@ 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.serialize.MapSerializer; +import com.android.internal.widget.remotecompose.core.serialize.Serializable; import java.util.List; @@ -34,7 +36,7 @@ import java.util.List; * [command][textID][before,after][flags] before and after define number of digits before and after * the decimal point */ -public class TextLookup extends Operation implements VariableSupport { +public class TextLookup extends Operation implements VariableSupport, Serializable { private static final int OP_CODE = Operations.TEXT_LOOKUP; private static final String CLASS_NAME = "TextFromFloat"; public int mTextId; @@ -150,4 +152,13 @@ public class TextLookup extends Operation implements VariableSupport { public String deepToString(@NonNull String indent) { return indent + toString(); } + + @Override + public void serialize(MapSerializer serializer) { + serializer + .add("type", CLASS_NAME) + .add("textId", mTextId) + .add("dataSetId", mDataSetId) + .add("indexId", mIndex, mOutIndex); + } } diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/TextLookupInt.java b/core/java/com/android/internal/widget/remotecompose/core/operations/TextLookupInt.java index 823b70656c86..de2025569d46 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/TextLookupInt.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/TextLookupInt.java @@ -26,11 +26,13 @@ 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.documentation.DocumentedOperation; +import com.android.internal.widget.remotecompose.core.serialize.MapSerializer; +import com.android.internal.widget.remotecompose.core.serialize.Serializable; import java.util.List; /** Operation convert int index of a list to text */ -public class TextLookupInt extends Operation implements VariableSupport { +public class TextLookupInt extends Operation implements VariableSupport, Serializable { private static final int OP_CODE = Operations.TEXT_LOOKUP_INT; private static final String CLASS_NAME = "TextFromINT"; public int mTextId; @@ -143,4 +145,13 @@ public class TextLookupInt extends Operation implements VariableSupport { public String deepToString(@NonNull String indent) { return indent + toString(); } + + @Override + public void serialize(MapSerializer serializer) { + serializer + .add("type", CLASS_NAME) + .add("textId", mTextId) + .add("dataSetId", mDataSetId) + .add("indexId", mIndex, mOutIndex); + } } diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/TextMerge.java b/core/java/com/android/internal/widget/remotecompose/core/operations/TextMerge.java index d69561566b56..262916dd9d0c 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/TextMerge.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/TextMerge.java @@ -25,11 +25,13 @@ import com.android.internal.widget.remotecompose.core.RemoteContext; import com.android.internal.widget.remotecompose.core.WireBuffer; import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder; import com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation; +import com.android.internal.widget.remotecompose.core.serialize.MapSerializer; +import com.android.internal.widget.remotecompose.core.serialize.Serializable; import java.util.List; /** Operation to deal with Text data */ -public class TextMerge extends Operation { +public class TextMerge extends Operation implements Serializable { private static final int OP_CODE = Operations.TEXT_MERGE; private static final String CLASS_NAME = "TextMerge"; public int mTextId; @@ -126,4 +128,13 @@ public class TextMerge extends Operation { public String deepToString(@NonNull String indent) { return indent + toString(); } + + @Override + public void serialize(MapSerializer serializer) { + serializer + .add("type", CLASS_NAME) + .add("id", mTextId) + .add("leftId", mSrcId1) + .add("rightId", mSrcId2); + } } diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/TouchExpression.java b/core/java/com/android/internal/widget/remotecompose/core/operations/TouchExpression.java index 997628140c46..2591a4c39778 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/TouchExpression.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/TouchExpression.java @@ -35,6 +35,8 @@ import com.android.internal.widget.remotecompose.core.operations.layout.RootLayo import com.android.internal.widget.remotecompose.core.operations.utilities.AnimatedFloatExpression; import com.android.internal.widget.remotecompose.core.operations.utilities.NanMap; import com.android.internal.widget.remotecompose.core.operations.utilities.touch.VelocityEasing; +import com.android.internal.widget.remotecompose.core.serialize.MapSerializer; +import com.android.internal.widget.remotecompose.core.serialize.Serializable; import java.util.Arrays; import java.util.List; @@ -44,7 +46,8 @@ import java.util.List; * touch behaviours. Including animating to Notched, positions. and tweaking the dynamics of the * animation. */ -public class TouchExpression extends Operation implements VariableSupport, TouchListener { +public class TouchExpression extends Operation + implements VariableSupport, TouchListener, Serializable { private static final int OP_CODE = Operations.TOUCH_EXPRESSION; private static final String CLASS_NAME = "TouchExpression"; private float mDefValue; @@ -709,4 +712,16 @@ public class TouchExpression extends Operation implements VariableSupport, Touch public String deepToString(@NonNull String indent) { return indent + toString(); } + + @Override + public void serialize(MapSerializer serializer) { + serializer + .add("type", CLASS_NAME) + .add("id", mId) + .add("mDefValue", mDefValue, mOutDefValue) + .add("min", mMin, mOutMin) + .add("max", mMax, mOutMax) + .add("mode", mMode) + .addFloatExpressionSrc("srcExp", mSrcExp); + } } 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/ImpulseProcess.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/ImpulseProcess.java index f896f3d8ee9c..8c9dd76c9ed5 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/ImpulseProcess.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/ImpulseProcess.java @@ -25,12 +25,15 @@ 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.serialize.MapSerializer; +import com.android.internal.widget.remotecompose.core.serialize.Serializable; import java.util.ArrayList; import java.util.List; /** Represents the repeating part of an Impulse. */ -public class ImpulseProcess extends PaintOperation implements VariableSupport, Container { +public class ImpulseProcess extends PaintOperation + implements VariableSupport, Container, Serializable { private static final int OP_CODE = Operations.IMPULSE_PROCESS; private static final String CLASS_NAME = "ImpulseProcess"; @@ -151,4 +154,9 @@ public class ImpulseProcess extends PaintOperation implements VariableSupport, C public int estimateIterations() { return 1; } + + @Override + public void serialize(MapSerializer serializer) { + serializer.add("type", CLASS_NAME).add("list", mList); + } } 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/LoopOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LoopOperation.java index 0f4cf562eae6..2b63cf246555 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LoopOperation.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LoopOperation.java @@ -27,12 +27,16 @@ import com.android.internal.widget.remotecompose.core.WireBuffer; import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder; import com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation; import com.android.internal.widget.remotecompose.core.operations.Utils; +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 loop of operations */ -public class LoopOperation extends PaintOperation implements Container, VariableSupport { +public class LoopOperation extends PaintOperation + implements Container, VariableSupport, Serializable { + private static final String CLASS_NAME = "LoopOperation"; private static final int OP_CODE = Operations.LOOP_START; @@ -198,4 +202,16 @@ public class LoopOperation extends PaintOperation implements Container, Variable } return 10; // this is a generic estmate if the values are variables; } + + @Override + public void serialize(MapSerializer serializer) { + serializer + .add("type", CLASS_NAME) + .add("indexVariableId", mIndexVariableId) + .add("until", mUntil, mUntilOut) + .add("from", mFrom, mFromOut) + .add("step", mStep, mStepOut) + .add("mUntilOut", mUntilOut) + .add("list", mList); + } } diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/TextLayout.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/TextLayout.java index 1cfb50724e0b..d5db74b5ca51 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/TextLayout.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/managers/TextLayout.java @@ -209,7 +209,7 @@ public class TextLayout extends LayoutManager implements VariableSupport, Access mPaint.setColor(mColor); mPaint.setTextSize(mFontSize); mPaint.setTextStyle(mType, (int) mFontWeight, mFontStyle == 1); - context.applyPaint(mPaint); + context.replacePaint(mPaint); if (mCachedString == null) { return; } @@ -330,7 +330,7 @@ public class TextLayout extends LayoutManager implements VariableSupport, Access mPaint.setTextSize(mFontSize); mPaint.setTextStyle(mType, (int) mFontWeight, mFontStyle == 1); mPaint.setColor(mColor); - context.applyPaint(mPaint); + context.replacePaint(mPaint); float[] bounds = new float[4]; if (mCachedString == null) { return; @@ -343,7 +343,7 @@ public class TextLayout extends LayoutManager implements VariableSupport, Access flags |= PaintContext.TEXT_COMPLEX; } context.getTextBounds(mTextId, 0, mCachedString.length(), flags, bounds); - if (bounds[2] - bounds[1] > maxWidth) { + if (bounds[2] - bounds[1] > maxWidth && mMaxLines > 1) { mComputedTextLayout = context.layoutComplexText( mTextId, diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/BackgroundModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/BackgroundModifierOperation.java index dc1b875f0f9c..fd5f8c9cdbe7 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/BackgroundModifierOperation.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/BackgroundModifierOperation.java @@ -196,7 +196,7 @@ public class BackgroundModifierOperation extends DecoratorModifierOperation { mPaint.reset(); mPaint.setStyle(PaintBundle.STYLE_FILL); mPaint.setColor(mR, mG, mB, mA); - context.applyPaint(mPaint); + context.replacePaint(mPaint); if (mShapeType == ShapeType.RECTANGLE) { context.drawRect(0f, 0f, mWidth, mHeight); } else if (mShapeType == ShapeType.CIRCLE) { diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/BorderModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/BorderModifierOperation.java index 3acbd88329c2..e5f318307a75 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/BorderModifierOperation.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/BorderModifierOperation.java @@ -252,7 +252,7 @@ public class BorderModifierOperation extends DecoratorModifierOperation { paint.setColor(mR, mG, mB, mA); paint.setStrokeWidth(mBorderWidth); paint.setStyle(PaintBundle.STYLE_STROKE); - context.applyPaint(paint); + context.replacePaint(paint); if (mShapeType == ShapeType.RECTANGLE) { context.drawRect(0f, 0f, mWidth, mHeight); } else { 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/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/RippleModifierOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/RippleModifierOperation.java index d3004aa73a77..69ace8478e08 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/RippleModifierOperation.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/RippleModifierOperation.java @@ -126,7 +126,7 @@ public class RippleModifierOperation extends DecoratorModifierOperation implemen float radius = Math.max(mWidth, mHeight) * tweenRadius; mPaint.setColor(paintedColor); - context.applyPaint(mPaint); + context.replacePaint(mPaint); context.clipRect(0f, 0f, mWidth, mHeight); context.drawCircle(mAnimateRippleX, mAnimateRippleY, radius); context.restorePaint(); diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/paint/PaintBundle.java b/core/java/com/android/internal/widget/remotecompose/core/operations/paint/PaintBundle.java index 4c7f503e0bf8..0f17b114133d 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/paint/PaintBundle.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/paint/PaintBundle.java @@ -15,6 +15,8 @@ */ package com.android.internal.widget.remotecompose.core.operations.paint; +import static com.android.internal.widget.remotecompose.core.serialize.MapSerializer.orderedOf; + import android.annotation.NonNull; import android.annotation.Nullable; @@ -23,11 +25,16 @@ 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.operations.Utils; +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.Arrays; +import java.util.List; +import java.util.Map; /** Paint Bundle represents a delta of changes to a paint object */ -public class PaintBundle { +public class PaintBundle implements Serializable { @NonNull int[] mArray = new int[200]; @Nullable int[] mOutArray = null; int mPos = 0; @@ -337,7 +344,6 @@ public class PaintBundle { } } } - len = array[ret++]; // stops for (int j = 0; j < len; j++) { registerFloat(array[ret++], context, support); @@ -1239,4 +1245,201 @@ public class PaintBundle { return ret; } + + @Override + public void serialize(MapSerializer serializer) { + serializer.add("type", "PaintBundle"); + List<Map<String, Object>> list = new ArrayList<>(); + int i = 0; + while (i < mPos) { + int cmd = mArray[i++]; + int type = cmd & 0xFFFF; + switch (type) { + case TEXT_SIZE: + list.add(orderedOf("type", "TextSize", "size", getVariable(mArray[i++]))); + break; + case TYPEFACE: + int style = (cmd >> 16); + float weight = (float) (style & 0x3ff); + boolean italic = (style >> 10) > 0; + int fontFamily = mArray[i++]; + list.add(orderedOf("type", "FontFamily", "fontFamily", fontFamily)); + list.add(orderedOf("type", "FontWeight", "weight", weight)); + list.add(orderedOf("type", "TypeFace", "italic", italic)); + break; + case COLOR: + list.add(orderedOf("type", "Color", "color", colorInt(mArray[i++]))); + break; + case COLOR_ID: + list.add(orderedOf("type", "ColorId", "id", mArray[i++])); + break; + case STROKE_WIDTH: + list.add(orderedOf("type", "StrokeWidth", "width", getVariable(mArray[i++]))); + break; + case STROKE_MITER: + list.add(orderedOf("type", "StrokeMiter", "miter", getVariable(mArray[i++]))); + break; + case STROKE_CAP: + list.add(orderedOf("type", "StrokeCap", "cap", cmd >> 16)); + break; + case STYLE: + list.add(orderedOf("type", "Style", "style", cmd >> 16)); + break; + case COLOR_FILTER: + list.add( + orderedOf( + "type", + "ColorFilter", + "color", + colorInt(mArray[i++]), + "mode", + blendModeString(cmd >> 16))); + break; + case COLOR_FILTER_ID: + list.add( + orderedOf( + "type", + "ColorFilterID", + "id", + mArray[i++], + "mode", + blendModeString(cmd >> 16))); + break; + case CLEAR_COLOR_FILTER: + list.add(orderedOf("type", "ClearColorFilter")); + break; + case SHADER: + list.add(orderedOf("type", "Shader", "id", mArray[i++])); + break; + case ALPHA: + list.add(orderedOf("type", "Alpha", "alpha", getVariable(mArray[i++]))); + break; + case IMAGE_FILTER_QUALITY: + list.add(orderedOf("type", "ImageFilterQuality", "quality", cmd >> 16)); + break; + case BLEND_MODE: + list.add(orderedOf("type", "BlendMode", "mode", blendModeString(cmd >> 16))); + break; + case FILTER_BITMAP: + list.add(orderedOf("type", "FilterBitmap", "enabled", !(cmd >> 16 == 0))); + break; + case STROKE_JOIN: + list.add(orderedOf("type", "StrokeJoin", "strokeJoin", cmd >> 16)); + break; + case ANTI_ALIAS: + list.add(orderedOf("type", "AntiAlias", "enabled", !(cmd >> 16 == 0))); + break; + case GRADIENT: + i = serializeGradient(cmd, mArray, i, list); + } + } + serializer.add("operations", list); + } + + private static Map<String, Object> getVariable(int value) { + float fValue = Float.intBitsToFloat(value); + if (Float.isNaN(fValue)) { + return orderedOf("type", "Variable", "id", Utils.idFromNan(fValue)); + } + return orderedOf("type", "Value", "value", fValue); + } + + private static int serializeGradient( + int cmd, int[] array, int i, List<Map<String, Object>> list) { + int ret = i; + int gradientType = (cmd >> 16); + + int len = 0xFF & array[ret++]; // maximum 256 colors + + String[] colors = null; + if (len > 0) { + colors = new String[len]; + for (int j = 0; j < colors.length; j++) { + colors[j] = colorInt(array[ret++]); + } + } + len = array[ret++]; + float[] stops = null; + if (len > 0) { + stops = new float[len]; + for (int j = 0; j < colors.length; j++) { + stops[j] = Float.intBitsToFloat(array[ret++]); + } + } + + if (colors == null) { + return ret; + } + + int tileMode; + int centerX; + int centerY; + + switch (gradientType) { + case LINEAR_GRADIENT: + int startX = array[ret++]; + int startY = array[ret++]; + int endX = array[ret++]; + int endY = array[ret++]; + tileMode = array[ret++]; + list.add( + orderedOf( + "type", + "LinearGradient", + "colors", + colors, + "stops", + stops == null ? List.of() : stops, + "startX", + getVariable(startX), + "startY", + getVariable(startY), + "endX", + getVariable(endX), + "endY", + getVariable(endY), + "tileMode", + tileMode)); + break; + case RADIAL_GRADIENT: + centerX = array[ret++]; + centerY = array[ret++]; + int radius = array[ret++]; + tileMode = array[ret++]; + list.add( + orderedOf( + "type", + "LinearGradient", + "colors", + colors, + "stops", + stops == null ? List.of() : stops, + "centerX", + getVariable(centerX), + "centerY", + getVariable(centerY), + "radius", + getVariable(radius), + "tileMode", + tileMode)); + break; + case SWEEP_GRADIENT: + centerX = array[ret++]; + centerY = array[ret++]; + list.add( + orderedOf( + "type", + "LinearGradient", + "colors", + colors, + "stops", + stops == null ? List.of() : stops, + "centerX", + getVariable(centerX), + "centerY", + getVariable(centerY))); + } + + return ret; + } } diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/easing/FloatAnimation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/easing/FloatAnimation.java index 65472c262206..cad76059f7a4 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/easing/FloatAnimation.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/utilities/easing/FloatAnimation.java @@ -18,8 +18,11 @@ package com.android.internal.widget.remotecompose.core.operations.utilities.easi import android.annotation.NonNull; import android.annotation.Nullable; +import com.android.internal.widget.remotecompose.core.serialize.MapSerializer; +import com.android.internal.widget.remotecompose.core.serialize.Serializable; + /** Support Animation of the FloatExpression */ -public class FloatAnimation extends Easing { +public class FloatAnimation extends Easing implements Serializable { float[] mSpec; // mSpec[0] = duration // int(mSpec[1]) = num_of_param << 16 | type @@ -391,4 +394,14 @@ public class FloatAnimation extends Easing { public float getInitialValue() { return mInitialValue; } + + @Override + public void serialize(MapSerializer serializer) { + serializer + .add("type", "FloatAnimation") + .add("initialValue", mInitialValue) + .add("targetValue", mInitialValue) + .add("duration", mInitialValue) + .add("easing", Easing.getString(mEasingCurve.getType())); + } } diff --git a/core/java/com/android/internal/widget/remotecompose/core/serialize/MapSerializer.java b/core/java/com/android/internal/widget/remotecompose/core/serialize/MapSerializer.java index bcdac22f7baa..f9ecf0f4f672 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/serialize/MapSerializer.java +++ b/core/java/com/android/internal/widget/remotecompose/core/serialize/MapSerializer.java @@ -17,6 +17,7 @@ package com.android.internal.widget.remotecompose.core.serialize; import android.annotation.Nullable; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -24,6 +25,24 @@ import java.util.Map; public interface MapSerializer { /** + * Add a float expression + * + * @param key + * @param value + * @return + */ + MapSerializer addFloatExpressionSrc(String key, float[] value); + + /** + * Add an int expression + * + * @param key The key + * @param value The int src + * @param mask For determining ID from int + */ + MapSerializer addIntExpressionSrc(String key, int[] value, int mask); + + /** * Add metadata to this map for filtering by the data format generator. * * @param value A set of tags to add @@ -146,4 +165,19 @@ public interface MapSerializer { * @param value The Enum */ <T extends Enum<T>> MapSerializer add(String key, @Nullable Enum<T> value); + + /** + * Similar to Map.of, but create a LinkedHashMap preserving insertion order for predictable + * serialization. + * + * @param keysAndValues a even number of items, repeating String key and Object value. + * @return A LinkedHashMap. + */ + static LinkedHashMap<String, Object> orderedOf(Object... keysAndValues) { + final LinkedHashMap<String, Object> map = new LinkedHashMap<>(); + for (int i = 0; i < keysAndValues.length; i += 2) { + map.put((String) keysAndValues[i], keysAndValues[i + 1]); + } + return map; + } } diff --git a/core/java/com/android/internal/widget/remotecompose/core/serialize/SerializeTags.java b/core/java/com/android/internal/widget/remotecompose/core/serialize/SerializeTags.java index 99cac0fc75a8..c29dd98fbd7d 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/serialize/SerializeTags.java +++ b/core/java/com/android/internal/widget/remotecompose/core/serialize/SerializeTags.java @@ -22,4 +22,5 @@ public enum SerializeTags { A11Y, ACTION, DRAW_OPERATION, + EXPRESSION, } diff --git a/core/java/com/android/internal/widget/remotecompose/core/types/BooleanConstant.java b/core/java/com/android/internal/widget/remotecompose/core/types/BooleanConstant.java index 2c874b183a62..cb759a61249a 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/types/BooleanConstant.java +++ b/core/java/com/android/internal/widget/remotecompose/core/types/BooleanConstant.java @@ -25,11 +25,15 @@ import com.android.internal.widget.remotecompose.core.RemoteContext; import com.android.internal.widget.remotecompose.core.WireBuffer; import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder; import com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation; +import com.android.internal.widget.remotecompose.core.serialize.MapSerializer; +import com.android.internal.widget.remotecompose.core.serialize.Serializable; import java.util.List; /** Used to represent a boolean */ -public class BooleanConstant extends Operation { +public class BooleanConstant extends Operation implements Serializable { + private static final String CLASS_NAME = "BooleanConstant"; + private static final int OP_CODE = Operations.DATA_BOOLEAN; private boolean mValue = false; private int mId; @@ -124,4 +128,9 @@ public class BooleanConstant extends Operation { .field(DocumentedOperation.INT, "id", "id of Int") .field(BYTE, "value", "8-bit 0 or 1"); } + + @Override + public void serialize(MapSerializer serializer) { + serializer.add("type", CLASS_NAME).add("id", mId).add("value", mValue); + } } diff --git a/core/java/com/android/internal/widget/remotecompose/core/types/IntegerConstant.java b/core/java/com/android/internal/widget/remotecompose/core/types/IntegerConstant.java index 5462d3e069ed..c734f813ede3 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/types/IntegerConstant.java +++ b/core/java/com/android/internal/widget/remotecompose/core/types/IntegerConstant.java @@ -25,13 +25,17 @@ import com.android.internal.widget.remotecompose.core.RemoteContext; import com.android.internal.widget.remotecompose.core.WireBuffer; import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder; import com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation; +import com.android.internal.widget.remotecompose.core.serialize.MapSerializer; +import com.android.internal.widget.remotecompose.core.serialize.Serializable; import java.util.List; /** Represents a single integer typically used for states or named for input into the system */ -public class IntegerConstant extends Operation { - private int mValue = 0; - private int mId; +public class IntegerConstant extends Operation implements Serializable { + private static final String CLASS_NAME = "IntegerConstant"; + + private final int mValue; + private final int mId; IntegerConstant(int id, int value) { mId = id; @@ -116,4 +120,9 @@ public class IntegerConstant extends Operation { .field(DocumentedOperation.INT, "id", "id of Int") .field(INT, "value", "32-bit int value"); } + + @Override + public void serialize(MapSerializer serializer) { + serializer.add("type", CLASS_NAME).add("id", mId).add("value", mValue); + } } diff --git a/core/java/com/android/internal/widget/remotecompose/core/types/LongConstant.java b/core/java/com/android/internal/widget/remotecompose/core/types/LongConstant.java index 1a3cdb1a96d7..50509f3636b3 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/types/LongConstant.java +++ b/core/java/com/android/internal/widget/remotecompose/core/types/LongConstant.java @@ -25,14 +25,18 @@ import com.android.internal.widget.remotecompose.core.RemoteContext; import com.android.internal.widget.remotecompose.core.WireBuffer; import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder; import com.android.internal.widget.remotecompose.core.documentation.DocumentedOperation; +import com.android.internal.widget.remotecompose.core.serialize.MapSerializer; +import com.android.internal.widget.remotecompose.core.serialize.Serializable; import java.util.List; /** Used to represent a long */ -public class LongConstant extends Operation { +public class LongConstant extends Operation implements Serializable { + private static final String CLASS_NAME = "LongConstant"; + private static final int OP_CODE = Operations.DATA_LONG; - private long mValue; - private int mId; + private final long mValue; + private final int mId; public LongConstant(int id, long value) { mId = id; @@ -107,4 +111,9 @@ public class LongConstant extends Operation { .field(DocumentedOperation.INT, "id", "id of Int") .field(LONG, "value", "The long Value"); } + + @Override + public void serialize(MapSerializer serializer) { + serializer.add("type", CLASS_NAME).add("id", mId).add("value", mValue); + } } diff --git a/core/java/com/android/internal/widget/remotecompose/player/RemoteComposePlayer.java b/core/java/com/android/internal/widget/remotecompose/player/RemoteComposePlayer.java index 77f4b6a83eef..1d1e579ebc2f 100644 --- a/core/java/com/android/internal/widget/remotecompose/player/RemoteComposePlayer.java +++ b/core/java/com/android/internal/widget/remotecompose/player/RemoteComposePlayer.java @@ -29,7 +29,6 @@ import android.hardware.SensorManager; import android.util.AttributeSet; import android.util.Log; import android.util.TypedValue; -import android.view.HapticFeedbackConstants; import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.HorizontalScrollView; @@ -596,29 +595,34 @@ public class RemoteComposePlayer extends FrameLayout implements RemoteContextAwa } } - private static int[] sHapticTable = { - HapticFeedbackConstants.NO_HAPTICS, - HapticFeedbackConstants.LONG_PRESS, - HapticFeedbackConstants.VIRTUAL_KEY, - HapticFeedbackConstants.KEYBOARD_TAP, - HapticFeedbackConstants.CLOCK_TICK, - HapticFeedbackConstants.CONTEXT_CLICK, - HapticFeedbackConstants.KEYBOARD_PRESS, - HapticFeedbackConstants.KEYBOARD_RELEASE, - HapticFeedbackConstants.VIRTUAL_KEY_RELEASE, - HapticFeedbackConstants.TEXT_HANDLE_MOVE, - HapticFeedbackConstants.GESTURE_START, - HapticFeedbackConstants.GESTURE_END, - HapticFeedbackConstants.CONFIRM, - HapticFeedbackConstants.REJECT, - HapticFeedbackConstants.TOGGLE_ON, - HapticFeedbackConstants.TOGGLE_OFF, - HapticFeedbackConstants.GESTURE_THRESHOLD_ACTIVATE, - HapticFeedbackConstants.GESTURE_THRESHOLD_DEACTIVATE, - HapticFeedbackConstants.DRAG_START, - HapticFeedbackConstants.SEGMENT_TICK, - HapticFeedbackConstants.SEGMENT_FREQUENT_TICK, - }; + private static final int[] sHapticTable; + + static { + sHapticTable = + new int[] { + android.view.HapticFeedbackConstants.NO_HAPTICS, + android.view.HapticFeedbackConstants.LONG_PRESS, + android.view.HapticFeedbackConstants.VIRTUAL_KEY, + android.view.HapticFeedbackConstants.KEYBOARD_TAP, + android.view.HapticFeedbackConstants.CLOCK_TICK, + android.view.HapticFeedbackConstants.CONTEXT_CLICK, + android.view.HapticFeedbackConstants.KEYBOARD_PRESS, + android.view.HapticFeedbackConstants.KEYBOARD_RELEASE, + android.view.HapticFeedbackConstants.VIRTUAL_KEY_RELEASE, + android.view.HapticFeedbackConstants.TEXT_HANDLE_MOVE, + android.view.HapticFeedbackConstants.GESTURE_START, + android.view.HapticFeedbackConstants.GESTURE_END, + android.view.HapticFeedbackConstants.CONFIRM, + android.view.HapticFeedbackConstants.REJECT, + android.view.HapticFeedbackConstants.TOGGLE_ON, + android.view.HapticFeedbackConstants.TOGGLE_OFF, + android.view.HapticFeedbackConstants.GESTURE_THRESHOLD_ACTIVATE, + android.view.HapticFeedbackConstants.GESTURE_THRESHOLD_DEACTIVATE, + android.view.HapticFeedbackConstants.DRAG_START, + android.view.HapticFeedbackConstants.SEGMENT_TICK, + android.view.HapticFeedbackConstants.SEGMENT_FREQUENT_TICK, + }; + } private void provideHapticFeedback(int type) { performHapticFeedback(sHapticTable[type % sHapticTable.length]); diff --git a/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidPaintContext.java b/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidPaintContext.java index 6b1a30a7545c..ac4a294b5e5e 100644 --- a/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidPaintContext.java +++ b/core/java/com/android/internal/widget/remotecompose/player/platform/AndroidPaintContext.java @@ -246,6 +246,12 @@ public class AndroidPaintContext extends PaintContext { } @Override + public void replacePaint(PaintBundle paintBundle) { + mPaint.reset(); + applyPaint(paintBundle); + } + + @Override public void drawRoundRect( float left, float top, float right, float bottom, float radiusX, float radiusY) { mCanvas.drawRoundRect(left, top, right, bottom, radiusX, radiusY, mPaint); diff --git a/core/java/com/android/internal/widget/remotecompose/player/platform/RemoteComposeCanvas.java b/core/java/com/android/internal/widget/remotecompose/player/platform/RemoteComposeCanvas.java index f76794fc0372..4d2dd05ca603 100644 --- a/core/java/com/android/internal/widget/remotecompose/player/platform/RemoteComposeCanvas.java +++ b/core/java/com/android/internal/widget/remotecompose/player/platform/RemoteComposeCanvas.java @@ -31,6 +31,7 @@ import android.widget.FrameLayout; import com.android.internal.widget.remotecompose.core.CoreDocument; import com.android.internal.widget.remotecompose.core.RemoteContext; +import com.android.internal.widget.remotecompose.core.operations.Header; import com.android.internal.widget.remotecompose.core.operations.RootContentBehavior; import com.android.internal.widget.remotecompose.core.operations.Theme; import com.android.internal.widget.remotecompose.player.RemoteComposeDocument; @@ -105,10 +106,16 @@ public class RemoteComposeCanvas extends FrameLayout implements View.OnAttachSta mARContext.setDensity(mDensity); mARContext.setUseChoreographer(true); setContentDescription(mDocument.getDocument().getContentDescription()); + updateClickAreas(); requestLayout(); mARContext.loadFloat(RemoteContext.ID_TOUCH_EVENT_TIME, -Float.MAX_VALUE); invalidate(); + Integer fps = (Integer) mDocument.getDocument().getProperty(Header.DOC_DESIRED_FPS); + if (fps != null && fps > 0) { + mMaxFrameRate = fps; + mMaxFrameDelay = (long) (1000 / mMaxFrameRate); + } } @Override 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_AudioRecord.cpp b/core/jni/android_media_AudioRecord.cpp index 102edc944c22..22298b3f9525 100644 --- a/core/jni/android_media_AudioRecord.cpp +++ b/core/jni/android_media_AudioRecord.cpp @@ -598,7 +598,7 @@ static jintArray android_media_AudioRecord_getRoutedDeviceIds(JNIEnv *env, jobje } jint *values = env->GetIntArrayElements(result, 0); for (unsigned int i = 0; i < deviceIds.size(); i++) { - values[i++] = static_cast<jint>(deviceIds[i]); + values[i] = static_cast<jint>(deviceIds[i]); } env->ReleaseIntArrayElements(result, values, 0); return result; diff --git a/core/jni/android_media_AudioTrack.cpp b/core/jni/android_media_AudioTrack.cpp index 3fc1a02f46b6..3cf5d5fdde24 100644 --- a/core/jni/android_media_AudioTrack.cpp +++ b/core/jni/android_media_AudioTrack.cpp @@ -1204,7 +1204,7 @@ static jintArray android_media_AudioTrack_getRoutedDeviceIds(JNIEnv *env, jobjec } jint *values = env->GetIntArrayElements(result, 0); for (unsigned int i = 0; i < deviceIds.size(); i++) { - values[i++] = static_cast<jint>(deviceIds[i]); + values[i] = static_cast<jint>(deviceIds[i]); } env->ReleaseIntArrayElements(result, values, 0); return result; diff --git a/core/proto/android/nfc/OWNERS b/core/proto/android/nfc/OWNERS index ca16721eacc1..36823aee4dbb 100644 --- a/core/proto/android/nfc/OWNERS +++ b/core/proto/android/nfc/OWNERS @@ -1 +1 @@ -include platform/packages/apps/Nfc:/OWNERS
\ No newline at end of file +include platform/packages/modules/Nfc:/OWNERS
\ No newline at end of file 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/layout/notification_2025_template_header.xml b/core/res/res/layout/notification_2025_template_header.xml index 72b3798e0780..5aae67802af9 100644 --- a/core/res/res/layout/notification_2025_template_header.xml +++ b/core/res/res/layout/notification_2025_template_header.xml @@ -59,7 +59,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentStart="true" - android:layout_toStartOf="@id/expand_button" + android:layout_toStartOf="@id/expand_button_container" android:layout_alignWithParentIfMissing="true" android:layout_marginVertical="@dimen/notification_2025_margin" android:clipChildren="false" @@ -81,12 +81,30 @@ android:focusable="false" /> - <include layout="@layout/notification_2025_expand_button" + + <LinearLayout + android:id="@+id/expand_button_container" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_gravity="top|end" - android:layout_alignParentEnd="true" /> + android:layout_alignParentEnd="true" + android:orientation="vertical" + > + <FrameLayout + android:id="@+id/expand_button_spacer" + android:layout_width="@dimen/notification_2025_expand_button_pill_width" + android:layout_height="@dimen/notification_2025_expand_button_pill_height" + android:layout_centerVertical="true" + android:layout_alignParentEnd="true" + android:layout_margin="@dimen/notification_2025_margin" + android:visibility="gone" /> + + <include layout="@layout/notification_2025_expand_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="top|end" + android:layout_alignParentEnd="true" /> + </LinearLayout> <include layout="@layout/notification_close_button" android:id="@+id/close_button" android:layout_width="@dimen/notification_close_button_size" diff --git a/core/res/res/layout/notification_template_header.xml b/core/res/res/layout/notification_template_header.xml index 57959361bd48..b7fd737a4e39 100644 --- a/core/res/res/layout/notification_template_header.xml +++ b/core/res/res/layout/notification_template_header.xml @@ -62,7 +62,7 @@ android:layout_height="match_parent" android:layout_alignParentStart="true" android:layout_centerVertical="true" - android:layout_toStartOf="@id/expand_button" + android:layout_toStartOf="@id/expand_button_container" android:layout_alignWithParentIfMissing="true" android:clipChildren="false" android:gravity="center_vertical" @@ -83,12 +83,28 @@ android:focusable="false" /> - <include layout="@layout/notification_expand_button" + <LinearLayout + android:id="@+id/expand_button_container" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_centerVertical="true" - android:layout_alignParentEnd="true" /> + android:layout_alignParentEnd="true" + android:orientation="vertical" + > + <FrameLayout + android:id="@+id/expand_button_spacer" + android:layout_width="@dimen/notification_expand_button_pill_height" + android:layout_height="@dimen/notification_header_height" + android:layout_centerVertical="true" + android:layout_alignParentEnd="true" + android:layout_marginHorizontal="16dp" + android:visibility="gone" /> + <include layout="@layout/notification_expand_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_centerVertical="true" + android:layout_alignParentEnd="true" /> + </LinearLayout> <include layout="@layout/notification_close_button" android:id="@+id/close_button" android:layout_width="@dimen/notification_close_button_size" diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index a1f85c380a95..66111785af4f 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -3313,7 +3313,6 @@ <!-- Provides brief supplemental information for the view, such as the purpose of the view when that purpose is not conveyed within its textual representation. This property is used primarily for accessibility. --> - <!-- @FlaggedApi("android.view.accessibility.supplemental_description") --> <attr name="supplementalDescription" format="string" localization="suggested" /> <!-- Sets the id of a view that screen readers are requested to visit after this view. diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 17acf9aed278..1a311d572e0b 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -60,6 +60,7 @@ <item><xliff:g id="id">@string/status_bar_oem_satellite</xliff:g></item> <item><xliff:g id="id">@string/status_bar_wifi</xliff:g></item> <item><xliff:g id="id">@string/status_bar_hotspot</xliff:g></item> + <item><xliff:g id="id">@string/status_bar_stacked_mobile</xliff:g></item> <item><xliff:g id="id">@string/status_bar_mobile</xliff:g></item> <item><xliff:g id="id">@string/status_bar_airplane</xliff:g></item> <item><xliff:g id="id">@string/status_bar_battery</xliff:g></item> @@ -94,6 +95,7 @@ <string translatable="false" name="status_bar_secure">secure</string> <string translatable="false" name="status_bar_clock">clock</string> <string translatable="false" name="status_bar_mobile">mobile</string> + <string translatable="false" name="status_bar_stacked_mobile">stacked_mobile</string> <string translatable="false" name="status_bar_vpn">vpn</string> <string translatable="false" name="status_bar_ethernet">ethernet</string> <string translatable="false" name="status_bar_microphone">microphone</string> diff --git a/core/res/res/values/public-final.xml b/core/res/res/values/public-final.xml index af1e5123096d..61e2a28562d2 100644 --- a/core/res/res/values/public-final.xml +++ b/core/res/res/values/public-final.xml @@ -3941,7 +3941,7 @@ <!-- @FlaggedApi("android.permission.flags.replace_body_sensor_permission_enabled") @hide @SystemApi --> <public name="backgroundPermission"/> - <!-- @FlaggedApi(android.view.accessibility.supplemental_description) --> + <!-- @FlaggedApi("android.view.accessibility.supplemental_description") --> <public name="supplementalDescription"/> <!-- @FlaggedApi("android.security.enable_intent_matching_flags") --> <public name="intentMatchingFlags"/> @@ -3969,7 +3969,7 @@ <!-- @FlaggedApi("android.permission.flags.replace_body_sensor_permission_enabled") @hide @SystemApi --> <public type="attr" name="backgroundPermission" id="0x010106a7" /> - <!-- @FlaggedApi(android.view.accessibility.supplemental_description) --> + <!-- @FlaggedApi("android.view.accessibility.supplemental_description") --> <public type="attr" name="supplementalDescription" id="0x010106a8" /> <!-- @FlaggedApi("android.security.enable_intent_matching_flags") --> <public type="attr" name="intentMatchingFlags" id="0x010106a9" /> 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 da6fb3b58f21..c62732d36038 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -3235,6 +3235,8 @@ <java-symbol type="id" name="header_text" /> <java-symbol type="id" name="header_text_secondary" /> <java-symbol type="id" name="expand_button" /> + <java-symbol type="id" name="expand_button_spacer" /> + <java-symbol type="id" name="expand_button_container" /> <java-symbol type="id" name="expand_button_pill" /> <java-symbol type="id" name="expand_button_pill_colorized_layer" /> <java-symbol type="id" name="expand_button_number" /> @@ -3318,6 +3320,7 @@ <java-symbol type="string" name="status_bar_no_calling" /> <java-symbol type="string" name="status_bar_call_strength" /> <java-symbol type="string" name="status_bar_mobile" /> + <java-symbol type="string" name="status_bar_stacked_mobile" /> <java-symbol type="string" name="status_bar_ethernet" /> <java-symbol type="string" name="status_bar_vpn" /> <java-symbol type="string" name="status_bar_microphone" /> 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/core/tests/coretests/src/com/android/internal/util/RateLimitingCacheTest.java b/core/tests/coretests/src/com/android/internal/util/RateLimitingCacheTest.java index 2edab6268bec..82d90fc71cb3 100644 --- a/core/tests/coretests/src/com/android/internal/util/RateLimitingCacheTest.java +++ b/core/tests/coretests/src/com/android/internal/util/RateLimitingCacheTest.java @@ -21,8 +21,7 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import android.os.SystemClock; - -import android.os.SystemClock; +import android.platform.test.annotations.Presubmit; import androidx.test.runner.AndroidJUnit4; @@ -36,6 +35,7 @@ import org.junit.runner.RunWith; /** * Test the RateLimitingCache class. */ +@Presubmit @RunWith(AndroidJUnit4.class) public class RateLimitingCacheTest { @@ -56,13 +56,13 @@ public class RateLimitingCacheTest { */ @Test public void testTtl_Zero() { - RateLimitingCache<Integer> s = new RateLimitingCache<>(0); + TestRateLimitingCache<Integer> s = new TestRateLimitingCache<>(0); int first = s.get(mFetcher); assertEquals(first, 0); int second = s.get(mFetcher); assertEquals(second, 1); - SystemClock.sleep(20); + s.advanceTime(20); int third = s.get(mFetcher); assertEquals(third, 2); } @@ -73,14 +73,14 @@ public class RateLimitingCacheTest { */ @Test public void testTtl_100() { - RateLimitingCache<Integer> s = new RateLimitingCache<>(100); + TestRateLimitingCache<Integer> s = new TestRateLimitingCache<>(100); int first = s.get(mFetcher); assertEquals(first, 0); int second = s.get(mFetcher); // Too early to change assertEquals(second, 0); - SystemClock.sleep(150); + s.advanceTime(150); int third = s.get(mFetcher); // Changed by now assertEquals(third, 1); @@ -95,11 +95,11 @@ public class RateLimitingCacheTest { */ @Test public void testTtl_Negative() { - RateLimitingCache<Integer> s = new RateLimitingCache<>(-1); + TestRateLimitingCache<Integer> s = new TestRateLimitingCache<>(-1); int first = s.get(mFetcher); assertEquals(first, 0); - SystemClock.sleep(200); + s.advanceTime(200); // Should return the original value every time int second = s.get(mFetcher); assertEquals(second, 0); @@ -111,7 +111,7 @@ public class RateLimitingCacheTest { */ @Test public void testTtl_Spam() { - RateLimitingCache<Integer> s = new RateLimitingCache<>(100); + TestRateLimitingCache<Integer> s = new TestRateLimitingCache<>(100); assertCount(s, 1000, 7, 15); } @@ -121,28 +121,13 @@ public class RateLimitingCacheTest { */ @Test public void testRate_10hz() { - RateLimitingCache<Integer> s = new RateLimitingCache<>(1000, 10); + TestRateLimitingCache<Integer> s = new TestRateLimitingCache<>(1000, 10); // At 10 per second, 2 seconds should not exceed about 30, assuming overlap into left and // right windows that allow 10 each assertCount(s, 2000, 20, 33); } /** - * Test that using a different timebase works correctly. - */ - @Test - public void testTimebase() { - RateLimitingCache<Integer> s = new RateLimitingCache<>(1000, 10) { - @Override - protected long getTime() { - return SystemClock.elapsedRealtime() / 2; - } - }; - // Timebase is moving at half the speed, so only allows for 1 second worth in 2 seconds. - assertCount(s, 2000, 10, 22); - } - - /** * Exercises concurrent access to the cache. */ @Test @@ -287,15 +272,37 @@ public class RateLimitingCacheTest { * @param minCount the lower end of the expected number of fetches, with a margin for error * @param maxCount the higher end of the expected number of fetches, with a margin for error */ - private void assertCount(RateLimitingCache<Integer> cache, long period, + private void assertCount(TestRateLimitingCache<Integer> cache, long period, int minCount, int maxCount) { - long startTime = SystemClock.elapsedRealtime(); - while (SystemClock.elapsedRealtime() < startTime + period) { + long startTime = cache.getTime(); + while (cache.getTime() < startTime + period) { int value = cache.get(mFetcher); - SystemClock.sleep(5); + cache.advanceTime(5); } int latest = cache.get(mFetcher); assertTrue("Latest should be between " + minCount + " and " + maxCount + " but is " + latest, latest <= maxCount && latest >= minCount); } + + private static class TestRateLimitingCache<Value> extends RateLimitingCache<Value> { + // Start at a non-zero time to avoid confusion with uninitialized state. + private long mTime = 1; + + public TestRateLimitingCache(long periodMillis) { + super(periodMillis); + } + + public TestRateLimitingCache(long periodMillis, int count) { + super(periodMillis, count); + } + + public void advanceTime(long time) { + mTime += time; + } + + @Override + public long getTime() { + return mTime; + } + } } diff --git a/data/etc/platform.xml b/data/etc/platform.xml index baaec86828eb..ca20aebf95d8 100644 --- a/data/etc/platform.xml +++ b/data/etc/platform.xml @@ -210,6 +210,7 @@ <assign-permission name="android.permission.STATSCOMPANION" uid="statsd" /> <assign-permission name="android.permission.UPDATE_APP_OPS_STATS" uid="statsd" /> + <assign-permission name="android.permission.REGISTER_STATS_PULL_ATOM" uid="mmd" /> <assign-permission name="android.permission.REGISTER_STATS_PULL_ATOM" uid="gpu_service" /> <assign-permission name="android.permission.REGISTER_STATS_PULL_ATOM" uid="keystore" /> diff --git a/data/fonts/script/test/test_commandline.py b/data/fonts/script/test/test_commandline.py index 75318cc10e68..dbcba417c5e1 100755 --- a/data/fonts/script/test/test_commandline.py +++ b/data/fonts/script/test/test_commandline.py @@ -75,20 +75,20 @@ class CommandlineTest(unittest.TestCase): functools.partial(CommandlineTest.fileread, filemap), ) - self.assertEquals("output.xml", args.outfile) + self.assertEqual("output.xml", args.outfile) - self.assertEquals(1, len(args.aliases)) - self.assertEquals("sans-serif-thin", args.aliases[0].name) - self.assertEquals("sans-serif", args.aliases[0].to) - self.assertEquals(100, args.aliases[0].weight) + self.assertEqual(1, len(args.aliases)) + self.assertEqual("sans-serif-thin", args.aliases[0].name) + self.assertEqual("sans-serif", args.aliases[0].to) + self.assertEqual(100, args.aliases[0].weight) - self.assertEquals(2, len(args.fallback)) + self.assertEqual(2, len(args.fallback)) # Order is not a part of expectation. Check the expected lang is included. langs = set(["und-Arab", "und-Ethi"]) self.assertTrue(args.fallback[0].lang in langs) self.assertTrue(args.fallback[1].lang in langs) - self.assertEquals(3, len(args.families)) + self.assertEqual(3, len(args.families)) # Order is not a part of expectation. Check the expected name is included. names = set(["sans-serif", "sans-serif-condensed", "roboto-flex"]) self.assertTrue(args.families[0].name in names) diff --git a/data/fonts/script/test/test_xml_builder.py b/data/fonts/script/test/test_xml_builder.py index 24a033b43cbc..f15c51379b46 100755 --- a/data/fonts/script/test/test_xml_builder.py +++ b/data/fonts/script/test/test_xml_builder.py @@ -328,16 +328,16 @@ class XmlBuilderTest(unittest.TestCase): self.expect_xml(xml) def expect_xml(self, xml): - self.assertEquals("sans-serif", xml.families[0].name) # _SANS_SERIF - self.assertEquals("serif", xml.families[1].name) # _SERIF - self.assertEquals("und-Arab", xml.families[2].lang) # __ARABIC - self.assertEquals("elegant", xml.families[2].variant) - self.assertEquals("und-Arab", xml.families[3].lang) # _ARABIC_UI - self.assertEquals("zh-Hans", xml.families[4].lang) # _HANS (_HANS_SERIF) - self.assertEquals(2, len(xml.families[4].fonts)) - self.assertEquals("serif", xml.families[4].fonts[1].fallback_for) - self.assertEquals("ja", xml.families[5].lang) # _HANS (_HANS_SERIF) - self.assertEquals("serif", xml.families[5].fonts[1].fallback_for) + self.assertEqual("sans-serif", xml.families[0].name) # _SANS_SERIF + self.assertEqual("serif", xml.families[1].name) # _SERIF + self.assertEqual("und-Arab", xml.families[2].lang) # __ARABIC + self.assertEqual("elegant", xml.families[2].variant) + self.assertEqual("und-Arab", xml.families[3].lang) # _ARABIC_UI + self.assertEqual("zh-Hans", xml.families[4].lang) # _HANS (_HANS_SERIF) + self.assertEqual(2, len(xml.families[4].fonts)) + self.assertEqual("serif", xml.families[4].fonts[1].fallback_for) + self.assertEqual("ja", xml.families[5].lang) # _HANS (_HANS_SERIF) + self.assertEqual("serif", xml.families[5].fonts[1].fallback_for) if __name__ == "__main__": 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/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt new file mode 100644 index 000000000000..9ebc3d78b3a7 --- /dev/null +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt @@ -0,0 +1,491 @@ +/* + * 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.bubbles + +import android.app.Notification +import android.app.PendingIntent +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.ShortcutInfo +import android.graphics.drawable.Icon +import android.os.UserHandle +import android.service.notification.NotificationListenerService.Ranking +import android.service.notification.StatusBarNotification +import android.view.View +import android.widget.FrameLayout +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import com.android.internal.protolog.ProtoLog +import com.android.wm.shell.R +import com.android.wm.shell.bubbles.Bubbles.BubbleMetadataFlagListener +import com.android.wm.shell.common.TestShellExecutor +import com.android.wm.shell.taskview.TaskView +import com.android.wm.shell.taskview.TaskViewController +import com.android.wm.shell.taskview.TaskViewTaskController +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.never +import org.mockito.Mockito.reset +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +/** + * Tests for [BubbleTaskViewListener]. + */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class BubbleTaskViewListenerTest { + + private val context = ApplicationProvider.getApplicationContext<Context>() + + private var taskViewController = mock<TaskViewController>() + private var listenerCallback = mock<BubbleTaskViewListener.Callback>() + private var expandedViewManager = mock<BubbleExpandedViewManager>() + + private lateinit var bubbleTaskViewListener: BubbleTaskViewListener + private lateinit var taskView: TaskView + private lateinit var bubbleTaskView: BubbleTaskView + private lateinit var parentView: ViewPoster + private lateinit var mainExecutor: TestShellExecutor + private lateinit var bgExecutor: TestShellExecutor + + @Before + fun setUp() { + ProtoLog.REQUIRE_PROTOLOGTOOL = false + ProtoLog.init() + + parentView = ViewPoster(context) + mainExecutor = TestShellExecutor() + bgExecutor = TestShellExecutor() + + taskView = TaskView(context, taskViewController, mock<TaskViewTaskController>()) + bubbleTaskView = BubbleTaskView(taskView, mainExecutor) + + bubbleTaskViewListener = + BubbleTaskViewListener( + context, + bubbleTaskView, + parentView, + expandedViewManager, + listenerCallback + ) + } + + @Test + fun createBubbleTaskViewListener_withCreatedTaskView() { + // Make the bubbleTaskView look like it's been created + val taskId = 123 + bubbleTaskView.listener.onTaskCreated(taskId, mock<ComponentName>()) + reset(listenerCallback) + + bubbleTaskViewListener = + BubbleTaskViewListener( + context, + bubbleTaskView, + parentView, + expandedViewManager, + listenerCallback + ) + + assertThat(bubbleTaskView.delegateListener).isEqualTo(bubbleTaskViewListener) + assertThat(bubbleTaskViewListener.taskView).isEqualTo(bubbleTaskView.taskView) + + verify(listenerCallback).onTaskCreated() + assertThat(bubbleTaskViewListener.taskId).isEqualTo(taskId) + } + + @Test + fun createBubbleTaskViewListener() { + bubbleTaskViewListener = + BubbleTaskViewListener( + context, + bubbleTaskView, + parentView, + expandedViewManager, + listenerCallback + ) + + assertThat(bubbleTaskView.delegateListener).isEqualTo(bubbleTaskViewListener) + assertThat(bubbleTaskViewListener.taskView).isEqualTo(bubbleTaskView.taskView) + verify(listenerCallback, never()).onTaskCreated() + } + + @Test + fun onInitialized_pendingIntentChatBubble() { + val target = Intent(context, TestActivity::class.java) + val pendingIntent = PendingIntent.getActivity(context, 0, target, + PendingIntent.FLAG_MUTABLE) + + val b = createChatBubble("key", pendingIntent) + bubbleTaskViewListener.setBubble(b) + + assertThat(b.isChat).isTrue() + // Has shortcut info + assertThat(b.shortcutInfo).isNotNull() + // But it didn't use that on bubble metadata + assertThat(b.metadataShortcutId).isNull() + + getInstrumentation().runOnMainSync { + bubbleTaskViewListener.onInitialized() + } + getInstrumentation().waitForIdleSync() + + // ..so it's pending intent-based, and launches that + assertThat(b.isPendingIntentActive).isTrue() + verify(taskViewController).startActivity(any(), eq(pendingIntent), any(), any(), any()) + } + + @Test + fun onInitialized_shortcutChatBubble() { + val shortcutInfo = ShortcutInfo.Builder(context) + .setId("mockShortcutId") + .build() + val b = createChatBubble("key", shortcutInfo) + bubbleTaskViewListener.setBubble(b) + + assertThat(b.isChat).isTrue() + assertThat(b.shortcutInfo).isNotNull() + // Chat bubble using a shortcut + assertThat(b.metadataShortcutId).isNotNull() + + getInstrumentation().runOnMainSync { + bubbleTaskViewListener.onInitialized() + } + getInstrumentation().waitForIdleSync() + + assertThat(b.isPendingIntentActive).isFalse() + verify(taskViewController).startShortcutActivity(any(), eq(shortcutInfo), any(), any()) + } + + @Test + fun onInitialized_appBubble() { + val b = createAppBubble() + bubbleTaskViewListener.setBubble(b) + + assertThat(b.isApp).isTrue() + + getInstrumentation().runOnMainSync { + bubbleTaskViewListener.onInitialized() + } + getInstrumentation().waitForIdleSync() + + assertThat(b.isPendingIntentActive).isFalse() + verify(taskViewController).startActivity(any(), any(), anyOrNull(), any(), any()) + } + + @Test + fun onInitialized_preparingTransition() { + val b = createAppBubble() + bubbleTaskViewListener.setBubble(b) + taskView = Mockito.spy(taskView) + val preparingTransition = mock<BubbleTransitions.BubbleTransition>() + b.preparingTransition = preparingTransition + + getInstrumentation().runOnMainSync { + bubbleTaskViewListener.onInitialized() + } + getInstrumentation().waitForIdleSync() + + verify(preparingTransition).surfaceCreated() + } + + @Test + fun onInitialized_destroyed() { + val b = createAppBubble() + bubbleTaskViewListener.setBubble(b) + + assertThat(b.isApp).isTrue() + + getInstrumentation().runOnMainSync { + bubbleTaskViewListener.onReleased() + bubbleTaskViewListener.onInitialized() + } + getInstrumentation().waitForIdleSync() + + verify(taskViewController, never()).startActivity(any(), any(), anyOrNull(), any(), any()) + } + + @Test + fun onInitialized_initialized() { + val b = createAppBubble() + bubbleTaskViewListener.setBubble(b) + + assertThat(b.isApp).isTrue() + + getInstrumentation().runOnMainSync { + bubbleTaskViewListener.onInitialized() + } + getInstrumentation().waitForIdleSync() + + reset(taskViewController) + + getInstrumentation().runOnMainSync { + bubbleTaskViewListener.onInitialized() + } + // Already initialized, so no activity should be started. + verify(taskViewController, never()).startActivity(any(), any(), anyOrNull(), any(), any()) + } + + @Test + fun onTaskCreated() { + val b = createAppBubble() + bubbleTaskViewListener.setBubble(b) + + getInstrumentation().runOnMainSync { + bubbleTaskViewListener.onInitialized() + } + getInstrumentation().waitForIdleSync() + verify(taskViewController).startActivity(any(), any(), anyOrNull(), any(), any()) + + val taskId = 123 + getInstrumentation().runOnMainSync { + bubbleTaskViewListener.onTaskCreated(taskId, mock<ComponentName>()) + } + getInstrumentation().waitForIdleSync() + + verify(listenerCallback).onTaskCreated() + verify(expandedViewManager, never()).setNoteBubbleTaskId(any(), any()) + assertThat(bubbleTaskViewListener.taskId).isEqualTo(taskId) + } + + @Test + fun onTaskCreated_noteBubble() { + val b = createNoteBubble() + bubbleTaskViewListener.setBubble(b) + assertThat(b.isNote).isTrue() + + getInstrumentation().runOnMainSync { + bubbleTaskViewListener.onInitialized() + } + getInstrumentation().waitForIdleSync() + verify(taskViewController).startActivity(any(), any(), anyOrNull(), any(), any()) + + val taskId = 123 + getInstrumentation().runOnMainSync { + bubbleTaskViewListener.onTaskCreated(taskId, mock<ComponentName>()) + } + getInstrumentation().waitForIdleSync() + + verify(listenerCallback).onTaskCreated() + verify(expandedViewManager).setNoteBubbleTaskId(eq(b.key), eq(taskId)) + assertThat(bubbleTaskViewListener.taskId).isEqualTo(taskId) + } + + @Test + fun onTaskVisibilityChanged_true() { + getInstrumentation().runOnMainSync { + bubbleTaskViewListener.onTaskVisibilityChanged(1, true) + } + verify(listenerCallback).onContentVisibilityChanged(eq(true)) + } + + @Test + fun onTaskVisibilityChanged_false() { + getInstrumentation().runOnMainSync { + bubbleTaskViewListener.onTaskVisibilityChanged(1, false) + } + verify(listenerCallback).onContentVisibilityChanged(eq(false)) + } + + @Test + fun onTaskRemovalStarted() { + val mockTaskView = mock<TaskView>() + bubbleTaskView = BubbleTaskView(mockTaskView, mainExecutor) + + bubbleTaskViewListener = + BubbleTaskViewListener( + context, + bubbleTaskView, + parentView, + expandedViewManager, + listenerCallback + ) + + val b = createAppBubble() + bubbleTaskViewListener.setBubble(b) + + getInstrumentation().runOnMainSync { + bubbleTaskViewListener.onInitialized() + } + getInstrumentation().waitForIdleSync() + verify(mockTaskView).startActivity(any(), anyOrNull(), any(), any()) + + getInstrumentation().runOnMainSync { + bubbleTaskViewListener.onTaskRemovalStarted(1) + } + + verify(expandedViewManager).removeBubble(eq(b.key), eq(Bubbles.DISMISS_TASK_FINISHED)) + verify(mockTaskView).release() + assertThat(parentView.lastRemovedView).isEqualTo(mockTaskView) + assertThat(bubbleTaskViewListener.taskView).isNull() + verify(listenerCallback).onTaskRemovalStarted() + } + + @Test + fun onBackPressedOnTaskRoot_expanded() { + val taskId = 123 + whenever(expandedViewManager.isStackExpanded()).doReturn(true) + + getInstrumentation().runOnMainSync { + bubbleTaskViewListener.onTaskCreated(taskId, mock<ComponentName>()) + bubbleTaskViewListener.onBackPressedOnTaskRoot(taskId) + } + verify(listenerCallback).onBackPressed() + } + + @Test + fun onBackPressedOnTaskRoot_notExpanded() { + val taskId = 123 + whenever(expandedViewManager.isStackExpanded()).doReturn(false) + + getInstrumentation().runOnMainSync { + bubbleTaskViewListener.onTaskCreated(taskId, mock<ComponentName>()) + bubbleTaskViewListener.onBackPressedOnTaskRoot(taskId) + } + verify(listenerCallback, never()).onBackPressed() + } + + @Test + fun onBackPressedOnTaskRoot_taskIdMissMatch() { + val taskId = 123 + whenever(expandedViewManager.isStackExpanded()).doReturn(true) + + getInstrumentation().runOnMainSync { + bubbleTaskViewListener.onTaskCreated(taskId, mock<ComponentName>()) + bubbleTaskViewListener.onBackPressedOnTaskRoot(42) + } + verify(listenerCallback, never()).onBackPressed() + } + + @Test + fun setBubble_isNew() { + val b = createAppBubble() + val isNew = bubbleTaskViewListener.setBubble(b) + assertThat(isNew).isTrue() + } + + @Test + fun setBubble_launchContentChanged() { + val target = Intent(context, TestActivity::class.java) + val pendingIntent = PendingIntent.getActivity( + context, 0, target, + PendingIntent.FLAG_MUTABLE + ) + + val b = createChatBubble("key", pendingIntent) + var isNew = bubbleTaskViewListener.setBubble(b) + // First time bubble is set, so it is "new" + assertThat(isNew).isTrue() + + val b2 = createChatBubble("key", pendingIntent) + isNew = bubbleTaskViewListener.setBubble(b2) + // Second time bubble is set & it uses same type of launch content, not "new" + assertThat(isNew).isFalse() + + val shortcutInfo = ShortcutInfo.Builder(context) + .setId("mockShortcutId") + .build() + val b3 = createChatBubble("key", shortcutInfo) + // bubble is using different content, so it is "new" + isNew = bubbleTaskViewListener.setBubble(b3) + assertThat(isNew).isTrue() + } + + private fun createAppBubble(): Bubble { + val target = Intent(context, TestActivity::class.java) + target.setPackage(context.packageName) + return Bubble.createAppBubble(target, mock<UserHandle>(), mock<Icon>(), + mainExecutor, bgExecutor) + } + + private fun createNoteBubble(): Bubble { + val target = Intent(context, TestActivity::class.java) + target.setPackage(context.packageName) + return Bubble.createNotesBubble(target, mock<UserHandle>(), mock<Icon>(), + mainExecutor, bgExecutor) + } + + private fun createChatBubble(key: String, shortcutInfo: ShortcutInfo): Bubble { + return Bubble( + key, + shortcutInfo, + 0 /* desiredHeight */, + 0 /* desiredHeightResId */, + "title", + -1 /*taskId */, + null /* locusId */, true /* isdismissabel */, + mainExecutor, bgExecutor, mock<BubbleMetadataFlagListener>() + ) + } + + private fun createChatBubble(key: String, pendingIntent: PendingIntent): Bubble { + val metadata = Notification.BubbleMetadata.Builder( + pendingIntent, + Icon.createWithResource(context, R.drawable.bubble_ic_create_bubble) + ).build() + val shortcutInfo = ShortcutInfo.Builder(context) + .setId("shortcutId") + .build() + val notification: Notification = + Notification.Builder(context, key) + .setSmallIcon(mock<Icon>()) + .setWhen(System.currentTimeMillis()) + .setContentTitle("title") + .setContentText("content") + .setBubbleMetadata(metadata) + .build() + val sbn = mock<StatusBarNotification>() + val ranking = mock<Ranking>() + whenever(sbn.getNotification()).thenReturn(notification) + whenever(sbn.getKey()).thenReturn(key) + whenever(ranking.getConversationShortcutInfo()).thenReturn(shortcutInfo) + val entry = BubbleEntry(sbn, ranking, true, false, false, false) + return Bubble( + entry, mock<BubbleMetadataFlagListener>(), null, mainExecutor, + bgExecutor + ) + } + + /** + * FrameLayout that immediately runs any runnables posted to it and tracks view removals. + */ + class ViewPoster(context: Context) : FrameLayout(context) { + + lateinit var lastRemovedView: View + + override fun post(r: Runnable): Boolean { + r.run() + return true + } + + override fun removeView(v: View) { + super.removeView(v) + lastRemovedView = v + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index 96e008e42f1c..e395341a5792 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -447,6 +447,17 @@ 80 dp for handle + 20 dp for room to grow on the sides when hovered. --> <dimen name="desktop_mode_fullscreen_decor_caption_width">100dp</dimen> + <!-- Horizontal padding for desktop mode caption in default unhovered untouched state. --> + <dimen name="desktop_mode_fullscreen_decor_caption_horizontal_padding_default">10dp</dimen> + + <!-- Horizontal padding for desktop mode caption when hovered. + 1/2 * (100 dp of total width - 80 dp for handle * 1.2 scaling factor). --> + <dimen name="desktop_mode_fullscreen_decor_caption_horizontal_padding_hovered">2dp</dimen> + + <!-- Horizontal padding for desktop mode caption when touched. + 1/2 * (100 dp of total width - 80 dp for handle * 0.85 scaling factor). --> + <dimen name="desktop_mode_fullscreen_decor_caption_horizontal_padding_touched">16dp</dimen> + <!-- Required empty space to be visible for partially offscreen tasks. --> <dimen name="freeform_required_visible_empty_space_in_header">48dp</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/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..00901a4d980d 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 @@ -226,6 +226,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 +240,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); } /** @@ -323,25 +323,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/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/BubbleTaskViewHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java deleted file mode 100644 index e47ac61a53dd..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java +++ /dev/null @@ -1,292 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.wm.shell.bubbles; - -import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS; -import static android.app.ActivityTaskManager.INVALID_TASK_ID; -import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; -import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT; - -import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES; - -import android.app.ActivityOptions; -import android.app.ActivityTaskManager; -import android.app.PendingIntent; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.graphics.Rect; -import android.util.Log; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.Nullable; - -import com.android.internal.protolog.ProtoLog; -import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper; -import com.android.wm.shell.taskview.TaskView; - -/** - * Handles creating and updating the {@link TaskView} associated with a {@link Bubble}. - */ -public class BubbleTaskViewHelper { - - private static final String TAG = BubbleTaskViewHelper.class.getSimpleName(); - - /** - * Listener for users of {@link BubbleTaskViewHelper} to use to be notified of events - * on the task. - */ - public interface Listener { - - /** Called when the task is first created. */ - void onTaskCreated(); - - /** Called when the visibility of the task changes. */ - void onContentVisibilityChanged(boolean visible); - - /** Called when back is pressed on the task root. */ - void onBackPressed(); - - /** Called when task removal has started. */ - void onTaskRemovalStarted(); - } - - private final Context mContext; - private final BubbleExpandedViewManager mExpandedViewManager; - private final BubbleTaskViewHelper.Listener mListener; - private final View mParentView; - - @Nullable - private Bubble mBubble; - @Nullable - private PendingIntent mPendingIntent; - @Nullable - private TaskView mTaskView; - private int mTaskId = INVALID_TASK_ID; - - private final TaskView.Listener mTaskViewListener = new TaskView.Listener() { - private boolean mInitialized = false; - private boolean mDestroyed = false; - - @Override - public void onInitialized() { - ProtoLog.d(WM_SHELL_BUBBLES, "onInitialized: destroyed=%b initialized=%b bubble=%s", - mDestroyed, mInitialized, getBubbleKey()); - - if (mDestroyed || mInitialized) { - return; - } - - // Custom options so there is no activity transition animation - ActivityOptions options = ActivityOptions.makeCustomAnimation(mContext, - 0 /* enterResId */, 0 /* exitResId */); - - Rect launchBounds = new Rect(); - mTaskView.getBoundsOnScreen(launchBounds); - - // TODO: I notice inconsistencies in lifecycle - // Post to keep the lifecycle normal - // TODO - currently based on type, really it's what the "launch item" is. - mParentView.post(() -> { - ProtoLog.d(WM_SHELL_BUBBLES, "onInitialized: calling startActivity, bubble=%s", - getBubbleKey()); - try { - options.setTaskAlwaysOnTop(true); - options.setPendingIntentBackgroundActivityStartMode( - MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS); - final boolean isShortcutBubble = (mBubble.hasMetadataShortcutId() - || (mBubble.isShortcut() - && BubbleAnythingFlagHelper.enableCreateAnyBubble())); - if (mBubble.getPreparingTransition() != null) { - mBubble.getPreparingTransition().surfaceCreated(); - } else if (mBubble.isApp() || mBubble.isNote()) { - Context context = - mContext.createContextAsUser( - mBubble.getUser(), Context.CONTEXT_RESTRICTED); - Intent fillInIntent = null; - //first try get pending intent from the bubble - PendingIntent pi = mBubble.getPendingIntent(); - if (pi == null) { - // if null - create new one - pi = PendingIntent.getActivity( - context, - /* requestCode= */ 0, - mBubble.getIntent() - .addFlags(FLAG_ACTIVITY_MULTIPLE_TASK), - PendingIntent.FLAG_IMMUTABLE - | PendingIntent.FLAG_UPDATE_CURRENT, - /* options= */ null); - } else { - fillInIntent = new Intent(pi.getIntent()); - fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); - } - mTaskView.startActivity(pi, fillInIntent, options, launchBounds); - } else if (isShortcutBubble) { - options.setLaunchedFromBubble(true); - options.setApplyActivityFlagsForBubbles(true); - mTaskView.startShortcutActivity(mBubble.getShortcutInfo(), - options, launchBounds); - } else { - options.setLaunchedFromBubble(true); - if (mBubble != null) { - mBubble.setPendingIntentActive(); - } - final Intent fillInIntent = new Intent(); - // Apply flags to make behaviour match documentLaunchMode=always. - fillInIntent.addFlags(FLAG_ACTIVITY_NEW_DOCUMENT); - fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); - mTaskView.startActivity(mPendingIntent, fillInIntent, options, - launchBounds); - } - } catch (RuntimeException e) { - // If there's a runtime exception here then there's something - // wrong with the intent, we can't really recover / try to populate - // the bubble again so we'll just remove it. - Log.w(TAG, "Exception while displaying bubble: " + getBubbleKey() - + ", " + e.getMessage() + "; removing bubble"); - mExpandedViewManager.removeBubble( - getBubbleKey(), Bubbles.DISMISS_INVALID_INTENT); - } - mInitialized = true; - }); - } - - @Override - public void onReleased() { - mDestroyed = true; - } - - @Override - public void onTaskCreated(int taskId, ComponentName name) { - ProtoLog.d(WM_SHELL_BUBBLES, "onTaskCreated: taskId=%d bubble=%s", - taskId, getBubbleKey()); - // The taskId is saved to use for removeTask, preventing appearance in recent tasks. - mTaskId = taskId; - - if (mBubble != null && mBubble.isNote()) { - // Let the controller know sooner what the taskId is. - mExpandedViewManager.setNoteBubbleTaskId(mBubble.getKey(), mTaskId); - } - - // With the task org, the taskAppeared callback will only happen once the task has - // already drawn - mListener.onTaskCreated(); - } - - @Override - public void onTaskVisibilityChanged(int taskId, boolean visible) { - mListener.onContentVisibilityChanged(visible); - } - - @Override - public void onTaskRemovalStarted(int taskId) { - ProtoLog.d(WM_SHELL_BUBBLES, "onTaskRemovalStarted: taskId=%d bubble=%s", - taskId, getBubbleKey()); - if (mBubble != null) { - mExpandedViewManager.removeBubble(mBubble.getKey(), Bubbles.DISMISS_TASK_FINISHED); - } - if (mTaskView != null) { - mTaskView.release(); - ((ViewGroup) mParentView).removeView(mTaskView); - mTaskView = null; - } - mListener.onTaskRemovalStarted(); - } - - @Override - public void onBackPressedOnTaskRoot(int taskId) { - if (mTaskId == taskId && mExpandedViewManager.isStackExpanded()) { - mListener.onBackPressed(); - } - } - }; - - public BubbleTaskViewHelper(Context context, - BubbleExpandedViewManager expandedViewManager, - BubbleTaskViewHelper.Listener listener, - BubbleTaskView bubbleTaskView, - View parent) { - mContext = context; - mExpandedViewManager = expandedViewManager; - mListener = listener; - mParentView = parent; - mTaskView = bubbleTaskView.getTaskView(); - bubbleTaskView.setDelegateListener(mTaskViewListener); - if (bubbleTaskView.isCreated()) { - mTaskId = bubbleTaskView.getTaskId(); - mListener.onTaskCreated(); - } - } - - /** - * Sets the bubble or updates the bubble used to populate the view. - * - * @return true if the bubble is new, false if it was an update to the same bubble. - */ - public boolean update(Bubble bubble) { - boolean isNew = mBubble == null || didBackingContentChange(bubble); - mBubble = bubble; - if (isNew) { - mPendingIntent = mBubble.getPendingIntent(); - return true; - } - return false; - } - - /** Returns the bubble key associated with this view. */ - @Nullable - public String getBubbleKey() { - return mBubble != null ? mBubble.getKey() : null; - } - - /** Returns the TaskView associated with this view. */ - @Nullable - public TaskView getTaskView() { - return mTaskView; - } - - /** - * Returns the task id associated with the task in this view. If the task doesn't exist then - * {@link ActivityTaskManager#INVALID_TASK_ID}. - */ - public int getTaskId() { - return mTaskId; - } - - /** Returns whether the bubble set on the helper is valid to populate the task view. */ - public boolean isValidBubble() { - return mBubble != null && (mPendingIntent != null || mBubble.hasMetadataShortcutId()); - } - - // TODO (b/274980695): Is this still relevant? - /** - * Bubbles are backed by a pending intent or a shortcut, once the activity is - * started we never change it / restart it on notification updates -- unless the bubble's - * backing data switches. - * - * This indicates if the new bubble is backed by a different data source than what was - * previously shown here (e.g. previously a pending intent & now a shortcut). - * - * @param newBubble the bubble this view is being updated with. - * @return true if the backing content has changed. - */ - private boolean didBackingContentChange(Bubble newBubble) { - boolean prevWasIntentBased = mBubble != null && mPendingIntent != null; - boolean newIsIntentBased = newBubble.getPendingIntent() != null; - return prevWasIntentBased != newIsIntentBased; - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewListener.java new file mode 100644 index 000000000000..a38debb702dc --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewListener.java @@ -0,0 +1,279 @@ +/* + * 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.bubbles; + +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS; +import static android.app.ActivityTaskManager.INVALID_TASK_ID; +import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; +import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT; + +import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES; + +import android.app.ActivityOptions; +import android.app.ActivityTaskManager; +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.graphics.Rect; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.Nullable; + +import com.android.internal.protolog.ProtoLog; +import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper; +import com.android.wm.shell.taskview.TaskView; + +/** + * A listener that works with task views for bubbles, manages launching the appropriate + * content into the task view from the bubble and sends updates of task view events back to + * the parent view via {@link BubbleTaskViewListener.Callback}. + */ +public class BubbleTaskViewListener implements TaskView.Listener { + private static final String TAG = BubbleTaskViewListener.class.getSimpleName(); + + /** + * Callback to let the view parent of TaskView to be notified of different events. + */ + public interface Callback { + + /** Called when the task is first created. */ + void onTaskCreated(); + + /** Called when the visibility of the task changes. */ + void onContentVisibilityChanged(boolean visible); + + /** Called when back is pressed on the task root. */ + void onBackPressed(); + + /** Called when task removal has started. */ + void onTaskRemovalStarted(); + } + + private final Context mContext; + private final BubbleExpandedViewManager mExpandedViewManager; + private final BubbleTaskViewListener.Callback mCallback; + private final View mParentView; + + private Bubble mBubble; + @Nullable + private PendingIntent mPendingIntent; + private int mTaskId = INVALID_TASK_ID; + private TaskView mTaskView; + + private boolean mInitialized = false; + private boolean mDestroyed = false; + + public BubbleTaskViewListener(Context context, BubbleTaskView bubbleTaskView, View parentView, + BubbleExpandedViewManager manager, BubbleTaskViewListener.Callback callback) { + mContext = context; + mTaskView = bubbleTaskView.getTaskView(); + mParentView = parentView; + mExpandedViewManager = manager; + mCallback = callback; + bubbleTaskView.setDelegateListener(this); + if (bubbleTaskView.isCreated()) { + mTaskId = bubbleTaskView.getTaskId(); + callback.onTaskCreated(); + } + } + + @Override + public void onInitialized() { + ProtoLog.d(WM_SHELL_BUBBLES, "onInitialized: destroyed=%b initialized=%b bubble=%s", + mDestroyed, mInitialized, getBubbleKey()); + + if (mDestroyed || mInitialized) { + return; + } + + // Custom options so there is no activity transition animation + ActivityOptions options = ActivityOptions.makeCustomAnimation(mContext, + 0 /* enterResId */, 0 /* exitResId */); + + Rect launchBounds = new Rect(); + mTaskView.getBoundsOnScreen(launchBounds); + + // TODO: I notice inconsistencies in lifecycle + // Post to keep the lifecycle normal + // TODO - currently based on type, really it's what the "launch item" is. + mParentView.post(() -> { + ProtoLog.d(WM_SHELL_BUBBLES, "onInitialized: calling startActivity, bubble=%s", + getBubbleKey()); + try { + options.setTaskAlwaysOnTop(true); + options.setPendingIntentBackgroundActivityStartMode( + MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS); + final boolean isShortcutBubble = (mBubble.hasMetadataShortcutId() + || (mBubble.isShortcut() + && BubbleAnythingFlagHelper.enableCreateAnyBubble())); + if (mBubble.getPreparingTransition() != null) { + mBubble.getPreparingTransition().surfaceCreated(); + } else if (mBubble.isApp() || mBubble.isNote()) { + Context context = + mContext.createContextAsUser( + mBubble.getUser(), Context.CONTEXT_RESTRICTED); + Intent fillInIntent = null; + // First try get pending intent from the bubble + PendingIntent pi = mBubble.getPendingIntent(); + if (pi == null) { + // If null - create new one + pi = PendingIntent.getActivity( + context, + /* requestCode= */ 0, + mBubble.getIntent() + .addFlags(FLAG_ACTIVITY_MULTIPLE_TASK), + PendingIntent.FLAG_IMMUTABLE + | PendingIntent.FLAG_UPDATE_CURRENT, + /* options= */ null); + } else { + fillInIntent = new Intent(pi.getIntent()); + fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); + } + mTaskView.startActivity(pi, fillInIntent, options, launchBounds); + } else if (isShortcutBubble) { + options.setLaunchedFromBubble(true); + options.setApplyActivityFlagsForBubbles(true); + mTaskView.startShortcutActivity(mBubble.getShortcutInfo(), + options, launchBounds); + } else { + options.setLaunchedFromBubble(true); + if (mBubble != null) { + mBubble.setPendingIntentActive(); + } + final Intent fillInIntent = new Intent(); + // Apply flags to make behaviour match documentLaunchMode=always. + fillInIntent.addFlags(FLAG_ACTIVITY_NEW_DOCUMENT); + fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); + mTaskView.startActivity(mPendingIntent, fillInIntent, options, + launchBounds); + } + } catch (RuntimeException e) { + // If there's a runtime exception here then there's something + // wrong with the intent, we can't really recover / try to populate + // the bubble again so we'll just remove it. + Log.w(TAG, "Exception while displaying bubble: " + getBubbleKey() + + ", " + e.getMessage() + "; removing bubble"); + mExpandedViewManager.removeBubble( + getBubbleKey(), Bubbles.DISMISS_INVALID_INTENT); + } + mInitialized = true; + }); + } + + @Override + public void onReleased() { + mDestroyed = true; + } + + @Override + public void onTaskCreated(int taskId, ComponentName name) { + ProtoLog.d(WM_SHELL_BUBBLES, "onTaskCreated: taskId=%d bubble=%s", + taskId, getBubbleKey()); + // The taskId is saved to use for removeTask, preventing appearance in recent tasks. + mTaskId = taskId; + + if (mBubble != null && mBubble.isNote()) { + // Let the controller know sooner what the taskId is. + mExpandedViewManager.setNoteBubbleTaskId(mBubble.getKey(), mTaskId); + } + + // With the task org, the taskAppeared callback will only happen once the task has + // already drawn + mCallback.onTaskCreated(); + } + + @Override + public void onTaskVisibilityChanged(int taskId, boolean visible) { + mCallback.onContentVisibilityChanged(visible); + } + + @Override + public void onTaskRemovalStarted(int taskId) { + ProtoLog.d(WM_SHELL_BUBBLES, "onTaskRemovalStarted: taskId=%d bubble=%s", + taskId, getBubbleKey()); + if (mBubble != null) { + mExpandedViewManager.removeBubble(mBubble.getKey(), Bubbles.DISMISS_TASK_FINISHED); + } + if (mTaskView != null) { + mTaskView.release(); + ((ViewGroup) mParentView).removeView(mTaskView); + mTaskView = null; + } + mCallback.onTaskRemovalStarted(); + } + + @Override + public void onBackPressedOnTaskRoot(int taskId) { + if (mTaskId == taskId && mExpandedViewManager.isStackExpanded()) { + mCallback.onBackPressed(); + } + } + + /** + * Sets the bubble or updates the bubble used to populate the view. + * + * @return true if the bubble is new or if the launch content of the bubble changed from the + * previous bubble. + */ + public boolean setBubble(Bubble bubble) { + boolean isNew = mBubble == null || didBackingContentChange(bubble); + mBubble = bubble; + if (isNew) { + mPendingIntent = mBubble.getPendingIntent(); + } + return isNew; + } + + /** Returns the TaskView associated with this view. */ + @Nullable + public TaskView getTaskView() { + return mTaskView; + } + + /** + * Returns the task id associated with the task in this view. If the task doesn't exist then + * {@link ActivityTaskManager#INVALID_TASK_ID}. + */ + public int getTaskId() { + return mTaskId; + } + + private String getBubbleKey() { + return mBubble != null ? mBubble.getKey() : ""; + } + + // TODO (b/274980695): Is this still relevant? + /** + * Bubbles are backed by a pending intent or a shortcut, once the activity is + * started we never change it / restart it on notification updates -- unless the bubble's + * backing data switches. + * + * This indicates if the new bubble is backed by a different data source than what was + * previously shown here (e.g. previously a pending intent & now a shortcut). + * + * @param newBubble the bubble this view is being updated with. + * @return true if the backing content has changed. + */ + private boolean didBackingContentChange(Bubble newBubble) { + boolean prevWasIntentBased = mBubble != null && mPendingIntent != null; + boolean newIsIntentBased = newBubble.getPendingIntent() != null; + return prevWasIntentBased != newIsIntentBased; + } +} 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 28227a1f8746..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; + } } /** @@ -189,14 +200,16 @@ public class BubbleTransitions { * * 1. Start inflating the bubble view * 2. Once inflated (but not-yet visible), tell WM to do the shell-transition. - * 3. Transition becomes ready, so notify Launcher - * 4. Launcher responds with showExpandedView which calls continueExpand() to make view visible - * 5. Surface is created which kicks off actual animation + * 3. When the transition becomes ready, notify Launcher in parallel + * 4. Wait for surface to be created + * 5. Once surface is ready, animate the task to a bubble * - * So, constructor -> onInflated -> startAnimation -> continueExpand -> surfaceCreated. + * While the animation is pending, we keep a reference to the pending transition in the bubble. + * This allows us to check in other parts of the code that this bubble will be shown via the + * transition animation. * - * continueExpand and surfaceCreated are set-up to happen in either order, though, to support - * UX/timing adjustments. + * startAnimation, continueExpand and surfaceCreated are set-up to happen in either order, + * to support UX/timing adjustments. */ @VisibleForTesting class ConvertToBubble implements Transitions.TransitionHandler, BubbleTransition { @@ -209,9 +222,9 @@ public class BubbleTransitions { final Rect mStartBounds = new Rect(); SurfaceControl mSnapshot = null; TaskInfo mTaskInfo; - boolean mFinishedExpand = false; BubbleViewProvider mPriorBubble = null; + private final TransitionProgress mTransitionProgress = new TransitionProgress(); private SurfaceControl.Transaction mFinishT; private SurfaceControl mTaskLeash; @@ -359,12 +372,12 @@ public class BubbleTransitions { startTransaction.apply(); mTaskViewTransitions.onExternalDone(transition); + mTransitionProgress.setTransitionReady(); + startExpandAnim(); return true; } - @Override - public void continueExpand() { - mFinishedExpand = true; + private void startExpandAnim() { final boolean animate = mLayerView.canExpandView(mBubble); if (animate) { mPriorBubble = mLayerView.prepareConvertedView(mBubble); @@ -375,19 +388,25 @@ public class BubbleTransitions { mLayerView.removeView(priorView); mPriorBubble = null; } - if (!animate || mBubble.getTaskView().getSurfaceControl() != null) { + if (!animate || mTransitionProgress.isReadyToAnimate()) { playAnimation(animate); } } @Override + public void continueExpand() { + mTransitionProgress.setReadyToExpand(); + } + + @Override public void surfaceCreated() { + mTransitionProgress.setSurfaceReady(); mMainExecutor.execute(() -> { final TaskViewTaskController tvc = mBubble.getTaskView().getController(); final TaskViewRepository.TaskViewState state = mRepository.byTaskView(tvc); if (state == null) return; state.mVisible = true; - if (mFinishedExpand) { + if (mTransitionProgress.isReadyToAnimate()) { playAnimation(true /* animate */); } }); @@ -403,9 +422,6 @@ public class BubbleTransitions { mFinishWct = null; } - // Preparation is complete. - mBubble.setPreparingTransition(null); - if (animate) { mLayerView.animateConvert(startT, mStartBounds, mSnapshot, mTaskLeash, () -> { mFinishCb.onTransitionFinished(mFinishWct); @@ -417,6 +433,42 @@ public class BubbleTransitions { mFinishCb = null; } } + + /** + * Keeps track of internal state of different steps of this BubbleTransition. + */ + private class TransitionProgress { + private boolean mTransitionReady; + private boolean mReadyToExpand; + private boolean mSurfaceReady; + + void setTransitionReady() { + mTransitionReady = true; + onUpdate(); + } + + void setReadyToExpand() { + mReadyToExpand = true; + onUpdate(); + } + + void setSurfaceReady() { + mSurfaceReady = true; + onUpdate(); + } + + boolean isReadyToAnimate() { + // Animation only depends on transition and surface state + return mTransitionReady && mSurfaceReady; + } + + private void onUpdate() { + if (mTransitionReady && mReadyToExpand && mSurfaceReady) { + // Clear the transition from bubble when all the steps are ready + mBubble.setPreparingTransition(null); + } + } + } } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java index 6798a88a6da7..d93dbc3c15d9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java @@ -43,9 +43,10 @@ import com.android.wm.shell.bubbles.BubbleLogger; import com.android.wm.shell.bubbles.BubbleOverflowContainerView; import com.android.wm.shell.bubbles.BubblePositioner; import com.android.wm.shell.bubbles.BubbleTaskView; -import com.android.wm.shell.bubbles.BubbleTaskViewHelper; +import com.android.wm.shell.bubbles.BubbleTaskViewListener; import com.android.wm.shell.bubbles.Bubbles; import com.android.wm.shell.bubbles.RegionSamplingProvider; +import com.android.wm.shell.dagger.HasWMComponent; import com.android.wm.shell.shared.bubbles.BubbleBarLocation; import com.android.wm.shell.shared.handles.RegionSamplingHelper; import com.android.wm.shell.taskview.TaskView; @@ -56,7 +57,7 @@ import java.util.function.Supplier; import javax.inject.Inject; /** Expanded view of a bubble when it's part of the bubble bar. */ -public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskViewHelper.Listener { +public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskViewListener.Callback { /** * The expanded view listener notifying the {@link BubbleBarLayerView} about the internal * actions and events @@ -110,7 +111,7 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView private BubbleExpandedViewManager mManager; private BubblePositioner mPositioner; private boolean mIsOverflow; - private BubbleTaskViewHelper mBubbleTaskViewHelper; + private BubbleTaskViewListener mBubbleTaskViewListener; private BubbleBarMenuViewController mMenuViewController; @Nullable private Supplier<Rect> mLayerBoundsSupplier; @@ -205,6 +206,9 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView protected void onFinishInflate() { super.onFinishInflate(); Context context = getContext(); + if (context instanceof HasWMComponent) { + ((HasWMComponent) context).getWMComponent().inject(this); + } setElevation(getResources().getDimensionPixelSize(R.dimen.bubble_elevation)); mCaptionHeight = context.getResources().getDimensionPixelSize( R.dimen.bubble_bar_expanded_view_caption_height); @@ -246,9 +250,10 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView mHandleView.setVisibility(View.GONE); } else { mTaskView = bubbleTaskView.getTaskView(); - mBubbleTaskViewHelper = new BubbleTaskViewHelper(mContext, expandedViewManager, - /* listener= */ this, bubbleTaskView, - /* viewParent= */ this); + mBubbleTaskViewListener = new BubbleTaskViewListener(mContext, bubbleTaskView, + /* viewParent= */ this, + expandedViewManager, + /* callback= */ this); // if the task view is already attached to a parent we need to remove it if (mTaskView.getParent() != null) { @@ -535,13 +540,15 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView /** Updates the bubble shown in the expanded view. */ public void update(Bubble bubble) { mBubble = bubble; - mBubbleTaskViewHelper.update(bubble); + mBubbleTaskViewListener.setBubble(bubble); mMenuViewController.updateMenu(bubble); } /** The task id of the activity shown in the task view, if it exists. */ public int getTaskId() { - return mBubbleTaskViewHelper != null ? mBubbleTaskViewHelper.getTaskId() : INVALID_TASK_ID; + return mBubbleTaskViewListener != null + ? mBubbleTaskViewListener.getTaskId() + : INVALID_TASK_ID; } /** Sets layer bounds supplier used for obscured touchable region of task view */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipDesktopState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipDesktopState.java index c10c2c905c97..c6afc313b239 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipDesktopState.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipDesktopState.java @@ -81,8 +81,8 @@ public class PipDesktopState { return false; } - /** Returns whether PiP is exiting while we're in a Desktop Mode session. */ - private boolean isPipExitingToDesktopMode() { + /** Returns whether PiP is active in a display that is in active Desktop Mode session. */ + public boolean isPipInDesktopMode() { // Early return if PiP in Desktop Windowing is not supported. if (!isDesktopWindowingPipEnabled()) { return false; @@ -137,7 +137,7 @@ public class PipDesktopState { // 1) If the display windowing mode is freeform, set windowing mode to UNDEFINED so it will // resolve the windowing mode to the display's windowing mode. // 2) If the display windowing mode is not FREEFORM, set windowing mode to FREEFORM. - if (isPipExitingToDesktopMode()) { + if (isPipInDesktopMode()) { if (isDisplayInFreeform()) { return WINDOWING_MODE_UNDEFINED; } else { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java index aebd94fc173a..34d840eed3f0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java @@ -92,7 +92,7 @@ public class TvWMShellModule { MultiInstanceHelper multiInstanceHelper, SplitState splitState, @ShellMainThread ShellExecutor mainExecutor, - Handler mainHandler, + @ShellMainThread Handler mainHandler, SystemWindows systemWindows) { return new TvSplitScreenController(context, shellInit, shellCommandHandler, shellController, shellTaskOrganizer, syncQueue, rootTDAOrganizer, displayController, 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 84bb830af9a3..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 @@ -134,6 +134,7 @@ import com.android.wm.shell.recents.RecentsTransitionHandler; import com.android.wm.shell.shared.TransactionPool; import com.android.wm.shell.shared.annotations.ShellAnimationThread; import com.android.wm.shell.shared.annotations.ShellBackgroundThread; +import com.android.wm.shell.shared.annotations.ShellDesktopThread; import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.shared.desktopmode.DesktopModeCompatPolicy; import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; @@ -750,6 +751,7 @@ public abstract class WMShellModule { MultiInstanceHelper multiInstanceHelper, @ShellMainThread ShellExecutor mainExecutor, @ShellMainThread Handler mainHandler, + @ShellDesktopThread ShellExecutor desktopExecutor, Optional<DesktopTasksLimiter> desktopTasksLimiter, Optional<RecentTasksController> recentTasksController, InteractionJankMonitor interactionJankMonitor, @@ -757,7 +759,6 @@ public abstract class WMShellModule { FocusTransitionObserver focusTransitionObserver, DesktopModeEventLogger desktopModeEventLogger, DesktopModeUiEventLogger desktopModeUiEventLogger, - DesktopTilingDecorViewModel desktopTilingDecorViewModel, DesktopWallpaperActivityTokenProvider desktopWallpaperActivityTokenProvider, Optional<BubbleController> bubbleController, OverviewToDesktopTransitionObserver overviewToDesktopTransitionObserver, @@ -789,13 +790,13 @@ public abstract class WMShellModule { recentsTransitionHandler, multiInstanceHelper, mainExecutor, + desktopExecutor, desktopTasksLimiter, recentTasksController.orElse(null), interactionJankMonitor, mainHandler, desktopModeEventLogger, desktopModeUiEventLogger, - desktopTilingDecorViewModel, desktopWallpaperActivityTokenProvider, bubbleController, overviewToDesktopTransitionObserver, @@ -987,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(); @@ -1003,7 +1005,8 @@ public abstract class WMShellModule { desktopTasksLimiter, appHandleEducationController, appToWebEducationController, windowDecorCaptionHandleRepository, activityOrientationChangeHandler, focusTransitionObserver, desktopModeEventLogger, desktopModeUiEventLogger, - taskResourceLoader, recentsTransitionHandler, desktopModeCompatPolicy)); + taskResourceLoader, recentsTransitionHandler, desktopModeCompatPolicy, + desktopTilingDecorViewModel)); } @WMSingleton @@ -1275,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 e7c76bbd91b2..7d80ee5f3bb6 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 @@ -174,6 +174,7 @@ public abstract class Pip2Module { @NonNull PipScheduler pipScheduler, @NonNull SizeSpecSource sizeSpecSource, @NonNull PipDisplayLayoutState pipDisplayLayoutState, + PipDesktopState pipDesktopState, DisplayController displayController, PipMotionHelper pipMotionHelper, FloatingContentCoordinator floatingContentCoordinator, @@ -182,8 +183,8 @@ public abstract class Pip2Module { Optional<PipPerfHintController> pipPerfHintControllerOptional) { return new PipTouchHandler(context, shellInit, shellCommandHandler, menuPhoneController, pipBoundsAlgorithm, pipBoundsState, pipTransitionState, pipScheduler, - sizeSpecSource, pipDisplayLayoutState, displayController, pipMotionHelper, - floatingContentCoordinator, pipUiEventLogger, mainExecutor, + sizeSpecSource, pipDisplayLayoutState, pipDesktopState, displayController, + pipMotionHelper, floatingContentCoordinator, pipUiEventLogger, mainExecutor, pipPerfHintControllerOptional); } 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/DesktopModeVisualIndicator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java index 27aed17762ff..aecbf1a23cb2 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 @@ -18,9 +18,6 @@ package com.android.wm.shell.desktopmode; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; -import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; -import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL; -import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR; import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR; @@ -28,37 +25,27 @@ import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.Indica import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR; import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR; -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.RectEvaluator; -import android.animation.ValueAnimator; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; import android.content.Context; -import android.content.res.Resources; -import android.graphics.PixelFormat; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.Region; -import android.graphics.drawable.LayerDrawable; -import android.util.DisplayMetrics; import android.view.SurfaceControl; -import android.view.SurfaceControlViewHost; -import android.view.View; -import android.view.WindowManager; -import android.view.WindowlessWindowManager; -import android.view.animation.DecelerateInterpolator; +import android.window.DesktopModeFlags; import androidx.annotation.VisibleForTesting; import com.android.internal.policy.SystemBarUtils; -import com.android.window.flags.Flags; import com.android.wm.shell.R; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.shared.annotations.ShellDesktopThread; +import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper; import com.android.wm.shell.shared.bubbles.BubbleDropTargetBoundsProvider; import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; @@ -115,37 +102,54 @@ public class DesktopModeVisualIndicator { } } + private final VisualIndicatorViewContainer mVisualIndicatorViewContainer; + private final Context mContext; private final DisplayController mDisplayController; - private final RootTaskDisplayAreaOrganizer mRootTdaOrganizer; private final ActivityManager.RunningTaskInfo mTaskInfo; - private final SurfaceControl mTaskSurface; - private final @Nullable BubbleDropTargetBoundsProvider mBubbleBoundsProvider; - private SurfaceControl mLeash; - - private final SyncTransactionQueue mSyncQueue; - private SurfaceControlViewHost mViewHost; - private View mView; private IndicatorType mCurrentType; - private DragStartState mDragStartState; - private boolean mIsReleased; + private final DragStartState mDragStartState; - public DesktopModeVisualIndicator(SyncTransactionQueue syncQueue, + public DesktopModeVisualIndicator(@ShellDesktopThread ShellExecutor desktopExecutor, + @ShellMainThread ShellExecutor mainExecutor, + SyncTransactionQueue syncQueue, ActivityManager.RunningTaskInfo taskInfo, DisplayController displayController, Context context, SurfaceControl taskSurface, RootTaskDisplayAreaOrganizer taskDisplayAreaOrganizer, DragStartState dragStartState, @Nullable BubbleDropTargetBoundsProvider bubbleBoundsProvider) { - mSyncQueue = syncQueue; + SurfaceControl.Builder builder = new SurfaceControl.Builder(); + taskDisplayAreaOrganizer.attachToDisplayArea(taskInfo.displayId, builder); + mVisualIndicatorViewContainer = new VisualIndicatorViewContainer( + DesktopModeFlags.ENABLE_DESKTOP_INDICATOR_IN_SEPARATE_THREAD_BUGFIX.isTrue() + ? desktopExecutor : mainExecutor, + mainExecutor, builder, syncQueue, bubbleBoundsProvider); mTaskInfo = taskInfo; mDisplayController = displayController; mContext = context; - mTaskSurface = taskSurface; - mRootTdaOrganizer = taskDisplayAreaOrganizer; - mBubbleBoundsProvider = bubbleBoundsProvider; mCurrentType = NO_INDICATOR; mDragStartState = dragStartState; + mVisualIndicatorViewContainer.createView( + mContext, + mDisplayController.getDisplay(mTaskInfo.displayId), + mDisplayController.getDisplayLayout(mTaskInfo.displayId), + mTaskInfo, + taskSurface + ); + } + + /** Start the fade out animation, running the callback on the main thread once it is done. */ + public void fadeOutIndicator( + @NonNull Runnable callback) { + mVisualIndicatorViewContainer.fadeOutIndicator( + mDisplayController.getDisplayLayout(mTaskInfo.displayId), mCurrentType, callback + ); + } + + /** Release the visual indicator view and its viewhost. */ + public void releaseVisualIndicator() { + mVisualIndicatorViewContainer.releaseVisualIndicator(); } /** @@ -202,7 +206,10 @@ public class DesktopModeVisualIndicator { } } if (mDragStartState != DragStartState.DRAGGED_INTENT) { - transitionIndicator(result); + mVisualIndicatorViewContainer.transitionIndicator( + mTaskInfo, mDisplayController, mCurrentType, result + ); + mCurrentType = result; } return result; } @@ -283,338 +290,8 @@ public class DesktopModeVisualIndicator { layout.width(), layout.height()); } - /** - * Create a fullscreen indicator with no animation - */ - private void createView() { - if (mIsReleased) return; - final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); - final Resources resources = mContext.getResources(); - final DisplayMetrics metrics = resources.getDisplayMetrics(); - final int screenWidth; - final int screenHeight; - if (Flags.enableBugFixesForSecondaryDisplay()) { - final DisplayLayout displayLayout = - mDisplayController.getDisplayLayout(mTaskInfo.displayId); - screenWidth = displayLayout.width(); - screenHeight = displayLayout.height(); - } else { - screenWidth = metrics.widthPixels; - screenHeight = metrics.heightPixels; - } - mView = new View(mContext); - final SurfaceControl.Builder builder = new SurfaceControl.Builder(); - mRootTdaOrganizer.attachToDisplayArea(mTaskInfo.displayId, builder); - mLeash = builder - .setName("Desktop Mode Visual Indicator") - .setContainerLayer() - .setCallsite("DesktopModeVisualIndicator.createView") - .build(); - t.show(mLeash); - final WindowManager.LayoutParams lp = - new WindowManager.LayoutParams(screenWidth, screenHeight, TYPE_APPLICATION, - FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT); - lp.setTitle("Desktop Mode Visual Indicator"); - lp.setTrustedOverlay(); - lp.inputFeatures |= INPUT_FEATURE_NO_INPUT_CHANNEL; - final WindowlessWindowManager windowManager = new WindowlessWindowManager( - mTaskInfo.configuration, mLeash, - /* hostInputToken= */ null); - mViewHost = new SurfaceControlViewHost(mContext, - mDisplayController.getDisplay(mTaskInfo.displayId), windowManager, - "DesktopModeVisualIndicator"); - mViewHost.setView(mView, lp); - // We want this indicator to be behind the dragged task, but in front of all others. - t.setRelativeLayer(mLeash, mTaskSurface, -1); - - mSyncQueue.runInSync(transaction -> { - transaction.merge(t); - t.close(); - }); - } - @VisibleForTesting Rect getIndicatorBounds() { - return mView.getBackground().getBounds(); - } - - /** - * Fade indicator in as provided type. Animator fades it in while expanding the bounds outwards. - */ - private void fadeInIndicator(IndicatorType type) { - mView.setBackgroundResource(R.drawable.desktop_windowing_transition_background); - final VisualIndicatorAnimator animator = VisualIndicatorAnimator - .fadeBoundsIn(mView, type, - mDisplayController.getDisplayLayout(mTaskInfo.displayId), - mBubbleBoundsProvider); - animator.start(); - mCurrentType = type; - } - - /** - * Fade out indicator without fully releasing it. Animator fades it out while shrinking bounds. - * - * @param finishCallback called when animation ends or gets cancelled - */ - void fadeOutIndicator(@Nullable Runnable finishCallback) { - if (mCurrentType == NO_INDICATOR) { - // In rare cases, fade out can be requested before the indicator has determined its - // initial type and started animating in. In this case, no animator is needed. - finishCallback.run(); - return; - } - final VisualIndicatorAnimator animator = VisualIndicatorAnimator - .fadeBoundsOut(mView, mCurrentType, - mDisplayController.getDisplayLayout(mTaskInfo.displayId), - mBubbleBoundsProvider); - animator.start(); - if (finishCallback != null) { - animator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - finishCallback.run(); - } - }); - } - mCurrentType = NO_INDICATOR; - } - - /** - * Takes existing indicator and animates it to bounds reflecting a new indicator type. - */ - private void transitionIndicator(IndicatorType newType) { - if (mCurrentType == newType) return; - if (mView == null) { - createView(); - } - if (mCurrentType == NO_INDICATOR) { - fadeInIndicator(newType); - } else if (newType == NO_INDICATOR) { - fadeOutIndicator(/* finishCallback= */ null); - } else { - final VisualIndicatorAnimator animator = VisualIndicatorAnimator.animateIndicatorType( - mView, mDisplayController.getDisplayLayout(mTaskInfo.displayId), mCurrentType, - newType, mBubbleBoundsProvider); - mCurrentType = newType; - animator.start(); - } - } - - /** - * Release the indicator and its components when it is no longer needed. - */ - public void releaseVisualIndicator(SurfaceControl.Transaction t) { - mIsReleased = true; - if (mViewHost == null) return; - if (mViewHost != null) { - mViewHost.release(); - mViewHost = null; - } - - if (mLeash != null) { - t.remove(mLeash); - mLeash = null; - } - } - - /** - * Animator for Desktop Mode transitions which supports bounds and alpha animation. - */ - private static class VisualIndicatorAnimator extends ValueAnimator { - private static final int FULLSCREEN_INDICATOR_DURATION = 200; - private static final float FULLSCREEN_SCALE_ADJUSTMENT_PERCENT = 0.015f; - private static final float INDICATOR_FINAL_OPACITY = 0.35f; - private static final int MAXIMUM_OPACITY = 255; - - /** - * Determines how this animator will interact with the view's alpha: - * Fade in, fade out, or no change to alpha - */ - private enum AlphaAnimType { - ALPHA_FADE_IN_ANIM, ALPHA_FADE_OUT_ANIM, ALPHA_NO_CHANGE_ANIM - } - - private final View mView; - private final Rect mStartBounds; - private final Rect mEndBounds; - private final RectEvaluator mRectEvaluator; - - private VisualIndicatorAnimator(View view, Rect startBounds, - Rect endBounds) { - mView = view; - mStartBounds = new Rect(startBounds); - mEndBounds = endBounds; - setFloatValues(0, 1); - mRectEvaluator = new RectEvaluator(new Rect()); - } - - private static VisualIndicatorAnimator fadeBoundsIn( - @NonNull View view, IndicatorType type, @NonNull DisplayLayout displayLayout, - @Nullable BubbleDropTargetBoundsProvider bubbleBoundsProvider) { - final Rect endBounds = getIndicatorBounds(displayLayout, type, bubbleBoundsProvider); - final Rect startBounds = getMinBounds(endBounds); - view.getBackground().setBounds(startBounds); - - final VisualIndicatorAnimator animator = new VisualIndicatorAnimator( - view, startBounds, endBounds); - animator.setInterpolator(new DecelerateInterpolator()); - setupIndicatorAnimation(animator, AlphaAnimType.ALPHA_FADE_IN_ANIM); - return animator; - } - - private static VisualIndicatorAnimator fadeBoundsOut( - @NonNull View view, IndicatorType type, @NonNull DisplayLayout displayLayout, - @Nullable BubbleDropTargetBoundsProvider bubbleBoundsProvider) { - final Rect startBounds = getIndicatorBounds(displayLayout, type, bubbleBoundsProvider); - final Rect endBounds = getMinBounds(startBounds); - view.getBackground().setBounds(startBounds); - - final VisualIndicatorAnimator animator = new VisualIndicatorAnimator( - view, startBounds, endBounds); - animator.setInterpolator(new DecelerateInterpolator()); - setupIndicatorAnimation(animator, AlphaAnimType.ALPHA_FADE_OUT_ANIM); - return animator; - } - - /** - * Create animator for visual indicator changing type (i.e., fullscreen to freeform, - * freeform to split, etc.) - * - * @param view the view for this indicator - * @param displayLayout information about the display the transitioning task is - * currently on - * @param origType the original indicator type - * @param newType the new indicator type - * @param bubbleBoundsProvider provides bounds for bubbles indicators - */ - private static VisualIndicatorAnimator animateIndicatorType(@NonNull View view, - @NonNull DisplayLayout displayLayout, IndicatorType origType, IndicatorType newType, - @Nullable BubbleDropTargetBoundsProvider bubbleBoundsProvider) { - final Rect startBounds = getIndicatorBounds(displayLayout, origType, - bubbleBoundsProvider); - final Rect endBounds = getIndicatorBounds(displayLayout, newType, bubbleBoundsProvider); - final VisualIndicatorAnimator animator = new VisualIndicatorAnimator( - view, startBounds, endBounds); - animator.setInterpolator(new DecelerateInterpolator()); - setupIndicatorAnimation(animator, AlphaAnimType.ALPHA_NO_CHANGE_ANIM); - return animator; - } - - /** Calculates the bounds the indicator should have when fully faded in. */ - private static Rect getIndicatorBounds(DisplayLayout layout, IndicatorType type, - @Nullable BubbleDropTargetBoundsProvider bubbleBoundsProvider) { - final Rect desktopStableBounds = new Rect(); - layout.getStableBounds(desktopStableBounds); - final int padding = desktopStableBounds.top; - switch (type) { - case TO_FULLSCREEN_INDICATOR: - desktopStableBounds.top += padding; - desktopStableBounds.bottom -= padding; - desktopStableBounds.left += padding; - desktopStableBounds.right -= padding; - return desktopStableBounds; - case TO_DESKTOP_INDICATOR: - final float adjustmentPercentage = 1f - - DesktopTasksController.DESKTOP_MODE_INITIAL_BOUNDS_SCALE; - return new Rect((int) (adjustmentPercentage * desktopStableBounds.width() / 2), - (int) (adjustmentPercentage * desktopStableBounds.height() / 2), - (int) (desktopStableBounds.width() - - (adjustmentPercentage * desktopStableBounds.width() / 2)), - (int) (desktopStableBounds.height() - - (adjustmentPercentage * desktopStableBounds.height() / 2))); - case TO_SPLIT_LEFT_INDICATOR: - return new Rect(padding, padding, - desktopStableBounds.width() / 2 - padding, - desktopStableBounds.height()); - case TO_SPLIT_RIGHT_INDICATOR: - return new Rect(desktopStableBounds.width() / 2 + padding, padding, - desktopStableBounds.width() - padding, - desktopStableBounds.height()); - case TO_BUBBLE_LEFT_INDICATOR: - if (bubbleBoundsProvider == null) { - return new Rect(); - } - return bubbleBoundsProvider.getBubbleBarExpandedViewDropTargetBounds( - /* onLeft= */ true); - case TO_BUBBLE_RIGHT_INDICATOR: - if (bubbleBoundsProvider == null) { - return new Rect(); - } - return bubbleBoundsProvider.getBubbleBarExpandedViewDropTargetBounds( - /* onLeft= */ false); - default: - throw new IllegalArgumentException("Invalid indicator type provided."); - } - } - - /** - * Add necessary listener for animation of indicator - */ - private static void setupIndicatorAnimation(@NonNull VisualIndicatorAnimator animator, - AlphaAnimType animType) { - animator.addUpdateListener(a -> { - if (animator.mView != null) { - animator.updateBounds(a.getAnimatedFraction(), animator.mView); - if (animType == AlphaAnimType.ALPHA_FADE_IN_ANIM) { - animator.updateIndicatorAlpha(a.getAnimatedFraction(), animator.mView); - } else if (animType == AlphaAnimType.ALPHA_FADE_OUT_ANIM) { - animator.updateIndicatorAlpha(1 - a.getAnimatedFraction(), animator.mView); - } - } else { - animator.cancel(); - } - }); - animator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - animator.mView.getBackground().setBounds(animator.mEndBounds); - } - }); - animator.setDuration(FULLSCREEN_INDICATOR_DURATION); - } - - /** - * Update bounds of view based on current animation fraction. - * Use of delta is to animate bounds independently, in case we need to - * run multiple animations simultaneously. - * - * @param fraction fraction to use, compared against previous fraction - * @param view the view to update - */ - private void updateBounds(float fraction, View view) { - if (mStartBounds.equals(mEndBounds)) { - return; - } - final Rect currentBounds = mRectEvaluator.evaluate(fraction, mStartBounds, mEndBounds); - view.getBackground().setBounds(currentBounds); - } - - /** - * Fade in the fullscreen indicator - * - * @param fraction current animation fraction - */ - private void updateIndicatorAlpha(float fraction, View view) { - final LayerDrawable drawable = (LayerDrawable) view.getBackground(); - drawable.findDrawableByLayerId(R.id.indicator_stroke) - .setAlpha((int) (MAXIMUM_OPACITY * fraction)); - drawable.findDrawableByLayerId(R.id.indicator_solid) - .setAlpha((int) (MAXIMUM_OPACITY * fraction * INDICATOR_FINAL_OPACITY)); - } - - /** - * Return the minimum bounds of a visual indicator, to be used at the end of fading out - * and the start of fading in. - */ - private static Rect getMinBounds(Rect maxBounds) { - return new Rect((int) (maxBounds.left - + (FULLSCREEN_SCALE_ADJUSTMENT_PERCENT * maxBounds.width())), - (int) (maxBounds.top - + (FULLSCREEN_SCALE_ADJUSTMENT_PERCENT * maxBounds.height())), - (int) (maxBounds.right - - (FULLSCREEN_SCALE_ADJUSTMENT_PERCENT * maxBounds.width())), - (int) (maxBounds.bottom - - (FULLSCREEN_SCALE_ADJUSTMENT_PERCENT * maxBounds.height()))); - } + return mVisualIndicatorViewContainer.getIndicatorBounds(); } } 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 3b2598450800..522d83ec50eb 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 @@ -116,6 +116,7 @@ import com.android.wm.shell.recents.RecentsTransitionStateListener.RecentsTransi import com.android.wm.shell.recents.RecentsTransitionStateListener.TRANSITION_STATE_NOT_RUNNING import com.android.wm.shell.shared.TransitionUtil import com.android.wm.shell.shared.annotations.ExternalThread +import com.android.wm.shell.shared.annotations.ShellDesktopThread import com.android.wm.shell.shared.annotations.ShellMainThread import com.android.wm.shell.shared.desktopmode.DesktopModeCompatPolicy import com.android.wm.shell.shared.desktopmode.DesktopModeStatus @@ -135,7 +136,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 @@ -143,7 +143,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 @@ -176,13 +176,13 @@ class DesktopTasksController( private val recentsTransitionHandler: RecentsTransitionHandler, private val multiInstanceHelper: MultiInstanceHelper, @ShellMainThread private val mainExecutor: ShellExecutor, + @ShellDesktopThread private val desktopExecutor: ShellExecutor, private val desktopTasksLimiter: Optional<DesktopTasksLimiter>, private val recentTasksController: RecentTasksController?, private val interactionJankMonitor: InteractionJankMonitor, @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, @@ -202,26 +202,21 @@ class DesktopTasksController( private var userId: Int private val desktopModeShellCommandHandler: DesktopModeShellCommandHandler = DesktopModeShellCommandHandler(this) - private val mOnAnimationFinishedCallback = - Consumer<SurfaceControl.Transaction> { t: SurfaceControl.Transaction -> - visualIndicator?.releaseVisualIndicator(t) - visualIndicator = null - } + + private val mOnAnimationFinishedCallback = { releaseVisualIndicator() } + private lateinit var snapEventHandler: SnapEventHandler private val dragToDesktopStateListener = object : DragToDesktopStateListener { - override fun onCommitToDesktopAnimationStart(tx: SurfaceControl.Transaction) { - removeVisualIndicator(tx) + override fun onCommitToDesktopAnimationStart() { + removeVisualIndicator() } - override fun onCancelToDesktopAnimationEnd(tx: SurfaceControl.Transaction) { - removeVisualIndicator(tx) + override fun onCancelToDesktopAnimationEnd() { + removeVisualIndicator() } - private fun removeVisualIndicator(tx: SurfaceControl.Transaction) { - visualIndicator?.fadeOutIndicator { - visualIndicator?.releaseVisualIndicator(tx) - visualIndicator = null - } + private fun removeVisualIndicator() { + visualIndicator?.fadeOutIndicator { releaseVisualIndicator() } } } @@ -274,7 +269,7 @@ class DesktopTasksController( RecentsTransitionStateListener.stateToString(state), ) recentsTransitionState = state - desktopTilingDecorViewModel.onOverviewAnimationStateChange( + snapEventHandler.onOverviewAnimationStateChange( RecentsTransitionStateListener.isAnimating(state) ) } @@ -305,6 +300,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) { @@ -789,7 +789,7 @@ class DesktopTasksController( taskInfo: RunningTaskInfo, ): ((IBinder) -> Unit)? { val taskId = taskInfo.taskId - desktopTilingDecorViewModel.removeTaskIfTiled(displayId, taskId) + snapEventHandler.removeTaskIfTiled(displayId, taskId) performDesktopExitCleanupIfNeeded(taskId, displayId, wct, forceToFullscreen = false) taskRepository.addClosingTask(displayId, taskId) taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate( @@ -838,6 +838,7 @@ class DesktopTasksController( val taskId = taskInfo.taskId val displayId = taskInfo.displayId val wct = WindowContainerTransaction() + snapEventHandler.removeTaskIfTiled(displayId, taskId) performDesktopExitCleanupIfNeeded(taskId, displayId, wct, forceToFullscreen = false) // Notify immersive handler as it might need to exit immersive state. val exitResult = @@ -848,7 +849,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, @@ -863,7 +866,7 @@ class DesktopTasksController( /** 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) } } @@ -871,7 +874,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) } } @@ -990,7 +993,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() @@ -1232,7 +1235,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)) } @@ -1358,7 +1361,6 @@ class DesktopTasksController( position: SnapPosition, resizeTrigger: ResizeTrigger, inputMethod: InputMethod, - desktopWindowDecoration: DesktopModeWindowDecoration, ) { desktopModeEventLogger.logTaskResizingStarted( resizeTrigger, @@ -1380,13 +1382,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) } @@ -1423,7 +1419,6 @@ class DesktopTasksController( position: SnapPosition, resizeTrigger: ResizeTrigger, inputMethod: InputMethod, - desktopModeWindowDecoration: DesktopModeWindowDecoration, ) { if (!isSnapResizingAllowed(taskInfo)) { Toast.makeText( @@ -1442,7 +1437,6 @@ class DesktopTasksController( position, resizeTrigger, inputMethod, - desktopModeWindowDecoration, ) } @@ -1454,7 +1448,6 @@ class DesktopTasksController( currentDragBounds: Rect, dragStartBounds: Rect, motionEvent: MotionEvent, - desktopModeWindowDecoration: DesktopModeWindowDecoration, ) { releaseVisualIndicator() if (!isSnapResizingAllowed(taskInfo)) { @@ -1502,7 +1495,6 @@ class DesktopTasksController( position, resizeTrigger, DesktopModeEventLogger.getInputMethodFromMotionEvent(motionEvent), - desktopModeWindowDecoration, ) } } @@ -1773,13 +1765,8 @@ class DesktopTasksController( } fun releaseVisualIndicator() { - val t = SurfaceControl.Transaction() - visualIndicator?.releaseVisualIndicator(t) + visualIndicator?.releaseVisualIndicator() visualIndicator = null - syncQueue.runInSync { transaction -> - transaction.merge(t) - t.close() - } } override fun getContext(): Context = context @@ -1999,6 +1986,9 @@ class DesktopTasksController( splitPosition, options.toBundle(), /* hideTaskToken= */ null, + if (enableFlexibleSplit()) + splitScreenController.determineNewInstanceIndex(callingTask) + else SPLIT_INDEX_UNDEFINED, ) } } @@ -2175,7 +2165,7 @@ class DesktopTasksController( return wct } if (!wct.isEmpty) { - desktopTilingDecorViewModel.removeTaskIfTiled(task.displayId, task.taskId) + snapEventHandler.removeTaskIfTiled(task.displayId, task.taskId) return wct } return null @@ -2271,7 +2261,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( @@ -2736,7 +2726,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, @@ -2757,6 +2747,8 @@ class DesktopTasksController( val indicator = visualIndicator ?: DesktopModeVisualIndicator( + desktopExecutor, + mainExecutor, syncQueue, taskInfo, displayController, @@ -2794,7 +2786,6 @@ class DesktopTasksController( validDragArea: Rect, dragStartBounds: Rect, motionEvent: MotionEvent, - desktopModeWindowDecoration: DesktopModeWindowDecoration, ) { if (taskInfo.configuration.windowConfiguration.windowingMode != WINDOWING_MODE_FREEFORM) { return @@ -2833,7 +2824,6 @@ class DesktopTasksController( currentDragBounds, dragStartBounds, motionEvent, - desktopModeWindowDecoration, ) } IndicatorType.TO_SPLIT_RIGHT_INDICATOR -> { @@ -2848,7 +2838,6 @@ class DesktopTasksController( currentDragBounds, dragStartBounds, motionEvent, - desktopModeWindowDecoration, ) } IndicatorType.NO_INDICATOR, @@ -3134,7 +3123,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 1aa081340bf9..aaecf8c2d727 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 @@ -257,8 +257,8 @@ sealed class DragToDesktopTransitionHandler( // 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 +318,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( @@ -497,8 +501,8 @@ sealed class DragToDesktopTransitionHandler( 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 } @@ -642,7 +646,7 @@ sealed class DragToDesktopTransitionHandler( startPosition.y.toInt() + unscaledStartHeight, ) - dragToDesktopStateListener?.onCommitToDesktopAnimationStart(startTransaction) + dragToDesktopStateListener?.onCommitToDesktopAnimationStart() // Accept the merge by applying the merging transaction (applied by #showResizeVeil) // and finish callback. Show the veil and position the task at the first frame before // starting the final animation. @@ -773,7 +777,7 @@ sealed class DragToDesktopTransitionHandler( addListener( object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { - dragToDesktopStateListener?.onCancelToDesktopAnimationEnd(tx) + dragToDesktopStateListener?.onCancelToDesktopAnimationEnd() // Start the cancel transition to restore order. startCancelDragToDesktopTransition() } @@ -866,9 +870,9 @@ sealed class DragToDesktopTransitionHandler( ) interface DragToDesktopStateListener { - fun onCommitToDesktopAnimationStart(tx: SurfaceControl.Transaction) + fun onCommitToDesktopAnimationStart() - fun onCancelToDesktopAnimationEnd(tx: SurfaceControl.Transaction) + fun onCancelToDesktopAnimationEnd() } sealed class TransitionState { @@ -1081,7 +1085,7 @@ constructor( val startBoundsWithOffset = Rect(startBounds).apply { offset(startPosition.x.toInt(), startPosition.y.toInt()) } - dragToDesktopStateListener?.onCommitToDesktopAnimationStart(startTransaction) + dragToDesktopStateListener?.onCommitToDesktopAnimationStart() // Accept the merge by applying the merging transaction (applied by #showResizeVeil) // and finish callback. Show the veil and position the task at the first frame before // starting the final animation. 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 f7f87ed63003..5ae1fca73d4e 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 @@ -50,9 +50,11 @@ import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource; import com.android.wm.shell.transition.Transitions; +import kotlin.Unit; +import kotlin.jvm.functions.Function0; + import java.util.ArrayList; import java.util.List; -import java.util.function.Consumer; import java.util.function.Supplier; @@ -69,7 +71,7 @@ public class ExitDesktopTaskTransitionHandler implements Transitions.TransitionH @ShellMainThread private final Handler mHandler; private final List<IBinder> mPendingTransitionTokens = new ArrayList<>(); - private Consumer<SurfaceControl.Transaction> mOnAnimationFinishedCallback; + private Function0<Unit> mOnAnimationFinishedCallback; private final Supplier<SurfaceControl.Transaction> mTransactionSupplier; private Point mPosition; @@ -106,7 +108,7 @@ public class ExitDesktopTaskTransitionHandler implements Transitions.TransitionH */ public void startTransition(@NonNull DesktopModeTransitionSource transitionSource, @NonNull WindowContainerTransaction wct, Point position, - Consumer<SurfaceControl.Transaction> onAnimationEndCallback) { + Function0<Unit> onAnimationEndCallback) { mPosition = position; mOnAnimationFinishedCallback = onAnimationEndCallback; final IBinder token = mTransitions.startTransition(getExitTransitionType(transitionSource), @@ -192,7 +194,7 @@ public class ExitDesktopTaskTransitionHandler implements Transitions.TransitionH @Override public void onAnimationEnd(Animator animation) { if (mOnAnimationFinishedCallback != null) { - mOnAnimationFinishedCallback.accept(finishT); + mOnAnimationFinishedCallback.invoke(); } mInteractionJankMonitor.end(Cuj.CUJ_DESKTOP_MODE_EXIT_MODE); mTransitions.getMainExecutor().execute( diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/VisualIndicatorViewContainer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/VisualIndicatorViewContainer.kt new file mode 100644 index 000000000000..2317274dbbf0 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/VisualIndicatorViewContainer.kt @@ -0,0 +1,491 @@ +/* + * 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 + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.RectEvaluator +import android.animation.ValueAnimator +import android.app.ActivityManager +import android.content.Context +import android.graphics.PixelFormat +import android.graphics.Rect +import android.graphics.drawable.LayerDrawable +import android.view.Display +import android.view.SurfaceControl +import android.view.SurfaceControlViewHost +import android.view.View +import android.view.WindowManager +import android.view.WindowlessWindowManager +import android.view.animation.DecelerateInterpolator +import com.android.internal.annotations.VisibleForTesting +import com.android.window.flags.Flags +import com.android.wm.shell.R +import com.android.wm.shell.common.DisplayController +import com.android.wm.shell.common.DisplayLayout +import com.android.wm.shell.common.ShellExecutor +import com.android.wm.shell.common.SyncTransactionQueue +import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType +import com.android.wm.shell.shared.annotations.ShellDesktopThread +import com.android.wm.shell.shared.annotations.ShellMainThread +import com.android.wm.shell.shared.bubbles.BubbleDropTargetBoundsProvider +import com.android.wm.shell.windowdecor.WindowDecoration.SurfaceControlViewHostFactory + +/** + * Container for the view / viewhost of the indicator, ensuring it is created / animated off the + * main thread. + */ +@VisibleForTesting +class VisualIndicatorViewContainer +@JvmOverloads +constructor( + @ShellDesktopThread private val desktopExecutor: ShellExecutor, + @ShellMainThread private val mainExecutor: ShellExecutor, + private val indicatorBuilder: SurfaceControl.Builder, + private val syncQueue: SyncTransactionQueue, + private val surfaceControlViewHostFactory: SurfaceControlViewHostFactory = + object : SurfaceControlViewHostFactory {}, + private val bubbleBoundsProvider: BubbleDropTargetBoundsProvider?, +) { + @VisibleForTesting var indicatorView: View? = null + private var indicatorViewHost: SurfaceControlViewHost? = null + // Below variables and the SyncTransactionQueue are the only variables that should + // be accessed from shell main thread. Everything else should be used exclusively + // from the desktop thread. + private var indicatorLeash: SurfaceControl? = null + private var isReleased = false + + /** Create a fullscreen indicator with no animation */ + @ShellMainThread + fun createView( + context: Context, + display: Display, + layout: DisplayLayout, + taskInfo: ActivityManager.RunningTaskInfo, + taskSurface: SurfaceControl, + ) { + if (isReleased) return + desktopExecutor.execute { + val resources = context.resources + val metrics = resources.displayMetrics + val screenWidth: Int + val screenHeight: Int + if (Flags.enableBugFixesForSecondaryDisplay()) { + screenWidth = layout.width() + screenHeight = layout.height() + } else { + screenWidth = metrics.widthPixels + screenHeight = metrics.heightPixels + } + indicatorView = View(context) + val leash = + indicatorBuilder + .setName("Desktop Mode Visual Indicator") + .setContainerLayer() + .setCallsite("DesktopModeVisualIndicator.createView") + .build() + val lp = + WindowManager.LayoutParams( + screenWidth, + screenHeight, + WindowManager.LayoutParams.TYPE_APPLICATION, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + PixelFormat.TRANSPARENT, + ) + lp.title = "Desktop Mode Visual Indicator" + lp.setTrustedOverlay() + lp.inputFeatures = + lp.inputFeatures or WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL + val windowManager = + WindowlessWindowManager( + taskInfo.configuration, + leash, + /* hostInputTransferToken= */ null, + ) + indicatorViewHost = + surfaceControlViewHostFactory.create( + context, + display, + windowManager, + "VisualIndicatorViewContainer", + ) + indicatorView?.let { indicatorViewHost?.setView(it, lp) } + showIndicator(taskSurface, leash) + } + } + + private fun showIndicator(taskSurface: SurfaceControl, leash: SurfaceControl) { + mainExecutor.execute { + indicatorLeash = leash + val t = SurfaceControl.Transaction() + t.show(indicatorLeash) + // We want this indicator to be behind the dragged task, but in front of all others. + t.setRelativeLayer(indicatorLeash, taskSurface, -1) + syncQueue.runInSync { transaction: SurfaceControl.Transaction -> + transaction.merge(t) + t.close() + } + } + } + + @VisibleForTesting + fun getIndicatorBounds(): Rect { + return indicatorView?.background?.getBounds() ?: Rect() + } + + /** + * Takes existing indicator and animates it to bounds reflecting a new indicator type. Should + * only be called from the main thread. + */ + @ShellMainThread + fun transitionIndicator( + taskInfo: ActivityManager.RunningTaskInfo, + displayController: DisplayController, + currentType: IndicatorType, + newType: IndicatorType, + ) { + if (currentType == newType || isReleased) return + desktopExecutor.execute { + val layout = + displayController.getDisplayLayout(taskInfo.displayId) + ?: error("Expected to find DisplayLayout for taskId${taskInfo.taskId}.") + if (currentType == IndicatorType.NO_INDICATOR) { + fadeInIndicator(layout, newType) + } else if (newType == IndicatorType.NO_INDICATOR) { + fadeOutIndicator(layout, currentType, /* finishCallback= */ null) + } else { + val animStartType = IndicatorType.valueOf(currentType.name) + val animator = + indicatorView?.let { + VisualIndicatorAnimator.animateIndicatorType( + it, + layout, + animStartType, + newType, + bubbleBoundsProvider, + ) + } ?: return@execute + animator.start() + } + } + } + + /** + * Fade indicator in as provided type. Animator fades it in while expanding the bounds outwards. + */ + @VisibleForTesting + fun fadeInIndicator(layout: DisplayLayout, type: IndicatorType) { + desktopExecutor.assertCurrentThread() + indicatorView?.let { + it.setBackgroundResource(R.drawable.desktop_windowing_transition_background) + val animator = + VisualIndicatorAnimator.fadeBoundsIn(it, type, layout, bubbleBoundsProvider) + animator.start() + } + } + + /** + * Fade out indicator without fully releasing it. Animator fades it out while shrinking bounds. + * + * @param finishCallback called when animation ends or gets cancelled + */ + fun fadeOutIndicator( + layout: DisplayLayout, + currentType: IndicatorType, + finishCallback: Runnable?, + ) { + if (currentType == IndicatorType.NO_INDICATOR) { + // In rare cases, fade out can be requested before the indicator has determined its + // initial type and started animating in. In this case, no animator is needed. + finishCallback?.run() + return + } + desktopExecutor.execute { + indicatorView?.let { + val animStartType = IndicatorType.valueOf(currentType.name) + val animator = + VisualIndicatorAnimator.fadeBoundsOut( + it, + animStartType, + layout, + bubbleBoundsProvider, + ) + animator.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + if (finishCallback != null) { + mainExecutor.execute(finishCallback) + } + } + } + ) + animator.start() + } + } + } + + /** Release the indicator and its components when it is no longer needed. */ + @ShellMainThread + fun releaseVisualIndicator() { + if (isReleased) return + desktopExecutor.execute { + indicatorViewHost?.release() + indicatorViewHost = null + } + indicatorLeash?.let { + val tx = SurfaceControl.Transaction() + tx.remove(it) + indicatorLeash = null + syncQueue.runInSync { transaction: SurfaceControl.Transaction -> + transaction.merge(tx) + tx.close() + } + } + isReleased = true + } + + /** + * Animator for Desktop Mode transitions which supports bounds and alpha animation. Functions + * should only be called from the desktop executor. + */ + @VisibleForTesting + class VisualIndicatorAnimator(view: View, startBounds: Rect, endBounds: Rect) : + ValueAnimator() { + /** + * Determines how this animator will interact with the view's alpha: Fade in, fade out, or + * no change to alpha + */ + private enum class AlphaAnimType { + ALPHA_FADE_IN_ANIM, + ALPHA_FADE_OUT_ANIM, + ALPHA_NO_CHANGE_ANIM, + } + + private val indicatorView: View = view + @VisibleForTesting val indicatorStartBounds = Rect(startBounds) + @VisibleForTesting val indicatorEndBounds = endBounds + private val mRectEvaluator: RectEvaluator + + init { + setFloatValues(0f, 1f) + mRectEvaluator = RectEvaluator(Rect()) + } + + /** + * Update bounds of view based on current animation fraction. Use of delta is to animate + * bounds independently, in case we need to run multiple animations simultaneously. + * + * @param fraction fraction to use, compared against previous fraction + * @param view the view to update + */ + @ShellDesktopThread + private fun updateBounds(fraction: Float, view: View?) { + if (indicatorStartBounds == indicatorEndBounds) { + return + } + val currentBounds = + mRectEvaluator.evaluate(fraction, indicatorStartBounds, indicatorEndBounds) + view?.background?.bounds = currentBounds + } + + /** + * Fade in the fullscreen indicator + * + * @param fraction current animation fraction + */ + @ShellDesktopThread + private fun updateIndicatorAlpha(fraction: Float, view: View?) { + val drawable = view?.background as LayerDrawable + drawable.findDrawableByLayerId(R.id.indicator_stroke).alpha = + (MAXIMUM_OPACITY * fraction).toInt() + drawable.findDrawableByLayerId(R.id.indicator_solid).alpha = + (MAXIMUM_OPACITY * fraction * INDICATOR_FINAL_OPACITY).toInt() + } + + companion object { + private const val FULLSCREEN_INDICATOR_DURATION = 200 + private const val FULLSCREEN_SCALE_ADJUSTMENT_PERCENT = 0.015f + private const val INDICATOR_FINAL_OPACITY = 0.35f + private const val MAXIMUM_OPACITY = 255 + + @ShellDesktopThread + fun fadeBoundsIn( + view: View, + type: IndicatorType, + displayLayout: DisplayLayout, + bubbleBoundsProvider: BubbleDropTargetBoundsProvider?, + ): VisualIndicatorAnimator { + val endBounds = getIndicatorBounds(displayLayout, type, bubbleBoundsProvider) + val startBounds = getMinBounds(endBounds) + view.background.bounds = startBounds + + val animator = VisualIndicatorAnimator(view, startBounds, endBounds) + animator.interpolator = DecelerateInterpolator() + setupIndicatorAnimation(animator, AlphaAnimType.ALPHA_FADE_IN_ANIM) + return animator + } + + @ShellDesktopThread + fun fadeBoundsOut( + view: View, + type: IndicatorType, + displayLayout: DisplayLayout, + bubbleBoundsProvider: BubbleDropTargetBoundsProvider?, + ): VisualIndicatorAnimator { + val startBounds = getIndicatorBounds(displayLayout, type, bubbleBoundsProvider) + val endBounds = getMinBounds(startBounds) + view.background.bounds = startBounds + + val animator = VisualIndicatorAnimator(view, startBounds, endBounds) + animator.interpolator = DecelerateInterpolator() + setupIndicatorAnimation(animator, AlphaAnimType.ALPHA_FADE_OUT_ANIM) + return animator + } + + /** + * Create animator for visual indicator changing type (i.e., fullscreen to freeform, + * freeform to split, etc.) + * + * @param view the view for this indicator + * @param displayLayout information about the display the transitioning task is + * currently on + * @param origType the original indicator type + * @param newType the new indicator type + * @param desktopExecutor: the executor for the ShellDesktopThread; should be the only + * thread this function runs on + */ + @ShellDesktopThread + fun animateIndicatorType( + view: View, + displayLayout: DisplayLayout, + origType: IndicatorType, + newType: IndicatorType, + bubbleBoundsProvider: BubbleDropTargetBoundsProvider?, + ): VisualIndicatorAnimator { + val startBounds = getIndicatorBounds(displayLayout, origType, bubbleBoundsProvider) + val endBounds = getIndicatorBounds(displayLayout, newType, bubbleBoundsProvider) + val animator = VisualIndicatorAnimator(view, startBounds, endBounds) + animator.interpolator = DecelerateInterpolator() + setupIndicatorAnimation(animator, AlphaAnimType.ALPHA_NO_CHANGE_ANIM) + return animator + } + + /** Calculates the bounds the indicator should have when fully faded in. */ + private fun getIndicatorBounds( + layout: DisplayLayout, + type: IndicatorType, + bubbleBoundsProvider: BubbleDropTargetBoundsProvider?, + ): Rect { + val desktopStableBounds = Rect() + layout.getStableBounds(desktopStableBounds) + val padding = desktopStableBounds.top + when (type) { + IndicatorType.TO_FULLSCREEN_INDICATOR -> { + desktopStableBounds.top += padding + desktopStableBounds.bottom -= padding + desktopStableBounds.left += padding + desktopStableBounds.right -= padding + return desktopStableBounds + } + + IndicatorType.TO_DESKTOP_INDICATOR -> { + val adjustmentPercentage = + (1f - DesktopTasksController.DESKTOP_MODE_INITIAL_BOUNDS_SCALE) + return Rect( + (adjustmentPercentage * desktopStableBounds.width() / 2).toInt(), + (adjustmentPercentage * desktopStableBounds.height() / 2).toInt(), + (desktopStableBounds.width() - + (adjustmentPercentage * desktopStableBounds.width() / 2)) + .toInt(), + (desktopStableBounds.height() - + (adjustmentPercentage * desktopStableBounds.height() / 2)) + .toInt(), + ) + } + + IndicatorType.TO_SPLIT_LEFT_INDICATOR -> + return Rect( + padding, + padding, + desktopStableBounds.width() / 2 - padding, + desktopStableBounds.height(), + ) + + IndicatorType.TO_SPLIT_RIGHT_INDICATOR -> + return Rect( + desktopStableBounds.width() / 2 + padding, + padding, + desktopStableBounds.width() - padding, + desktopStableBounds.height(), + ) + IndicatorType.TO_BUBBLE_LEFT_INDICATOR -> + return bubbleBoundsProvider?.getBubbleBarExpandedViewDropTargetBounds( + /* onLeft= */ true + ) ?: Rect() + IndicatorType.TO_BUBBLE_RIGHT_INDICATOR -> + return bubbleBoundsProvider?.getBubbleBarExpandedViewDropTargetBounds( + /* onLeft= */ false + ) ?: Rect() + else -> throw IllegalArgumentException("Invalid indicator type provided.") + } + } + + /** Add necessary listener for animation of indicator */ + private fun setupIndicatorAnimation( + animator: VisualIndicatorAnimator, + animType: AlphaAnimType, + ) { + animator.addUpdateListener { a: ValueAnimator -> + animator.updateBounds(a.animatedFraction, animator.indicatorView) + if (animType == AlphaAnimType.ALPHA_FADE_IN_ANIM) { + animator.updateIndicatorAlpha(a.animatedFraction, animator.indicatorView) + } else if (animType == AlphaAnimType.ALPHA_FADE_OUT_ANIM) { + animator.updateIndicatorAlpha( + 1 - a.animatedFraction, + animator.indicatorView, + ) + } + } + animator.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + animator.indicatorView.background.bounds = animator.indicatorEndBounds + } + } + ) + animator.setDuration(FULLSCREEN_INDICATOR_DURATION.toLong()) + } + + /** + * Return the minimum bounds of a visual indicator, to be used at the end of fading out + * and the start of fading in. + */ + private fun getMinBounds(maxBounds: Rect): Rect { + return Rect( + (maxBounds.left + (FULLSCREEN_SCALE_ADJUSTMENT_PERCENT * maxBounds.width())) + .toInt(), + (maxBounds.top + (FULLSCREEN_SCALE_ADJUSTMENT_PERCENT * maxBounds.height())) + .toInt(), + (maxBounds.right - (FULLSCREEN_SCALE_ADJUSTMENT_PERCENT * maxBounds.width())) + .toInt(), + (maxBounds.bottom - (FULLSCREEN_SCALE_ADJUSTMENT_PERCENT * maxBounds.height())) + .toInt(), + ) + } + } + } +} 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/pip2/phone/PipDragToResizeHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipDragToResizeHandler.java new file mode 100644 index 000000000000..bd0b810b2a44 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipDragToResizeHandler.java @@ -0,0 +1,218 @@ +/* + * 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; + +import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_BOTTOM; +import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_LEFT; +import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_RIGHT; +import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_TOP; +import static com.android.wm.shell.pip2.phone.PipMenuView.ANIM_TYPE_NONE; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Point; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.Region; +import android.view.MotionEvent; + +import com.android.internal.policy.TaskResizingAlgorithm; +import com.android.wm.shell.R; +import com.android.wm.shell.common.pip.PipBoundsAlgorithm; +import com.android.wm.shell.common.pip.PipBoundsState; + +import java.util.function.Function; + +/** Helper for handling drag-corner-to-resize gestures. */ +public class PipDragToResizeHandler { + private final Context mContext; + private final PipResizeGestureHandler mPipResizeGestureHandler; + private final PipBoundsState mPipBoundsState; + private final PhonePipMenuController mPhonePipMenuController; + private final PipBoundsAlgorithm mPipBoundsAlgorithm; + private final PipScheduler mPipScheduler; + + private final Region mTmpRegion = new Region(); + private final Rect mDragCornerSize = new Rect(); + private final Rect mTmpTopLeftCorner = new Rect(); + private final Rect mTmpTopRightCorner = new Rect(); + private final Rect mTmpBottomLeftCorner = new Rect(); + private final Rect mTmpBottomRightCorner = new Rect(); + private final Rect mDisplayBounds = new Rect(); + private final Function<Rect, Rect> mMovementBoundsSupplier; + private int mDelta; + + public PipDragToResizeHandler(Context context, PipResizeGestureHandler pipResizeGestureHandler, + PipBoundsState pipBoundsState, + PhonePipMenuController phonePipMenuController, PipBoundsAlgorithm pipBoundsAlgorithm, + PipScheduler pipScheduler, Function<Rect, Rect> movementBoundsSupplier) { + mContext = context; + mPipResizeGestureHandler = pipResizeGestureHandler; + mPipBoundsState = pipBoundsState; + mPhonePipMenuController = phonePipMenuController; + mPipBoundsAlgorithm = pipBoundsAlgorithm; + mPipScheduler = pipScheduler; + mMovementBoundsSupplier = movementBoundsSupplier; + } + + /** Invoked by {@link PipResizeGestureHandler#reloadResources}. */ + void reloadResources() { + final Resources res = mContext.getResources(); + mDelta = res.getDimensionPixelSize(R.dimen.pip_resize_edge_size); + } + + /** Invoked by {@link PipResizeGestureHandler#onInputEvent} if drag-corner-to-resize is + * enabled. */ + void onDragCornerResize(MotionEvent ev, Rect lastResizeBounds, PointF downPoint, + Rect downBounds, Point minSize, Point maxSize, float touchSlop) { + int action = ev.getActionMasked(); + float x = ev.getX(); + float y = ev.getY(); + if (action == MotionEvent.ACTION_DOWN) { + lastResizeBounds.setEmpty(); + final boolean allowGesture = isWithinDragResizeRegion((int) x, (int) y); + mPipResizeGestureHandler.setAllowGesture(allowGesture); + if (allowGesture) { + setCtrlType((int) x, (int) y); + downPoint.set(x, y); + downBounds.set(mPipBoundsState.getBounds()); + } + } else if (mPipResizeGestureHandler.getAllowGesture()) { + switch (action) { + case MotionEvent.ACTION_POINTER_DOWN: + // We do not support multi touch for resizing via drag + mPipResizeGestureHandler.setAllowGesture(false); + break; + case MotionEvent.ACTION_MOVE: + final boolean thresholdCrossed = mPipResizeGestureHandler.getThresholdCrossed(); + // Capture inputs + if (!mPipResizeGestureHandler.getThresholdCrossed() + && Math.hypot(x - downPoint.x, y - downPoint.y) > touchSlop) { + mPipResizeGestureHandler.setThresholdCrossed(true); + // Reset the down to begin resizing from this point + downPoint.set(x, y); + mPipResizeGestureHandler.pilferPointers(); + } + if (mPipResizeGestureHandler.getThresholdCrossed()) { + if (mPhonePipMenuController.isMenuVisible()) { + mPhonePipMenuController.hideMenu(ANIM_TYPE_NONE, + false /* resize */); + } + final Rect currentPipBounds = mPipBoundsState.getBounds(); + lastResizeBounds.set(TaskResizingAlgorithm.resizeDrag(x, y, + downPoint.x, downPoint.y, currentPipBounds, + mPipResizeGestureHandler.getCtrlType(), minSize.x, + minSize.y, maxSize, true, + downBounds.width() > downBounds.height())); + mPipBoundsAlgorithm.transformBoundsToAspectRatio(lastResizeBounds, + mPipBoundsState.getAspectRatio(), false /* useCurrentMinEdgeSize */, + true /* useCurrentSize */); + mPipScheduler.scheduleUserResizePip(lastResizeBounds); + mPipBoundsState.setHasUserResizedPip(true); + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + mPipResizeGestureHandler.finishResize(); + break; + } + } + } + + /** + * Check whether the current x,y coordinate is within the region in which drag-resize should + * start. + * This consists of 4 small squares on the 4 corners of the PIP window, a quarter of which + * overlaps with the PIP window while the rest goes outside of the PIP window. + * _ _ _ _ + * |_|_|_________|_|_| + * |_|_| |_|_| + * | PIP | + * | WINDOW | + * _|_ _|_ + * |_|_|_________|_|_| + * |_|_| |_|_| + */ + boolean isWithinDragResizeRegion(int x, int y) { + final Rect currentPipBounds = mPipBoundsState.getBounds(); + if (currentPipBounds == null) { + return false; + } + resetDragCorners(); + mTmpTopLeftCorner.offset(currentPipBounds.left - mDelta / 2, + currentPipBounds.top - mDelta / 2); + mTmpTopRightCorner.offset(currentPipBounds.right - mDelta / 2, + currentPipBounds.top - mDelta / 2); + mTmpBottomLeftCorner.offset(currentPipBounds.left - mDelta / 2, + currentPipBounds.bottom - mDelta / 2); + mTmpBottomRightCorner.offset(currentPipBounds.right - mDelta / 2, + currentPipBounds.bottom - mDelta / 2); + + mTmpRegion.setEmpty(); + mTmpRegion.op(mTmpTopLeftCorner, Region.Op.UNION); + mTmpRegion.op(mTmpTopRightCorner, Region.Op.UNION); + mTmpRegion.op(mTmpBottomLeftCorner, Region.Op.UNION); + mTmpRegion.op(mTmpBottomRightCorner, Region.Op.UNION); + + return mTmpRegion.contains(x, y); + } + + private void resetDragCorners() { + mDragCornerSize.set(0, 0, mDelta, mDelta); + mTmpTopLeftCorner.set(mDragCornerSize); + mTmpTopRightCorner.set(mDragCornerSize); + mTmpBottomLeftCorner.set(mDragCornerSize); + mTmpBottomRightCorner.set(mDragCornerSize); + } + + private void setCtrlType(int x, int y) { + final Rect currentPipBounds = mPipBoundsState.getBounds(); + int ctrlType = mPipResizeGestureHandler.getCtrlType(); + + Rect movementBounds = mMovementBoundsSupplier.apply(currentPipBounds); + + mDisplayBounds.set(movementBounds.left, + movementBounds.top, + movementBounds.right + currentPipBounds.width(), + movementBounds.bottom + currentPipBounds.height()); + + if (mTmpTopLeftCorner.contains(x, y) && currentPipBounds.top != mDisplayBounds.top + && currentPipBounds.left != mDisplayBounds.left) { + ctrlType |= CTRL_LEFT; + ctrlType |= CTRL_TOP; + } + if (mTmpTopRightCorner.contains(x, y) && currentPipBounds.top != mDisplayBounds.top + && currentPipBounds.right != mDisplayBounds.right) { + ctrlType |= CTRL_RIGHT; + ctrlType |= CTRL_TOP; + } + if (mTmpBottomRightCorner.contains(x, y) + && currentPipBounds.bottom != mDisplayBounds.bottom + && currentPipBounds.right != mDisplayBounds.right) { + ctrlType |= CTRL_RIGHT; + ctrlType |= CTRL_BOTTOM; + } + if (mTmpBottomLeftCorner.contains(x, y) + && currentPipBounds.bottom != mDisplayBounds.bottom + && currentPipBounds.left != mDisplayBounds.left) { + ctrlType |= CTRL_LEFT; + ctrlType |= CTRL_BOTTOM; + } + + mPipResizeGestureHandler.setCtrlType(ctrlType); + } + +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipPinchToResizeHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipPinchToResizeHandler.java new file mode 100644 index 000000000000..1e41af379864 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipPinchToResizeHandler.java @@ -0,0 +1,129 @@ +/* + * 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; + +import android.graphics.Point; +import android.graphics.PointF; +import android.graphics.Rect; +import android.view.MotionEvent; + +import com.android.wm.shell.common.pip.PipBoundsState; +import com.android.wm.shell.common.pip.PipPinchResizingAlgorithm; + +/** Helper for handling pinch-to-resize gestures. */ +public class PipPinchToResizeHandler { + private final PipResizeGestureHandler mPipResizeGestureHandler; + private final PipBoundsState mPipBoundsState; + private final PhonePipMenuController mPhonePipMenuController; + private final PipScheduler mPipScheduler; + private final PipPinchResizingAlgorithm mPinchResizingAlgorithm; + + private int mFirstIndex = -1; + private int mSecondIndex = -1; + + public PipPinchToResizeHandler(PipResizeGestureHandler pipResizeGestureHandler, + PipBoundsState pipBoundsState, PhonePipMenuController phonePipMenuController, + PipScheduler pipScheduler) { + mPipResizeGestureHandler = pipResizeGestureHandler; + mPipBoundsState = pipBoundsState; + mPhonePipMenuController = phonePipMenuController; + mPipScheduler = pipScheduler; + + mPinchResizingAlgorithm = new PipPinchResizingAlgorithm(); + } + + /** Invoked by {@link PipResizeGestureHandler#onInputEvent} if pinch-to-resize is enabled. */ + void onPinchResize(MotionEvent ev, PointF downPoint, PointF downSecondPoint, Rect downBounds, + PointF lastPoint, PointF lastSecondPoint, Rect lastResizeBounds, float touchSlop, + Point minSize, Point maxSize) { + int action = ev.getActionMasked(); + + if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { + mFirstIndex = -1; + mSecondIndex = -1; + mPipResizeGestureHandler.setAllowGesture(false); + mPipResizeGestureHandler.finishResize(); + } + + if (ev.getPointerCount() != 2) { + return; + } + + final Rect pipBounds = mPipBoundsState.getBounds(); + if (action == MotionEvent.ACTION_POINTER_DOWN) { + if (mFirstIndex == -1 && mSecondIndex == -1 + && pipBounds.contains((int) ev.getRawX(0), (int) ev.getRawY(0)) + && pipBounds.contains((int) ev.getRawX(1), (int) ev.getRawY(1))) { + mPipResizeGestureHandler.setAllowGesture(true); + mFirstIndex = 0; + mSecondIndex = 1; + downPoint.set(ev.getRawX(mFirstIndex), ev.getRawY(mFirstIndex)); + downSecondPoint.set(ev.getRawX(mSecondIndex), ev.getRawY(mSecondIndex)); + downBounds.set(pipBounds); + + lastPoint.set(downPoint); + lastSecondPoint.set(lastSecondPoint); + lastResizeBounds.set(downBounds); + + // start the high perf session as the second pointer gets detected + mPipResizeGestureHandler.startHighPerfSession(); + } + } + + if (action == MotionEvent.ACTION_MOVE) { + if (mFirstIndex == -1 || mSecondIndex == -1) { + return; + } + + float x0 = ev.getRawX(mFirstIndex); + float y0 = ev.getRawY(mFirstIndex); + float x1 = ev.getRawX(mSecondIndex); + float y1 = ev.getRawY(mSecondIndex); + lastPoint.set(x0, y0); + lastSecondPoint.set(x1, y1); + + // Capture inputs + if (!mPipResizeGestureHandler.getThresholdCrossed() + && (distanceBetween(downSecondPoint, lastSecondPoint) > touchSlop + || distanceBetween(downPoint, lastPoint) > touchSlop)) { + mPipResizeGestureHandler.pilferPointers(); + mPipResizeGestureHandler.setThresholdCrossed(true); + // Reset the down to begin resizing from this point + downPoint.set(lastPoint); + downSecondPoint.set(lastSecondPoint); + + if (mPhonePipMenuController.isMenuVisible()) { + mPhonePipMenuController.hideMenu(); + } + } + + if (mPipResizeGestureHandler.getThresholdCrossed()) { + final float angle = mPinchResizingAlgorithm.calculateBoundsAndAngle(downPoint, + downSecondPoint, lastPoint, lastSecondPoint, minSize, maxSize, + downBounds, lastResizeBounds); + + mPipResizeGestureHandler.setAngle(angle); + mPipScheduler.scheduleUserResizePip(lastResizeBounds, angle); + mPipBoundsState.setHasUserResizedPip(true); + } + } + } + + private float distanceBetween(PointF p1, PointF p2) { + return (float) Math.hypot(p2.x - p1.x, p2.y - p1.y); + } + +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java index e4be3f60f86e..b869bf153c34 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java @@ -44,13 +44,14 @@ import com.android.wm.shell.R; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; import com.android.wm.shell.common.pip.PipBoundsState; +import com.android.wm.shell.common.pip.PipDesktopState; import com.android.wm.shell.common.pip.PipDisplayLayoutState; import com.android.wm.shell.common.pip.PipPerfHintController; -import com.android.wm.shell.common.pip.PipPinchResizingAlgorithm; import com.android.wm.shell.common.pip.PipUiEventLogger; import com.android.wm.shell.pip2.animation.PipResizeAnimator; import java.io.PrintWriter; +import java.util.function.Function; /** * Helper on top of PipTouchHandler that handles inputs OUTSIDE of the PIP window, which is used to @@ -72,8 +73,8 @@ public class PipResizeGestureHandler implements private final PipTransitionState mPipTransitionState; private final PhonePipMenuController mPhonePipMenuController; private final PipDisplayLayoutState mPipDisplayLayoutState; + private final PipDesktopState mPipDesktopState; private final PipUiEventLogger mPipUiEventLogger; - private final PipPinchResizingAlgorithm mPinchResizingAlgorithm; private final ShellExecutor mMainExecutor; private final PointF mDownPoint = new PointF(); @@ -93,16 +94,18 @@ public class PipResizeGestureHandler implements private boolean mIsAttached; private boolean mIsEnabled; private boolean mEnablePinchResize; + private boolean mEnableDragCornerResize; private boolean mIsSysUiStateValid; private boolean mThresholdCrossed; private boolean mOngoingPinchToResize = false; private boolean mWaitingForBoundsChangeTransition = false; private float mAngle = 0; - int mFirstIndex = -1; - int mSecondIndex = -1; + private InputMonitor mInputMonitor; private InputEventReceiver mInputEventReceiver; + private PipDragToResizeHandler mPipDragToResizeHandler; + private PipPinchToResizeHandler mPipPinchToResizeHandler; @Nullable private final PipPerfHintController mPipPerfHintController; @@ -121,7 +124,9 @@ public class PipResizeGestureHandler implements PipTransitionState pipTransitionState, PipUiEventLogger pipUiEventLogger, PhonePipMenuController menuActivityController, + Function<Rect, Rect> movementBoundsSupplier, PipDisplayLayoutState pipDisplayLayoutState, + PipDesktopState pipDesktopState, ShellExecutor mainExecutor, @Nullable PipPerfHintController pipPerfHintController) { mContext = context; @@ -137,8 +142,13 @@ public class PipResizeGestureHandler implements mPhonePipMenuController = menuActivityController; mPipDisplayLayoutState = pipDisplayLayoutState; + mPipDesktopState = pipDesktopState; mPipUiEventLogger = pipUiEventLogger; - mPinchResizingAlgorithm = new PipPinchResizingAlgorithm(); + + mPipDragToResizeHandler = new PipDragToResizeHandler(context, this, pipBoundsState, + menuActivityController, pipBoundsAlgorithm, pipScheduler, movementBoundsSupplier); + mPipPinchToResizeHandler = new PipPinchToResizeHandler(this, pipBoundsState, + menuActivityController, pipScheduler); } void init() { @@ -163,6 +173,7 @@ public class PipResizeGestureHandler implements } private void reloadResources() { + mPipDragToResizeHandler.reloadResources(); mTouchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop(); } @@ -180,6 +191,8 @@ public class PipResizeGestureHandler implements void onActivityPinned() { mIsAttached = true; updateIsEnabled(); + // Only enable drag-corner-to-resize if PiP was entered when Desktop Mode session is active. + mEnableDragCornerResize = mPipDesktopState.isPipInDesktopMode(); } void onActivityUnpinned() { @@ -211,9 +224,44 @@ public class PipResizeGestureHandler implements } } + boolean getAllowGesture() { + return mAllowGesture; + } + + void setAllowGesture(boolean allowGesture) { + mAllowGesture = allowGesture; + } + + boolean getThresholdCrossed() { + return mThresholdCrossed; + } + + void setThresholdCrossed(boolean thresholdCrossed) { + mThresholdCrossed = thresholdCrossed; + } + + int getCtrlType() { + return mCtrlType; + } + + void setCtrlType(int ctrlType) { + mCtrlType = ctrlType; + } + + void setAngle(float angle) { + mAngle = angle; + } + + void startHighPerfSession() { + if (mPipPerfHintController != null) { + mPipHighPerfSession = mPipPerfHintController.startSession( + this::onHighPerfSessionTimeout, "onPinchResize"); + } + } + @VisibleForTesting void onInputEvent(InputEvent ev) { - if (!mEnablePinchResize) { + if (!mEnableDragCornerResize && !mEnablePinchResize) { // No need to handle anything if resizing isn't enabled. return; } @@ -240,7 +288,12 @@ public class PipResizeGestureHandler implements } if (mOngoingPinchToResize) { - onPinchResize(mv); + mPipPinchToResizeHandler.onPinchResize(mv, mDownPoint, mDownSecondPoint, + mDownBounds, mLastPoint, mLastSecondPoint, mLastResizeBounds, mTouchSlop, + mMinSize, mMaxSize); + } else if (mEnableDragCornerResize) { + mPipDragToResizeHandler.onDragCornerResize(mv, mLastResizeBounds, mDownPoint, + mDownBounds, mMinSize, mMaxSize, mTouchSlop); } } } @@ -261,20 +314,31 @@ public class PipResizeGestureHandler implements } boolean willStartResizeGesture(MotionEvent ev) { - if (ev.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) { - if (mEnablePinchResize && ev.getPointerCount() == 2) { - onPinchResize(ev); - mOngoingPinchToResize = mAllowGesture; - return mAllowGesture; - } + switch (ev.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + if (mEnableDragCornerResize && mPipDragToResizeHandler.isWithinDragResizeRegion( + (int) ev.getRawX(), + (int) ev.getRawY())) { + return true; + } + break; + + case MotionEvent.ACTION_POINTER_DOWN: + if (mEnablePinchResize && ev.getPointerCount() == 2) { + mPipPinchToResizeHandler.onPinchResize(ev, mDownPoint, mDownSecondPoint, + mDownBounds, mLastPoint, mLastSecondPoint, mLastResizeBounds, + mTouchSlop, mMinSize, mMaxSize); + mOngoingPinchToResize = mAllowGesture; + return mAllowGesture; + } + break; + + default: + break; } return false; } - private boolean isInValidSysUiState() { - return mIsSysUiStateValid; - } - private void onHighPerfSessionTimeout(PipPerfHintController.PipHighPerfSession session) {} private void cleanUpHighPerfSessionMaybe() { @@ -285,83 +349,6 @@ public class PipResizeGestureHandler implements } } - @VisibleForTesting - void onPinchResize(MotionEvent ev) { - int action = ev.getActionMasked(); - - if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { - mFirstIndex = -1; - mSecondIndex = -1; - mAllowGesture = false; - finishResize(); - } - - if (ev.getPointerCount() != 2) { - return; - } - - final Rect pipBounds = mPipBoundsState.getBounds(); - if (action == MotionEvent.ACTION_POINTER_DOWN) { - if (mFirstIndex == -1 && mSecondIndex == -1 - && pipBounds.contains((int) ev.getRawX(0), (int) ev.getRawY(0)) - && pipBounds.contains((int) ev.getRawX(1), (int) ev.getRawY(1))) { - mAllowGesture = true; - mFirstIndex = 0; - mSecondIndex = 1; - mDownPoint.set(ev.getRawX(mFirstIndex), ev.getRawY(mFirstIndex)); - mDownSecondPoint.set(ev.getRawX(mSecondIndex), ev.getRawY(mSecondIndex)); - mDownBounds.set(pipBounds); - - mLastPoint.set(mDownPoint); - mLastSecondPoint.set(mLastSecondPoint); - mLastResizeBounds.set(mDownBounds); - - // start the high perf session as the second pointer gets detected - if (mPipPerfHintController != null) { - mPipHighPerfSession = mPipPerfHintController.startSession( - this::onHighPerfSessionTimeout, "onPinchResize"); - } - } - } - - if (action == MotionEvent.ACTION_MOVE) { - if (mFirstIndex == -1 || mSecondIndex == -1) { - return; - } - - float x0 = ev.getRawX(mFirstIndex); - float y0 = ev.getRawY(mFirstIndex); - float x1 = ev.getRawX(mSecondIndex); - float y1 = ev.getRawY(mSecondIndex); - mLastPoint.set(x0, y0); - mLastSecondPoint.set(x1, y1); - - // Capture inputs - if (!mThresholdCrossed - && (distanceBetween(mDownSecondPoint, mLastSecondPoint) > mTouchSlop - || distanceBetween(mDownPoint, mLastPoint) > mTouchSlop)) { - pilferPointers(); - mThresholdCrossed = true; - // Reset the down to begin resizing from this point - mDownPoint.set(mLastPoint); - mDownSecondPoint.set(mLastSecondPoint); - - if (mPhonePipMenuController.isMenuVisible()) { - mPhonePipMenuController.hideMenu(); - } - } - - if (mThresholdCrossed) { - mAngle = mPinchResizingAlgorithm.calculateBoundsAndAngle(mDownPoint, - mDownSecondPoint, mLastPoint, mLastSecondPoint, mMinSize, mMaxSize, - mDownBounds, mLastResizeBounds); - - mPipScheduler.scheduleUserResizePip(mLastResizeBounds, mAngle); - mPipBoundsState.setHasUserResizedPip(true); - } - } - } - private void snapToMovementBoundsEdge(Rect bounds, Rect movementBounds) { final int leftEdge = bounds.left; @@ -404,17 +391,21 @@ public class PipResizeGestureHandler implements // mPipTaskOrganizer.scheduleFinishResizePip(finalBounds, mUpdateResizeBoundsCallback); } - private void finishResize() { + /** Handles additional resizing and state changes after gesture resizing is done. */ + void finishResize() { if (mLastResizeBounds.isEmpty()) { resetState(); } - if (!mOngoingPinchToResize) { - return; - } // Cache initial bounds after release for animation before mLastResizeBounds are modified. mStartBoundsAfterRelease.set(mLastResizeBounds); + // Drag-corner-to-resize - we don't need to adjust the bounds at this point + if (!mOngoingPinchToResize) { + scheduleBoundsChange(); + return; + } + // If user resize is pretty close to max size, just auto resize to max. if (mLastResizeBounds.width() >= PINCH_RESIZE_AUTO_MAX_RATIO * mMaxSize.x || mLastResizeBounds.height() >= PINCH_RESIZE_AUTO_MAX_RATIO * mMaxSize.y) { @@ -438,6 +429,10 @@ public class PipResizeGestureHandler implements mLastResizeBounds, movementBounds); mPipBoundsAlgorithm.applySnapFraction(mLastResizeBounds, snapFraction); + scheduleBoundsChange(); + } + + private void scheduleBoundsChange() { // Update the transition state to schedule a resize transition. Bundle extra = new Bundle(); extra.putBoolean(RESIZE_BOUNDS_CHANGE, true); @@ -489,10 +484,6 @@ public class PipResizeGestureHandler implements mOhmOffset = offset; } - private float distanceBetween(PointF p1, PointF p2) { - return (float) Math.hypot(p2.x - p1.x, p2.y - p1.y); - } - private void resizeRectAboutCenter(Rect rect, int w, int h) { int cx = rect.centerX(); int cy = rect.centerY(); @@ -573,6 +564,7 @@ public class PipResizeGestureHandler implements pw.println(innerPrefix + "mIsAttached=" + mIsAttached); pw.println(innerPrefix + "mIsEnabled=" + mIsEnabled); pw.println(innerPrefix + "mEnablePinchResize=" + mEnablePinchResize); + pw.println(innerPrefix + "mEnableDragCornerResize=" + mEnableDragCornerResize); pw.println(innerPrefix + "mThresholdCrossed=" + mThresholdCrossed); pw.println(innerPrefix + "mOhmOffset=" + mOhmOffset); pw.println(innerPrefix + "mMinSize=" + mMinSize); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java index e405f3339054..72346b335a8e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java @@ -59,6 +59,7 @@ import com.android.wm.shell.common.FloatingContentCoordinator; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; import com.android.wm.shell.common.pip.PipBoundsState; +import com.android.wm.shell.common.pip.PipDesktopState; import com.android.wm.shell.common.pip.PipDisplayLayoutState; import com.android.wm.shell.common.pip.PipDoubleTapHelper; import com.android.wm.shell.common.pip.PipPerfHintController; @@ -187,6 +188,7 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha @NonNull PipScheduler pipScheduler, @NonNull SizeSpecSource sizeSpecSource, @NonNull PipDisplayLayoutState pipDisplayLayoutState, + PipDesktopState pipDesktopState, DisplayController displayController, PipMotionHelper pipMotionHelper, FloatingContentCoordinator floatingContentCoordinator, @@ -226,7 +228,8 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha mainExecutor); mPipResizeGestureHandler = new PipResizeGestureHandler(context, pipBoundsAlgorithm, pipBoundsState, mTouchState, mPipScheduler, mPipTransitionState, pipUiEventLogger, - menuController, mPipDisplayLayoutState, mainExecutor, mPipPerfHintController); + menuController, this::getMovementBounds, mPipDisplayLayoutState, pipDesktopState, + mainExecutor, mPipPerfHintController); mPipBoundsState.addOnAspectRatioChangedCallback(aspectRatio -> { updateMinMaxSize(aspectRatio); onAspectRatioChanged(); 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..9adaa3614a0f 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 @@ -300,6 +300,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; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationRunner.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationRunner.aidl index 8cdb8c4512a9..f8d84e4f3c21 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationRunner.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationRunner.aidl @@ -59,11 +59,12 @@ oneway interface IRecentsAnimationRunner { void onAnimationStart(in IRecentsAnimationController controller, in RemoteAnimationTarget[] apps, in RemoteAnimationTarget[] wallpapers, in Rect homeContentInsets, in Rect minimizedHomeBounds, in Bundle extras, - in TransitionInfo info) = 2; + in @nullable TransitionInfo info) = 2; /** * Called when the task of an activity that has been started while the recents animation * was running becomes ready for control. */ - void onTasksAppeared(in RemoteAnimationTarget[] app) = 3; + void onTasksAppeared(in RemoteAnimationTarget[] app, + in @nullable TransitionInfo transitionInfo) = 3; } 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 7751741ae082..a969845fb8e8 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 @@ -1214,13 +1214,16 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, // Since we're accepting the merge, update the finish transaction so that changes via // that transaction will be applied on top of those of the merged transitions mFinishTransaction = finishT; - // not using the incoming anim-only surfaces - info.releaseAnimSurfaces(); + boolean passTransitionInfo = ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX.isTrue(); + if (!passTransitionInfo) { + // not using the incoming anim-only surfaces + info.releaseAnimSurfaces(); + } if (appearedTargets != null) { try { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "[%d] RecentsController.merge: calling onTasksAppeared", mInstanceId); - mListener.onTasksAppeared(appearedTargets); + mListener.onTasksAppeared(appearedTargets, passTransitionInfo ? info : null); } catch (RemoteException e) { Slog.e(TAG, "Error sending appeared tasks to recents animation", e); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java index e9f8a4a86d27..ba30d924e0b1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java @@ -638,6 +638,14 @@ public class SplitScreenController implements SplitDragPolicy.Starter, } /** + * Starts an existing task via StageCoordinator. + */ + public void startTask(int taskId, @SplitPosition int position, @Nullable Bundle options, + @Nullable WindowContainerToken hideTaskToken, @SplitIndex int index) { + mStageCoordinator.startTask(taskId, position, options, hideTaskToken, index); + } + + /** * See {@link #startShortcut(String, String, int, Bundle, UserHandle)} * @param instanceId to be used by {@link SplitscreenEventLogger} */ 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..add2c54f0e29 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 { @@ -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 diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleImageButton.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleImageButton.kt index b21c3f522eab..458815d19658 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleImageButton.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleImageButton.kt @@ -17,9 +17,12 @@ package com.android.wm.shell.windowdecor import android.animation.ValueAnimator +import android.annotation.DimenRes +import android.content.res.Resources; import android.content.Context import android.util.AttributeSet import android.widget.ImageButton +import com.android.wm.shell.R /** * [ImageButton] for the handle at the top of fullscreen apps. Has custom hover @@ -30,13 +33,23 @@ class HandleImageButton (context: Context?, attrs: AttributeSet?) : ImageButton(context, attrs) { private val handleAnimator = ValueAnimator() + /** Final horizontal padding for hover enter. **/ + private val HANDLE_HOVER_ENTER_PADDING = loadDimensionPixelSize( + R.dimen.desktop_mode_fullscreen_decor_caption_horizontal_padding_hovered) + /** Final horizontal padding for press down. **/ + private val HANDLE_PRESS_DOWN_PADDING = loadDimensionPixelSize( + R.dimen.desktop_mode_fullscreen_decor_caption_horizontal_padding_touched) + /** Default horizontal padding. **/ + private val HANDLE_DEFAULT_PADDING = loadDimensionPixelSize( + R.dimen.desktop_mode_fullscreen_decor_caption_horizontal_padding_default) + override fun onHoverChanged(hovered: Boolean) { super.onHoverChanged(hovered) if (hovered) { - animateHandle(HANDLE_HOVER_ANIM_DURATION, HANDLE_HOVER_ENTER_SCALE) + animateHandle(HANDLE_HOVER_ANIM_DURATION, HANDLE_HOVER_ENTER_PADDING) } else { if (!isPressed) { - animateHandle(HANDLE_HOVER_ANIM_DURATION, HANDLE_DEFAULT_SCALE) + animateHandle(HANDLE_HOVER_ANIM_DURATION, HANDLE_DEFAULT_PADDING) } } } @@ -45,35 +58,37 @@ class HandleImageButton (context: Context?, attrs: AttributeSet?) : if (isPressed != pressed) { super.setPressed(pressed) if (pressed) { - animateHandle(HANDLE_PRESS_ANIM_DURATION, HANDLE_PRESS_DOWN_SCALE) + animateHandle(HANDLE_PRESS_ANIM_DURATION, HANDLE_PRESS_DOWN_PADDING) } else { - animateHandle(HANDLE_PRESS_ANIM_DURATION, HANDLE_DEFAULT_SCALE) + animateHandle(HANDLE_PRESS_ANIM_DURATION, HANDLE_DEFAULT_PADDING) } } } - private fun animateHandle(duration: Long, endScale: Float) { + private fun animateHandle(duration: Long, endPadding: Int) { if (handleAnimator.isRunning) { handleAnimator.cancel() } handleAnimator.duration = duration - handleAnimator.setFloatValues(scaleX, endScale) + handleAnimator.setIntValues(paddingLeft, endPadding) handleAnimator.addUpdateListener { animator -> - scaleX = animator.animatedValue as Float + val padding = animator.animatedValue as Int + setPadding(padding, paddingTop, padding, paddingBottom) } handleAnimator.start() } + private fun loadDimensionPixelSize(@DimenRes resourceId: Int): Int { + if (resourceId == Resources.ID_NULL) { + return 0 + } + return context.resources.getDimensionPixelSize(resourceId) + } + companion object { /** The duration of animations related to hover state. **/ private const val HANDLE_HOVER_ANIM_DURATION = 300L /** The duration of animations related to pressed state. **/ private const val HANDLE_PRESS_ANIM_DURATION = 200L - /** Ending scale for hover enter. **/ - private const val HANDLE_HOVER_ENTER_SCALE = 1.2f - /** Ending scale for press down. **/ - private const val HANDLE_PRESS_DOWN_SCALE = 0.85f - /** Default scale for handle. **/ - private const val HANDLE_DEFAULT_SCALE = 1f } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositioner.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositioner.kt index a9c2e68e62a6..bb20292a51d4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositioner.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositioner.kt @@ -237,8 +237,12 @@ class MultiDisplayVeiledResizeTaskPositioner( val startDisplayLayout = displayController.getDisplayLayout(startDisplayId) val currentDisplayLayout = displayController.getDisplayLayout(displayId) - if (startDisplayLayout == null || currentDisplayLayout == null) { - // Fall back to single-display drag behavior if any display layout is unavailable. + if (startDisplayId == displayId + || startDisplayLayout == null || currentDisplayLayout == null) { + // Fall back to single-display drag behavior if: + // 1. The drag destination display is the same as the start display. This prevents + // unnecessary animations caused by minor width/height changes due to DPI scaling. + // 2. Either the starting or current display layout is unavailable. DragPositioningCallbackUtility.updateTaskBounds( repositionTaskBounds, taskBoundsAtDragStart, 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 76005c22972a..25dadfde274d 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 @@ -911,7 +911,6 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> } } - @VisibleForTesting public interface SurfaceControlViewHostFactory { default SurfaceControlViewHost create(Context c, Display d, WindowlessWindowManager wmm) { return new SurfaceControlViewHost(c, d, wmm, "WindowDecoration"); 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 b5911bfa54f2..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 @@ -186,15 +186,20 @@ public class BubbleTransitionsTest extends ShellTestCase { verify(startT).setPosition(any(), eq(0f), eq(0f)); verify(mBubbleData).notificationEntryUpdated(eq(mBubble), anyBoolean(), anyBoolean()); - ctb.continueExpand(); clearInvocations(mBubble); verify(mBubble, never()).setPreparingTransition(any()); ctb.surfaceCreated(); - verify(mBubble).setPreparingTransition(isNull()); + // Check that preparing transition is not reset before continueExpand is called + verify(mBubble, never()).setPreparingTransition(any()); ArgumentCaptor<Runnable> animCb = ArgumentCaptor.forClass(Runnable.class); verify(mLayerView).animateConvert(any(), any(), any(), any(), animCb.capture()); + + // continueExpand is now called, check that preparing transition is cleared + ctb.continueExpand(); + verify(mBubble).setPreparingTransition(isNull()); + assertFalse(finishCalled[0]); animCb.getValue().run(); assertTrue(finishCalled[0]); @@ -210,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/desktopmode/DesktopMixedTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt index 0b41952a89d7..77cd1e56853d 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 @@ -128,12 +129,21 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { @Test fun startMinimizedModeTransition_callsFreeformTaskTransitionHandler() { val wct = WindowContainerTransaction() - whenever(freeformTaskTransitionHandler.startMinimizedModeTransition(any())) + val taskId = 1 + val isLastTask = false + whenever( + freeformTaskTransitionHandler.startMinimizedModeTransition( + any(), + anyInt(), + anyBoolean(), + ) + ) .thenReturn(mock()) - mixedHandler.startMinimizedModeTransition(wct) + mixedHandler.startMinimizedModeTransition(wct, taskId, isLastTask) - verify(freeformTaskTransitionHandler).startMinimizedModeTransition(wct) + verify(freeformTaskTransitionHandler) + .startMinimizedModeTransition(eq(wct), eq(taskId), eq(isLastTask)) } @Test @@ -531,6 +541,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/DesktopModeVisualIndicatorTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt index 8f45cf4d08fc..20d50aa32f7f 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt @@ -23,6 +23,7 @@ import android.graphics.Rect import android.platform.test.annotations.EnableFlags import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper +import android.view.Display import android.view.SurfaceControl import androidx.test.filters.SmallTest import com.android.internal.policy.SystemBarUtils @@ -30,6 +31,7 @@ import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE import com.android.wm.shell.R import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.TestShellExecutor import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayLayout import com.android.wm.shell.common.SyncTransactionQueue @@ -60,18 +62,22 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() { private lateinit var taskInfo: RunningTaskInfo @Mock private lateinit var syncQueue: SyncTransactionQueue @Mock private lateinit var displayController: DisplayController + @Mock private lateinit var display: Display @Mock private lateinit var taskSurface: SurfaceControl @Mock private lateinit var taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer @Mock private lateinit var displayLayout: DisplayLayout @Mock private lateinit var bubbleBoundsProvider: BubbleDropTargetBoundsProvider private lateinit var visualIndicator: DesktopModeVisualIndicator + private val desktopExecutor = TestShellExecutor() + private val mainExecutor = TestShellExecutor() @Before fun setUp() { whenever(displayLayout.width()).thenReturn(DISPLAY_BOUNDS.width()) whenever(displayLayout.height()).thenReturn(DISPLAY_BOUNDS.height()) whenever(displayLayout.stableInsets()).thenReturn(STABLE_INSETS) + whenever(displayController.getDisplay(anyInt())).thenReturn(display) whenever(displayController.getDisplayLayout(anyInt())).thenReturn(displayLayout) whenever(displayController.getDisplay(anyInt())).thenReturn(mContext.display) whenever(bubbleBoundsProvider.getBubbleBarExpandedViewDropTargetBounds(any())) @@ -305,7 +311,11 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() { whenever(bubbleBoundsProvider.getBubbleBarExpandedViewDropTargetBounds(/* onLeft= */ true)) .thenReturn(dropTargetBounds) createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FULLSCREEN) + desktopExecutor.flushAll() + mainExecutor.flushAll() visualIndicator.updateIndicatorType(PointF(100f, 1500f)) + desktopExecutor.flushAll() + mainExecutor.flushAll() animatorTestRule.advanceTimeBy(200) @@ -322,7 +332,11 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() { whenever(bubbleBoundsProvider.getBubbleBarExpandedViewDropTargetBounds(/* onLeft= */ false)) .thenReturn(dropTargetBounds) createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FULLSCREEN) + desktopExecutor.flushAll() + mainExecutor.flushAll() visualIndicator.updateIndicatorType(PointF(2300f, 1500f)) + desktopExecutor.flushAll() + mainExecutor.flushAll() animatorTestRule.advanceTimeBy(200) @@ -332,6 +346,8 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() { private fun createVisualIndicator(dragStartState: DesktopModeVisualIndicator.DragStartState) { visualIndicator = DesktopModeVisualIndicator( + desktopExecutor, + mainExecutor, syncQueue, taskInfo, displayController, 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 be53272185b6..08a4bb2db1ee 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 @@ -29,6 +29,7 @@ import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED import android.content.ComponentName +import android.content.Context import android.content.Intent import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo.CONFIG_DENSITY @@ -52,6 +53,7 @@ import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization import android.testing.TestableContext +import android.view.Display import android.view.Display.DEFAULT_DISPLAY import android.view.DragEvent import android.view.Gravity @@ -149,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 @@ -177,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 @@ -210,6 +212,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Mock lateinit var shellController: ShellController @Mock lateinit var displayController: DisplayController @Mock lateinit var displayLayout: DisplayLayout + @Mock lateinit var display: Display @Mock lateinit var shellTaskOrganizer: ShellTaskOrganizer @Mock lateinit var syncQueue: SyncTransactionQueue @Mock lateinit var rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer @@ -230,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 @@ -242,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 @@ -258,6 +260,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Mock private lateinit var userProfileContexts: UserProfileContexts @Mock private lateinit var desksTransitionsObserver: DesksTransitionObserver @Mock private lateinit var packageManager: PackageManager + @Mock private lateinit var mockDisplayContext: Context private lateinit var controller: DesktopTasksController private lateinit var shellInit: ShellInit @@ -270,6 +273,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() private lateinit var spyContext: TestableContext private val shellExecutor = TestShellExecutor() + private val bgExecutor = TestShellExecutor() // Mock running tasks are registered here so we can get the list from mock shell task organizer private val runningTasks = mutableListOf<RunningTaskInfo>() @@ -326,6 +330,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() } whenever(enterDesktopTransitionHandler.moveToDesktop(any(), any())).thenAnswer { Binder() } whenever(displayController.getDisplayLayout(anyInt())).thenReturn(displayLayout) + whenever(displayController.getDisplayContext(anyInt())).thenReturn(mockDisplayContext) + whenever(displayController.getDisplay(anyInt())).thenReturn(display) whenever(displayLayout.getStableBounds(any())).thenAnswer { i -> (i.arguments.first() as Rect).set(STABLE_BOUNDS) } @@ -372,6 +378,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() recentsTransitionStateListener = captor.firstValue controller.taskbarDesktopTaskListener = taskbarDesktopTaskListener + controller.setSnapEventHandler(snapEventHandler) assumeTrue(ENABLE_SHELL_TRANSITIONS) @@ -408,13 +415,13 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() recentsTransitionHandler, multiInstanceHelper, shellExecutor, + bgExecutor, Optional.of(desktopTasksLimiter), recentTasksController, mockInteractionJankMonitor, mockHandler, desktopModeEventLogger, desktopModeUiEventLogger, - desktopTilingDecorViewModel, desktopWallpaperActivityTokenProvider, Optional.of(bubbleController), overviewToDesktopTransitionObserver, @@ -2755,13 +2762,20 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() 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() } @@ -2777,18 +2791,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()) } @@ -2812,13 +2834,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 } } @@ -2827,14 +2856,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) } @@ -2843,7 +2879,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) @@ -2851,7 +2893,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() } @@ -2862,13 +2905,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() } @@ -2880,7 +2930,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) @@ -2888,7 +2944,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) } @@ -2897,7 +2954,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) @@ -2910,7 +2973,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( @@ -4412,7 +4481,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) @@ -4450,7 +4518,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() validDragArea = Rect(0, 50, 2000, 2000), dragStartBounds = Rect(), motionEvent, - desktopWindowDecoration, ) verify(transitions) @@ -4490,7 +4557,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() validDragArea = Rect(0, 50, 2000, 2000), dragStartBounds = Rect(), motionEvent, - desktopWindowDecoration, ) verify(transitions) @@ -4531,7 +4597,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() validDragArea = Rect(0, 50, 2000, 2000), dragStartBounds = Rect(), motionEvent, - desktopWindowDecoration, ) // Assert the task exits desktop mode @@ -4569,7 +4634,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() validDragArea = Rect(0, 50, 2000, 2000), dragStartBounds = Rect(), motionEvent, - desktopWindowDecoration, ) // Assert bounds set to stable bounds @@ -4625,7 +4689,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() validDragArea = Rect(0, 50, 2000, 2000), dragStartBounds = Rect(), motionEvent, - desktopWindowDecoration, ) // Assert that task is NOT updated via WCT @@ -4857,7 +4920,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() val optionsCaptor = argumentCaptor<Bundle>() runOpenInstance(task, taskToRequest.taskId) verify(splitScreenController) - .startTask(anyInt(), anyInt(), optionsCaptor.capture(), anyOrNull()) + .startTask(anyInt(), anyInt(), optionsCaptor.capture(), anyOrNull(), any()) assertThat(ActivityOptions.fromBundle(optionsCaptor.firstValue).launchWindowingMode) .isEqualTo(WINDOWING_MODE_MULTI_WINDOW) } @@ -4871,7 +4934,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() val optionsCaptor = argumentCaptor<Bundle>() runOpenInstance(task, taskToRequest.taskId) verify(splitScreenController) - .startTask(anyInt(), anyInt(), optionsCaptor.capture(), anyOrNull()) + .startTask(anyInt(), anyInt(), optionsCaptor.capture(), anyOrNull(), any()) assertThat(ActivityOptions.fromBundle(optionsCaptor.firstValue).launchWindowingMode) .isEqualTo(WINDOWING_MODE_MULTI_WINDOW) } @@ -5045,7 +5108,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) @@ -5091,7 +5153,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()) @@ -5135,7 +5196,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() currentDragBounds, preDragBounds, motionEvent, - desktopWindowDecoration, ) val wct = getLatestToggleResizeDesktopTaskWct(currentDragBounds) assertThat(findBoundsChange(wct, task)).isEqualTo(expectedBounds) @@ -5165,7 +5225,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() currentDragBounds, preDragBounds, motionEvent, - desktopWindowDecoration, ) verify(mReturnToDragStartAnimator) .start( @@ -5190,7 +5249,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() SnapPosition.LEFT, ResizeTrigger.SNAP_LEFT_MENU, InputMethod.MOUSE, - desktopWindowDecoration, ) // Assert that task is NOT updated via WCT @@ -5217,7 +5275,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() SnapPosition.LEFT, ResizeTrigger.SNAP_LEFT_MENU, InputMethod.MOUSE, - desktopWindowDecoration, ) // Assert bounds set to half of the stable bounds 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/VisualIndicatorViewContainerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/VisualIndicatorViewContainerTest.kt new file mode 100644 index 000000000000..79b0f1c7eadd --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/VisualIndicatorViewContainerTest.kt @@ -0,0 +1,250 @@ +/* + * 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 + +import android.app.ActivityManager +import android.app.ActivityManager.RunningTaskInfo +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.graphics.drawable.LayerDrawable +import android.platform.test.annotations.EnableFlags +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper.RunWithLooper +import android.view.Display +import android.view.SurfaceControl +import android.view.SurfaceControlViewHost +import android.view.View +import androidx.test.filters.SmallTest +import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.TestRunningTaskInfoBuilder +import com.android.wm.shell.TestShellExecutor +import com.android.wm.shell.common.DisplayController +import com.android.wm.shell.common.DisplayLayout +import com.android.wm.shell.common.SyncTransactionQueue +import com.android.wm.shell.shared.bubbles.BubbleDropTargetBoundsProvider +import com.android.wm.shell.windowdecor.WindowDecoration.SurfaceControlViewHostFactory +import com.google.common.truth.Truth.assertThat +import kotlin.test.Test +import org.junit.Before +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mock +import org.mockito.Mockito.mock +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.eq +import org.mockito.kotlin.spy +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyZeroInteractions +import org.mockito.kotlin.whenever + +/** + * Test class for [VisualIndicatorViewContainer] and [VisualIndicatorAnimator] + * + * Usage: atest WMShellUnitTests:VisualIndicatorViewContainerTest + */ +@SmallTest +@RunWithLooper +@RunWith(AndroidTestingRunner::class) +@EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) +class VisualIndicatorViewContainerTest : ShellTestCase() { + @Mock private lateinit var view: View + @Mock private lateinit var displayLayout: DisplayLayout + @Mock private lateinit var displayController: DisplayController + @Mock private lateinit var taskSurface: SurfaceControl + @Mock private lateinit var syncQueue: SyncTransactionQueue + @Mock private lateinit var mockSurfaceControlViewHostFactory: SurfaceControlViewHostFactory + @Mock private lateinit var mockBackground: LayerDrawable + @Mock private lateinit var bubbleDropTargetBoundsProvider: BubbleDropTargetBoundsProvider + private val taskInfo: RunningTaskInfo = createTaskInfo() + private val mainExecutor = TestShellExecutor() + private val desktopExecutor = TestShellExecutor() + + @Before + fun setUp() { + whenever(displayController.getDisplayLayout(anyInt())).thenReturn(displayLayout) + whenever(displayLayout.getStableBounds(any())).thenAnswer { i -> + (i.arguments.first() as Rect).set(DISPLAY_BOUNDS) + } + whenever(mockSurfaceControlViewHostFactory.create(any(), any(), any())) + .thenReturn(mock(SurfaceControlViewHost::class.java)) + } + + @Test + fun testTransitionIndicator_sameTypeReturnsEarly() { + val spyViewContainer = setupSpyViewContainer() + // Test early return on startType == endType. + spyViewContainer.transitionIndicator( + taskInfo, + displayController, + DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR, + DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR, + ) + desktopExecutor.flushAll() + verify(spyViewContainer) + .transitionIndicator( + eq(taskInfo), + eq(displayController), + eq(DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR), + eq(DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR), + ) + // Assert fadeIn, fadeOut, and animateIndicatorType were not called. + verifyZeroInteractions(spyViewContainer) + } + + @Test + fun testTransitionIndicator_firstTypeNoIndicator_callsFadeIn() { + val spyViewContainer = setupSpyViewContainer() + spyViewContainer.transitionIndicator( + taskInfo, + displayController, + DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR, + DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR, + ) + desktopExecutor.flushAll() + verify(spyViewContainer).fadeInIndicator(any(), any()) + } + + @Test + fun testTransitionIndicator_secondTypeNoIndicator_callsFadeOut() { + val spyViewContainer = setupSpyViewContainer() + spyViewContainer.transitionIndicator( + taskInfo, + displayController, + DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR, + DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR, + ) + desktopExecutor.flushAll() + verify(spyViewContainer) + .fadeOutIndicator( + any(), + eq(DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR), + anyOrNull(), + ) + } + + @Test + fun testTransitionIndicator_differentTypes_callsTransitionIndicator() { + val spyViewContainer = setupSpyViewContainer() + spyViewContainer.transitionIndicator( + taskInfo, + displayController, + DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR, + DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR, + ) + desktopExecutor.flushAll() + verify(spyViewContainer) + .transitionIndicator( + any(), + any(), + eq(DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR), + eq(DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR), + ) + } + + @Test + fun testFadeInBoundsCalculation() { + val spyIndicator = setupSpyViewContainer() + val animator = + spyIndicator.indicatorView?.let { + VisualIndicatorViewContainer.VisualIndicatorAnimator.fadeBoundsIn( + it, + DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR, + displayLayout, + bubbleDropTargetBoundsProvider, + ) + } + assertThat(animator?.indicatorStartBounds).isEqualTo(Rect(15, 15, 985, 985)) + assertThat(animator?.indicatorEndBounds).isEqualTo(Rect(0, 0, 1000, 1000)) + } + + @Test + fun testFadeOutBoundsCalculation() { + val spyIndicator = setupSpyViewContainer() + val animator = + spyIndicator.indicatorView?.let { + VisualIndicatorViewContainer.VisualIndicatorAnimator.fadeBoundsOut( + it, + DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR, + displayLayout, + bubbleDropTargetBoundsProvider, + ) + } + assertThat(animator?.indicatorStartBounds).isEqualTo(Rect(0, 0, 1000, 1000)) + assertThat(animator?.indicatorEndBounds).isEqualTo(Rect(15, 15, 985, 985)) + } + + @Test + fun testChangeIndicatorTypeBoundsCalculation() { + // Test fullscreen to split-left bounds. + var animator = + VisualIndicatorViewContainer.VisualIndicatorAnimator.animateIndicatorType( + view, + displayLayout, + DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR, + DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR, + bubbleDropTargetBoundsProvider, + ) + // Test desktop to split-right bounds. + animator = + VisualIndicatorViewContainer.VisualIndicatorAnimator.animateIndicatorType( + view, + displayLayout, + DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR, + DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR, + bubbleDropTargetBoundsProvider, + ) + } + + private fun setupSpyViewContainer(): VisualIndicatorViewContainer { + val viewContainer = + VisualIndicatorViewContainer( + desktopExecutor, + mainExecutor, + SurfaceControl.Builder(), + syncQueue, + mockSurfaceControlViewHostFactory, + bubbleDropTargetBoundsProvider, + ) + viewContainer.createView( + context, + mock(Display::class.java), + displayLayout, + taskInfo, + taskSurface, + ) + desktopExecutor.flushAll() + viewContainer.indicatorView?.background = mockBackground + whenever(mockBackground.findDrawableByLayerId(anyInt())) + .thenReturn(mock(Drawable::class.java)) + return spy(viewContainer) + } + + private fun createTaskInfo(): RunningTaskInfo { + val taskDescriptionBuilder = ActivityManager.TaskDescription.Builder() + return TestRunningTaskInfoBuilder() + .setDisplayId(Display.DEFAULT_DISPLAY) + .setTaskDescriptionBuilder(taskDescriptionBuilder) + .setVisible(true) + .build() + } + + companion object { + private val DISPLAY_BOUNDS = Rect(0, 0, 1000, 1000) + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java index 439be9155b26..fd5e567f69ed 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java @@ -34,6 +34,7 @@ import static com.google.common.truth.Truth.assertThat; 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.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; @@ -293,6 +294,31 @@ public class RecentsTransitionHandlerTest extends ShellTestCase { @Test @EnableFlags(FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX) + public void testMerge_openingTasks_callsOnTasksAppeared() throws Exception { + final IRecentsAnimationRunner animationRunner = mock(IRecentsAnimationRunner.class); + TransitionInfo mergeTransitionInfo = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN, new TestRunningTaskInfoBuilder().build()) + .build(); + final IBinder transition = startRecentsTransition(/* synthetic= */ false, animationRunner); + SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); + mRecentsTransitionHandler.startAnimation( + transition, createTransitionInfo(), new StubTransaction(), new StubTransaction(), + mock(Transitions.TransitionFinishCallback.class)); + + mRecentsTransitionHandler.findController(transition).merge( + mergeTransitionInfo, + new StubTransaction(), + finishT, + transition, + mock(Transitions.TransitionFinishCallback.class)); + mMainExecutor.flushAll(); + + verify(animationRunner).onTasksAppeared( + /* appearedTargets= */ any(), eq(mergeTransitionInfo)); + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX) public void testMergeAndFinish_openingFreeformTasks_setsCornerRadius() { ActivityManager.RunningTaskInfo freeformTask = new TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build(); 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..741a0fdcf63c 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 @@ -157,23 +157,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 +200,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) 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/MultiDisplayVeiledResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositionerTest.kt index 63babc5e1c3c..937938df82c8 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositionerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositionerTest.kt @@ -249,6 +249,25 @@ class MultiDisplayVeiledResizeTaskPositionerTest : ShellTestCase() { } @Test + fun testDragResize_movesTaskOnSameDisplay_noPxDpConversion() = runOnUiThread { + taskPositioner.onDragPositioningStart( + CTRL_TYPE_UNDEFINED, + DISPLAY_ID_0, + STARTING_BOUNDS.left.toFloat(), + STARTING_BOUNDS.top.toFloat(), + ) + + taskPositioner.onDragPositioningEnd( + DISPLAY_ID_0, + STARTING_BOUNDS.left.toFloat() + 70, + STARTING_BOUNDS.top.toFloat() + 20, + ) + + verify(spyDisplayLayout0, never()).localPxToGlobalDp(any(), any()) + verify(spyDisplayLayout0, never()).globalDpToLocalPx(any(), any()) + } + + @Test fun testDragResize_movesTaskToNewDisplay() = runOnUiThread { taskPositioner.onDragPositioningStart( CTRL_TYPE_UNDEFINED, 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/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/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..7e1f2e2a3490 100644 --- a/libs/hwui/aconfig/hwui_flags.aconfig +++ b/libs/hwui/aconfig/hwui_flags.aconfig @@ -174,7 +174,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 +187,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/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/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/jni/android_media_MediaPlayer.cpp b/media/jni/android_media_MediaPlayer.cpp index 647b55353257..9d197f48ed8d 100644 --- a/media/jni/android_media_MediaPlayer.cpp +++ b/media/jni/android_media_MediaPlayer.cpp @@ -1378,7 +1378,7 @@ static jintArray android_media_MediaPlayer_getRoutedDeviceIds(JNIEnv *env, jobje } jint* values = env->GetIntArrayElements(result, 0); for (unsigned int i = 0; i < deviceIds.size(); i++) { - values[i++] = static_cast<jint>(deviceIds[i]); + values[i] = static_cast<jint>(deviceIds[i]); } env->ReleaseIntArrayElements(result, values, 0); return result; diff --git a/media/jni/android_media_MediaRecorder.cpp b/media/jni/android_media_MediaRecorder.cpp index 643fc8a2d925..2975a39c5fa5 100644 --- a/media/jni/android_media_MediaRecorder.cpp +++ b/media/jni/android_media_MediaRecorder.cpp @@ -743,7 +743,7 @@ android_media_MediaRecorder_getRoutedDeviceIds(JNIEnv *env, jobject thiz) } jint* values = env->GetIntArrayElements(result, 0); for (unsigned int i = 0; i < deviceIds.size(); i++) { - values[i++] = static_cast<jint>(deviceIds[i]); + values[i] = static_cast<jint>(deviceIds[i]); } env->ReleaseIntArrayElements(result, values, 0); return result; diff --git a/nfc-extras/OWNERS b/nfc-extras/OWNERS index 35e9713f5715..2b82bc8ad073 100644 --- a/nfc-extras/OWNERS +++ b/nfc-extras/OWNERS @@ -1,2 +1,2 @@ # Bug component: 48448 -include platform/packages/apps/Nfc:/OWNERS +include platform/packages/modules/Nfc:/OWNERS diff --git a/nfc-non-updatable/java/android/nfc/cardemulation/ApduServiceInfo.java b/nfc-non-updatable/java/android/nfc/cardemulation/ApduServiceInfo.java index d0de1fc14b0e..f2a68afbfcbe 100644 --- a/nfc-non-updatable/java/android/nfc/cardemulation/ApduServiceInfo.java +++ b/nfc-non-updatable/java/android/nfc/cardemulation/ApduServiceInfo.java @@ -70,6 +70,11 @@ import java.util.regex.Pattern; public final class ApduServiceInfo implements Parcelable { private static final String TAG = "ApduServiceInfo"; + private static final Pattern PLPF_PATTERN = + Pattern.compile("[0-9A-Fa-f]{2,}[0-9A-Fa-f,\\?,\\*\\.]*"); + private static final Pattern PLF_PATTERN = + Pattern.compile("[0-9A-Fa-f]{2,}"); + /** * Component level {@link android.content.pm.PackageManager.Property PackageManager * .Property} for a system application to change its icon and label @@ -472,7 +477,12 @@ public final class ApduServiceInfo implements Parcelable { boolean autoTransact = a.getBoolean( com.android.internal.R.styleable.PollingLoopFilter_autoTransact, false); - if (!mOnHost && !autoTransact) { + boolean isValidFilter = PLF_PATTERN.matcher(plf).matches() + && plf.length() % 2 == 0; + if (!isValidFilter) { + Log.e(TAG, "Ignoring polling-loop-filter " + plf + + " it is not a valid filter"); + } else if (!mOnHost && !autoTransact) { Log.e(TAG, "Ignoring polling-loop-filter " + plf + " for offhost service that isn't autoTransact"); } else { @@ -489,8 +499,12 @@ public final class ApduServiceInfo implements Parcelable { boolean autoTransact = a.getBoolean( com.android.internal.R.styleable.PollingLoopFilter_autoTransact, false); - if (!mOnHost && !autoTransact) { - Log.e(TAG, "Ignoring polling-loop-filter " + plf + boolean isValidFilter = PLPF_PATTERN.matcher(plf).matches(); + if (!isValidFilter) { + Log.e(TAG, "Ignoring polling-loop-pattern-filter " + plf + + " it is not a valid pattern filter"); + } else if (!mOnHost && !autoTransact) { + Log.e(TAG, "Ignoring polling-loop-pattern-filter " + plf + " for offhost service that isn't autoTransact"); } else { mAutoTransactPatterns.put(Pattern.compile(plf), autoTransact); @@ -814,6 +828,12 @@ public final class ApduServiceInfo implements Parcelable { @FlaggedApi(Flags.FLAG_NFC_READ_POLLING_LOOP) public void addPollingLoopFilter(@NonNull String pollingLoopFilter, boolean autoTransact) { + if (!PLF_PATTERN.matcher(pollingLoopFilter).matches() + || pollingLoopFilter.length() % 2 != 0) { + throw new IllegalArgumentException( + "Polling loop filter must contain an even number of characters 0-9 or A-F" + ); + } if (!mOnHost && !autoTransact) { return; } @@ -842,6 +862,11 @@ public final class ApduServiceInfo implements Parcelable { @FlaggedApi(Flags.FLAG_NFC_READ_POLLING_LOOP) public void addPollingLoopPatternFilter(@NonNull String pollingLoopPatternFilter, boolean autoTransact) { + if (!PLPF_PATTERN.matcher(pollingLoopPatternFilter).matches()) { + throw new IllegalArgumentException( + "Polling loop pattern filter is invalid" + ); + } if (!mOnHost && !autoTransact) { return; } diff --git a/omapi/OWNERS b/omapi/OWNERS index 39c5c5bdeb09..375d643f7ed1 100644 --- a/omapi/OWNERS +++ b/omapi/OWNERS @@ -1,2 +1,2 @@ # Bug component: 456592 -include platform/packages/apps/Nfc:/OWNERS +include platform/packages/modules/Nfc:/OWNERS diff --git a/omapi/java/android/se/OWNERS b/omapi/java/android/se/OWNERS index 39c5c5bdeb09..375d643f7ed1 100644 --- a/omapi/java/android/se/OWNERS +++ b/omapi/java/android/se/OWNERS @@ -1,2 +1,2 @@ # Bug component: 456592 -include platform/packages/apps/Nfc:/OWNERS +include platform/packages/modules/Nfc:/OWNERS diff --git a/omapi/java/android/se/omapi/OWNERS b/omapi/java/android/se/omapi/OWNERS index 39c5c5bdeb09..375d643f7ed1 100644 --- a/omapi/java/android/se/omapi/OWNERS +++ b/omapi/java/android/se/omapi/OWNERS @@ -1,2 +1,2 @@ # Bug component: 456592 -include platform/packages/apps/Nfc:/OWNERS +include platform/packages/modules/Nfc:/OWNERS diff --git a/packages/SettingsLib/IntroPreference/res/layout/settingslib_expressive_preference_intro.xml b/packages/SettingsLib/IntroPreference/res/layout/settingslib_expressive_preference_intro.xml index 7adcbf6c6601..cacd2740818c 100644 --- a/packages/SettingsLib/IntroPreference/res/layout/settingslib_expressive_preference_intro.xml +++ b/packages/SettingsLib/IntroPreference/res/layout/settingslib_expressive_preference_intro.xml @@ -17,6 +17,7 @@ <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/entity_header" android:layout_width="match_parent" android:layout_height="wrap_content" @@ -38,7 +39,9 @@ android:id="@+id/collapsable_summary" android:layout_width="match_parent" android:layout_height="wrap_content" - android:gravity="center"/> + android:gravity="center" + android:minLines="1" + app:isCollapsable="true"/> </LinearLayout> diff --git a/packages/SettingsLib/IntroPreference/src/com/android/settingslib/widget/IntroPreference.kt b/packages/SettingsLib/IntroPreference/src/com/android/settingslib/widget/IntroPreference.kt index f9931cf3238d..9d037e91a86f 100644 --- a/packages/SettingsLib/IntroPreference/src/com/android/settingslib/widget/IntroPreference.kt +++ b/packages/SettingsLib/IntroPreference/src/com/android/settingslib/widget/IntroPreference.kt @@ -33,8 +33,8 @@ class IntroPreference @JvmOverloads constructor( defStyleRes: Int = 0 ) : Preference(context, attrs, defStyleAttr, defStyleRes), GroupSectionDividerMixin { - private var isCollapsable: Boolean = false - private var minLines: Int = 2 + private var isCollapsable: Boolean = true + private var minLines: Int = DEFAULT_MIN_LINES private var hyperlinkListener: View.OnClickListener? = null private var learnMoreListener: View.OnClickListener? = null private var learnMoreText: CharSequence? = null @@ -42,22 +42,6 @@ class IntroPreference @JvmOverloads constructor( init { layoutResource = R.layout.settingslib_expressive_preference_intro isSelectable = false - - initAttributes(context, attrs, defStyleAttr) - } - - private fun initAttributes(context: Context, attrs: AttributeSet?, defStyleAttr: Int) { - context.obtainStyledAttributes( - attrs, - COLLAPSABLE_TEXT_VIEW_ATTRS, defStyleAttr, 0 - ).apply { - isCollapsable = getBoolean(IS_COLLAPSABLE, false) - minLines = getInt( - MIN_LINES, - if (isCollapsable) DEFAULT_MIN_LINES else DEFAULT_MAX_LINES - ).coerceIn(1, DEFAULT_MAX_LINES) - recycle() - } } override fun onBindViewHolder(holder: PreferenceViewHolder) { @@ -139,13 +123,6 @@ class IntroPreference @JvmOverloads constructor( companion object { private const val DEFAULT_MAX_LINES = 10 - private const val DEFAULT_MIN_LINES = 2 - - private val COLLAPSABLE_TEXT_VIEW_ATTRS = - com.android.settingslib.widget.theme.R.styleable.CollapsableTextView - private val MIN_LINES = - com.android.settingslib.widget.theme.R.styleable.CollapsableTextView_android_minLines - private val IS_COLLAPSABLE = - com.android.settingslib.widget.theme.R.styleable.CollapsableTextView_isCollapsable + private const val DEFAULT_MIN_LINES = 1 } -}
\ No newline at end of file +} 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/CollapsableTextView.kt b/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/CollapsableTextView.kt index 7eb9840e3e98..976711bdc5f3 100644 --- a/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/CollapsableTextView.kt +++ b/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/CollapsableTextView.kt @@ -40,7 +40,7 @@ class CollapsableTextView @JvmOverloads constructor( defStyleAttr: Int = 0 ) : ConstraintLayout(context, attrs, defStyleAttr) { - private var isCollapsable: Boolean = false + private var isCollapsable: Boolean = DEFAULT_COLLAPSABLE private var isCollapsed: Boolean = false private var minLines: Int = DEFAULT_MIN_LINES @@ -78,24 +78,38 @@ class CollapsableTextView @JvmOverloads constructor( private fun initAttributes(context: Context, attrs: AttributeSet?, defStyleAttr: Int) { context.obtainStyledAttributes( - attrs, Attrs, defStyleAttr, 0 + attrs, R.styleable.CollapsableTextView, defStyleAttr, 0 ).apply { - val gravity = getInt(GravityAttr, Gravity.START) + val gravity = getInt(gravityAttr, Gravity.START) when (gravity) { Gravity.CENTER_VERTICAL, Gravity.CENTER, Gravity.CENTER_HORIZONTAL -> { centerHorizontally(titleTextView) centerHorizontally(collapseButton) } } + isCollapsable = getBoolean(isCollapsableAttr, DEFAULT_COLLAPSABLE) + minLines = getInt(minLinesAttr, DEFAULT_MIN_LINES) recycle() } } private fun centerHorizontally(view: View) { - (view.layoutParams as LayoutParams).apply { - startToStart = LayoutParams.PARENT_ID - endToEnd = LayoutParams.PARENT_ID - horizontalBias = 0.5f + when (view) { + is MaterialButton -> { + (view.layoutParams as LayoutParams).apply { + startToStart = LayoutParams.PARENT_ID + endToEnd = LayoutParams.PARENT_ID + } + } + is TextView -> { + view.gravity = Gravity.CENTER + } + else -> { + (view.layoutParams as LayoutParams).apply { + startToStart = LayoutParams.PARENT_ID + endToEnd = LayoutParams.PARENT_ID + } + } } } @@ -113,6 +127,8 @@ class CollapsableTextView @JvmOverloads constructor( */ fun setCollapsable(collapsable: Boolean) { isCollapsable = collapsable + // Make is collapsed when it's collapsable + if (isCollapsable) isCollapsed = true updateView() } @@ -120,8 +136,8 @@ class CollapsableTextView @JvmOverloads constructor( * Sets the minimum number of lines to display when collapsed. * @param lines The minimum number of lines. */ - fun setMinLines(line: Int) { - minLines = line.coerceIn(1, DEFAULT_MAX_LINES) + fun setMinLines(lines: Int) { + minLines = lines.coerceIn(1, DEFAULT_MAX_LINES) updateView() } @@ -198,7 +214,7 @@ class CollapsableTextView @JvmOverloads constructor( } learnMoreSpan = LearnMoreSpan(clickListener = learnMoreListener!!) spannableLearnMoreText.setSpan(learnMoreSpan, 0, learnMoreText!!.length, 0) - learnMoreTextView.setText(spannableLearnMoreText) + learnMoreTextView.text = spannableLearnMoreText learnMoreTextView.visibility = VISIBLE isLearnMoreEnabled = true } @@ -211,6 +227,8 @@ class CollapsableTextView @JvmOverloads constructor( icon = collapseButtonResources.expandIcon } titleTextView.maxLines = minLines + titleTextView.ellipsize = null + titleTextView.scrollBarSize = 0 } else -> { @@ -219,6 +237,7 @@ class CollapsableTextView @JvmOverloads constructor( icon = collapseButtonResources.collapseIcon } titleTextView.maxLines = DEFAULT_MAX_LINES + titleTextView.ellipsize = TextUtils.TruncateAt.END } } collapseButton.visibility = if (isCollapsable) VISIBLE else GONE @@ -235,17 +254,19 @@ class CollapsableTextView @JvmOverloads constructor( companion object { private const val DEFAULT_MAX_LINES = 10 private const val DEFAULT_MIN_LINES = 2 + private const val DEFAULT_COLLAPSABLE = true private const val LINK_BEGIN_MARKER = "LINK_BEGIN" private const val LINK_END_MARKER = "LINK_END" - private val Attrs = R.styleable.CollapsableTextView - private val GravityAttr = R.styleable.CollapsableTextView_android_gravity + private val gravityAttr = R.styleable.CollapsableTextView_android_gravity + private val minLinesAttr = R.styleable.CollapsableTextView_android_minLines + private val isCollapsableAttr = R.styleable.CollapsableTextView_isCollapsable } } internal class LearnMoreSpan( - val url: String = "", + url: String = "", val clickListener: View.OnClickListener) : URLSpan(url) { override fun onClick(widget: View) { clickListener.onClick(widget) diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml index a3e42f1d1e51..4b0400fb3441 100644 --- a/packages/SettingsLib/res/values/strings.xml +++ b/packages/SettingsLib/res/values/strings.xml @@ -236,6 +236,14 @@ <string name="bluetooth_active_media_only_battery_level">Active (media only). <xliff:g id="battery_level_as_percentage">%1$s</xliff:g> battery.</string> <!-- Connected devices settings. Message when Bluetooth is connected and active for media only, showing remote device status and battery level for untethered headset. [CHAR LIMIT=NONE] --> <string name="bluetooth_active_media_only_battery_level_untethered">Active (media only). L: <xliff:g id="battery_level_as_percentage" example="25%">%1$s</xliff:g>, R: <xliff:g id="battery_level_as_percentage" example="25%">%2$s</xliff:g> battery.</string> + <!-- Connected devices settings. Message when Bluetooth is connected but not in use, showing remote device status and battery level for temporary bond device. [CHAR LIMIT=NONE] --> + <string name="bluetooth_guest_battery_level">Guest device. <xliff:g id="battery_level_as_percentage">%1$s</xliff:g> battery.</string> + <!-- Connected devices settings. Message when Bluetooth is connected but not in use, showing remote device status and battery level for temporary bond untethered headset. [CHAR LIMIT=NONE] --> + <string name="bluetooth_guest_battery_level_untethered">Guest device. L: <xliff:g id="battery_level_as_percentage" example="25%">%1$s</xliff:g>, R: <xliff:g id="battery_level_as_percentage" example="25%">%2$s</xliff:g> battery.</string> + <!-- Connected devices settings. Message when Bluetooth is connected and active for media only, showing remote device status and battery level for temporary bond device. [CHAR LIMIT=NONE] --> + <string name="bluetooth_guest_media_only_battery_level">Guest device (media only). <xliff:g id="battery_level_as_percentage">%1$s</xliff:g> battery.</string> + <!-- Connected devices settings. Message when Bluetooth is connected and active for media only, showing remote device status and battery level for temporary bond untethered headset. [CHAR LIMIT=NONE] --> + <string name="bluetooth_guest_media_only_battery_level_untethered">Guest device (media only). L: <xliff:g id="battery_level_as_percentage" example="25%">%1$s</xliff:g>, R: <xliff:g id="battery_level_as_percentage" example="25%">%2$s</xliff:g> battery.</string> <!-- Connected devices settings. Message when Bluetooth is connected but not in use, showing remote device battery level, supports audio sharing. [CHAR LIMIT=NONE] --> <string name="bluetooth_battery_level_lea_support">Connected (supports audio sharing). <xliff:g id="battery_level_as_percentage">%1$s</xliff:g> battery.</string> <!-- Connected devices settings. Message when Bluetooth is connected but not in use, showing remote device battery level for untethered headset, supports audio sharing. [CHAR LIMIT=NONE] --> @@ -246,10 +254,22 @@ <string name="bluetooth_battery_level_untethered_right_lea_support">Connected (supports audio sharing). Right: <xliff:g id="battery_level_as_percentage" example="25%">%1$s</xliff:g> battery.</string> <!-- Connected devices settings. Message when Bluetooth is connected but not in use, showing no battery information, supports audio sharing. [CHAR LIMIT=NONE] --> <string name="bluetooth_no_battery_level_lea_support">Connected (supports audio sharing)</string> + <!-- Connected devices settings. Message when Bluetooth is connected but not in use, showing remote device battery level, supports audio sharing. [CHAR LIMIT=NONE] --> + <string name="bluetooth_guest_battery_level_lea_support">Guest device (supports audio sharing). <xliff:g id="battery_level_as_percentage">%1$s</xliff:g> battery.</string> + <!-- Connected devices settings. Message when Bluetooth is connected but not in use, showing remote device battery level for untethered headset, supports audio sharing. [CHAR LIMIT=NONE] --> + <string name="bluetooth_guest_battery_level_untethered_lea_support">Guest device (supports audio sharing). L: <xliff:g id="battery_level_as_percentage" example="25%">%1$s</xliff:g>, R: <xliff:g id="battery_level_as_percentage" example="25%">%2$s</xliff:g> battery.</string> + <!-- Connected devices settings. Message when Bluetooth is connected but not in use, showing no battery information, supports audio sharing. [CHAR LIMIT=NONE] --> + <string name="bluetooth_guest_no_battery_level_lea_support">Guest device (supports audio sharing)</string> <!-- Connected devices settings. Message when Bluetooth is connected and active for media only but no battery information, showing remote device status. [CHAR LIMIT=NONE] --> <string name="bluetooth_active_media_only_no_battery_level">Active (media only)</string> + <!-- Connected devices settings. Message when Bluetooth is connected but not in use with no battery information, showing remote device status for temporary bond device. [CHAR LIMIT=NONE] --> + <string name="bluetooth_guest_no_battery_level">Guest device</string> + <!-- Connected devices settings. Message when Bluetooth is connected and active for media only but no battery information, showing remote device status for temporary bond device. [CHAR LIMIT=NONE] --> + <string name="bluetooth_guest_media_only_no_battery_level">Guest device (media only)</string> <!-- Connected devices settings. Message shown when bluetooth device is disconnected but is a known, previously connected device, supports audio sharing [CHAR LIMIT=NONE] --> <string name="bluetooth_saved_device_lea_support">Supports audio sharing</string> + <!-- Connected devices settings. Message shown when temporary bond bluetooth device is disconnected but is a known, previously connected device, supports audio sharing [CHAR LIMIT=NONE] --> + <string name="bluetooth_guest_saved_device_lea_support">Guest device. Supports audio sharing</string> <!-- Connected device settings. Message when the left-side hearing aid device is active for media only. [CHAR LIMIT=NONE] --> <string name="bluetooth_hearing_aid_media_only_left_active">Active (media only), left only</string> @@ -1103,6 +1123,13 @@ <!-- Developer settings: text for the WebView provider selection toast shown if an invalid provider was chosen (i.e. the setting list was stale). [CHAR LIMIT=NONE] --> <string name="select_webview_provider_toast_text">This choice is no longer valid. Try again.</string> + <!-- [CHAR LIMIT=50] Label for button to launch current WebView provider's DevTools (developer tools) UI --> + <string name="webview_launch_devtools_title">WebView DevTools</string> + <!-- [CHAR LIMIT=50] Error toast shown to user when no (current) WebView package is found (when trying to launch DevTools) --> + <string name="webview_launch_devtools_no_package">WebView package not found.</string> + <!-- [CHAR LIMIT=50] Error toast shown to user when DevTools (developer tools) activity could not be launched (despite there being a WebView package) --> + <string name="webview_launch_devtools_no_activity">Could not launch DevTools.</string> + <!-- Name of feature to change color setting for the display [CHAR LIMIT=60] --> <string name="picture_color_mode">Picture color mode</string> diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java index 7374f80fd9db..97a345efd566 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, 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/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java index 4f5203136bbc..ed11e12c32ff 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java @@ -209,7 +209,7 @@ public class SettingsHelper { // Get datatype for B&R metrics logging. String datatype = ""; - if (Flags.enableMetricsSettingsBackupAgents()) { + if (areAgentMetricsEnabled()) { datatype = SettingsBackupRestoreKeys.getKeyFromUri(destination); } @@ -299,7 +299,7 @@ public class SettingsHelper { contentValues.put(Settings.NameValueTable.NAME, name); contentValues.put(Settings.NameValueTable.VALUE, value); cr.insert(destination, contentValues); - if (Flags.enableMetricsSettingsBackupAgents()) { + if (areAgentMetricsEnabled()) { mBackupRestoreEventLogger.logItemsRestored(datatype, /* count= */ 1); } } catch (Exception e) { @@ -308,7 +308,7 @@ public class SettingsHelper { sendBroadcastSystemUI = false; sendBroadcastAccessibility = false; Log.e(TAG, "Failed to restore setting name: " + name + " + value: " + value, e); - if (Flags.enableMetricsSettingsBackupAgents()) { + if (areAgentMetricsEnabled()) { mBackupRestoreEventLogger.logItemsRestoreFailed( datatype, /* count= */ 1, ERROR_FAILED_TO_RESTORE_SETTING); } @@ -785,12 +785,12 @@ public class SettingsHelper { am.updatePersistentConfigurationWithAttribution(config, mContext.getOpPackageName(), mContext.getAttributionTag()); - if (Flags.enableMetricsSettingsBackupAgents() && mBackupRestoreEventLogger != null) { + if (areAgentMetricsEnabled()) { mBackupRestoreEventLogger .logItemsRestored(SettingsBackupRestoreKeys.KEY_LOCALE, localeList.size()); } } catch (RemoteException e) { - if (Flags.enableMetricsSettingsBackupAgents() && mBackupRestoreEventLogger != null) { + if (areAgentMetricsEnabled()) { mBackupRestoreEventLogger .logItemsRestoreFailed( SettingsBackupRestoreKeys.KEY_LOCALE, @@ -817,4 +817,8 @@ public class SettingsHelper { void setBackupRestoreEventLogger(BackupRestoreEventLogger backupRestoreEventLogger) { mBackupRestoreEventLogger = backupRestoreEventLogger; } + + private boolean areAgentMetricsEnabled() { + return Flags.enableMetricsSettingsBackupAgents() && mBackupRestoreEventLogger != null; + } } diff --git a/packages/SettingsProvider/src/com/android/providers/settings/WritableNamespacePrefixes.java b/packages/SettingsProvider/src/com/android/providers/settings/WritableNamespacePrefixes.java index 74fd828f97ea..274fd3593918 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/WritableNamespacePrefixes.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/WritableNamespacePrefixes.java @@ -124,6 +124,7 @@ final class WritableNamespacePrefixes { "privacy", "private_compute_services", "profcollect_native_boot", + "profiling_testing", "remote_auth", "remote_key_provisioning_native", "rollback", diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperRestoreTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperRestoreTest.java index 62c03ddc42b9..cb3f54e3c6be 100644 --- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperRestoreTest.java +++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperRestoreTest.java @@ -20,6 +20,8 @@ import static com.google.common.truth.Truth.assertThat; import static junit.framework.Assert.assertEquals; +import android.app.backup.BackupAnnotations.OperationType; +import android.app.backup.BackupRestoreEventLogger; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; @@ -60,11 +62,14 @@ public class SettingsHelperRestoreTest { private static final float FLOAT_TOLERANCE = 0.01f; private ContentResolver mContentResolver; private SettingsHelper mSettingsHelper; + private BackupRestoreEventLogger mBackupRestoreEventLogger; @Before public void setUp() { mContentResolver = mInterceptingContext.getContentResolver(); mSettingsHelper = new SettingsHelper(mInterceptingContext); + mBackupRestoreEventLogger = new BackupRestoreEventLogger(OperationType.RESTORE); + mSettingsHelper.setBackupRestoreEventLogger(mBackupRestoreEventLogger); } @After diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index 19806e7cdf64..49cdec11e104 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -60,6 +60,7 @@ filegroup { "src-release/**/*.kt", "src-release/**/*.java", ], + path: "src-release", visibility: ["//visibility:private"], } @@ -69,6 +70,7 @@ filegroup { "src-debug/**/*.kt", "src-debug/**/*.java", ], + path: "src-debug", visibility: ["//visibility:private"], } @@ -315,11 +317,11 @@ filegroup { "tests/src/**/systemui/temporarydisplay/TemporaryViewDisplayControllerTest.kt", "tests/src/**/systemui/statusbar/policy/WalletControllerImplTest.kt", "tests/src/**/keyguard/ClockEventControllerTest.kt", - "tests/src/**/systemui/bluetooth/qsdialog/BluetoothStateInteractorTest.kt", "tests/src/**/systemui/bluetooth/qsdialog/BluetoothDetailsContentManagerTest.kt", + "tests/src/**/systemui/bluetooth/qsdialog/BluetoothDetailsContentViewModelTest.kt", + "tests/src/**/systemui/bluetooth/qsdialog/BluetoothStateInteractorTest.kt", "tests/src/**/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt", "tests/src/**/systemui/bluetooth/qsdialog/BluetoothTileDialogRepositoryTest.kt", - "tests/src/**/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt", "tests/src/**/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt", "tests/src/**/systemui/bluetooth/qsdialog/DeviceItemInteractorTest.kt", "tests/src/**/systemui/broadcast/UserBroadcastDispatcherTest.kt", @@ -430,12 +432,21 @@ android_library { ], } -android_library { - name: "SystemUI-core", +filegroup { + name: "SystemUI-core-srcs", srcs: [ "src/**/*.kt", "src/**/*.java", "src/**/I*.aidl", + ], + path: "src", + visibility: ["//visibility:private"], +} + +android_library { + name: "SystemUI-core", + srcs: [ + ":SystemUI-core-srcs", ":ReleaseJavaFiles", "compose/features/src/**/*.kt", "compose/facade/enabled/src/**/*.kt", @@ -731,9 +742,7 @@ android_library { srcs: [ "tests/src/**/*.kt", "tests/src/**/*.java", - "src/**/*.kt", - "src/**/*.java", - "src/**/I*.aidl", + ":SystemUI-core-srcs", ":ReleaseJavaFiles", ":SystemUI-tests-multivalent", ":SystemUI-tests-utils", @@ -841,7 +850,7 @@ java_library { "androidx.test.uiautomator_uiautomator", "androidx.core_core-animation-testing", "androidx.test.ext.junit", - "inline-mockito-robolectric-prebuilt", + "inline-mockito5-robolectric-prebuilt", "mockito-kotlin-nodeps", "platform-parametric-runner-lib", "SystemUICustomizationTestUtils", diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 910f71276376..0ccb20ce3e3f 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" @@ -957,16 +964,6 @@ flag { } flag { - name: "dedicated_notif_inflation_thread" - namespace: "systemui" - description: "Create a separate background thread for inflating notifications" - bug: "308967184" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { name: "bind_keyguard_media_visibility" namespace: "systemui" description: "Binds Keyguard Media Controller Visibility to MediaContainerView" @@ -1258,13 +1255,6 @@ flag { } flag { - name: "glanceable_hub_back_action" - namespace: "systemui" - description: "Support back action from glanceable hub" - bug: "382771533" -} - -flag { name: "dream_overlay_updated_font" namespace: "systemui" description: "Flag to enable updated font settings for dream overlay" @@ -1997,6 +1987,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." @@ -2016,3 +2016,13 @@ 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 + } +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/Icon.kt b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/Icon.kt index 8b0c00535262..09db2d653326 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/Icon.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/Icon.kt @@ -19,10 +19,12 @@ package com.android.systemui.common.ui.compose import androidx.compose.material3.Icon import androidx.compose.material3.LocalContentColor import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.res.painterResource -import com.android.compose.ui.graphics.painter.rememberDrawablePainter +import androidx.core.graphics.drawable.toBitmap import com.android.systemui.common.shared.model.Icon /** @@ -35,7 +37,12 @@ fun Icon(icon: Icon, modifier: Modifier = Modifier, tint: Color = LocalContentCo val contentDescription = icon.contentDescription?.load() when (icon) { is Icon.Loaded -> { - Icon(rememberDrawablePainter(icon.drawable), contentDescription, modifier, tint) + Icon( + remember(icon.drawable) { icon.drawable.toBitmap().asImageBitmap() }, + contentDescription, + modifier, + tint, + ) } is Icon.Resource -> Icon(painterResource(icon.res), contentDescription, modifier, tint) } 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 6e25c8a5bc42..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 @@ -31,7 +30,7 @@ import androidx.compose.ui.layout.Layout import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntRect -import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.compose.ui.unit.dp import com.android.compose.animation.scene.ContentScope import com.android.compose.modifiers.padding import com.android.systemui.compose.modifiers.sysuiResTag @@ -44,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 @@ -70,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() } @@ -91,7 +89,9 @@ constructor( modifier = Modifier.fillMaxWidth() .padding( - horizontal = { unfoldTranslations.start.roundToInt() } + horizontal = { + viewModel.unfoldTranslations.start.roundToInt() + } ) ) } @@ -100,29 +100,29 @@ 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, - isShadeLayoutWide = true, + areNotificationsVisible = + viewModel.areNotificationsVisible, burnInParams = null, modifier = Modifier.fillMaxWidth(0.5f) .fillMaxHeight() - .align(alignment = Alignment.TopEnd), + .align(notificationsPlacement.alignment) + .padding(top = 12.dp), ) } } @@ -137,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( @@ -149,14 +149,13 @@ constructor( ) } Notifications( - areNotificationsVisible = areNotificationsVisible, - isShadeLayoutWide = false, + 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/NotificationSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt index f51049a10569..d903c3d16fdb 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt @@ -18,12 +18,13 @@ package com.android.systemui.keyguard.ui.composable.section import android.view.ViewGroup import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition import androidx.compose.animation.core.MutableTransitionState import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -31,11 +32,9 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.res.dimensionResource -import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.compose.animation.scene.ContentScope -import com.android.compose.modifiers.thenIf import com.android.systemui.common.ui.ConfigurationState import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.ui.composable.blueprint.rememberBurnIn @@ -118,12 +117,19 @@ constructor( val isVisible by keyguardRootViewModel.isAodPromotedNotifVisible.collectAsStateWithLifecycle() + val transitionState = remember { MutableTransitionState(isVisible.value) } + LaunchedEffect(key1 = isVisible, key2 = transitionState.isIdle) { + transitionState.targetState = isVisible.value + if (isVisible.isAnimating && transitionState.isIdle) { + isVisible.stopAnimating() + } + } val burnIn = rememberBurnIn(keyguardClockViewModel) AnimatedVisibility( - visible = isVisible, - enter = fadeIn(), - exit = fadeOut(), + visibleState = transitionState, + enter = if (isVisible.isAnimating) fadeIn() else EnterTransition.None, + exit = if (isVisible.isAnimating) fadeOut() else ExitTransition.None, modifier = modifier.burnInAware(aodBurnInViewModel, burnIn.parameters), ) { AODPromotedNotification(aodPromotedNotificationViewModelFactory) @@ -186,7 +192,6 @@ constructor( @Composable fun ContentScope.Notifications( areNotificationsVisible: Boolean, - isShadeLayoutWide: Boolean, burnInParams: BurnInParameters?, modifier: Modifier = Modifier, ) { @@ -198,16 +203,13 @@ constructor( stackScrollView = stackScrollView.get(), viewModel = rememberViewModel("Notifications") { viewModelFactory.create() }, modifier = - modifier - .fillMaxWidth() - .thenIf(isShadeLayoutWide) { Modifier.padding(top = 12.dp) } - .let { - if (burnInParams == null) { - it - } else { - it.burnInAware(viewModel = aodBurnInViewModel, params = burnInParams) - } - }, + modifier.fillMaxWidth().let { + if (burnInParams == null) { + it + } else { + it.burnInAware(viewModel = aodBurnInViewModel, params = burnInParams) + } + }, ) } } 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/multivalentTests/src/com/android/systemui/back/domain/interactor/BackActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/back/domain/interactor/BackActionInteractorTest.kt index 4d238ac3798d..8c5fad3906ed 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/back/domain/interactor/BackActionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/back/domain/interactor/BackActionInteractorTest.kt @@ -16,7 +16,6 @@ package com.android.systemui.back.domain.interactor -import android.platform.test.annotations.EnableFlags import android.platform.test.annotations.RequiresFlagsDisabled import android.platform.test.annotations.RequiresFlagsEnabled import android.platform.test.flag.junit.DeviceFlagsValueProvider @@ -32,7 +31,6 @@ import androidx.test.filters.SmallTest import com.android.internal.statusbar.IStatusBarService import com.android.systemui.Flags import com.android.systemui.SysuiTestCase -import com.android.systemui.communal.domain.interactor.CommunalBackActionInteractor import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope @@ -93,7 +91,6 @@ class BackActionInteractorTest : SysuiTestCase() { @Mock private lateinit var onBackInvokedDispatcher: WindowOnBackInvokedDispatcher @Mock private lateinit var iStatusBarService: IStatusBarService @Mock private lateinit var headsUpManager: HeadsUpManager - @Mock private lateinit var communalBackActionInteractor: CommunalBackActionInteractor private val keyguardRepository = FakeKeyguardRepository() private val windowRootViewVisibilityInteractor: WindowRootViewVisibilityInteractor by lazy { @@ -118,7 +115,6 @@ class BackActionInteractorTest : SysuiTestCase() { windowRootViewVisibilityInteractor, shadeBackActionInteractor, qsController, - communalBackActionInteractor, ) } @@ -297,19 +293,6 @@ class BackActionInteractorTest : SysuiTestCase() { verify(shadeBackActionInteractor).onBackProgressed(0.4f) } - @Test - @EnableFlags(Flags.FLAG_GLANCEABLE_HUB_BACK_ACTION) - fun onBackAction_communalCanBeDismissed_communalBackActionInteractorCalled() { - backActionInteractor.start() - windowRootViewVisibilityInteractor.setIsLockscreenOrShadeVisible(true) - powerInteractor.setAwakeForTest() - val callback = getBackInvokedCallback() - whenever(communalBackActionInteractor.canBeDismissed()).thenReturn(true) - callback.onBackInvoked() - - verify(communalBackActionInteractor).onBackPressed() - } - private fun getBackInvokedCallback(): OnBackInvokedCallback { testScope.runCurrent() val captor = argumentCaptor<OnBackInvokedCallback>() 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/domain/interactor/CommunalBackActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalBackActionInteractorTest.kt deleted file mode 100644 index 70f38f7bc94e..000000000000 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalBackActionInteractorTest.kt +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.communal.domain.interactor - -import android.platform.test.annotations.EnableFlags -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.SmallTest -import com.android.systemui.Flags.FLAG_COMMUNAL_HUB -import com.android.systemui.SysuiTestCase -import com.android.systemui.communal.data.repository.communalSceneRepository -import com.android.systemui.communal.shared.model.CommunalScenes -import com.android.systemui.kosmos.Kosmos -import com.android.systemui.kosmos.Kosmos.Fixture -import com.android.systemui.kosmos.runCurrent -import com.android.systemui.kosmos.runTest -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 CommunalBackActionInteractorTest : SysuiTestCase() { - private val kosmos = testKosmos() - - private var Kosmos.underTest by Fixture { communalBackActionInteractor } - - @Test - @EnableFlags(FLAG_COMMUNAL_HUB) - fun communalShowing_canBeDismissed() = - kosmos.runTest { - setCommunalAvailable(true) - assertThat(underTest.canBeDismissed()).isEqualTo(false) - communalInteractor.changeScene(CommunalScenes.Communal, "test") - runCurrent() - assertThat(underTest.canBeDismissed()).isEqualTo(true) - } - - @Test - @EnableFlags(FLAG_COMMUNAL_HUB) - fun onBackPressed_invokesSceneChange() = - kosmos.runTest { - underTest.onBackPressed() - runCurrent() - assertThat(communalSceneRepository.currentScene.value).isEqualTo(CommunalScenes.Blank) - } -} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt index feee9e3d62d2..6eace1b50ea7 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractorTest.kt @@ -128,6 +128,19 @@ class CommunalTutorialInteractorTest : SysuiTestCase() { } @Test + fun tutorialState_startedAndCommunalSceneShowing_stateWillNotUpdate() = + testScope.runTest { + val tutorialSettingState by + collectLastValue(communalTutorialRepository.tutorialSettingState) + + communalTutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_STARTED) + + goToCommunal() + + assertThat(tutorialSettingState).isEqualTo(HUB_MODE_TUTORIAL_STARTED) + } + + @Test fun tutorialState_completedAndCommunalSceneShowing_stateWillNotUpdate() = testScope.runTest { val tutorialSettingState by 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 da25bcac6c95..1a3606e413cc 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,7 +35,6 @@ 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 @@ -45,7 +44,6 @@ 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() { @@ -67,7 +65,7 @@ class CommunalTransitionViewModelTest(flags: FlagsParameterization) : SysuiTestC private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository private val communalSceneRepository = kosmos.fakeCommunalSceneRepository - private val sceneInteractor by lazy { kosmos.sceneInteractor } + private val sceneInteractor = 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 e36d2455d316..329627af8ec2 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,7 +61,6 @@ 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 @@ -74,7 +73,6 @@ 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() { @@ -82,26 +80,21 @@ 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 by lazy { kosmos.keyguardUpdateMonitor } + private val keyguardUpdateMonitor = kosmos.keyguardUpdateMonitor private val faceWakeUpTriggersConfig = kosmos.fakeFaceWakeUpTriggersConfig private val trustManager = kosmos.trustManager - - 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 - } + private val deviceEntryFaceAuthStatusInteractor = kosmos.deviceEntryFaceAuthStatusInteractor @Before fun setup() { 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 2feabf8221ad..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 @@ -34,23 +34,32 @@ package com.android.systemui.keyguard.domain.interactor import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags -import androidx.test.ext.junit.runners.AndroidJUnit4 +import android.platform.test.flag.junit.FlagsParameterization import androidx.test.filters.SmallTest import com.android.systemui.Flags +import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2 import com.android.systemui.SysuiTestCase import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository +import com.android.systemui.communal.data.repository.fakeCommunalSceneRepository import com.android.systemui.communal.domain.interactor.communalInteractor +import com.android.systemui.communal.domain.interactor.communalSceneInteractor +import com.android.systemui.communal.domain.interactor.setCommunalV2ConfigEnabled +import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepositorySpy import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.util.KeyguardTransitionRepositorySpySubject.Companion.assertThat +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runCurrent +import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testScope import com.android.systemui.power.data.repository.fakePowerRepository import com.android.systemui.power.shared.model.WakeSleepReason import com.android.systemui.power.shared.model.WakefulnessState import com.android.systemui.testKosmos +import com.google.common.truth.Truth import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest @@ -58,10 +67,25 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito.reset +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters @SmallTest -@RunWith(AndroidJUnit4::class) -class FromAlternateBouncerTransitionInteractorTest : SysuiTestCase() { +@RunWith(ParameterizedAndroidJunit4::class) +class FromAlternateBouncerTransitionInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { + + companion object { + @JvmStatic + @Parameters(name = "{0}") + fun getParams(): List<FlagsParameterization> { + return FlagsParameterization.allCombinationsOf(FLAG_GLANCEABLE_HUB_V2) + } + } + + init { + mSetFlagsRule.setFlagsParameterization(flags) + } + private val kosmos = testKosmos().apply { this.keyguardTransitionRepository = fakeKeyguardTransitionRepositorySpy @@ -74,6 +98,7 @@ class FromAlternateBouncerTransitionInteractorTest : SysuiTestCase() { fun setup() { transitionRepository = kosmos.fakeKeyguardTransitionRepositorySpy underTest = kosmos.fromAlternateBouncerTransitionInteractor + kosmos.setCommunalV2ConfigEnabled(true) underTest.start() } @@ -171,6 +196,72 @@ class FromAlternateBouncerTransitionInteractorTest : SysuiTestCase() { } @Test + @EnableFlags(FLAG_GLANCEABLE_HUB_V2) + fun transitionToOccluded_glanceableHubShowing() = + kosmos.runTest { + val currentScene by collectLastValue(communalSceneInteractor.currentScene) + + fakePowerRepository.updateWakefulness( + WakefulnessState.AWAKE, + WakeSleepReason.POWER_BUTTON, + WakeSleepReason.POWER_BUTTON, + false, + ) + fakeKeyguardRepository.setKeyguardOccluded(false) + fakeKeyguardBouncerRepository.setAlternateVisible(true) + fakeCommunalSceneRepository.changeScene(CommunalScenes.Communal) + runCurrent() + + Truth.assertThat(currentScene).isEqualTo(CommunalScenes.Communal) + + transitionRepository.sendTransitionSteps( + from = KeyguardState.GLANCEABLE_HUB, + to = KeyguardState.ALTERNATE_BOUNCER, + testScope, + ) + reset(transitionRepository) + + fakeKeyguardRepository.setKeyguardOccluded(true) + fakeKeyguardBouncerRepository.setAlternateVisible(false) + testScope.advanceTimeBy(200) // advance past delay + + Truth.assertThat(currentScene).isEqualTo(CommunalScenes.Blank) + } + + @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/FromPrimaryBouncerTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorTest.kt index 63bb1001c873..d9e76222e51f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorTest.kt @@ -17,13 +17,17 @@ package com.android.systemui.keyguard.domain.interactor import android.platform.test.annotations.EnableFlags -import androidx.test.ext.junit.runners.AndroidJUnit4 +import android.platform.test.flag.junit.FlagsParameterization import androidx.test.filters.SmallTest import com.android.compose.animation.scene.ObservableTransitionState +import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2 import com.android.systemui.Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR import com.android.systemui.SysuiTestCase import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository import com.android.systemui.communal.data.repository.fakeCommunalSceneRepository +import com.android.systemui.communal.domain.interactor.communalSceneInteractor +import com.android.systemui.communal.domain.interactor.setCommunalV2Available +import com.android.systemui.communal.domain.interactor.setCommunalV2ConfigEnabled import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.coroutines.collectValues import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepositorySpy @@ -33,30 +37,55 @@ import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.keyguard.util.KeyguardTransitionRepositorySpySubject.Companion.assertThat +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runCurrent +import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testScope +import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest +import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.testKosmos -import com.android.systemui.user.domain.interactor.selectedUserInteractor +import com.google.common.truth.Truth import junit.framework.Assert.assertEquals import kotlinx.coroutines.flow.flowOf 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.Mockito.reset +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters @SmallTest -@RunWith(AndroidJUnit4::class) -class FromPrimaryBouncerTransitionInteractorTest : SysuiTestCase() { +@RunWith(ParameterizedAndroidJunit4::class) +class FromPrimaryBouncerTransitionInteractorTest(flags: FlagsParameterization) : SysuiTestCase() { + + companion object { + @JvmStatic + @Parameters(name = "{0}") + fun getParams(): List<FlagsParameterization> { + return FlagsParameterization.allCombinationsOf(FLAG_GLANCEABLE_HUB_V2) + } + } + + init { + mSetFlagsRule.setFlagsParameterization(flags) + } + private val kosmos = testKosmos().apply { this.keyguardTransitionRepository = fakeKeyguardTransitionRepositorySpy } val underTest = kosmos.fromPrimaryBouncerTransitionInteractor val testScope = kosmos.testScope - val selectedUserInteractor = kosmos.selectedUserInteractor val transitionRepository = kosmos.fakeKeyguardTransitionRepositorySpy val bouncerRepository = kosmos.fakeKeyguardBouncerRepository + @Before + fun setUp() { + kosmos.setCommunalV2ConfigEnabled(true) + } + @Test fun testSurfaceBehindVisibility() = testScope.runTest { @@ -213,4 +242,33 @@ class FromPrimaryBouncerTransitionInteractorTest : SysuiTestCase() { to = KeyguardState.OCCLUDED, ) } + + @Test + @EnableFlags(FLAG_GLANCEABLE_HUB_V2) + fun testTransitionToDozing_bouncerShowingOnTopOfGlanceableHub() = + kosmos.runTest { + underTest.start() + setCommunalV2Available(true) + + val currentScene by collectLastValue(communalSceneInteractor.currentScene) + // Communal is showing. + fakeCommunalSceneRepository.changeScene(CommunalScenes.Communal) + + Truth.assertThat(currentScene).isEqualTo(CommunalScenes.Communal) + + // Bouncer is shown on top of the Glanceable Hub. + bouncerRepository.setPrimaryShow(true) + transitionRepository.sendTransitionSteps( + from = KeyguardState.GLANCEABLE_HUB, + to = KeyguardState.PRIMARY_BOUNCER, + testScope, + ) + + reset(transitionRepository) + + powerInteractor.setAsleepForTest() + runCurrent() + + Truth.assertThat(currentScene).isEqualTo(CommunalScenes.Blank) + } } 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 fee2dfc88020..29e95cd911f8 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 @@ -46,7 +46,6 @@ 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 @@ -54,15 +53,13 @@ import org.junit.Assert.assertThrows import org.junit.Test import org.junit.runner.RunWith -@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class KeyguardTransitionInteractorTest : SysuiTestCase() { val kosmos = testKosmos() - val testScope = kosmos.testScope + val underTest = kosmos.keyguardTransitionInteractor val repository = kosmos.fakeKeyguardTransitionRepository - - val underTest by lazy { kosmos.keyguardTransitionInteractor } + val testScope = kosmos.testScope @Test fun transitionCollectorsReceivesOnlyAppropriateEvents() = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt index a090faba5bf7..8df70ef0fd2e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt @@ -17,6 +17,7 @@ package com.android.systemui.keyguard.domain.interactor import android.app.StatusBarManager.CAMERA_LAUNCH_SOURCE_POWER_DOUBLE_TAP +import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization import androidx.test.filters.SmallTest @@ -30,7 +31,6 @@ import com.android.systemui.Flags.glanceableHubV2 import com.android.systemui.SysuiTestCase import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository import com.android.systemui.communal.domain.interactor.CommunalSceneTransitionInteractor -import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.communal.domain.interactor.communalSceneInteractor import com.android.systemui.communal.domain.interactor.communalSceneTransitionInteractor import com.android.systemui.communal.domain.interactor.setCommunalAvailable @@ -126,14 +126,14 @@ class KeyguardTransitionScenariosTest(flags: FlagsParameterization?) : SysuiTest } private val powerInteractor by lazy { kosmos.powerInteractor } - private val communalInteractor by lazy { kosmos.communalInteractor } private val communalSceneInteractor by lazy { kosmos.communalSceneInteractor } companion object { @JvmStatic @Parameters(name = "{0}") fun getParams(): List<FlagsParameterization> { - return FlagsParameterization.allCombinationsOf().andSceneContainer() + return FlagsParameterization.allCombinationsOf(FLAG_GLANCEABLE_HUB_V2) + .andSceneContainer() } } @@ -902,6 +902,7 @@ class KeyguardTransitionScenariosTest(flags: FlagsParameterization?) : SysuiTest @Test @DisableSceneContainer + @DisableFlags(FLAG_GLANCEABLE_HUB_V2) fun primaryBouncerToGlanceableHubWhileDreaming() = testScope.runTest { // Setup - Move past initial delay with [KeyguardInteractor#isAbleToDream] 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 new file mode 100644 index 000000000000..052dfd52887f --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DozingToDreamingTransitionViewModelTest.kt @@ -0,0 +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. + */ + +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..25c157208513 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,6 +17,7 @@ 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 @@ -26,10 +27,13 @@ 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 +45,9 @@ 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.shared.model.ShadeMode import com.android.systemui.testKosmos import com.android.systemui.unfold.fakeUnfoldTransitionProgressProvider import com.android.systemui.util.mockito.whenever @@ -99,136 +106,140 @@ 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() = + 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() = + 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() = + 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 +249,7 @@ class LockscreenContentViewModelTest(flags: FlagsParameterization) : SysuiTestCa sceneInteractor.snapToScene(Scenes.Shade, "") runCurrent() - assertThat(isContentVisible).isFalse() + assertThat(underTest.isContentVisible).isFalse() } @Test @@ -260,17 +271,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 +288,30 @@ 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) + 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(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/MediaOutputAdapterTest.java index 95d9c8f4e819..f5a71113235a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/dialog/MediaOutputAdapterTest.java @@ -240,6 +240,8 @@ public class MediaOutputAdapterTest extends SysuiTestCase { @DisableFlags(Flags.FLAG_ENABLE_OUTPUT_SWITCHER_DEVICE_GROUPING) @Test public void onBindViewHolder_bindConnectedRemoteDevice_verifyView() { + when(mMediaSwitchingController.getSelectedMediaDevice()) + .thenReturn(ImmutableList.of(mMediaDevice1)); when(mMediaSwitchingController.getSelectableMediaDevice()) .thenReturn(ImmutableList.of(mMediaDevice2)); when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(true); @@ -854,6 +856,8 @@ public class MediaOutputAdapterTest extends SysuiTestCase { .thenReturn(ImmutableList.of(mMediaDevice2)); when(mMediaSwitchingController.getDeselectableMediaDevice()) .thenReturn(ImmutableList.of(mMediaDevice1)); + when(mMediaSwitchingController.getSelectedMediaDevice()) + .thenReturn(ImmutableList.of(mMediaDevice1)); when(mMediaSwitchingController.isCurrentConnectedDeviceRemote()).thenReturn(true); mViewHolder = (MediaOutputAdapter.MediaDeviceViewHolder) mMediaOutputAdapter .onCreateViewHolder(new LinearLayout(mContext), 0); 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 9e400a6c0a4c..c775bfd75f6e 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,7 +19,6 @@ 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 @@ -35,7 +34,6 @@ 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 9fe783b98046..2e7aeb433e04 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,7 +22,6 @@ 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 @@ -77,7 +76,6 @@ 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 d5e502e99de5..fdbf42c9afd8 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,7 +21,6 @@ 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 @@ -37,7 +36,6 @@ 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 @@ -45,7 +43,6 @@ 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() { @@ -66,7 +63,6 @@ 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 4912c319bf2e..241cdbfbef83 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,7 +21,6 @@ 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 @@ -89,7 +88,6 @@ class QSColumnsViewModelTest : SysuiTestCase() { } @Test - @EnableSceneContainer fun mediaLocationNull_dualShade_alwaysDualShadeColumns() = with(kosmos) { testScope.runTest { @@ -113,7 +111,6 @@ class QSColumnsViewModelTest : SysuiTestCase() { } @Test - @EnableSceneContainer fun mediaLocationQS_dualShade_alwaysDualShadeColumns() = with(kosmos) { testScope.runTest { @@ -136,7 +133,6 @@ class QSColumnsViewModelTest : SysuiTestCase() { } @Test - @EnableSceneContainer fun mediaLocationQQS_dualShade_alwaysDualShadeColumns() = with(kosmos) { testScope.runTest { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/InternetTileNewImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/InternetTileNewImplTest.kt index 64e6f4bd48b8..7ed3cb3e4586 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/InternetTileNewImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/InternetTileNewImplTest.kt @@ -34,7 +34,6 @@ import com.android.systemui.qs.flags.QSComposeFragment import com.android.systemui.qs.logging.QSLogger import com.android.systemui.qs.tiles.dialog.InternetDetailsViewModel import com.android.systemui.qs.tiles.dialog.InternetDialogManager -import com.android.systemui.qs.tiles.dialog.WifiStateWorker import com.android.systemui.res.R import com.android.systemui.statusbar.connectivity.AccessPointController import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository @@ -61,9 +60,6 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.MockitoAnnotations -import org.mockito.kotlin.eq -import org.mockito.kotlin.times -import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import platform.test.runner.parameterized.ParameterizedAndroidJunit4 import platform.test.runner.parameterized.Parameters @@ -99,7 +95,6 @@ class InternetTileNewImplTest(flags: FlagsParameterization) : SysuiTestCase() { @Mock private lateinit var activityStarter: ActivityStarter @Mock private lateinit var logger: QSLogger @Mock private lateinit var dialogManager: InternetDialogManager - @Mock private lateinit var wifiStateWorker: WifiStateWorker @Mock private lateinit var accessPointController: AccessPointController @Mock private lateinit var internetDetailsViewModelFactory: InternetDetailsViewModel.Factory @@ -136,7 +131,6 @@ class InternetTileNewImplTest(flags: FlagsParameterization) : SysuiTestCase() { logger, viewModel, dialogManager, - wifiStateWorker, accessPointController, internetDetailsViewModelFactory, ) @@ -245,26 +239,6 @@ class InternetTileNewImplTest(flags: FlagsParameterization) : SysuiTestCase() { assertThat(underTest.state.secondaryLabel).isEqualTo(WIFI_SSID) } - @Test - fun secondaryClick_turnsWifiOff() { - whenever(wifiStateWorker.isWifiEnabled).thenReturn(true) - - underTest.secondaryClick(null) - looper.processAllMessages() - - verify(wifiStateWorker, times(1)).isWifiEnabled = eq(false) - } - - @Test - fun secondaryClick_turnsWifiOn() { - whenever(wifiStateWorker.isWifiEnabled).thenReturn(false) - - underTest.secondaryClick(null) - looper.processAllMessages() - - verify(wifiStateWorker, times(1)).isWifiEnabled = eq(true) - } - companion object { const val WIFI_SSID = "test ssid" val ACTIVE_WIFI = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java index 3a3f5371d195..61d0c8dbb087 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java @@ -345,11 +345,21 @@ public class ScreenRecordTileTest extends SysuiTestCase { public void testNotStartingAndRecording_returnDetailsViewModel() { when(mController.isStarting()).thenReturn(false); when(mController.isRecording()).thenReturn(false); + when(mController.isScreenCaptureDisabled()).thenReturn(false); mTile.getDetailsViewModel(Assert::assertNotNull); } @Test @EnableFlags(QsDetailedView.FLAG_NAME) + public void testRecordingDisabled_notReturnDetailsViewModel() { + when(mController.isStarting()).thenReturn(false); + when(mController.isRecording()).thenReturn(false); + when(mController.isScreenCaptureDisabled()).thenReturn(true); + mTile.getDetailsViewModel(Assert::assertNull); + } + + @Test + @EnableFlags(QsDetailedView.FLAG_NAME) public void testStarting_notReturnDetailsViewModel() { when(mController.isStarting()).thenReturn(true); when(mController.isRecording()).thenReturn(false); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapperTest.kt index b087bbc29bf7..54a653df696f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapperTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapperTest.kt @@ -191,11 +191,7 @@ class InternetTileMapperTest : SysuiTestCase() { label, activationState, secondaryLabel, - setOf( - QSTileState.UserAction.CLICK, - QSTileState.UserAction.TOGGLE_CLICK, - QSTileState.UserAction.LONG_CLICK, - ), + setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK), contentDescription, null, QSTileState.SideViewIcon.Chevron, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractorTest.kt index ce4a3432a5b4..3db5efcb6eb8 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractorTest.kt @@ -29,7 +29,6 @@ import com.android.systemui.qs.tiles.base.interactor.QSTileInputTestKtx import com.android.systemui.qs.tiles.dialog.InternetDetailsContentManager import com.android.systemui.qs.tiles.dialog.InternetDetailsViewModel import com.android.systemui.qs.tiles.dialog.InternetDialogManager -import com.android.systemui.qs.tiles.dialog.WifiStateWorker import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileModel import com.android.systemui.statusbar.connectivity.AccessPointController import com.android.systemui.util.mockito.nullable @@ -56,7 +55,6 @@ class InternetTileUserActionInteractorTest : SysuiTestCase() { private lateinit var underTest: InternetTileUserActionInteractor private lateinit var internetDialogManager: InternetDialogManager - private lateinit var wifiStateWorker: WifiStateWorker private lateinit var controller: AccessPointController private lateinit var internetDetailsViewModelFactory: InternetDetailsViewModel.Factory private lateinit var internetDetailsContentManagerFactory: InternetDetailsContentManager.Factory @@ -65,7 +63,6 @@ class InternetTileUserActionInteractorTest : SysuiTestCase() { @Before fun setup() { internetDialogManager = mock<InternetDialogManager>() - wifiStateWorker = mock<WifiStateWorker>() controller = mock<AccessPointController>() internetDetailsViewModelFactory = mock<InternetDetailsViewModel.Factory>() internetDetailsContentManagerFactory = mock<InternetDetailsContentManager.Factory>() @@ -81,7 +78,6 @@ class InternetTileUserActionInteractorTest : SysuiTestCase() { InternetTileUserActionInteractor( kosmos.testScope.coroutineContext, internetDialogManager, - wifiStateWorker, controller, inputHandler, internetDetailsViewModelFactory, @@ -133,26 +129,6 @@ class InternetTileUserActionInteractorTest : SysuiTestCase() { } @Test - fun handleSecondaryClickWhenWifiOn() = - kosmos.testScope.runTest { - whenever(wifiStateWorker.isWifiEnabled).thenReturn(true) - - underTest.handleInput(QSTileInputTestKtx.toggleClick(InternetTileModel.Active())) - - verify(wifiStateWorker, times(1)).isWifiEnabled = eq(false) - } - - @Test - fun handleSecondaryClickWhenWifiOff() = - kosmos.testScope.runTest { - whenever(wifiStateWorker.isWifiEnabled).thenReturn(false) - - underTest.handleInput(QSTileInputTestKtx.toggleClick(InternetTileModel.Inactive())) - - verify(wifiStateWorker, times(1)).isWifiEnabled = eq(true) - } - - @Test fun detailsViewModel() = kosmos.testScope.runTest { assertThat(underTest.detailsViewModel.getTitle()).isEqualTo("Internet") 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 d26e195d360a..668f568d7f46 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,7 +20,6 @@ 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 @@ -32,7 +31,6 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) -@EnableSceneContainer class ShadeModeInteractorImplTest : SysuiTestCase() { private val kosmos = testKosmos() @@ -82,7 +80,7 @@ class ShadeModeInteractorImplTest : SysuiTestCase() { } @Test - fun isDualShade_settingEnabledSceneContainerEnabled_returnsTrue() = + fun isDualShade_settingEnabled_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 dde867814159..b8f66acf6413 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,7 +48,6 @@ 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 @@ -60,7 +59,6 @@ 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() { @@ -105,7 +103,6 @@ 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/system/QuickStepContractTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/system/QuickStepContractTest.kt index d92781a5f3ce..ef03fab95778 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/system/QuickStepContractTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/system/QuickStepContractTest.kt @@ -16,10 +16,8 @@ package com.android.systemui.shared.system -import android.platform.test.annotations.DisableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_BACK_ACTION import com.android.systemui.SysuiTestCase import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BOUNCER_SHOWING import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_COMMUNAL_HUB_SHOWING @@ -32,7 +30,6 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class QuickStepContractTest : SysuiTestCase() { @Test - @DisableFlags(FLAG_GLANCEABLE_HUB_BACK_ACTION) fun isBackGestureDisabled_hubShowing() { val sysuiStateFlags = SYSUI_STATE_COMMUNAL_HUB_SHOWING diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt index d2ea62da0940..83361dad9ff0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/NotificationShadeDepthControllerTest.kt @@ -76,7 +76,6 @@ import org.mockito.junit.MockitoJUnit class NotificationShadeDepthControllerTest : SysuiTestCase() { private val kosmos = testKosmos() - private val applicationScope = kosmos.testScope.backgroundScope @Mock private lateinit var windowRootViewBlurInteractor: WindowRootViewBlurInteractor @Mock private lateinit var statusBarStateController: StatusBarStateController @Mock private lateinit var blurUtils: BlurUtils @@ -134,7 +133,6 @@ class NotificationShadeDepthControllerTest : SysuiTestCase() { context, ResourcesSplitShadeStateController(), windowRootViewBlurInteractor, - applicationScope, appZoomOutOptional, dumpManager, configurationController, 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 05f2585cfaa5..cabe4afdea60 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 @@ -306,7 +306,7 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() { } @Test - fun notificationChip_appIsVisibleOnCreation_emitsNull() = + fun notificationChip_appIsVisibleOnCreation_emitsIsAppVisibleTrue() = kosmos.runTest { activityManagerRepository.fake.startingIsAppVisibleValue = true @@ -323,11 +323,12 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() { val latest by collectLastValue(underTest.notificationChip) - assertThat(latest).isNull() + assertThat(latest).isNotNull() + assertThat(latest!!.isAppVisible).isTrue() } @Test - fun notificationChip_appNotVisibleOnCreation_emitsValue() = + fun notificationChip_appNotVisibleOnCreation_emitsIsAppVisibleFalse() = kosmos.runTest { activityManagerRepository.fake.startingIsAppVisibleValue = false @@ -345,10 +346,11 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() { val latest by collectLastValue(underTest.notificationChip) assertThat(latest).isNotNull() + assertThat(latest!!.isAppVisible).isFalse() } @Test - fun notificationChip_hidesWhenAppIsVisible() = + fun notificationChip_updatesWhenAppIsVisible() = kosmos.runTest { val underTest = factory.create( @@ -364,13 +366,13 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() { val latest by collectLastValue(underTest.notificationChip) activityManagerRepository.fake.setIsAppVisible(UID, false) - assertThat(latest).isNotNull() + assertThat(latest!!.isAppVisible).isFalse() activityManagerRepository.fake.setIsAppVisible(UID, true) - assertThat(latest).isNull() + assertThat(latest!!.isAppVisible).isTrue() activityManagerRepository.fake.setIsAppVisible(UID, false) - assertThat(latest).isNotNull() + assertThat(latest!!.isAppVisible).isFalse() } // Note: This test is theoretically impossible because the notification key should contain the @@ -396,6 +398,7 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() { ) val latest by collectLastValue(underTest.notificationChip) assertThat(latest).isNotNull() + assertThat(latest!!.isAppVisible).isFalse() // WHEN the notif gets a new UID that starts as visible activityManagerRepository.fake.startingIsAppVisibleValue = true @@ -408,9 +411,8 @@ class SingleNotificationChipInteractorTest : SysuiTestCase() { ) ) - // THEN we re-fetch the app visibility state with the new UID, and since that UID is - // visible, we hide the chip - assertThat(latest).isNull() + // THEN we re-fetch the app visibility state with the new UID + assertThat(latest!!.isAppVisible).isTrue() } companion object { 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 e89c929a5827..d8e4cd927bec 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 @@ -21,8 +21,8 @@ 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.coroutines.collectLastValue -import com.android.systemui.coroutines.collectValues +import com.android.systemui.activity.data.repository.activityManagerRepository +import com.android.systemui.activity.data.repository.fake import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.collectValues @@ -41,7 +41,6 @@ import com.android.systemui.testKosmos import com.android.systemui.util.time.fakeSystemClock import com.google.common.truth.Truth.assertThat import kotlin.test.Test -import kotlinx.coroutines.test.runTest import org.junit.runner.RunWith import org.mockito.kotlin.mock @@ -55,9 +54,9 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { @Test @DisableFlags(StatusBarNotifChips.FLAG_NAME) - fun notificationChips_flagOff_noNotifs() = + fun shownNotificationChips_flagOff_noNotifs() = kosmos.runTest { - val latest by collectLastValue(underTest.notificationChips) + val latest by collectLastValue(underTest.shownNotificationChips) setNotifs( listOf( @@ -74,9 +73,9 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) - fun notificationChips_noNotifs_empty() = + fun shownNotificationChips_noNotifs_empty() = kosmos.runTest { - val latest by collectLastValue(underTest.notificationChips) + val latest by collectLastValue(underTest.shownNotificationChips) setNotifs(emptyList()) @@ -86,9 +85,9 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) @DisableFlags(StatusBarConnectedDisplays.FLAG_NAME) - fun notificationChips_notifMissingStatusBarChipIconView_cdFlagOff_empty() = + fun shownNotificationChips_notifMissingStatusBarChipIconView_cdFlagOff_empty() = kosmos.runTest { - val latest by collectLastValue(underTest.notificationChips) + val latest by collectLastValue(underTest.shownNotificationChips) setNotifs( listOf( @@ -105,9 +104,9 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME, StatusBarConnectedDisplays.FLAG_NAME) - fun notificationChips_notifMissingStatusBarChipIconView_cdFlagOn_notEmpty() = + fun shownNotificationChips_notifMissingStatusBarChipIconView_cdFlagOn_notEmpty() = kosmos.runTest { - val latest by collectLastValue(underTest.notificationChips) + val latest by collectLastValue(underTest.shownNotificationChips) setNotifs( listOf( @@ -124,9 +123,9 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) - fun notificationChips_onePromotedNotif_statusBarIconViewMatches() = + fun shownNotificationChips_onePromotedNotif_statusBarIconViewMatches() = kosmos.runTest { - val latest by collectLastValue(underTest.notificationChips) + val latest by collectLastValue(underTest.shownNotificationChips) val icon = mock<StatusBarIconView>() setNotifs( @@ -146,9 +145,9 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) - fun notificationChips_onlyForPromotedNotifs() = + fun shownNotificationChips_onlyForPromotedNotifs() = kosmos.runTest { - val latest by collectLastValue(underTest.notificationChips) + val latest by collectLastValue(underTest.shownNotificationChips) val firstIcon = mock<StatusBarIconView>() val secondIcon = mock<StatusBarIconView>() @@ -179,12 +178,42 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { assertThat(latest!![1].statusBarChipIconView).isEqualTo(secondIcon) } + @Test + @EnableFlags(StatusBarNotifChips.FLAG_NAME) + fun shownNotificationChips_onlyForNotVisibleApps() = + kosmos.runTest { + activityManagerRepository.fake.startingIsAppVisibleValue = false + + val latest by collectLastValue(underTest.shownNotificationChips) + + val uid = 433 + setNotifs( + listOf( + activeNotificationModel( + key = "notif", + uid = uid, + statusBarChipIcon = mock<StatusBarIconView>(), + promotedContent = PromotedNotificationContentModel.Builder("notif1").build(), + ) + ) + ) + + activityManagerRepository.fake.setIsAppVisible(uid, isAppVisible = false) + assertThat(latest).hasSize(1) + + activityManagerRepository.fake.setIsAppVisible(uid, isAppVisible = true) + assertThat(latest).isEmpty() + + activityManagerRepository.fake.setIsAppVisible(uid, isAppVisible = false) + assertThat(latest).hasSize(1) + } + /** Regression test for b/388521980. */ @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) - fun notificationChips_callNotifIsAlsoPromoted_callNotifExcluded() = + fun shownNotificationChips_callNotifIsAlsoPromoted_callNotifExcluded() = kosmos.runTest { - val latest by collectLastValue(underTest.notificationChips) + val latest by collectLastValue(underTest.shownNotificationChips) setNotifs( listOf( @@ -212,9 +241,9 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) - fun notificationChips_notifUpdatesGoThrough() = + fun shownNotificationChips_notifUpdatesGoThrough() = kosmos.runTest { - val latest by collectLastValue(underTest.notificationChips) + val latest by collectLastValue(underTest.shownNotificationChips) val firstIcon = mock<StatusBarIconView>() val secondIcon = mock<StatusBarIconView>() @@ -262,9 +291,9 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) - fun notificationChips_promotedNotifDisappearsThenReappears() = + fun shownNotificationChips_promotedNotifDisappearsThenReappears() = kosmos.runTest { - val latest by collectLastValue(underTest.notificationChips) + val latest by collectLastValue(underTest.shownNotificationChips) setNotifs( listOf( @@ -304,9 +333,9 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) - fun notificationChips_sortedBasedOnFirstAppearanceTime() = + fun shownNotificationChips_sortedBasedOnFirstAppearanceTime() = kosmos.runTest { - val latest by collectLastValue(underTest.notificationChips) + val latest by collectLastValue(underTest.shownNotificationChips) val firstIcon = mock<StatusBarIconView>() val secondIcon = mock<StatusBarIconView>() @@ -391,9 +420,9 @@ class StatusBarNotificationChipsInteractorTest : SysuiTestCase() { @Test @EnableFlags(StatusBarNotifChips.FLAG_NAME) - fun notificationChips_notifChangesKey() = + fun shownNotificationChips_notifChangesKey() = kosmos.runTest { - val latest by collectLastValue(underTest.notificationChips) + val latest by collectLastValue(underTest.shownNotificationChips) val firstIcon = mock<StatusBarIconView>() val secondIcon = mock<StatusBarIconView>() 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/notification/collection/coordinator/ColorizedFgsCoordinatorTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinatorTest.java deleted file mode 100644 index 544d20145db3..000000000000 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinatorTest.java +++ /dev/null @@ -1,209 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.statusbar.notification.collection.coordinator; - -import static android.app.Notification.FLAG_FOREGROUND_SERVICE; -import static android.app.Notification.FLAG_PROMOTED_ONGOING; -import static android.app.NotificationManager.IMPORTANCE_DEFAULT; -import static android.app.NotificationManager.IMPORTANCE_HIGH; -import static android.app.NotificationManager.IMPORTANCE_MIN; - -import static junit.framework.Assert.assertFalse; -import static junit.framework.Assert.assertTrue; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; - -import android.app.Notification; -import android.app.PendingIntent; -import android.app.Person; -import android.content.Intent; -import android.graphics.Color; -import android.os.UserHandle; -import android.platform.test.annotations.DisableFlags; -import android.platform.test.annotations.EnableFlags; -import android.testing.TestableLooper; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.SmallTest; - -import com.android.systemui.SysuiTestCase; -import com.android.systemui.statusbar.notification.collection.NotifPipeline; -import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder; -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 org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -@SmallTest -@RunWith(AndroidJUnit4.class) -@TestableLooper.RunWithLooper -public class ColorizedFgsCoordinatorTest extends SysuiTestCase { - - private static final int NOTIF_USER_ID = 0; - @Mock private NotifPipeline mNotifPipeline; - - private NotificationEntryBuilder mEntryBuilder; - private ColorizedFgsCoordinator mColorizedFgsCoordinator; - private NotifSectioner mFgsSection; - - @Before - public void setup() { - MockitoAnnotations.initMocks(this); - allowTestableLooperAsMainThread(); - - mColorizedFgsCoordinator = new ColorizedFgsCoordinator(); - - mEntryBuilder = new NotificationEntryBuilder() - .setUser(new UserHandle(NOTIF_USER_ID)); - - mColorizedFgsCoordinator.attach(mNotifPipeline); - - mFgsSection = mColorizedFgsCoordinator.getSectioner(); - } - - @Test - public void testIncludeFGSInSection_importanceDefault() { - // GIVEN the notification represents a colorized foreground service with > min importance - mEntryBuilder - .setFlag(mContext, FLAG_FOREGROUND_SERVICE, true) - .setImportance(IMPORTANCE_DEFAULT) - .modifyNotification(mContext) - .setColorized(true).setColor(Color.WHITE); - - // THEN the entry is in the fgs section - assertTrue(mFgsSection.isInSection(mEntryBuilder.build())); - } - - @Test - public void testDiscludeFGSInSection_importanceMin() { - // GIVEN the notification represents a colorized foreground service with min importance - mEntryBuilder - .setFlag(mContext, FLAG_FOREGROUND_SERVICE, true) - .setImportance(IMPORTANCE_MIN) - .modifyNotification(mContext) - .setColorized(true).setColor(Color.WHITE); - - // THEN the entry is NOT in the fgs section - assertFalse(mFgsSection.isInSection(mEntryBuilder.build())); - } - - @Test - public void testDiscludeNonFGSInSection() { - // GIVEN the notification represents a colorized notification with high importance that - // is NOT a foreground service - mEntryBuilder - .setImportance(IMPORTANCE_HIGH) - .setFlag(mContext, FLAG_FOREGROUND_SERVICE, false) - .modifyNotification(mContext).setColorized(false); - - // THEN the entry is NOT in the fgs section - assertFalse(mFgsSection.isInSection(mEntryBuilder.build())); - } - - @Test - public void testIncludeCallInSection_importanceDefault() { - // GIVEN the notification represents a call with > min importance - mEntryBuilder - .setImportance(IMPORTANCE_DEFAULT) - .modifyNotification(mContext) - .setStyle(makeCallStyle()); - - // THEN the entry is in the fgs section - assertTrue(mFgsSection.isInSection(mEntryBuilder.build())); - } - - @Test - public void testDiscludeCallInSection_importanceMin() { - // GIVEN the notification represents a call with min importance - mEntryBuilder - .setImportance(IMPORTANCE_MIN) - .modifyNotification(mContext) - .setStyle(makeCallStyle()); - - // THEN the entry is NOT in the fgs section - assertFalse(mFgsSection.isInSection(mEntryBuilder.build())); - } - - @Test - @EnableFlags(PromotedNotificationUi.FLAG_NAME) - public void testIncludePromotedOngoingInSection_flagEnabled() { - // GIVEN the notification has FLAG_PROMOTED_ONGOING - mEntryBuilder.setFlag(mContext, FLAG_PROMOTED_ONGOING, true); - - // THEN the entry is in the fgs section - assertTrue(mFgsSection.isInSection(mEntryBuilder.build())); - } - - @Test - @DisableFlags(PromotedNotificationUi.FLAG_NAME) - public void testDiscludePromotedOngoingInSection_flagDisabled() { - // GIVEN the notification has FLAG_PROMOTED_ONGOING - mEntryBuilder.setFlag(mContext, FLAG_PROMOTED_ONGOING, true); - - // THEN the entry is NOT in the fgs section - assertFalse(mFgsSection.isInSection(mEntryBuilder.build())); - } - - @Test - @EnableFlags(PromotedNotificationUi.FLAG_NAME) - public void promoterSelectsPromotedOngoing_flagEnabled() { - ArgumentCaptor<NotifPromoter> captor = ArgumentCaptor.forClass(NotifPromoter.class); - verify(mNotifPipeline).addPromoter(captor.capture()); - NotifPromoter promoter = captor.getValue(); - - // GIVEN the notification has FLAG_PROMOTED_ONGOING - mEntryBuilder.setFlag(mContext, FLAG_PROMOTED_ONGOING, true); - - // THEN the entry is promoted to top level - assertTrue(promoter.shouldPromoteToTopLevel(mEntryBuilder.build())); - } - - @Test - @EnableFlags(PromotedNotificationUi.FLAG_NAME) - public void promoterIgnoresNonPromotedOngoing_flagEnabled() { - ArgumentCaptor<NotifPromoter> captor = ArgumentCaptor.forClass(NotifPromoter.class); - verify(mNotifPipeline).addPromoter(captor.capture()); - NotifPromoter promoter = captor.getValue(); - - // GIVEN the notification does not have FLAG_PROMOTED_ONGOING - mEntryBuilder.setFlag(mContext, FLAG_PROMOTED_ONGOING, false); - - // THEN the entry is NOT promoted to top level - assertFalse(promoter.shouldPromoteToTopLevel(mEntryBuilder.build())); - } - - @Test - @DisableFlags(PromotedNotificationUi.FLAG_NAME) - public void noPromoterAdded_flagDisabled() { - verify(mNotifPipeline, never()).addPromoter(any()); - } - - private Notification.CallStyle makeCallStyle() { - final PendingIntent pendingIntent = PendingIntent.getBroadcast(mContext, 0, - new Intent("action"), PendingIntent.FLAG_IMMUTABLE); - final Person person = new Person.Builder().setName("person").build(); - return Notification.CallStyle.forIncomingCall(person, pendingIntent, pendingIntent); - } -} 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 new file mode 100644 index 000000000000..e93c74252251 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/coordinator/ColorizedFgsCoordinatorTest.kt @@ -0,0 +1,191 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.statusbar.notification.collection.coordinator + +import android.app.Notification +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.Person +import android.content.Intent +import android.graphics.Color +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +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.kosmos.useUnconfinedTestDispatcher +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.promoted.PromotedNotificationUi +import com.android.systemui.testKosmos +import com.android.systemui.util.mockito.withArgCaptor +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.never +import org.mockito.kotlin.verify + +@SmallTest +@RunWith(AndroidJUnit4::class) +@RunWithLooper +class ColorizedFgsCoordinatorTest : SysuiTestCase() { + private val kosmos = testKosmos().useUnconfinedTestDispatcher() + private val notifPipeline + get() = kosmos.notifPipeline + + private lateinit var colorizedFgsCoordinator: ColorizedFgsCoordinator + private lateinit var sectioner: NotifSectioner + + @Before + fun setup() { + allowTestableLooperAsMainThread() + + colorizedFgsCoordinator = ColorizedFgsCoordinator() + colorizedFgsCoordinator.attach(notifPipeline) + sectioner = colorizedFgsCoordinator.sectioner + } + + @Test + fun testIncludeFGSInSection_importanceDefault() { + // GIVEN the notification represents a colorized foreground service with > min importance + val entry = buildEntry { + setFlag(mContext, Notification.FLAG_FOREGROUND_SERVICE, true) + setImportance(NotificationManager.IMPORTANCE_DEFAULT) + modifyNotification(mContext).setColorized(true).setColor(Color.WHITE) + } + + // THEN the entry is in the fgs section + assertTrue(sectioner.isInSection(entry)) + } + + @Test + fun testDiscludeFGSInSection_importanceMin() { + // GIVEN the notification represents a colorized foreground service with min importance + val entry = buildEntry { + setFlag(mContext, Notification.FLAG_FOREGROUND_SERVICE, true) + setImportance(NotificationManager.IMPORTANCE_MIN) + modifyNotification(mContext).setColorized(true).setColor(Color.WHITE) + } + + // THEN the entry is NOT in the fgs section + assertFalse(sectioner.isInSection(entry)) + } + + @Test + fun testDiscludeNonFGSInSection() { + // GIVEN the notification represents a colorized notification with high importance that + // is NOT a foreground service + val entry = buildEntry { + setImportance(NotificationManager.IMPORTANCE_HIGH) + setFlag(mContext, Notification.FLAG_FOREGROUND_SERVICE, false) + modifyNotification(mContext).setColorized(false) + } + + // THEN the entry is NOT in the fgs section + assertFalse(sectioner.isInSection(entry)) + } + + @Test + fun testIncludeCallInSection_importanceDefault() { + // GIVEN the notification represents a call with > min importance + val entry = buildEntry { + setImportance(NotificationManager.IMPORTANCE_DEFAULT) + modifyNotification(mContext).setStyle(makeCallStyle()) + } + + // THEN the entry is in the fgs section + assertTrue(sectioner.isInSection(entry)) + } + + @Test + fun testDiscludeCallInSection_importanceMin() { + // GIVEN the notification represents a call with min importance + val entry = buildEntry { + setImportance(NotificationManager.IMPORTANCE_MIN) + modifyNotification(mContext).setStyle(makeCallStyle()) + } + + // THEN the entry is NOT in the fgs section + assertFalse(sectioner.isInSection(entry)) + } + + @Test + @EnableFlags(PromotedNotificationUi.FLAG_NAME) + fun testIncludePromotedOngoingInSection_flagEnabled() { + // GIVEN the notification has FLAG_PROMOTED_ONGOING + val entry = buildEntry { setFlag(mContext, Notification.FLAG_PROMOTED_ONGOING, true) } + + // THEN the entry is in the fgs section + assertTrue(sectioner.isInSection(entry)) + } + + @Test + @DisableFlags(PromotedNotificationUi.FLAG_NAME) + fun testDiscludePromotedOngoingInSection_flagDisabled() { + // GIVEN the notification has FLAG_PROMOTED_ONGOING + val entry = buildEntry { setFlag(mContext, Notification.FLAG_PROMOTED_ONGOING, true) } + + // THEN the entry is NOT in the fgs section + assertFalse(sectioner.isInSection(entry)) + } + + @Test + @EnableFlags(PromotedNotificationUi.FLAG_NAME) + fun promoterSelectsPromotedOngoing_flagEnabled() { + val promoter: NotifPromoter = withArgCaptor { verify(notifPipeline).addPromoter(capture()) } + + // GIVEN the notification has FLAG_PROMOTED_ONGOING + val entry = buildEntry { setFlag(mContext, Notification.FLAG_PROMOTED_ONGOING, true) } + + // THEN the entry is promoted to top level + assertTrue(promoter.shouldPromoteToTopLevel(entry)) + } + + @Test + @EnableFlags(PromotedNotificationUi.FLAG_NAME) + fun promoterIgnoresNonPromotedOngoing_flagEnabled() { + val promoter: NotifPromoter = withArgCaptor { verify(notifPipeline).addPromoter(capture()) } + + // GIVEN the notification does not have FLAG_PROMOTED_ONGOING + val entry = buildEntry { setFlag(mContext, Notification.FLAG_PROMOTED_ONGOING, false) } + + // THEN the entry is NOT promoted to top level + assertFalse(promoter.shouldPromoteToTopLevel(entry)) + } + + @Test + @DisableFlags(PromotedNotificationUi.FLAG_NAME) + fun noPromoterAdded_flagDisabled() { + verify(notifPipeline, never()).addPromoter(any()) + } + + private fun makeCallStyle(): Notification.CallStyle { + val pendingIntent = + PendingIntent.getBroadcast(mContext, 0, Intent("action"), PendingIntent.FLAG_IMMUTABLE) + val person = Person.Builder().setName("person").build() + return Notification.CallStyle.forOngoingCall(person, pendingIntent) + } + + companion object { + private const val NOTIF_USER_ID = 0 + } +} 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/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/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 6381b4e0fef7..d570f18e35d8 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,12 +57,11 @@ 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/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java index 6f785a3731e1..dde6e2ee1866 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java @@ -65,11 +65,9 @@ import com.android.keyguard.KeyguardMessageArea; import com.android.keyguard.KeyguardMessageAreaController; import com.android.keyguard.KeyguardSecurityModel; import com.android.keyguard.KeyguardUpdateMonitor; -import com.android.keyguard.KeyguardUpdateMonitorCallback; import com.android.keyguard.ViewMediatorCallback; import com.android.systemui.Flags; import com.android.systemui.SysuiTestCase; -import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor; import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.bouncer.domain.interactor.BouncerInteractor; import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerCallbackInteractor; @@ -146,7 +144,6 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { @Mock private SysuiStatusBarStateController mStatusBarStateController; @Mock private KeyguardUpdateMonitor mKeyguardUpdateMonitor; @Mock private View mNotificationContainer; - @Mock private KeyguardBypassController mBypassController; @Mock private KeyguardMessageAreaController.Factory mKeyguardMessageAreaFactory; @Mock private KeyguardMessageAreaController mKeyguardMessageAreaController; @Mock private KeyguardMessageArea mKeyguardMessageArea; @@ -158,7 +155,6 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { @Mock private PrimaryBouncerCallbackInteractor mPrimaryBouncerCallbackInteractor; @Mock private PrimaryBouncerInteractor mPrimaryBouncerInteractor; @Mock private AlternateBouncerInteractor mAlternateBouncerInteractor; - @Mock private UdfpsOverlayInteractor mUdfpsOverlayInteractor; @Mock private ActivityStarter mActivityStarter; @Mock private BouncerView mBouncerView; @Mock private BouncerViewDelegate mBouncerViewDelegate; @@ -167,7 +163,6 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { @Mock private NotificationShadeWindowView mNotificationShadeWindowView; @Mock private WindowInsetsController mWindowInsetsController; @Mock private TaskbarDelegate mTaskbarDelegate; - @Mock private StatusBarKeyguardViewManager.KeyguardViewManagerCallback mCallback; @Mock private SelectedUserInteractor mSelectedUserInteractor; @Mock private DeviceEntryInteractor mDeviceEntryInteractor; @Mock private SceneInteractor mSceneInteractor; @@ -190,8 +185,6 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { private KeyguardTransitionInteractor mKeyguardTransitionInteractor; @Captor private ArgumentCaptor<OnBackInvokedCallback> mBackCallbackCaptor; - @Captor - private ArgumentCaptor<KeyguardUpdateMonitorCallback> mKeyguardUpdateMonitorCallback; @Mock private KeyguardDismissActionInteractor mKeyguardDismissActionInteractor; @@ -227,7 +220,6 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { mock(DockManager.class), mNotificationShadeWindowController, mKeyguardStateController, - mKeyguardMessageAreaFactory, Optional.of(mSysUiUnfoldComponent), () -> mShadeController, mLatencyTracker, @@ -236,7 +228,6 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { mPrimaryBouncerInteractor, mBouncerView, mAlternateBouncerInteractor, - mUdfpsOverlayInteractor, mActivityStarter, mKeyguardTransitionInteractor, mock(KeyguardDismissTransitionInteractor.class), @@ -732,7 +723,6 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { mock(DockManager.class), mock(NotificationShadeWindowController.class), mKeyguardStateController, - mKeyguardMessageAreaFactory, Optional.of(mSysUiUnfoldComponent), () -> mShadeController, mLatencyTracker, @@ -741,7 +731,6 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { mPrimaryBouncerInteractor, mBouncerView, mAlternateBouncerInteractor, - mUdfpsOverlayInteractor, mActivityStarter, mock(KeyguardTransitionInteractor.class), mock(KeyguardDismissTransitionInteractor.class), 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/window/ui/viewmodel/WindowRootViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModelTest.kt index a023b3c430ad..3da4f29a6fcb 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModelTest.kt @@ -23,14 +23,12 @@ import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope -import com.android.systemui.lifecycle.activateIn import com.android.systemui.testKosmos import com.android.systemui.window.data.repository.fakeWindowRootViewBlurRepository import com.android.systemui.window.data.repository.windowRootViewBlurRepository 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 @@ -43,11 +41,6 @@ class WindowRootViewModelTest : SysuiTestCase() { val underTest by lazy { kosmos.windowRootViewModel } - @Before - fun setup() { - underTest.activateIn(testScope) - } - @Test fun bouncerTransitionChangesWindowBlurRadius() = testScope.runTest { 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..61d6a9046144 --- /dev/null +++ b/packages/SystemUI/res-keyguard/drawable/pin_bouncer_confirm.xml @@ -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. + --> + +<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="#F7DAEE" + android:fillType="evenOdd" + android:pathData="M20.76,19C21.65,19 22.096,17.924 21.467,17.294L19.284,15.105C18.895,14.716 18.895,14.085 19.285,13.695C19.674,13.306 20.306,13.306 20.695,13.695L26.293,19.293C26.683,19.683 26.683,20.317 26.293,20.707L20.705,26.295C20.315,26.685 19.683,26.686 19.292,26.298C18.9,25.907 18.898,25.272 19.29,24.88L21.463,22.707C22.093,22.077 21.647,21 20.756,21H10C9.448,21 9,20.552 9,20C9,19.448 9.448,19 10,19H20.76ZM32,26C32,26.552 31.552,27 31,27C30.448,27 30,26.552 30,26V14C30,13.448 30.448,13 31,13C31.552,13 32,13.448 32,14V26Z" + android:strokeColor="#F7DAEE" + android:strokeLineCap="round" + android:strokeLineJoin="round" + android:strokeWidth="2" /> +</vector> diff --git a/packages/SystemUI/res-keyguard/drawable/pin_bouncer_delete.xml b/packages/SystemUI/res-keyguard/drawable/pin_bouncer_delete.xml new file mode 100644 index 000000000000..044656d6fc7d --- /dev/null +++ b/packages/SystemUI/res-keyguard/drawable/pin_bouncer_delete.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="#ECDFE5" + android:pathData="M18.792,26.5L23.333,21.958L27.875,26.5L29.875,24.542L25.292,20L29.792,15.458L27.833,13.5L23.333,18.042L18.792,13.5L16.792,15.458L21.375,20L16.792,24.542L18.792,26.5ZM14.708,33.333C14.292,33.333 13.875,33.236 13.458,33.042C13.069,32.847 12.75,32.569 12.5,32.208L3.333,20L12.458,7.792C12.708,7.431 13.028,7.153 13.417,6.958C13.833,6.764 14.264,6.667 14.708,6.667H33.917C34.694,6.667 35.347,6.944 35.875,7.5C36.431,8.028 36.708,8.681 36.708,9.458V30.542C36.708,31.319 36.431,31.986 35.875,32.542C35.347,33.069 34.694,33.333 33.917,33.333H14.708Z" /> +</vector> diff --git a/packages/SystemUI/res/layout/battery_status_chip.xml b/packages/SystemUI/res/layout/battery_status_chip.xml index 74371839e247..7399651d4248 100644 --- a/packages/SystemUI/res/layout/battery_status_chip.xml +++ b/packages/SystemUI/res/layout/battery_status_chip.xml @@ -24,21 +24,13 @@ <LinearLayout android:id="@+id/rounded_container" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:minHeight="@dimen/ongoing_appops_chip_height" - android:layout_gravity="center" - android:background="@drawable/statusbar_chip_bg" - android:clipToOutline="true" - android:gravity="center" - android:maxWidth="@dimen/ongoing_appops_chip_max_width" - android:minWidth="@dimen/ongoing_appops_chip_min_width"> + style="@style/StatusBar.EventChip"> <com.android.systemui.battery.BatteryMeterView android:id="@+id/battery_meter_view" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginHorizontal="10dp" /> + android:layout_marginHorizontal="@dimen/ongoing_appops_chip_content_horizontal_margin" /> </LinearLayout> </merge>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/bindable_status_bar_compose_icon.xml b/packages/SystemUI/res/layout/bindable_status_bar_compose_icon.xml new file mode 100644 index 000000000000..fa9318bc151c --- /dev/null +++ b/packages/SystemUI/res/layout/bindable_status_bar_compose_icon.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<!-- Base layout that provides a single bindable compose view --> +<com.android.systemui.statusbar.pipeline.shared.ui.view.SingleBindableStatusBarComposeIconView + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:gravity="center_vertical" + > + + <androidx.compose.ui.platform.ComposeView + android:id="@+id/compose_view" + android:layout_height="@dimen/status_bar_bindable_icon_size" + android:layout_width="wrap_content" + android:layout_gravity="center_vertical" + android:padding="4sp" + /> + +</com.android.systemui.statusbar.pipeline.shared.ui.view.SingleBindableStatusBarComposeIconView> diff --git a/packages/SystemUI/res/layout/magic_action_button.xml b/packages/SystemUI/res/layout/magic_action_button.xml index 82d8d7043ff0..381d6b17dec5 100644 --- a/packages/SystemUI/res/layout/magic_action_button.xml +++ b/packages/SystemUI/res/layout/magic_action_button.xml @@ -1,4 +1,5 @@ -<Button xmlns:android="http://schemas.android.com/apk/res/android" +<com.android.systemui.statusbar.notification.row.MagicActionButton + xmlns:android="http://schemas.android.com/apk/res/android" style="@android:style/Widget.Material.Button" android:layout_width="wrap_content" android:layout_height="@dimen/magic_action_button_touch_target_height" 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/promoted_notification_info.xml b/packages/SystemUI/res/layout/promoted_notification_info.xml index 5d170a98a806..2e0a0ca1185c 100644 --- a/packages/SystemUI/res/layout/promoted_notification_info.xml +++ b/packages/SystemUI/res/layout/promoted_notification_info.xml @@ -323,31 +323,43 @@ asked for it --> android:layout_width="match_parent" android:layout_height="wrap_content" android:minHeight="60dp" + android:layout_marginTop="@dimen/notification_importance_button_separation" android:gravity="center_vertical" - android:paddingStart="4dp" - android:paddingEnd="4dp" + android:background="@drawable/rounded_corners" + android:backgroundTint="@androidprv:color/materialColorPrimaryContainer" > <TextView - android:id="@+id/promoted_demote" - android:text="@string/notification_inline_disable_promotion" - android:layout_width="wrap_content" + android:id="@+id/promoted_explain_title" + android:text="@string/live_notifications_title" + android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentStart="true" android:gravity="start|center_vertical" - android:minWidth="@dimen/notification_importance_toggle_size" - android:minHeight="@dimen/notification_importance_toggle_size" - android:maxWidth="200dp" + android:padding="16dp" + android:color="@androidprv:color/materialColorOnPrimary" style="@style/TextAppearance.NotificationInfo.Button"/> <TextView - android:id="@+id/promoted_dismiss" - android:text="@string/notification_inline_dismiss" + android:id="@+id/promoted_explain" + android:text="@string/live_notifications_desc" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_below="@id/promoted_explain_title" + android:gravity="start|center_vertical" + android:maxWidth="200dp" + android:padding="16dp" + style="@style/TextAppearance.NotificationImportanceDetail"/> + <TextView + android:id="@+id/promoted_demote" + android:text="@string/notification_inline_disable_promotion" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_below="@id/promoted_explain" android:layout_alignParentEnd="true" - android:gravity="end|center_vertical" + android:padding="16dp" + android:gravity="start|center_vertical" android:minWidth="@dimen/notification_importance_toggle_size" android:minHeight="@dimen/notification_importance_toggle_size" - android:maxWidth="125dp" + android:maxWidth="200dp" style="@style/TextAppearance.NotificationInfo.Button"/> </RelativeLayout> diff --git a/packages/SystemUI/res/layout/status_bar_event_chip_compose.xml b/packages/SystemUI/res/layout/status_bar_event_chip_compose.xml new file mode 100644 index 000000000000..ff96ab15cd15 --- /dev/null +++ b/packages/SystemUI/res/layout/status_bar_event_chip_compose.xml @@ -0,0 +1,34 @@ +<?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. + --> + +<merge xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:layout_gravity="center_vertical|end"> + + <LinearLayout + android:id="@+id/rounded_container" + style="@style/StatusBar.EventChip"> + + <!-- Stub for the composable --> + <androidx.compose.ui.platform.ComposeView + android:id="@+id/compose_view" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginHorizontal="@dimen/ongoing_appops_chip_content_horizontal_margin" /> + + </LinearLayout> +</merge> diff --git a/packages/SystemUI/res/layout/volume_dialog.xml b/packages/SystemUI/res/layout/volume_dialog.xml index 8ad99abccdfe..889aefed0c5c 100644 --- a/packages/SystemUI/res/layout/volume_dialog.xml +++ b/packages/SystemUI/res/layout/volume_dialog.xml @@ -14,7 +14,6 @@ limitations under the License. --> <androidx.constraintlayout.motion.widget.MotionLayout xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/volume_dialog" android:layout_width="match_parent" @@ -30,42 +29,47 @@ android:layout_marginTop="@dimen/volume_dialog_background_top_margin" android:layout_marginBottom="@dimen/volume_dialog_background_vertical_margin" android:background="@drawable/volume_dialog_background" - app:layout_constraintBottom_toBottomOf="@id/volume_dialog_settings" + app:layout_constraintBottom_toBottomOf="@id/volume_dialog_bottom_section_container" app:layout_constraintEnd_toEndOf="@id/volume_dialog_main_slider_container" app:layout_constraintStart_toStartOf="@id/volume_dialog_main_slider_container" app:layout_constraintTop_toTopOf="@id/volume_dialog_main_slider_container" /> - <include - android:id="@id/volume_ringer_drawer" - layout="@layout/volume_ringer_drawer" + <FrameLayout + android:id="@+id/volume_dialog_top_section_container" android:layout_width="0dp" android:layout_height="0dp" - android:layout_marginEnd="@dimen/volume_dialog_ringer_drawer_diff_end_margin" android:layout_marginBottom="@dimen/volume_dialog_components_spacing" + android:clipChildren="false" app:layout_constraintBottom_toTopOf="@id/volume_dialog_main_slider_container" app:layout_constraintEnd_toEndOf="@id/volume_dialog_main_slider_container" + app:layout_constraintHeight_default="spread" app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintWidth_default="spread"> + + <include layout="@layout/volume_dialog_top_section" /> + </FrameLayout> <include android:id="@+id/volume_dialog_main_slider_container" layout="@layout/volume_dialog_slider" /> - <ImageButton - android:id="@+id/volume_dialog_settings" - android:layout_width="@dimen/volume_dialog_button_size" - android:layout_height="@dimen/volume_dialog_button_size" + <FrameLayout + android:id="@+id/volume_dialog_bottom_section_container" + android:layout_width="0dp" + android:layout_height="0dp" android:layout_marginTop="@dimen/volume_dialog_components_spacing" - android:background="@drawable/ripple_drawable_20dp" - android:contentDescription="@string/accessibility_volume_settings" - android:scaleType="centerInside" - android:soundEffectsEnabled="false" - android:tint="@androidprv:color/materialColorPrimary" + android:clipChildren="false" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="@id/volume_dialog_main_slider_container" + app:layout_constraintHeight_default="wrap" app:layout_constraintStart_toStartOf="@id/volume_dialog_main_slider_container" app:layout_constraintTop_toBottomOf="@id/volume_dialog_main_slider_container" - app:layout_constraintVertical_bias="0" /> + app:layout_constraintVertical_bias="0" + app:layout_constraintWidth_default="wrap"> + + <include layout="@layout/volume_dialog_bottom_section" /> + </FrameLayout> <LinearLayout android:id="@+id/volume_dialog_floating_sliders_container" diff --git a/packages/SystemUI/res/layout/volume_dialog_bottom_section.xml b/packages/SystemUI/res/layout/volume_dialog_bottom_section.xml new file mode 100644 index 000000000000..b94c430e011c --- /dev/null +++ b/packages/SystemUI/res/layout/volume_dialog_bottom_section.xml @@ -0,0 +1,26 @@ +<!-- + Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<ImageButton xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:id="@+id/volume_dialog_settings" + android:layout_width="@dimen/volume_dialog_button_size" + android:layout_height="@dimen/volume_dialog_button_size" + android:layout_gravity="center" + android:background="@drawable/ripple_drawable_20dp" + android:contentDescription="@string/accessibility_volume_settings" + android:scaleType="centerInside" + android:soundEffectsEnabled="false" + android:tint="@androidprv:color/materialColorPrimary" />
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/volume_ringer_drawer.xml b/packages/SystemUI/res/layout/volume_dialog_top_section.xml index 8f51dbca2774..4fc20e218c5e 100644 --- a/packages/SystemUI/res/layout/volume_ringer_drawer.xml +++ b/packages/SystemUI/res/layout/volume_dialog_top_section.xml @@ -23,16 +23,17 @@ android:clipToPadding="false" android:gravity="center" android:layoutDirection="ltr" + android:paddingEnd="@dimen/volume_dialog_ringer_drawer_diff_end_margin" app:layoutDescription="@xml/volume_dialog_ringer_drawer_motion_scene"> <View android:id="@+id/ringer_buttons_background" android:layout_width="@dimen/volume_dialog_width" android:layout_height="0dp" - android:visibility="gone" android:layout_marginTop="@dimen/volume_dialog_background_vertical_margin" android:layout_marginBottom="@dimen/volume_dialog_background_vertical_margin" - android:background="@drawable/volume_dialog_ringer_background" /> + android:background="@drawable/volume_dialog_ringer_background" + android:visibility="gone" /> <!-- add ringer buttons here --> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 648e4c2e3ac7..2bac87d01bdd 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1242,8 +1242,11 @@ <dimen name="min_window_blur_radius">1px</dimen> <dimen name="max_window_blur_radius">23px</dimen> + <!-- Blur radius of the Notification Shade content (notifications, footer, shelf) --> + <dimen name="max_shade_content_blur_radius">@dimen/max_window_blur_radius</dimen> + <!-- Blur radius behind Notification Shade --> - <dimen name="max_shade_window_blur_radius">60dp</dimen> + <dimen name="max_shade_window_blur_radius">34dp</dimen> <!-- How much into a DisplayCutout's bounds we can go, on each side --> <dimen name="display_cutout_margin_consumption">0px</dimen> @@ -1254,6 +1257,8 @@ <dimen name="ongoing_appops_chip_side_padding">8dp</dimen> <!-- Margin between icons of Ongoing App Ops chip --> <dimen name="ongoing_appops_chip_icon_margin">4dp</dimen> + <!-- Side margins for the content of an appops chip --> + <dimen name="ongoing_appops_chip_content_horizontal_margin">10dp</dimen> <!-- Icon size of Ongoing App Ops chip --> <dimen name="ongoing_appops_chip_icon_size">16sp</dimen> <!-- Radius of Ongoing App Ops chip corners --> @@ -1305,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> @@ -2136,18 +2142,25 @@ <dimen name="volume_dialog_width">60dp</dimen> <dimen name="volume_dialog_background_corner_radius">30dp</dimen> - <dimen name="volume_dialog_background_vertical_margin">-10dp</dimen> + <dimen name="volume_dialog_background_vertical_margin"> + @dimen/volume_dialog_buttons_margin_negative + </dimen> <!-- top margin covers half the ringer button + components spacing --> <dimen name="volume_dialog_background_top_margin">-28dp</dimen> + <dimen name="volume_dialog_window_margin">14dp</dimen> <dimen name="volume_dialog_components_spacing">8dp</dimen> <dimen name="volume_dialog_floating_sliders_spacing">8dp</dimen> <dimen name="volume_dialog_floating_sliders_vertical_padding">10dp</dimen> - <dimen name="volume_dialog_floating_sliders_vertical_padding_negative">-10dp</dimen> + <dimen name="volume_dialog_floating_sliders_vertical_padding_negative"> + @dimen/volume_dialog_buttons_margin_negative + </dimen> <dimen name="volume_dialog_floating_sliders_horizontal_padding">4dp</dimen> <dimen name="volume_dialog_button_size">40dp</dimen> <dimen name="volume_dialog_slider_width">52dp</dimen> <dimen name="volume_dialog_slider_height">254dp</dimen> + <dimen name="volume_dialog_buttons_margin">10dp</dimen> + <dimen name="volume_dialog_buttons_margin_negative">-10dp</dimen> <!-- A primary goal of this margin is to vertically constraint slider height in the landscape orientation when the vertical space is limited @@ -2160,7 +2173,11 @@ <dimen name="volume_dialog_background_square_corner_radius">12dp</dimen> - <dimen name="volume_dialog_ringer_drawer_margin">10dp</dimen> + <dimen name="volume_dialog_ringer_drawer_margin">@dimen/volume_dialog_buttons_margin</dimen> + <!-- + (volume_dialog_slider_width - volume_dialog_button_size) / 2 + This centers ringer drawer against the volume slider + --> <dimen name="volume_dialog_ringer_drawer_diff_end_margin">6dp</dimen> <dimen name="volume_dialog_ringer_drawer_button_size">@dimen/volume_dialog_button_size</dimen> <dimen name="volume_dialog_ringer_drawer_button_icon_radius">10dp</dimen> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 86292039d93d..e077b41a6f59 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2098,7 +2098,13 @@ <string name="notification_inline_dismiss">Dismiss</string> <!-- [CHAR LIMIT=30] Text shown in button used to prevent app from showing Live Updates, for this notification and all future ones --> - <string name="notification_inline_disable_promotion">Don\'t show again</string> + <string name="notification_inline_disable_promotion">Don\'t show as pinned</string> + + <!-- Text accompanying the "Show live updates" switch explaining the purpose of the setting --> + <string name="live_notifications_title">Showing Live Updates</string> + + <!-- Text accompanying the "Show live updates" switch explaining the purpose of the setting --> + <string name="live_notifications_desc">Pinned notifications display live info from apps, and always appear on the status bar and lock screen</string> <!-- Notification: Control panel: Label that displays when the app's notifications cannot be blocked. --> <string name="notification_unblockable_desc">These notifications can\'t be modified.</string> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index 4961a7ece69a..8f808d389203 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -93,6 +93,19 @@ <item name="android:textColor">?android:attr/colorPrimary</item> </style> + <style name="StatusBar.EventChip"> + <item name="android:orientation">horizontal</item> + <item name="android:layout_width">wrap_content</item> + <item name="android:layout_height">wrap_content</item> + <item name="android:layout_gravity">center</item> + <item name="android:gravity">center</item> + <item name="android:clipToOutline">true</item> + <item name="android:background">@drawable/statusbar_chip_bg</item> + <item name="android:minHeight">@dimen/ongoing_appops_chip_height</item> + <item name="android:maxWidth">@dimen/ongoing_appops_chip_max_width</item> + <item name="android:minWidth">@dimen/ongoing_appops_chip_min_width</item> + </style> + <style name="Chipbar" /> <style name="Chipbar.Text" parent="@*android:style/TextAppearance.DeviceDefault.Notification.Title"> 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/res/xml/volume_dialog_constraint_set.xml b/packages/SystemUI/res/xml/volume_dialog_constraint_set.xml index a8f616c2427d..dcc5d4f6635f 100644 --- a/packages/SystemUI/res/xml/volume_dialog_constraint_set.xml +++ b/packages/SystemUI/res/xml/volume_dialog_constraint_set.xml @@ -8,7 +8,7 @@ android:layout_width="@dimen/volume_dialog_slider_width" android:layout_height="0dp" android:layout_marginTop="@dimen/volume_dialog_slider_vertical_margin" - android:layout_marginEnd="@dimen/volume_dialog_components_spacing" + android:layout_marginEnd="@dimen/volume_dialog_window_margin" android:layout_marginBottom="@dimen/volume_dialog_slider_vertical_margin" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" diff --git a/packages/SystemUI/res/xml/volume_dialog_half_folded_constraint_set.xml b/packages/SystemUI/res/xml/volume_dialog_half_folded_constraint_set.xml index b4d8ae791f36..3a5e41d5781a 100644 --- a/packages/SystemUI/res/xml/volume_dialog_half_folded_constraint_set.xml +++ b/packages/SystemUI/res/xml/volume_dialog_half_folded_constraint_set.xml @@ -8,7 +8,7 @@ android:layout_width="@dimen/volume_dialog_slider_width" android:layout_height="0dp" android:layout_marginTop="@dimen/volume_dialog_slider_vertical_margin" - android:layout_marginEnd="@dimen/volume_dialog_components_spacing" + android:layout_marginEnd="@dimen/volume_dialog_window_margin" android:layout_marginBottom="@dimen/volume_dialog_slider_vertical_margin" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" diff --git a/packages/SystemUI/res/xml/volume_dialog_ringer_drawer_motion_scene.xml b/packages/SystemUI/res/xml/volume_dialog_ringer_drawer_motion_scene.xml index 1607121f230f..4c3dbd78cc3e 100644 --- a/packages/SystemUI/res/xml/volume_dialog_ringer_drawer_motion_scene.xml +++ b/packages/SystemUI/res/xml/volume_dialog_ringer_drawer_motion_scene.xml @@ -20,30 +20,9 @@ android:id="@+id/close_to_open_transition" app:constraintSetEnd="@+id/volume_dialog_ringer_drawer_open" app:constraintSetStart="@+id/volume_dialog_ringer_drawer_close" - app:transitionEasing="cubic(0.05, 0.7, 0.1, 1.0)" - app:duration="400"> - </Transition> - - <ConstraintSet android:id="@+id/volume_dialog_ringer_drawer_close"> - <Constraint - android:id="@+id/volume_ringer_drawer" - android:layout_width="match_parent" - android:layout_height="wrap_content" - app:layout_constraintTop_toTopOf="parent" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintEnd_toEndOf="parent"/> - </ConstraintSet> - - <ConstraintSet android:id="@+id/volume_dialog_ringer_drawer_open"> - <Constraint - android:id="@+id/volume_ringer_drawer" - android:layout_width="match_parent" - android:layout_height="wrap_content" - app:layout_constraintTop_toTopOf="parent" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintEnd_toEndOf="parent"/> - </ConstraintSet> + app:duration="400" + app:transitionEasing="cubic(0.05, 0.7, 0.1, 1.0)" /> + <ConstraintSet android:id="@+id/volume_dialog_ringer_drawer_close" /> + <ConstraintSet android:id="@+id/volume_dialog_ringer_drawer_open" /> </MotionScene>
\ No newline at end of file diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java index 82ac78c6db15..0372a6c6d2c5 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java @@ -20,7 +20,6 @@ import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_2BUTTON; import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON; import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL; -import static com.android.systemui.Flags.glanceableHubBackAction; import static com.android.systemui.shared.Flags.shadeAllowBackGesture; import android.annotation.LongDef; @@ -361,10 +360,6 @@ public class QuickStepContract { } // Disable back gesture on the hub, but not when the shade is showing. if ((sysuiStateFlags & SYSUI_STATE_COMMUNAL_HUB_SHOWING) != 0) { - // Allow back gesture on Glanceable Hub with back action support. - if (glanceableHubBackAction()) { - return false; - } // Use QS expanded signal as the notification panel is always considered visible // expanded when on the lock screen and when opening hub over lock screen. This does // mean that back gesture is disabled when opening shade over hub while in portrait diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java index ff6bcdb150f8..fcde508b07a8 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/RecentsAnimationListener.java @@ -16,6 +16,7 @@ package com.android.systemui.shared.system; +import android.annotation.Nullable; import android.graphics.Rect; import android.os.Bundle; import android.view.RemoteAnimationTarget; @@ -42,5 +43,5 @@ public interface RecentsAnimationListener { * Called when the task of an activity that has been started while the recents animation * was running becomes ready for control. */ - void onTasksAppeared(RemoteAnimationTarget[] app); + void onTasksAppeared(RemoteAnimationTarget[] app, @Nullable TransitionInfo transitionInfo); } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java b/packages/SystemUI/src/com/android/keyguard/KeyguardPinBasedInputView.java index fbe9edfd6680..245283da75ab 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,13 @@ 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.setImageResource(R.drawable.pin_bouncer_delete); + } mDeleteButton.setVisibility(View.VISIBLE); mButtons[0] = findViewById(R.id.key0); diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java index 5a9cbce73e4b..892851cd7056 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardViewController.java @@ -173,6 +173,11 @@ public interface KeyguardViewController { boolean isBouncerShowing(); /** + * Report when the UI is ready for dismissing the whole Keyguard. + */ + void readyForKeyguardDone(); + + /** * Stop showing the alternate bouncer, if showing. * * <p>Should be like calling {@link #hideAlternateBouncer(boolean, boolean)} with a {@code true} 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/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/CameraAvailabilityListener.kt b/packages/SystemUI/src/com/android/systemui/CameraAvailabilityListener.kt index ca83724aaf07..7af37bd720e3 100644 --- a/packages/SystemUI/src/com/android/systemui/CameraAvailabilityListener.kt +++ b/packages/SystemUI/src/com/android/systemui/CameraAvailabilityListener.kt @@ -129,6 +129,11 @@ class CameraAvailabilityListener( listeners.remove(callback) } + fun debugFaceAuth(id: Int) { + val info = cameraProtectionInfoList?.getOrNull(id) + if (info != null) notifyCameraActive(info) + } + private fun isExcluded(packageId: String): Boolean { return excludedPackageIds.contains(packageId) } diff --git a/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt b/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt index dbdf93d81eee..8bb236b91860 100644 --- a/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt +++ b/packages/SystemUI/src/com/android/systemui/FaceScanningOverlay.kt @@ -87,7 +87,7 @@ class FaceScanningOverlay( override fun enableShowProtection(isCameraActive: Boolean) { val scanningAnimationRequiredWhenCameraActive = - keyguardUpdateMonitor.isFaceDetectionRunning || authController.isShowing + keyguardUpdateMonitor.isFaceDetectionRunning || authController.isShowing || mDebug val faceAuthSucceeded = keyguardUpdateMonitor.isFaceAuthenticated val showScanningAnimationNow = scanningAnimationRequiredWhenCameraActive && isCameraActive if (showScanningAnimationNow == showScanningAnim) { diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java index da206facd1fb..e7253533aa42 100644 --- a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java +++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java @@ -189,7 +189,7 @@ public class ScreenDecorations implements @VisibleForTesting protected void showCameraProtection(@NonNull Path protectionPath, @NonNull Rect bounds) { - if (mFaceScanningFactory.shouldShowFaceScanningAnim()) { + if (mDebug || mFaceScanningFactory.shouldShowFaceScanningAnim()) { DisplayCutoutView overlay = (DisplayCutoutView) getOverlayView( mFaceScanningViewId); if (overlay != null) { @@ -395,6 +395,12 @@ public class ScreenDecorations implements setupDecorations(); }); } + + if (cmd.getFaceAuthScreen() != null) { + mExecutor.execute(() -> { + debugTriggerFaceAuth(cmd.getFaceAuthScreen()); + }); + } }; @Override @@ -630,6 +636,15 @@ public class ScreenDecorations implements } } + private void debugTriggerFaceAuth(int screen) { + DisplayCutoutView overlay = (DisplayCutoutView) getOverlayView( + mFaceScanningViewId); + if (overlay != null) { + overlay.setDebug(true); + mCameraListener.debugFaceAuth(screen); + } + } + private void setupDecorations() { Trace.beginSection("ScreenDecorations#setupDecorations"); setupDecorationsInner(); @@ -1360,6 +1375,7 @@ public class ScreenDecorations implements final List<Rect> mBounds = new ArrayList(); final Rect mBoundingRect = new Rect(); Rect mTotalBounds = new Rect(); + boolean mDebug = false; private int mColor = Color.BLACK; private int mRotation; @@ -1378,6 +1394,10 @@ public class ScreenDecorations implements } } + public void setDebug(boolean debug) { + mDebug = debug; + } + public void setColor(int color) { if (color == mColor) { return; diff --git a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java index e2065f175c79..85f18800f20b 100644 --- a/packages/SystemUI/src/com/android/systemui/SwipeHelper.java +++ b/packages/SystemUI/src/com/android/systemui/SwipeHelper.java @@ -126,7 +126,7 @@ public class SwipeHelper implements Gefingerpoken, Dumpable { } }; - private final int mFalsingThreshold; + private int mFalsingThreshold; private boolean mTouchAboveFalsingThreshold; private boolean mDisableHwLayers; private final boolean mFadeDependingOnAmountSwiped; @@ -149,8 +149,7 @@ public class SwipeHelper implements Gefingerpoken, Dumpable { // Extra long-press! mLongPressTimeout = (long) (ViewConfiguration.getLongPressTimeout() * 1.5f); - mDensityScale = resources.getDisplayMetrics().density; - mFalsingThreshold = resources.getDimensionPixelSize(R.dimen.swipe_helper_falsing_threshold); + updateResourceProperties(resources); mFadeDependingOnAmountSwiped = resources.getBoolean( R.bool.config_fadeDependingOnAmountSwiped); mFalsingManager = falsingManager; @@ -165,6 +164,14 @@ public class SwipeHelper implements Gefingerpoken, Dumpable { getMaxEscapeAnimDuration() / 1000f); } + /** Update ane properties that depend on Resources */ + public void updateResourceProperties(Resources resources) { + float density = resources.getDisplayMetrics().density; + setDensityScale(density); + mCallback.onDensityScaleChange(density); + mFalsingThreshold = resources.getDimensionPixelSize(R.dimen.swipe_helper_falsing_threshold); + } + public void setDensityScale(float densityScale) { mDensityScale = densityScale; } @@ -1001,5 +1008,8 @@ public class SwipeHelper implements Gefingerpoken, Dumpable { * @return If true, the given view is draggable. */ default boolean canChildBeDragged(@NonNull View animView) { return true; } + + /** The density scale has changed */ + void onDensityScaleChange(float density); } } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/FullscreenMagnificationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/FullscreenMagnificationController.java index b2f3df60c82b..d7bb667b02a9 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/FullscreenMagnificationController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/FullscreenMagnificationController.java @@ -352,7 +352,13 @@ public class FullscreenMagnificationController implements ComponentCallbacks { mTransaction .addTransactionCommittedListener( mExecutor, - this::showBorder) + () -> { + if (getState() == ENABLING) { + // Ensure that we are in the ENABLING process to avoid performing + // animation on a null view. + mShowBorderRunnable.run(); + } + }) .setPosition(mBorderSurfaceControl, -mBorderOffset, -mBorderOffset) .setLayer(mBorderSurfaceControl, Integer.MAX_VALUE) .show(mBorderSurfaceControl) 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/extradim/ExtraDimDialogManager.kt b/packages/SystemUI/src/com/android/systemui/accessibility/extradim/ExtraDimDialogManager.kt index 3f717e282a61..f24c3be0004c 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/extradim/ExtraDimDialogManager.kt +++ b/packages/SystemUI/src/com/android/systemui/accessibility/extradim/ExtraDimDialogManager.kt @@ -20,6 +20,7 @@ import com.android.systemui.animation.DialogCuj import com.android.systemui.animation.DialogTransitionAnimator import com.android.systemui.animation.Expandable import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.plugins.ActivityStarter import com.android.systemui.statusbar.phone.SystemUIDialog import javax.inject.Inject @@ -33,7 +34,7 @@ constructor( private val extraDimDialogDelegateProvider: Provider<ExtraDimDialogDelegate>, private val mActivityStarter: ActivityStarter, private val dialogTransitionAnimator: DialogTransitionAnimator, - private val mainHandler: Handler, + @Main private val mainHandler: Handler, ) { private var dialog: SystemUIDialog? = null diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java index 67aa4ff577b8..f8e4bda15d01 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/AccessibilityFloatingMenuController.java @@ -40,6 +40,7 @@ import com.android.systemui.accessibility.AccessibilityButtonModeObserver; import com.android.systemui.accessibility.AccessibilityButtonModeObserver.AccessibilityButtonMode; import com.android.systemui.accessibility.AccessibilityButtonTargetsObserver; import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.navigationbar.NavigationModeController; import com.android.systemui.settings.DisplayTracker; import com.android.systemui.util.settings.SecureSettings; @@ -114,7 +115,7 @@ public class AccessibilityFloatingMenuController implements SecureSettings secureSettings, DisplayTracker displayTracker, NavigationModeController navigationModeController, - Handler handler) { + @Main Handler handler) { mContext = context; mWindowManager = windowManager; mViewCaptureAwareWindowManager = viewCaptureAwareWindowManager; 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/back/domain/interactor/BackActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/back/domain/interactor/BackActionInteractor.kt index 11a6cb9334ae..0b578c65e915 100644 --- a/packages/SystemUI/src/com/android/systemui/back/domain/interactor/BackActionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/back/domain/interactor/BackActionInteractor.kt @@ -23,9 +23,7 @@ import android.window.OnBackInvokedDispatcher import android.window.WindowOnBackInvokedDispatcher import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.CoreStartable -import com.android.systemui.Flags.glanceableHubBackAction import com.android.systemui.Flags.predictiveBackAnimateShade -import com.android.systemui.communal.domain.interactor.CommunalBackActionInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.plugins.statusbar.StatusBarStateController @@ -52,7 +50,6 @@ constructor( private val windowRootViewVisibilityInteractor: WindowRootViewVisibilityInteractor, private val shadeBackActionInteractor: ShadeBackActionInteractor, private val qsController: QuickSettingsController, - private val communalBackActionInteractor: CommunalBackActionInteractor, ) : CoreStartable { private var isCallbackRegistered = false @@ -114,12 +111,6 @@ constructor( shadeBackActionInteractor.animateCollapseQs(false) return true } - if (glanceableHubBackAction()) { - if (communalBackActionInteractor.canBeDismissed()) { - communalBackActionInteractor.onBackPressed() - return true - } - } if (shouldBackBeHandled()) { if (shadeBackActionInteractor.canBeCollapsed()) { // this is the Shade dismiss animation, so make sure QQS closes when it ends. diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationService.java b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationService.java index e5c22677dbcc..1ec7799a0b94 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationService.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/BiometricNotificationService.java @@ -149,7 +149,7 @@ public class BiometricNotificationService implements CoreStartable { public BiometricNotificationService(@NonNull @Main Context context, @NonNull KeyguardUpdateMonitor keyguardUpdateMonitor, @NonNull KeyguardStateController keyguardStateController, - @NonNull Handler handler, @NonNull NotificationManager notificationManager, + @NonNull @Main Handler handler, @NonNull NotificationManager notificationManager, @NonNull BiometricNotificationBroadcastReceiver biometricNotificationBroadcastReceiver, @NonNull Optional<FingerprintReEnrollNotification> fingerprintReEnrollNotification, @Nullable FingerprintManager fingerprintManager, diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsContent.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsContent.kt index 710fde5c2130..c6644562a9cb 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsContent.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsContent.kt @@ -24,7 +24,7 @@ import androidx.compose.ui.viewinterop.AndroidView import com.android.systemui.res.R @Composable -fun BluetoothDetailsContent(detailsContentViewModel: BluetoothTileDialogViewModel) { +fun BluetoothDetailsContent(detailsContentViewModel: BluetoothDetailsContentViewModel) { AndroidView( modifier = Modifier.fillMaxSize(), factory = { context -> diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsContentManager.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsContentManager.kt index d873f41309cc..eebcf0b0f0c1 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsContentManager.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsContentManager.kt @@ -62,7 +62,7 @@ data class DeviceItemClick(val deviceItem: DeviceItem, val clickedView: View, va class BluetoothDetailsContentManager @AssistedInject constructor( - @Assisted private val initialUiProperties: BluetoothTileDialogViewModel.UiProperties, + @Assisted private val initialUiProperties: BluetoothDetailsContentViewModel.UiProperties, @Assisted private val cachedContentHeight: Int, @Assisted private val bluetoothTileDialogCallback: BluetoothTileDialogCallback, @Assisted private val isInDialog: Boolean, @@ -114,7 +114,7 @@ constructor( @AssistedFactory interface Factory { fun create( - initialUiProperties: BluetoothTileDialogViewModel.UiProperties, + initialUiProperties: BluetoothDetailsContentViewModel.UiProperties, cachedContentHeight: Int, dialogCallback: BluetoothTileDialogCallback, isInDialog: Boolean, @@ -226,7 +226,7 @@ constructor( internal fun onBluetoothStateUpdated( isEnabled: Boolean, - uiProperties: BluetoothTileDialogViewModel.UiProperties, + uiProperties: BluetoothDetailsContentViewModel.UiProperties, ) { bluetoothToggle.apply { isChecked = isEnabled diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsContentViewModel.kt index 308c9d10db93..ff2d9efa1b58 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsContentViewModel.kt @@ -61,12 +61,9 @@ import kotlinx.coroutines.withContext /** * ViewModel for Bluetooth Dialog or Bluetooth Details View after clicking on the Bluetooth QS tile. - * - * TODO: b/378513956 Rename this class to BluetoothDetailsContentViewModel, since it's not only used - * by the dialog view. */ @SysUISingleton -class BluetoothTileDialogViewModel +class BluetoothDetailsContentViewModel @Inject constructor( private val deviceItemInteractor: DeviceItemInteractor, @@ -312,7 +309,7 @@ constructor( return bluetoothDialogDelegateFactory.create( getUiProperties(), getCachedContentHeight(), - this@BluetoothTileDialogViewModel, + this@BluetoothDetailsContentViewModel, { cancelJob() }, ) } @@ -321,7 +318,7 @@ constructor( return bluetoothDetailsContentManagerFactory.create( getUiProperties(), getCachedContentHeight(), - this@BluetoothTileDialogViewModel, + this@BluetoothDetailsContentViewModel, /* isInDialog= */ false, /* doneButtonCallback= */ fun() {}, ) diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsViewModel.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsViewModel.kt index 44475318a61e..5863a9385234 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsViewModel.kt @@ -20,7 +20,7 @@ import com.android.systemui.plugins.qs.TileDetailsViewModel class BluetoothDetailsViewModel( private val onSettingsClick: () -> Unit, - val detailsContentViewModel: BluetoothTileDialogViewModel, + val detailsContentViewModel: BluetoothDetailsContentViewModel, ) : TileDetailsViewModel() { override fun clickOnSettingsButton() { onSettingsClick() diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt index 01be820a2fde..c55f60587527 100644 --- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt @@ -32,7 +32,7 @@ import dagger.assisted.AssistedInject class BluetoothTileDialogDelegate @AssistedInject constructor( - @Assisted private val initialUiProperties: BluetoothTileDialogViewModel.UiProperties, + @Assisted private val initialUiProperties: BluetoothDetailsContentViewModel.UiProperties, @Assisted private val cachedContentHeight: Int, @Assisted private val bluetoothTileDialogCallback: BluetoothTileDialogCallback, @Assisted private val dismissListener: Runnable, @@ -48,7 +48,7 @@ constructor( @AssistedFactory interface Factory { fun create( - initialUiProperties: BluetoothTileDialogViewModel.UiProperties, + initialUiProperties: BluetoothDetailsContentViewModel.UiProperties, cachedContentHeight: Int, dialogCallback: BluetoothTileDialogCallback, dimissListener: Runnable, 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..149efcdcbb8a 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 @@ -126,5 +126,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/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/CommunalBackupRestoreStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/CommunalBackupRestoreStartable.kt index 7abad1448318..18d461add52b 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/CommunalBackupRestoreStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalBackupRestoreStartable.kt @@ -29,6 +29,7 @@ import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.communal.widgets.CommunalWidgetModule import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.Logger import com.android.systemui.log.dagger.CommunalLog @@ -43,7 +44,7 @@ constructor( private val communalInteractor: CommunalInteractor, @CommunalLog logBuffer: LogBuffer, private val secureSettings: SecureSettings, - handler: Handler, + @Main handler: Handler, ) : CoreStartable, BroadcastReceiver() { private val logger = Logger(logBuffer, TAG) diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalBackActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalBackActionInteractor.kt deleted file mode 100644 index 2ccf96abff79..000000000000 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalBackActionInteractor.kt +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.systemui.communal.domain.interactor - -import com.android.systemui.communal.shared.model.CommunalScenes -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.scene.domain.interactor.SceneInteractor -import com.android.systemui.scene.shared.flag.SceneContainerFlag -import com.android.systemui.scene.shared.model.Scenes -import javax.inject.Inject - -/** - * {@link CommunalBackActionInteractor} is responsible for handling back gestures on the glanceable - * hub. When invoked SystemUI should navigate back to the lockscreen. - */ -@SysUISingleton -class CommunalBackActionInteractor -@Inject -constructor( - private val communalInteractor: CommunalInteractor, - private val communalSceneInteractor: CommunalSceneInteractor, - private val sceneInteractor: SceneInteractor, -) { - fun canBeDismissed(): Boolean { - return communalInteractor.isCommunalShowing.value - } - - fun onBackPressed() { - if (SceneContainerFlag.isEnabled) { - // TODO(b/384610333): Properly determine whether to go to dream or lockscreen on back. - sceneInteractor.changeScene( - toScene = Scenes.Lockscreen, - loggingReason = "CommunalBackActionInteractor", - ) - } else { - communalSceneInteractor.changeScene( - newScene = CommunalScenes.Blank, - loggingReason = "CommunalBackActionInteractor", - ) - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt index 6dab32a66c94..564628d3f52f 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt @@ -327,7 +327,7 @@ constructor( * use [isIdleOnCommunal]. */ // TODO(b/323215860): rename to something more appropriate after cleaning up usages - val isCommunalShowing: StateFlow<Boolean> = + val isCommunalShowing: Flow<Boolean> = flow { emit(SceneContainerFlag.isEnabled) } .flatMapLatest { sceneContainerEnabled -> if (sceneContainerEnabled) { @@ -345,10 +345,10 @@ constructor( columnName = "isCommunalShowing", initialValue = false, ) - .stateIn( + .shareIn( scope = applicationScope, - started = SharingStarted.Eagerly, - initialValue = false, + started = SharingStarted.WhileSubscribed(), + replay = 1, ) /** diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt index 740555f40dea..477b87119563 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractor.kt @@ -32,6 +32,7 @@ import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionInfo import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.util.kotlin.pairwise import java.util.UUID @@ -65,6 +66,7 @@ constructor( @Application private val applicationScope: CoroutineScope, private val sceneInteractor: CommunalSceneInteractor, private val repository: CommunalSceneTransitionRepository, + private val powerInteractor: PowerInteractor, keyguardInteractor: KeyguardInteractor, ) : CoreStartable, CommunalSceneInteractor.OnSceneAboutToChangeListener { @@ -88,12 +90,15 @@ constructor( combine( // Don't use delayed dreaming signal as otherwise we might go to occluded or lock // screen when closing hub if dream just started under the hub. + powerInteractor.isAsleep, keyguardInteractor.isDreamingWithOverlay, keyguardInteractor.isKeyguardOccluded, keyguardInteractor.isKeyguardGoingAway, keyguardInteractor.isKeyguardShowing, - ) { dreaming, occluded, keyguardGoingAway, keyguardShowing -> - if (keyguardGoingAway) { + ) { asleep, dreaming, occluded, keyguardGoingAway, keyguardShowing -> + if (asleep) { + KeyguardState.DOZING + } else if (keyguardGoingAway) { KeyguardState.GONE } else if (occluded && !dreaming) { KeyguardState.OCCLUDED diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java index 8e0beda9eff7..7354f4096801 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java @@ -212,13 +212,6 @@ public class FrameworkServicesModule { return new UserScopedServiceImpl<>(context, CaptioningManager.class); } - /** */ - @Provides - @Singleton - public Choreographer providesChoreographer() { - return Choreographer.getInstance(); - } - @Provides @Singleton static ColorDisplayManager provideColorDisplayManager(Context context) { 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/decor/ScreenDecorCommand.kt b/packages/SystemUI/src/com/android/systemui/decor/ScreenDecorCommand.kt index fa1d898de850..c5d5ca26b14c 100644 --- a/packages/SystemUI/src/com/android/systemui/decor/ScreenDecorCommand.kt +++ b/packages/SystemUI/src/com/android/systemui/decor/ScreenDecorCommand.kt @@ -53,6 +53,14 @@ class ScreenDecorCommand( val roundedBottom: RoundedCornerSubCommand? by subCommand(RoundedCornerSubCommand("rounded-bottom")) + val faceAuthScreen: Int? by + param( + longName = "faceAuthScreen", + description = + "Specify a screen to show face auth animation. 0:outer(default screen), 1:inner", + valueParser = Type.Int, + ) + override fun execute(pw: PrintWriter) { callback.onExecute(this, pw) } @@ -61,6 +69,7 @@ class ScreenDecorCommand( return "ScreenDecorCommand(" + "debug=$debug, " + "color=$color, " + + "faceAuthScreen=$faceAuthScreen, " + "roundedTop=$roundedTop, " + "roundedBottom=$roundedBottom)" } diff --git a/packages/SystemUI/src/com/android/systemui/doze/DozeScreenBrightness.java b/packages/SystemUI/src/com/android/systemui/doze/DozeScreenBrightness.java index abc810afca98..6b762bacb440 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/DozeScreenBrightness.java +++ b/packages/SystemUI/src/com/android/systemui/doze/DozeScreenBrightness.java @@ -40,6 +40,7 @@ import android.util.IndentingPrintWriter; import android.view.Display; import com.android.internal.R; +import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.doze.dagger.BrightnessSensor; import com.android.systemui.doze.dagger.DozeScope; import com.android.systemui.doze.dagger.WrappedService; @@ -118,7 +119,7 @@ public class DozeScreenBrightness extends BroadcastReceiver implements DozeMachi @WrappedService DozeMachine.Service service, AsyncSensorManager sensorManager, @BrightnessSensor Optional<Sensor>[] lightSensorOptional, - DozeHost host, Handler handler, + DozeHost host, @Main Handler handler, AlwaysOnDisplayPolicy alwaysOnDisplayPolicy, WakefulnessLifecycle wakefulnessLifecycle, DozeParameters dozeParameters, diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java index a56a63c0b104..3132ec2b98e3 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java @@ -53,7 +53,6 @@ import com.android.internal.logging.UiEventLogger; import com.android.internal.policy.PhoneWindow; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; -import com.android.systemui.Flags; import com.android.systemui.ambient.touch.TouchHandler; import com.android.systemui.ambient.touch.TouchMonitor; import com.android.systemui.ambient.touch.dagger.AmbientTouchComponent; @@ -211,7 +210,6 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ mCommunalVisible = communalVisible; updateLifecycleStateLocked(); - updateGestureBlockingLocked(); }); } }; @@ -594,8 +592,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ private void updateGestureBlockingLocked() { final boolean shouldBlock = mStarted && !mShadeExpanded && !mBouncerShowing - && !isDreamInPreviewMode() - && !(Flags.glanceableHubBackAction() && mCommunalVisible); + && !isDreamInPreviewMode(); if (shouldBlock) { mGestureInteractor.addGestureBlockedMatcher(DREAM_TYPE_MATCHER, diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/service/TaskFragmentComponent.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/service/TaskFragmentComponent.kt index 67de30c8fa5c..c81583864d45 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/service/TaskFragmentComponent.kt +++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/service/TaskFragmentComponent.kt @@ -83,7 +83,8 @@ constructor( resultT.addTaskFragmentOperation( fragmentToken, TaskFragmentOperation.Builder( - TaskFragmentOperation.OP_TYPE_REORDER_TO_TOP_OF_TASK + TaskFragmentOperation + .OP_TYPE_PRIVILEGED_REORDER_TO_TOP_OF_TASK ) .build(), ) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index caf0fd4450fc..efa9c21f96b4 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -16,6 +16,7 @@ package com.android.systemui.keyguard; +import static android.app.KeyguardManager.LOCK_ON_USER_SWITCH_CALLBACK; import static android.app.StatusBarManager.SESSION_KEYGUARD; import static android.provider.Settings.Secure.LOCK_SCREEN_LOCK_AFTER_TIMEOUT; import static android.provider.Settings.System.LOCKSCREEN_SOUNDS_ENABLED; @@ -75,6 +76,7 @@ import android.os.Bundle; import android.os.DeadObjectException; import android.os.Handler; import android.os.IBinder; +import android.os.IRemoteCallback; import android.os.Looper; import android.os.Message; import android.os.PowerManager; @@ -192,6 +194,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Arrays; +import java.util.Iterator; +import java.util.List; import java.util.Objects; import java.util.concurrent.Executor; import java.util.function.Consumer; @@ -282,6 +286,9 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, private static final int SYSTEM_READY = 18; private static final int CANCEL_KEYGUARD_EXIT_ANIM = 19; private static final int BOOT_INTERACTOR = 20; + private static final int BEFORE_USER_SWITCHING = 21; + private static final int USER_SWITCHING = 22; + private static final int USER_SWITCH_COMPLETE = 23; /** Enum for reasons behind updating wakeAndUnlock state. */ @Retention(RetentionPolicy.SOURCE) @@ -299,6 +306,8 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, int WAKE_AND_UNLOCK = 3; } + private final List<LockNowCallback> mLockNowCallbacks = new ArrayList<>(); + /** * The default amount of time we stay awake (used for all key input) */ @@ -357,13 +366,18 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, private final Lazy<NotificationShadeDepthController> mNotificationShadeDepthController; private final Lazy<ShadeController> mShadeController; private final Lazy<CommunalSceneInteractor> mCommunalSceneInteractor; + /* + * Records the user id on request to go away, for validation when WM calls back to start the + * exit animation. + */ + private int mGoingAwayRequestedForUserId = -1; + private boolean mSystemReady; private boolean mBootCompleted; private boolean mBootSendUserPresent; private boolean mShuttingDown; private boolean mDozing; private boolean mAnimatingScreenOff; - private boolean mIgnoreDismiss; private final Context mContext; private final FalsingCollector mFalsingCollector; @@ -626,41 +640,92 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } }; - KeyguardUpdateMonitorCallback mUpdateCallback = new KeyguardUpdateMonitorCallback() { + @VisibleForTesting + protected UserTracker.Callback mUserChangedCallback = new UserTracker.Callback() { @Override - public void onKeyguardVisibilityChanged(boolean visible) { - synchronized (KeyguardViewMediator.this) { - if (!visible && mPendingPinLock) { - Log.i(TAG, "PIN lock requested, starting keyguard"); + public void onBeforeUserSwitching(int newUser, @NonNull Runnable resultCallback) { + mHandler.sendMessage(mHandler.obtainMessage(BEFORE_USER_SWITCHING, + newUser, 0, resultCallback)); + } - // Bring the keyguard back in order to show the PIN lock - mPendingPinLock = false; - doKeyguardLocked(null); - } - } + @Override + public void onUserChanging(int newUser, @NonNull Context userContext, + @NonNull Runnable resultCallback) { + mHandler.sendMessage(mHandler.obtainMessage(USER_SWITCHING, + newUser, 0, resultCallback)); } @Override - public void onUserSwitching(int userId) { - Log.d(TAG, String.format("onUserSwitching %d", userId)); - synchronized (KeyguardViewMediator.this) { - mIgnoreDismiss = true; - notifyTrustedChangedLocked(mUpdateMonitor.getUserHasTrust(userId)); - resetKeyguardDonePendingLocked(); + public void onUserChanged(int newUser, Context userContext) { + mHandler.sendMessage(mHandler.obtainMessage(USER_SWITCH_COMPLETE, + newUser, 0)); + } + }; + + /** + * Handle {@link #BEFORE_USER_SWITCHING} + */ + @VisibleForTesting + void handleBeforeUserSwitching(int userId, Runnable resultCallback) { + Log.d(TAG, String.format("onBeforeUserSwitching %d", userId)); + synchronized (KeyguardViewMediator.this) { + mHandler.removeMessages(DISMISS); + notifyTrustedChangedLocked(mUpdateMonitor.getUserHasTrust(userId)); + resetKeyguardDonePendingLocked(); + adjustStatusBarLocked(); + mKeyguardStateController.notifyKeyguardGoingAway(false); + if (mLockPatternUtils.isSecure(userId) && !mShowing) { + doKeyguardLocked(null); + } else { resetStateLocked(); - adjustStatusBarLocked(); } + resultCallback.run(); } + } - @Override - public void onUserSwitchComplete(int userId) { - mIgnoreDismiss = false; - Log.d(TAG, String.format("onUserSwitchComplete %d", userId)); + /** + * Handle {@link #USER_SWITCHING} + */ + @VisibleForTesting + void handleUserSwitching(int userId, Runnable resultCallback) { + Log.d(TAG, String.format("onUserSwitching %d", userId)); + synchronized (KeyguardViewMediator.this) { + if (!mLockPatternUtils.isSecure(userId)) { + dismiss(null, null); + } + resultCallback.run(); + } + } + + /** + * Handle {@link #USER_SWITCH_COMPLETE} + */ + @VisibleForTesting + void handleUserSwitchComplete(int userId) { + Log.d(TAG, String.format("onUserSwitchComplete %d", userId)); + // Calling dismiss on a secure user will show the bouncer + if (mLockPatternUtils.isSecure(userId)) { // We are calling dismiss with a delay as there are race conditions in some scenarios // caused by async layout listeners mHandler.postDelayed(() -> dismiss(null /* callback */, null /* message */), 500); } + } + + KeyguardUpdateMonitorCallback mUpdateCallback = new KeyguardUpdateMonitorCallback() { + + @Override + public void onKeyguardVisibilityChanged(boolean visible) { + synchronized (KeyguardViewMediator.this) { + if (!visible && mPendingPinLock) { + Log.i(TAG, "PIN lock requested, starting keyguard"); + + // Bring the keyguard back in order to show the PIN lock + mPendingPinLock = false; + doKeyguardLocked(null); + } + } + } @Override public void onDeviceProvisioned() { @@ -1671,7 +1736,13 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, com.android.internal.R.anim.lock_screen_behind_enter); mWorkLockController = new WorkLockActivityController(mContext, mUserTracker); - + mUserTracker.addCallback(mUserChangedCallback, mContext.getMainExecutor()); + // start() can be invoked in the middle of user switching, so check for this state and issue + // the call manually as that important event was missed. + if (mUserTracker.isUserSwitching()) { + handleBeforeUserSwitching(mUserTracker.getUserId(), () -> {}); + handleUserSwitching(mUserTracker.getUserId(), () -> {}); + } mJavaAdapter.alwaysCollectFlow( mWallpaperRepository.getWallpaperSupportsAmbientMode(), this::setWallpaperSupportsAmbientMode); @@ -1720,7 +1791,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, // System ready can be invoked in the middle of user switching, so check for this state // and issue the call manually as that important event was missed. if (mUserTracker.isUserSwitching()) { - mUpdateCallback.onUserSwitching(mUserTracker.getUserId()); + mUserChangedCallback.onUserChanging(mUserTracker.getUserId(), mContext, () -> {}); } } // Most services aren't available until the system reaches the ready state, so we @@ -2361,12 +2432,23 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mCommunalSceneInteractor.get().showHubFromPowerButton(); } + int currentUserId = mSelectedUserInteractor.getSelectedUserId(); + if (options != null && options.getBinder(LOCK_ON_USER_SWITCH_CALLBACK) != null) { + LockNowCallback callback = new LockNowCallback(currentUserId, + IRemoteCallback.Stub.asInterface( + options.getBinder(LOCK_ON_USER_SWITCH_CALLBACK))); + synchronized (mLockNowCallbacks) { + mLockNowCallbacks.add(callback); + } + Log.d(TAG, "LockNowCallback required for user: " + callback.mUserId); + } + // if another app is disabling us, don't show if (!mExternallyEnabled && !mLockPatternUtils.isUserInLockdown( mSelectedUserInteractor.getSelectedUserId())) { if (DEBUG) Log.d(TAG, "doKeyguard: not showing because externally disabled"); - + notifyLockNowCallback(); mNeedToReshowWhenReenabled = true; return; } @@ -2384,6 +2466,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, // We're removing "reset" in the refactor - "resetting" the views will happen // as a reaction to the root cause of the "reset" signal. if (KeyguardWmStateRefactor.isEnabled()) { + notifyLockNowCallback(); return; } @@ -2396,6 +2479,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, + "previously hiding. It should be safe to short-circuit " + "here."); resetStateLocked(/* hideBouncer= */ false); + notifyLockNowCallback(); return; } } else { @@ -2422,6 +2506,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, Log.d(TAG, "doKeyguard: not showing because device isn't provisioned and the sim is" + " not locked or missing"); } + notifyLockNowCallback(); return; } @@ -2429,6 +2514,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, if (mLockPatternUtils.isLockScreenDisabled(mSelectedUserInteractor.getSelectedUserId()) && !lockedOrMissing && !forceShow) { if (DEBUG) Log.d(TAG, "doKeyguard: not showing because lockscreen is off"); + notifyLockNowCallback(); return; } @@ -2476,11 +2562,6 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } public void dismiss(IKeyguardDismissCallback callback, CharSequence message) { - if (mIgnoreDismiss) { - android.util.Log.i(TAG, "Ignoring request to dismiss (user switch in progress?)"); - return; - } - if (mKeyguardStateController.isKeyguardGoingAway()) { Log.i(TAG, "Ignoring dismiss because we're already going away."); return; @@ -2498,7 +2579,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } private void resetStateLocked(boolean hideBouncer) { - if (DEBUG) Log.e(TAG, "resetStateLocked"); + if (DEBUG) Log.d(TAG, "resetStateLocked"); Message msg = mHandler.obtainMessage(RESET, hideBouncer ? 1 : 0, 0); mHandler.sendMessage(msg); } @@ -2746,6 +2827,18 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, message = "BOOT_INTERACTOR"; handleBootInteractor(); break; + case BEFORE_USER_SWITCHING: + message = "BEFORE_USER_SWITCHING"; + handleBeforeUserSwitching(msg.arg1, (Runnable) msg.obj); + break; + case USER_SWITCHING: + message = "USER_SWITCHING"; + handleUserSwitching(msg.arg1, (Runnable) msg.obj); + break; + case USER_SWITCH_COMPLETE: + message = "USER_SWITCH_COMPLETE"; + handleUserSwitchComplete(msg.arg1); + break; } Log.d(TAG, "KeyguardViewMediator queue processing message: " + message); } @@ -2887,6 +2980,9 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mUiBgExecutor.execute(() -> { Log.d(TAG, "updateActivityLockScreenState(" + showing + ", " + aodShowing + ", " + reason + ")"); + if (showing) { + notifyLockNowCallback(); + } if (KeyguardWmStateRefactor.isEnabled()) { // Handled in WmLockscreenVisibilityManager if flag is enabled. @@ -2931,6 +3027,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, synchronized (KeyguardViewMediator.this) { if (!mSystemReady) { if (DEBUG) Log.d(TAG, "ignoring handleShow because system is not ready."); + notifyLockNowCallback(); return; } if (DEBUG) Log.d(TAG, "handleShow"); @@ -2989,12 +3086,11 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } } - private final Runnable mKeyguardGoingAwayRunnable = new Runnable() { + final Runnable mKeyguardGoingAwayRunnable = new Runnable() { @SuppressLint("MissingPermission") @Override public void run() { Trace.beginSection("KeyguardViewMediator.mKeyGuardGoingAwayRunnable"); - Log.d(TAG, "keyguardGoingAwayRunnable"); mKeyguardViewControllerLazy.get().keyguardGoingAway(); int flags = 0; @@ -3031,6 +3127,10 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, // Handled in WmLockscreenVisibilityManager if flag is enabled. if (!KeyguardWmStateRefactor.isEnabled()) { + mGoingAwayRequestedForUserId = mSelectedUserInteractor.getSelectedUserId(); + Log.d(TAG, "keyguardGoingAway requested for userId: " + + mGoingAwayRequestedForUserId); + // Don't actually hide the Keyguard at the moment, wait for window manager // until it tells us it's safe to do so with startKeyguardExitAnimation. // Posting to mUiOffloadThread to ensure that calls to ActivityTaskManager @@ -3169,6 +3269,30 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, RemoteAnimationTarget[] nonApps, IRemoteAnimationFinishedCallback finishedCallback) { Log.d(TAG, "handleStartKeyguardExitAnimation startTime=" + startTime + " fadeoutDuration=" + fadeoutDuration); + int currentUserId = mSelectedUserInteractor.getSelectedUserId(); + if (!KeyguardWmStateRefactor.isEnabled() && mGoingAwayRequestedForUserId != currentUserId) { + Log.e(TAG, "Not executing handleStartKeyguardExitAnimationInner() due to userId " + + "mismatch. Requested: " + mGoingAwayRequestedForUserId + ", current: " + + currentUserId); + if (finishedCallback != null) { + // There will not execute animation, send a finish callback to ensure the remote + // animation won't hang there. + try { + finishedCallback.onAnimationFinished(); + } catch (RemoteException e) { + Slog.w(TAG, "Failed to call onAnimationFinished", e); + } + } + mHiding = false; + if (mLockPatternUtils.isSecure(currentUserId)) { + doKeyguardLocked(null); + } else { + resetStateLocked(); + dismiss(null, null); + } + return; + } + synchronized (KeyguardViewMediator.this) { mIsKeyguardExitAnimationCanceled = false; // Tell ActivityManager that we canceled the keyguard animation if @@ -3413,6 +3537,13 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, * app transition before finishing the current RemoteAnimation, or the keyguard being re-shown). */ private void handleCancelKeyguardExitAnimation() { + if (!KeyguardWmStateRefactor.isEnabled() + && mGoingAwayRequestedForUserId != mSelectedUserInteractor.getSelectedUserId()) { + Log.e(TAG, "Setting pendingLock = true due to userId mismatch. Requested: " + + mGoingAwayRequestedForUserId + ", current: " + + mSelectedUserInteractor.getSelectedUserId()); + setPendingLock(true); + } if (mPendingLock) { Log.d(TAG, "#handleCancelKeyguardExitAnimation: keyguard exit animation cancelled. " + "There's a pending lock, so we were cancelled because the device was locked " @@ -3513,6 +3644,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mSurfaceBehindRemoteAnimationRequested = true; if (ENABLE_NEW_KEYGUARD_SHELL_TRANSITIONS && !KeyguardWmStateRefactor.isEnabled()) { + mGoingAwayRequestedForUserId = mSelectedUserInteractor.getSelectedUserId(); startKeyguardTransition(false /* keyguardShowing */, false /* aodShowing */); return; } @@ -3533,6 +3665,9 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, if (!KeyguardWmStateRefactor.isEnabled()) { // Handled in WmLockscreenVisibilityManager. + mGoingAwayRequestedForUserId = mSelectedUserInteractor.getSelectedUserId(); + Log.d(TAG, "keyguardGoingAway requested for userId: " + + mGoingAwayRequestedForUserId); mActivityTaskManagerService.keyguardGoingAway(flags); } } catch (RemoteException e) { @@ -3988,6 +4123,29 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mUiBgExecutor.execute(mTrustManager::reportKeyguardShowingChanged); } + private void notifyLockNowCallback() { + List<LockNowCallback> callbacks; + synchronized (mLockNowCallbacks) { + callbacks = new ArrayList<LockNowCallback>(mLockNowCallbacks); + mLockNowCallbacks.clear(); + } + Iterator<LockNowCallback> iter = callbacks.listIterator(); + while (iter.hasNext()) { + LockNowCallback callback = iter.next(); + iter.remove(); + if (callback.mUserId != mSelectedUserInteractor.getSelectedUserId()) { + Log.i(TAG, "Not notifying lockNowCallback due to user mismatch"); + continue; + } + Log.i(TAG, "Notifying lockNowCallback"); + try { + callback.mRemoteCallback.sendResult(null); + } catch (RemoteException e) { + Log.e(TAG, "Could not issue LockNowCallback sendResult", e); + } + } + } + private void notifyTrustedChangedLocked(boolean trusted) { int size = mKeyguardStateCallbacks.size(); for (int i = size - 1; i >= 0; i--) { @@ -4152,4 +4310,14 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } }; } + + private class LockNowCallback { + final int mUserId; + final IRemoteCallback mRemoteCallback; + + LockNowCallback(int userId, IRemoteCallback remoteCallback) { + mUserId = userId; + mRemoteCallback = remoteCallback; + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt index 2a1cb12c153e..f7ed10f40842 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt @@ -24,6 +24,7 @@ import android.annotation.SuppressLint import android.os.Trace import android.util.Log import com.android.app.animation.Interpolators +import com.android.app.tracing.coroutines.flow.traceAs import com.android.app.tracing.coroutines.withContextTraced as withContext import com.android.systemui.Flags.transitionRaceCondition import com.android.systemui.dagger.SysUISingleton @@ -43,7 +44,6 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter @@ -143,11 +143,12 @@ constructor( @SuppressLint("SharedFlowCreation") private val _transitions = MutableSharedFlow<TransitionStep>( - replay = 2, - extraBufferCapacity = 20, - onBufferOverflow = BufferOverflow.DROP_OLDEST, - ) - override val transitions = _transitions.asSharedFlow().distinctUntilChanged() + replay = 2, + extraBufferCapacity = 20, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) + .traceAs("KTR-transitions") + override val transitions = _transitions.distinctUntilChanged() private var lastStep: TransitionStep = TransitionStep() private var lastAnimator: ValueAnimator? = null private var animatorListener: AnimatorListenerAdapter? = null 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 c755c4b02feb..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 @@ -17,8 +17,12 @@ package com.android.systemui.keyguard.domain.interactor import android.animation.ValueAnimator +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor import com.android.systemui.communal.domain.interactor.CommunalInteractor +import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor +import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor +import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main @@ -46,7 +50,6 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart -import com.android.app.tracing.coroutines.launchTraced as launch @SysUISingleton class FromAlternateBouncerTransitionInteractor @@ -60,6 +63,8 @@ constructor( @Main mainDispatcher: CoroutineDispatcher, keyguardInteractor: KeyguardInteractor, private val communalInteractor: CommunalInteractor, + private val communalSettingsInteractor: CommunalSettingsInteractor, + private val communalSceneInteractor: CommunalSceneInteractor, powerInteractor: PowerInteractor, keyguardOcclusionInteractor: KeyguardOcclusionInteractor, private val primaryBouncerInteractor: PrimaryBouncerInteractor, @@ -86,7 +91,7 @@ constructor( .transition( edge = Edge.create(from = KeyguardState.ALTERNATE_BOUNCER, to = Scenes.Gone), edgeWithoutSceneContainer = - Edge.create(from = KeyguardState.ALTERNATE_BOUNCER, to = KeyguardState.GONE) + Edge.create(from = KeyguardState.ALTERNATE_BOUNCER, to = KeyguardState.GONE), ) .map { // The alt bouncer is pretty fast to hide, so start the surface behind animation @@ -112,27 +117,21 @@ constructor( keyguardInteractor.primaryBouncerShowing, powerInteractor.isAwake, keyguardInteractor.isAodAvailable, - communalInteractor.isIdleOnCommunal, - communalInteractor.editModeOpen, + communalSceneInteractor.isIdleOnCommunal, + 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) { if (isAodAvailable) { @@ -141,16 +140,32 @@ constructor( KeyguardState.DOZING } } else { - if (isIdleOnCommunal) { + 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 } else { KeyguardState.LOCKSCREEN } } - startTransitionTo(to) + + if (hubV2 && to != KeyguardState.GLANCEABLE_HUB && isIdleOnCommunal) { + // If bouncer is showing over the hub, we need to make sure we + // properly dismiss the hub when transitioning away. + communalSceneInteractor.changeScene( + newScene = CommunalScenes.Blank, + loggingReason = "alternate bouncer no longer showing over GH", + keyguardState = to, + ) + } else { + startTransitionTo(to) + } } } } @@ -173,7 +188,7 @@ constructor( } else { emptyFlow() } - } + }, ) .filterRelevantKeyguardState() .collect { startTransitionTo(KeyguardState.GONE) } 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 6f5f662d6fa3..0700ec639153 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,6 +159,7 @@ 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) { @@ -191,6 +192,13 @@ 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/FromPrimaryBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt index 30c1aac7b2a7..75d6631008ca 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt @@ -21,6 +21,7 @@ import android.util.Log import com.android.app.tracing.coroutines.launchTraced as launch import com.android.keyguard.KeyguardSecurityModel import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor +import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background @@ -35,6 +36,7 @@ import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.user.domain.interactor.SelectedUserInteractor import com.android.systemui.util.kotlin.Utils.Companion.sample +import com.android.systemui.util.kotlin.sample import com.android.wm.shell.shared.animation.Interpolators import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds @@ -42,6 +44,7 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart @@ -57,6 +60,7 @@ constructor( @Main mainDispatcher: CoroutineDispatcher, keyguardInteractor: KeyguardInteractor, private val communalSceneInteractor: CommunalSceneInteractor, + private val communalSettingsInteractor: CommunalSettingsInteractor, private val keyguardSecurityModel: KeyguardSecurityModel, private val selectedUserInteractor: SelectedUserInteractor, powerInteractor: PowerInteractor, @@ -140,14 +144,17 @@ constructor( ) .collect { (_, isAwake, isDreaming, isIdleOnCommunal) -> val isOccluded = keyguardInteractor.isKeyguardOccluded.value + val hubV2 = communalSettingsInteractor.isV2FlagEnabled() val toState = if (isAwake) { if (isOccluded && !isDreaming) { KeyguardState.OCCLUDED - } else if (isIdleOnCommunal) { + } else if (!hubV2 && isIdleOnCommunal) { KeyguardState.GLANCEABLE_HUB } else if (isDreaming) { KeyguardState.DREAMING + } else if (hubV2 && isIdleOnCommunal) { + KeyguardState.GLANCEABLE_HUB } else { KeyguardState.LOCKSCREEN } @@ -161,7 +168,17 @@ constructor( ) keyguardInteractor.asleepKeyguardState.value } - startTransitionTo(toState) + if (hubV2 && toState != KeyguardState.GLANCEABLE_HUB && isIdleOnCommunal) { + // If bouncer is showing over the hub, we need to make sure we + // properly dismiss the hub when transitioning away. + communalSceneInteractor.changeScene( + newScene = CommunalScenes.Blank, + loggingReason = "bouncer no longer showing over GH", + keyguardState = toState, + ) + } else { + startTransitionTo(toState) + } } } } @@ -184,7 +201,32 @@ constructor( private fun listenForPrimaryBouncerToAsleep() { if (SceneContainerFlag.isEnabled) return - scope.launch { listenForSleepTransition() } + scope.launch { + if (communalSettingsInteractor.isV2FlagEnabled()) { + powerInteractor.isAsleep + .filter { isAsleep -> isAsleep } + .filterRelevantKeyguardState() + .sample(communalSceneInteractor.isIdleOnCommunal) + .collect { isIdleOnCommunal -> + if (isIdleOnCommunal) { + // If the bouncer is showing on top of the hub, then ensure we also + // hide the hub. + communalSceneInteractor.changeScene( + newScene = CommunalScenes.Blank, + loggingReason = "Sleep while primary bouncer showing over hub", + keyguardState = keyguardInteractor.asleepKeyguardState.value, + ) + } else { + startTransitionTo( + toState = keyguardInteractor.asleepKeyguardState.value, + ownerReason = "Sleep transition triggered", + ) + } + } + } else { + listenForSleepTransition() + } + } } private fun listenForPrimaryBouncerToGone() { 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 96e05cb7cd63..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,10 +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 @@ -45,6 +47,7 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn @@ -57,7 +60,8 @@ class KeyguardClockInteractor constructor( mediaCarouselInteractor: MediaCarouselInteractor, activeNotificationsInteractor: ActiveNotificationsInteractor, - shadeInteractor: ShadeInteractor, + aodPromotedNotificationInteractor: AODPromotedNotificationInteractor, + shadeModeInteractor: ShadeModeInteractor, keyguardInteractor: KeyguardInteractor, keyguardTransitionInteractor: KeyguardTransitionInteractor, headsUpNotificationInteractor: HeadsUpNotificationInteractor, @@ -66,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 @@ -80,53 +89,91 @@ constructor( var clock: ClockController? by keyguardClockRepository.clockEventController::clock - val clockSize: StateFlow<ClockSize> = + private val isAodPromotedNotificationPresent: Flow<Boolean> = + if (PromotedNotificationUiAod.isEnabled) { + aodPromotedNotificationInteractor.isPresent + } else { + flowOf(false) + } + + private val areAnyNotificationsPresent: Flow<Boolean> = + if (PromotedNotificationUiAod.isEnabled) { + combine( + activeNotificationsInteractor.areAnyNotificationsPresent, + isAodPromotedNotificationPresent, + ) { areAnyNotificationsPresent, isAodPromotedNotificationPresent -> + areAnyNotificationsPresent || isAodPromotedNotificationPresent + } + } else { + activeNotificationsInteractor.areAnyNotificationsPresent + } + + private val dynamicClockSize: Flow<ClockSize> = if (SceneContainerFlag.isEnabled) { combine( - shadeInteractor.isShadeLayoutWide, - activeNotificationsInteractor.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, - activeNotificationsInteractor.areAnyNotificationsPresent, + shadeModeInteractor.isShadeLayoutWide, + areAnyNotificationsPresent, + isAodPromotedNotificationPresent, isOnAod, headsUpNotificationInteractor.isHeadsUpOrAnimatingAway, keyguardInteractor.isDozing, - ) { isShadeLayoutWide, areAnyNotificationsPresent, isOnAod, isHeadsUp, isDozing -> + ) { + isShadeLayoutWide, + areAnyNotificationsPresent, + isAodPromotedNotificationPresent, + isOnAod, + isHeadsUp, + isDozing -> when { !isShadeLayoutWide -> true !areAnyNotificationsPresent -> true // Pulsing notification appears on the right. Move clock left to avoid overlap. isHeadsUp && isDozing -> false + isAodPromotedNotificationPresent -> false else -> isOnAod } } } else { combine( - shadeInteractor.isShadeLayoutWide, - activeNotificationsInteractor.areAnyNotificationsPresent, + shadeModeInteractor.isShadeLayoutWide, + areAnyNotificationsPresent, + isAodPromotedNotificationPresent, keyguardInteractor.dozeTransitionModel, keyguardTransitionInteractor.startedKeyguardTransitionStep.map { it.to == AOD }, keyguardTransitionInteractor.startedKeyguardTransitionStep.map { @@ -140,6 +187,7 @@ constructor( ) { isShadeLayoutWide, areAnyNotificationsPresent, + isAodPromotedNotificationPresent, dozeTransitionModel, startedToAod, startedToLockScreen, @@ -156,7 +204,7 @@ constructor( // use null to skip emitting wrong value startedToGone || startedToDoze -> null startedToLockScreen -> !areAnyNotificationsPresent - startedToAod -> !isPulsing + startedToAod -> !(isPulsing || isAodPromotedNotificationPresent) else -> true } } @@ -170,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/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt index 58fb4230ccf5..3b4a1488095a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt @@ -19,7 +19,10 @@ package com.android.systemui.keyguard.domain.interactor import android.annotation.SuppressLint import android.util.Log +import com.android.app.tracing.coroutines.flow.filterTraced +import com.android.app.tracing.coroutines.flow.traceAs import com.android.app.tracing.coroutines.launchTraced as launch +import com.android.app.tracing.coroutines.traceCoroutine import com.android.compose.animation.scene.ObservableTransitionState import com.android.compose.animation.scene.SceneKey import com.android.systemui.Flags.keyguardTransitionForceFinishOnScreenOff @@ -90,6 +93,7 @@ constructor( onBufferOverflow = BufferOverflow.DROP_OLDEST, ) .also { it.tryEmit(0f) } + .traceAs("KTF-${state.name}") } } @@ -222,10 +226,11 @@ constructor( val flow: Flow<TransitionStep> = transitionMap.getOrPut(mappedEdge) { - MutableSharedFlow( - extraBufferCapacity = 10, - onBufferOverflow = BufferOverflow.DROP_OLDEST, - ) + MutableSharedFlow<TransitionStep>( + extraBufferCapacity = 10, + onBufferOverflow = BufferOverflow.DROP_OLDEST, + ) + .traceAs("KTF-${mappedEdge.from}-to-${mappedEdge.to}") } if (!SceneContainerFlag.isEnabled) { @@ -234,7 +239,7 @@ constructor( if (edge.isSceneWildcardEdge()) { return simulateTransitionStepsForSceneTransitions(edge) } - return flow.filter { step -> + return flow.filterTraced("stl-filter") { step -> val fromScene = when (edge) { is Edge.StateToState -> edge.from?.mapToSceneContainerScene() @@ -273,7 +278,7 @@ constructor( step.transitionState == TransitionState.CANCELED) && sceneTransitionPair.value.previousValue.isTransitioning(fromScene, toScene) - return@filter isTransitioningBetweenLockscreenStates || + return@filterTraced isTransitioningBetweenLockscreenStates || isTransitioningBetweenDesiredScenes || terminalStepBelongsToPreviousTransition || belongsToInstantReversedTransition @@ -362,27 +367,27 @@ constructor( coroutineScope { collect { value -> - job?.cancelAndJoin() + traceCoroutine("cancelAndJoin") { job?.cancelAndJoin() } - job = launch { + job = launch("inner") { val innerFlow = transform(value) try { innerFlow.collect { step -> if (step.transitionState == TransitionState.STARTED) { startedEmitted = true } - send(step) + traceCoroutine("send($step)") { send(step) } } } finally { if (startedEmitted) { - send( + val step = TransitionStep( from = UNDEFINED, to = UNDEFINED, value = 1f, transitionState = TransitionState.FINISHED, ) - ) + traceCoroutine("send($step)") { send(step) } startedEmitted = false } } @@ -390,6 +395,7 @@ constructor( } } } + .traceAs("flatMapLatestWithFinished") /** * Converts old KTF states to UNDEFINED when [SceneContainerFlag] is enabled. @@ -545,6 +551,7 @@ constructor( } } .onStart { emit(false) } + .traceAs("isInTransition-$edge-$edgeWithoutSceneContainer") .distinctUntilChanged() } 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 00710c97d00a..8e385385b8c4 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 @@ -300,7 +300,7 @@ object KeyguardRootViewBinder { } launch { - viewModel.isNotifIconContainerVisible.collect { isVisible -> + viewModel.isAodPromotedNotifVisible.collect { isVisible -> if (isVisible.value) { blueprintViewModel.refreshBlueprint() } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/LightRevealScrimViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/LightRevealScrimViewBinder.kt index 97fa3f19a82e..f0113a5b6020 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/LightRevealScrimViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/LightRevealScrimViewBinder.kt @@ -26,7 +26,6 @@ import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.shared.Flags.ambientAod import com.android.systemui.statusbar.LightRevealScrim import com.android.systemui.wallpapers.ui.viewmodel.WallpaperViewModel -import com.android.app.tracing.coroutines.launchTraced as launch object LightRevealScrimViewBinder { @JvmStatic 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 e6a85c6860c5..9018c58a7e36 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,4 +39,6 @@ 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/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt index 62a5ebab29e0..8e21745e1a31 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt @@ -25,6 +25,7 @@ import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor +import com.android.systemui.dump.DumpManager import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.domain.interactor.PulseExpansionInteractor @@ -44,9 +45,11 @@ import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.ui.viewmodel.NotificationShadeWindowModel import com.android.systemui.statusbar.notification.domain.interactor.NotificationsKeyguardInteractor import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerAlwaysOnDisplayViewModel +import com.android.systemui.statusbar.notification.promoted.domain.interactor.AODPromotedNotificationInteractor import com.android.systemui.statusbar.phone.DozeParameters import com.android.systemui.statusbar.phone.ScreenOffAnimationController import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf +import com.android.systemui.util.kotlin.FlowDumperImpl import com.android.systemui.util.kotlin.pairwise import com.android.systemui.util.kotlin.sample import com.android.systemui.util.ui.AnimatableEvent @@ -82,6 +85,7 @@ constructor( private val notificationsKeyguardInteractor: NotificationsKeyguardInteractor, private val pulseExpansionInteractor: PulseExpansionInteractor, notificationShadeWindowModel: NotificationShadeWindowModel, + private val aodPromotedNotificationInteractor: AODPromotedNotificationInteractor, private val aodNotificationIconViewModel: NotificationIconContainerAlwaysOnDisplayViewModel, private val alternateBouncerToAodTransitionViewModel: AlternateBouncerToAodTransitionViewModel, private val alternateBouncerToGoneTransitionViewModel: @@ -134,17 +138,21 @@ constructor( private val aodBurnInViewModel: AodBurnInViewModel, private val shadeInteractor: ShadeInteractor, wallpaperFocalAreaInteractor: WallpaperFocalAreaInteractor, -) { + dumpManager: DumpManager, +) : FlowDumperImpl(dumpManager) { val burnInLayerVisibility: Flow<Int> = keyguardTransitionInteractor.startedKeyguardTransitionStep .filter { it.to == AOD || it.to == LOCKSCREEN } .map { VISIBLE } + .dumpWhileCollecting("burnInLayerVisibility") val goneToAodTransition = - keyguardTransitionInteractor.transition( - edge = Edge.create(Scenes.Gone, AOD), - edgeWithoutSceneContainer = Edge.create(GONE, AOD), - ) + keyguardTransitionInteractor + .transition( + edge = Edge.create(Scenes.Gone, AOD), + edgeWithoutSceneContainer = Edge.create(GONE, AOD), + ) + .dumpWhileCollecting("goneToAodTransition") private val goneToAodTransitionRunning: Flow<Boolean> = goneToAodTransition @@ -222,13 +230,15 @@ constructor( ) /** Last point that the root view was tapped */ - val lastRootViewTapPosition: Flow<Point?> = keyguardInteractor.lastRootViewTapPosition + val lastRootViewTapPosition: Flow<Point?> = + keyguardInteractor.lastRootViewTapPosition.dumpWhileCollecting("lastRootViewTapPosition") /** * The keyguard root view can be clipped as the shade is pulled down, typically only for * non-split shade cases. */ - val topClippingBounds: Flow<Int?> = keyguardInteractor.topClippingBounds + val topClippingBounds: Flow<Int?> = + keyguardInteractor.topClippingBounds.dumpWhileCollecting("topClippingBounds") /** An observable for the alpha level for the entire keyguard root view. */ fun alpha(viewState: ViewStateAccessor): Flow<Float> { @@ -287,40 +297,56 @@ constructor( } } .distinctUntilChanged() + .dumpWhileCollecting("alpha") } val scaleFromZoomOut: Flow<Float> = - keyguardInteractor.zoomOut.map { 1 - it * PUSHBACK_SCALE_FOR_LOCKSCREEN } + keyguardInteractor.zoomOut + .map { 1 - it * PUSHBACK_SCALE_FOR_LOCKSCREEN } + .dumpWhileCollecting("scaleFromZoomOut") - val translationY: Flow<Float> = aodBurnInViewModel.movement.map { it.translationY.toFloat() } + val translationY: Flow<Float> = + aodBurnInViewModel.movement + .map { it.translationY.toFloat() } + .dumpWhileCollecting("translationY") val translationX: Flow<StateToValue> = merge( - aodBurnInViewModel.movement.map { - StateToValue(to = AOD, value = it.translationX.toFloat()) - }, - lockscreenToGlanceableHubTransitionViewModel.keyguardTranslationX, - glanceableHubToLockscreenTransitionViewModel.keyguardTranslationX, - ) + aodBurnInViewModel.movement.map { + StateToValue(to = AOD, value = it.translationX.toFloat()) + }, + lockscreenToGlanceableHubTransitionViewModel.keyguardTranslationX, + glanceableHubToLockscreenTransitionViewModel.keyguardTranslationX, + ) + .dumpWhileCollecting("translationX") fun updateBurnInParams(params: BurnInParameters) { aodBurnInViewModel.updateBurnInParams(params) } val scale: Flow<BurnInScaleViewModel> = - aodBurnInViewModel.movement.map { - BurnInScaleViewModel(scale = it.scale, scaleClockOnly = it.scaleClockOnly) - } + aodBurnInViewModel.movement + .map { BurnInScaleViewModel(scale = it.scale, scaleClockOnly = it.scaleClockOnly) } + .dumpWhileCollecting("scale") - val isAodPromotedNotifVisible: StateFlow<Boolean> = - keyguardTransitionInteractor - .transitionValue(AOD) - .map { it == 1f } + val isAodPromotedNotifVisible: StateFlow<AnimatedValue<Boolean>> = + combine( + areNotifsFullyHiddenAnimated(), + isPulseExpandingAnimated(), + aodPromotedNotificationInteractor.isPresent, + ) { notifsFullyHiddenAnimated, pulseExpandingAnimated, haveAodPromotedNotif -> + zip(notifsFullyHiddenAnimated, pulseExpandingAnimated) { + notifsFullyHidden, + pulseExpanding -> + notifsFullyHidden && !pulseExpanding && haveAodPromotedNotif + } + } .stateIn( scope = applicationScope, started = SharingStarted.WhileSubscribed(), - initialValue = false, + initialValue = AnimatedValue.NotAnimating(false), ) + .dumpValue("isAodPromotedNotifVisible") /** Is the notification icon container visible? */ val isNotifIconContainerVisible: StateFlow<AnimatedValue<Boolean>> = @@ -376,6 +402,7 @@ constructor( started = SharingStarted.WhileSubscribed(), initialValue = AnimatedValue.NotAnimating(false), ) + .dumpValue("isNotifIconContainerVisible") fun onNotificationContainerBoundsChanged(top: Float, bottom: Float, animate: Boolean = false) { keyguardInteractor.setNotificationContainerBounds( 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/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/dialog/MediaOutputAdapter.java b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java index 322c35116222..f5e62323e769 100644 --- a/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java +++ b/packages/SystemUI/src/com/android/systemui/media/dialog/MediaOutputAdapter.java @@ -185,6 +185,7 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { } boolean isDeviceGroup = false; + boolean hideGroupItem = false; GroupStatus groupStatus = null; OngoingSessionStatus ongoingSessionStatus = null; ConnectionState connectionState = ConnectionState.DISCONNECTED; @@ -216,68 +217,38 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { clickListener = v -> cancelMuteAwaitConnection(); } else if (device.getState() == MediaDeviceState.STATE_GROUPING) { connectionState = ConnectionState.CONNECTING; - } else if (mShouldGroupSelectedMediaItems - && mController.getSelectedMediaDevice().size() > 1 - && isDeviceIncluded(mController.getSelectedMediaDevice(), device)) { - if (!mediaItem.isFirstDeviceInGroup()) { - mItemLayout.setVisibility(View.GONE); - return; - } else { + } else if (mShouldGroupSelectedMediaItems && hasMultipleSelectedDevices() + && isSelected) { + if (mediaItem.isFirstDeviceInGroup()) { isDeviceGroup = true; + } else { + hideGroupItem = true; } - } else if (device.hasSubtext()) { - subtitle = device.getSubtextString(); - boolean isActiveWithOngoingSession = - device.hasOngoingSession() && (currentlyConnected || isSelected); - if (isActiveWithOngoingSession) { - ongoingSessionStatus = new OngoingSessionStatus( - device.isHostForOngoingSession()); + } else { // A connected or disconnected device. + subtitle = device.hasSubtext() ? device.getSubtextString() : null; + ongoingSessionStatus = getOngoingSessionStatus(device); + groupStatus = getGroupStatus(isSelected, isSelectable, isDeselectable); + + if (device.getState() == MediaDeviceState.STATE_CONNECTING_FAILED) { + deviceStatusIcon = mContext.getDrawable( + R.drawable.media_output_status_failed); + subtitle = mContext.getString(R.string.media_output_dialog_connect_failed); + clickListener = v -> onItemClick(v, device); + } else if (currentlyConnected || isSelected) { connectionState = ConnectionState.CONNECTED; - } else { - if (currentlyConnected) { - connectionState = ConnectionState.CONNECTED; + } else { // disconnected + if (isSelectable) { // groupable device + if (!Flags.disableTransferWhenAppsDoNotSupport() || isTransferable + || hasRouteListingPreferenceItem) { + clickListener = v -> onItemClick(v, device); + } + } else { + deviceStatusIcon = getDeviceStatusIcon(device, + device.hasOngoingSession()); + clickListener = getClickListenerBasedOnSelectionBehavior(device); } - clickListener = getClickListenerBasedOnSelectionBehavior(device); deviceDisabled = clickListener == null; - deviceStatusIcon = getDeviceStatusIcon(device, device.hasOngoingSession()); - } - } else if (device.getState() == MediaDeviceState.STATE_CONNECTING_FAILED) { - deviceStatusIcon = mContext.getDrawable(R.drawable.media_output_status_failed); - subtitle = mContext.getString(R.string.media_output_dialog_connect_failed); - clickListener = v -> onItemClick(v, device); - } else if (mController.getSelectedMediaDevice().size() > 1 && isSelected) { - // selected device in group - groupStatus = new GroupStatus( - true /* selected */, - isDeselectable /* deselectable */); - connectionState = ConnectionState.CONNECTED; - } else if (currentlyConnected) { - connectionState = ConnectionState.CONNECTED; - // single selected device - if (device.hasOngoingSession()) { - ongoingSessionStatus = new OngoingSessionStatus( - device.isHostForOngoingSession()); - } else if (mController.isCurrentConnectedDeviceRemote() - && !mController.getSelectableMediaDevice().isEmpty()) { - //If device is connected and there's other selectable devices, layout as - // one of selected devices. - groupStatus = new GroupStatus( - true /* selected */, - isDeselectable /* isDeselectable */); - } - } else if (isSelectable) { - //groupable device - groupStatus = new GroupStatus(false /* selected */, true /* deselectable */); - if (!Flags.disableTransferWhenAppsDoNotSupport() - || isTransferable - || hasRouteListingPreferenceItem) { - clickListener = v -> onItemClick(v, device); } - deviceDisabled = clickListener == null; - } else { - deviceStatusIcon = getDeviceStatusIcon(device, device.hasOngoingSession()); - clickListener = getClickListenerBasedOnSelectionBehavior(device); - deviceDisabled = clickListener == null; } } @@ -286,28 +257,71 @@ public class MediaOutputAdapter extends MediaOutputBaseAdapter { } if (isDeviceGroup) { - String sessionName = mController.getSessionName() == null ? "" - : mController.getSessionName().toString(); - updateTitle(sessionName); - updateUnmutedVolumeIcon(null /* device */); - updateGroupSeekBar(getGroupItemContentDescription(sessionName)); - updateEndAreaForDeviceGroup(); - updateItemBackground(ConnectionState.CONNECTED); + renderDeviceGroupItem(); } else { - 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); + renderDeviceItem(hideGroupItem, device, connectionState, restrictVolumeAdjustment, + groupStatus, ongoingSessionStatus, clickListener, deviceDisabled, subtitle, + deviceStatusIcon); } } + private 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); + } + + private void renderDeviceGroupItem() { + String sessionName = mController.getSessionName() == null ? "" + : mController.getSessionName().toString(); + updateTitle(sessionName); + updateUnmutedVolumeIcon(null /* device */); + updateGroupSeekBar(getGroupItemContentDescription(sessionName)); + updateEndAreaForDeviceGroup(); + updateItemBackground(ConnectionState.CONNECTED); + } + + private OngoingSessionStatus getOngoingSessionStatus(MediaDevice device) { + return device.hasOngoingSession() ? new OngoingSessionStatus( + device.isHostForOngoingSession()) : null; + } + + private GroupStatus getGroupStatus(boolean isSelected, boolean isSelectable, + boolean isDeselectable) { + // A device should either be selectable or, when the device selected, the list should + // have other selectable or selected devices. + boolean selectedWithOtherGroupDevices = + isSelected && (hasMultipleSelectedDevices() || hasSelectableDevices()); + if (isSelectable || selectedWithOtherGroupDevices) { + return new GroupStatus(isSelected, isDeselectable); + } + return null; + } + + private boolean hasMultipleSelectedDevices() { + return mController.getSelectedMediaDevice().size() > 1; + } + + private boolean hasSelectableDevices() { + return !mController.getSelectableMediaDevice().isEmpty(); + } + /** Renders the right side round pill button / checkbox. */ private void updateEndArea(@NonNull MediaDevice device, ConnectionState connectionState, @Nullable GroupStatus groupStatus, 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/tiles/BluetoothTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java index 973265c6c9b1..fd5861fed20c 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java @@ -43,8 +43,8 @@ import com.android.settingslib.bluetooth.BluetoothUtils; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.satellite.SatelliteDialogUtils; import com.android.systemui.animation.Expandable; +import com.android.systemui.bluetooth.qsdialog.BluetoothDetailsContentViewModel; import com.android.systemui.bluetooth.qsdialog.BluetoothDetailsViewModel; -import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogViewModel; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.flags.FeatureFlags; @@ -84,7 +84,7 @@ public class BluetoothTile extends QSTileImpl<BooleanState> { private final Executor mExecutor; - private final BluetoothTileDialogViewModel mDialogViewModel; + private final BluetoothDetailsContentViewModel mDetailsContentViewModel; private final FeatureFlags mFeatureFlags; @Nullable @@ -104,7 +104,7 @@ public class BluetoothTile extends QSTileImpl<BooleanState> { QSLogger qsLogger, BluetoothController bluetoothController, FeatureFlags featureFlags, - BluetoothTileDialogViewModel dialogViewModel + BluetoothDetailsContentViewModel detailsContentViewModel ) { super(host, uiEventLogger, backgroundLooper, mainHandler, falsingManager, metricsLogger, statusBarStateController, activityStarter, qsLogger); @@ -112,7 +112,7 @@ public class BluetoothTile extends QSTileImpl<BooleanState> { mController.observe(getLifecycle(), mCallback); mExecutor = new HandlerExecutor(mainHandler); mFeatureFlags = featureFlags; - mDialogViewModel = dialogViewModel; + mDetailsContentViewModel = detailsContentViewModel; } @Override @@ -133,7 +133,7 @@ public class BluetoothTile extends QSTileImpl<BooleanState> { callback.accept(new BluetoothDetailsViewModel(() -> { longClick(null); return null; - }, mDialogViewModel)) + }, mDetailsContentViewModel)) ); return true; } @@ -158,7 +158,7 @@ public class BluetoothTile extends QSTileImpl<BooleanState> { private void handleClickEvent(@Nullable Expandable expandable) { if (mFeatureFlags.isEnabled(Flags.BLUETOOTH_QS_TILE_DIALOG)) { - mDialogViewModel.showDetailsContent(expandable, /* view= */ null); + mDetailsContentViewModel.showDetailsContent(expandable, /* view= */ null); } else { // Secondary clicks are header clicks, just toggle. toggleBluetooth(); diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTileNewImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTileNewImpl.kt index 4d0e80854853..f80b8fb8cb1f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTileNewImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/InternetTileNewImpl.kt @@ -36,7 +36,6 @@ import com.android.systemui.qs.logging.QSLogger import com.android.systemui.qs.tileimpl.QSTileImpl import com.android.systemui.qs.tiles.dialog.InternetDetailsViewModel import com.android.systemui.qs.tiles.dialog.InternetDialogManager -import com.android.systemui.qs.tiles.dialog.WifiStateWorker import com.android.systemui.res.R import com.android.systemui.statusbar.connectivity.AccessPointController import com.android.systemui.statusbar.pipeline.shared.ui.binder.InternetTileBinder @@ -58,7 +57,6 @@ constructor( qsLogger: QSLogger, viewModel: InternetTileViewModel, private val internetDialogManager: InternetDialogManager, - private val wifiStateWorker: WifiStateWorker, private val accessPointController: AccessPointController, private val internetDetailsViewModelFactory: InternetDetailsViewModel.Factory, ) : @@ -86,10 +84,7 @@ constructor( mContext.getString(R.string.quick_settings_internet_label) override fun newTileState(): QSTile.BooleanState { - return QSTile.BooleanState().also { - it.forceExpandIcon = true - it.handlesSecondaryClick = true - } + return QSTile.BooleanState().also { it.forceExpandIcon = true } } override fun handleClick(expandable: Expandable?) { @@ -107,12 +102,6 @@ constructor( return internetDetailsViewModelFactory.create { longClick(null) } } - override fun handleSecondaryClick(expandable: Expandable?) { - // TODO(b/358352265): Figure out the correct action for the secondary click - // Toggle wifi - wifiStateWorker.isWifiEnabled = !wifiStateWorker.isWifiEnabled - } - override fun handleUpdateState(state: QSTile.BooleanState, arg: Any?) { state.label = mContext.resources.getString(R.string.quick_settings_internet_label) state.expandedAccessibilityClassName = Button::class.java.name diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java index 42b35c736d42..7b88d6930279 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java @@ -26,6 +26,7 @@ import android.text.TextUtils; import android.util.Log; import android.widget.Button; import android.widget.Switch; +import android.widget.Toast; import androidx.annotation.Nullable; @@ -192,8 +193,19 @@ public class ScreenRecordTile extends QSTileImpl<QSTile.BooleanState> @Override public boolean getDetailsViewModel(Consumer<TileDetailsViewModel> callback) { handleClick(() -> executeWhenUnlockedKeyguard( - () -> callback.accept(new ScreenRecordDetailsViewModel(mController, - this::onStartRecordingClicked))) + () -> { + if (mController.isScreenCaptureDisabled()) { + // Close the panel first so that the toast can show up. + mDialogTransitionAnimator.disableAllCurrentDialogsExitAnimations(); + mPanelInteractor.collapsePanels(); + + showDisabledByPolicyToast(); + return; + } + + callback.accept(new ScreenRecordDetailsViewModel(mController, + this::onStartRecordingClicked)); + }) ); return true; } @@ -244,6 +256,12 @@ public class ScreenRecordTile extends QSTileImpl<QSTile.BooleanState> return mContext.getString(R.string.quick_settings_screen_record_label); } + void showDisabledByPolicyToast() { + Toast.makeText(mContext, + R.string.screen_capturing_disabled_by_policy_dialog_description, Toast.LENGTH_SHORT) + .show(); + } + private void cancelCountdown() { Log.d(TAG, "Cancelling countdown"); mController.cancelCountdown(); diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsContent.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsContent.kt index bf1a51d8cd59..3eb73d83bc1a 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsContent.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/ScreenRecordDetailsContent.kt @@ -28,13 +28,6 @@ import com.android.systemui.screenrecord.ScreenRecordPermissionViewBinder @Composable fun ScreenRecordDetailsContent(viewModel: ScreenRecordDetailsViewModel) { - // TODO(b/378514312): Finish implementing this function. - - if (viewModel.recordingController.isScreenCaptureDisabled) { - // TODO(b/388345506): Show disabled page here. - return - } - val viewBinder: ScreenRecordPermissionViewBinder = remember { viewModel.recordingController.createScreenRecordPermissionViewBinder( viewModel.onStartRecordingClicked diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt index 7ad01e463399..8d5880554277 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt @@ -92,10 +92,6 @@ constructor( else QSTileState.ActivationState.INACTIVE supportedActions = - setOf( - QSTileState.UserAction.CLICK, - QSTileState.UserAction.TOGGLE_CLICK, - QSTileState.UserAction.LONG_CLICK, - ) + setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt index 6e2c437b9c16..8e48fe492e13 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileUserActionInteractor.kt @@ -26,7 +26,6 @@ import com.android.systemui.qs.tiles.base.interactor.QSTileInput import com.android.systemui.qs.tiles.base.interactor.QSTileUserActionInteractor import com.android.systemui.qs.tiles.dialog.InternetDetailsViewModel import com.android.systemui.qs.tiles.dialog.InternetDialogManager -import com.android.systemui.qs.tiles.dialog.WifiStateWorker import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileModel import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction import com.android.systemui.statusbar.connectivity.AccessPointController @@ -40,7 +39,6 @@ class InternetTileUserActionInteractor constructor( @Main private val mainContext: CoroutineContext, private val internetDialogManager: InternetDialogManager, - private val wifiStateWorker: WifiStateWorker, private val accessPointController: AccessPointController, private val qsTileIntentUserActionHandler: QSTileIntentUserInputHandler, private val internetDetailsViewModelFactory: InternetDetailsViewModel.Factory, @@ -59,24 +57,17 @@ constructor( ) } } - is QSTileUserAction.ToggleClick -> { - // TODO(b/358352265): Figure out the correct action for the secondary click - // Toggle Wifi - wifiStateWorker.isWifiEnabled = !wifiStateWorker.isWifiEnabled - } is QSTileUserAction.LongClick -> { handleLongClick(action.expandable) } + else -> {} } } override val detailsViewModel: TileDetailsViewModel = internetDetailsViewModelFactory.create { handleLongClick(null) } - private fun handleLongClick(expandable:Expandable?){ - qsTileIntentUserActionHandler.handle( - expandable, - Intent(Settings.ACTION_WIFI_SETTINGS) - ) + private fun handleLongClick(expandable: Expandable?) { + qsTileIntentUserActionHandler.handle(expandable, Intent(Settings.ACTION_WIFI_SETTINGS)) } } 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 475c0794861f..e9e7deca0abf 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 @@ -16,6 +16,7 @@ package com.android.systemui.scene.domain.interactor +import com.android.app.tracing.coroutines.flow.stateInTraced import com.android.compose.animation.scene.ContentKey import com.android.compose.animation.scene.ObservableTransitionState import com.android.compose.animation.scene.OverlayKey @@ -52,7 +53,6 @@ import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -124,7 +124,8 @@ constructor( val transitionState: StateFlow<ObservableTransitionState> = repository.transitionState .onEach { logger.logSceneTransition(it) } - .stateIn( + .stateInTraced( + name = "transitionState", scope = applicationScope, started = SharingStarted.Eagerly, initialValue = repository.transitionState.value, @@ -145,7 +146,8 @@ constructor( is ObservableTransitionState.Transition -> state.toContent } } - .stateIn( + .stateInTraced( + name = "transitioningTo", scope = applicationScope, started = SharingStarted.WhileSubscribed(), initialValue = null, @@ -164,7 +166,8 @@ constructor( is ObservableTransitionState.Idle -> flowOf(false) } } - .stateIn( + .stateInTraced( + name = "isTransitionUserInputOngoing", scope = applicationScope, started = SharingStarted.WhileSubscribed(), initialValue = false, @@ -183,7 +186,8 @@ constructor( activeTransitionAnimationCount = activeTransitionAnimationCount, ) } - .stateIn( + .stateInTraced( + name = "isVisible", scope = applicationScope, started = SharingStarted.WhileSubscribed(), initialValue = isVisibleInternal(), 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..16adf5ef976e 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 @@ -610,15 +611,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)", + ) + } } + } } } @@ -1013,6 +1023,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/screenshot/scroll/ImageTileSet.java b/packages/SystemUI/src/com/android/systemui/screenshot/scroll/ImageTileSet.java index 76a72f7e4adf..25c38050e95a 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/scroll/ImageTileSet.java +++ b/packages/SystemUI/src/com/android/systemui/screenshot/scroll/ImageTileSet.java @@ -29,6 +29,7 @@ import androidx.annotation.UiThread; import com.android.internal.util.CallbackRegistry; import com.android.internal.util.CallbackRegistry.NotifierCallback; +import com.android.systemui.dagger.qualifiers.Main; import java.util.ArrayList; import java.util.Iterator; @@ -49,7 +50,7 @@ class ImageTileSet { private CallbackRegistry<OnContentChangedListener, ImageTileSet, Rect> mContentListeners; @Inject - ImageTileSet(@UiThread Handler handler) { + ImageTileSet(@Main Handler handler) { mHandler = handler; } diff --git a/packages/SystemUI/src/com/android/systemui/shade/BaseShadeControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/BaseShadeControllerImpl.kt index b271c6979b31..71977ef1f234 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/BaseShadeControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/BaseShadeControllerImpl.kt @@ -16,19 +16,19 @@ package com.android.systemui.shade +import com.android.keyguard.KeyguardViewController import com.android.systemui.assist.AssistManager import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.NotificationPresenter import com.android.systemui.statusbar.NotificationShadeWindowController -import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager import dagger.Lazy /** A base class for non-empty implementations of ShadeController. */ abstract class BaseShadeControllerImpl( protected val commandQueue: CommandQueue, - protected val statusBarKeyguardViewManager: StatusBarKeyguardViewManager, + protected val keyguardViewController: KeyguardViewController, protected val notificationShadeWindowController: NotificationShadeWindowController, - protected val assistManagerLazy: Lazy<AssistManager> + protected val assistManagerLazy: Lazy<AssistManager>, ) : ShadeController { protected lateinit var notifPresenter: NotificationPresenter /** Runnables to run after completing a collapse of the shade. */ @@ -66,7 +66,7 @@ abstract class BaseShadeControllerImpl( for (r in clonedList) { r.run() } - statusBarKeyguardViewManager.readyForKeyguardDone() + keyguardViewController.readyForKeyguardDone() } final override fun onLaunchAnimationEnd(launchIsFullScreen: Boolean) { @@ -77,6 +77,7 @@ abstract class BaseShadeControllerImpl( instantCollapseShade() } } + final override fun onLaunchAnimationCancelled(isLaunchForActivity: Boolean) { if ( notifPresenter.isPresenterFullyCollapsed() && diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java index 0e30f2b4bb30..acae1bc81b97 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java @@ -23,6 +23,7 @@ import android.view.MotionEvent; import android.view.ViewTreeObserver; import android.view.WindowManagerGlobal; +import com.android.keyguard.KeyguardViewController; import com.android.systemui.DejankUtils; import com.android.systemui.assist.AssistManager; import com.android.systemui.dagger.SysUISingleton; @@ -35,7 +36,6 @@ import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.notification.row.NotificationGutsManager; -import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.window.StatusBarWindowControllerStore; @@ -61,7 +61,6 @@ public final class ShadeControllerImpl extends BaseShadeControllerImpl { private final KeyguardStateController mKeyguardStateController; private final NotificationShadeWindowController mNotificationShadeWindowController; private final StatusBarStateController mStatusBarStateController; - private final StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; private final StatusBarWindowControllerStore mStatusBarWindowControllerStore; private final DeviceProvisionedController mDeviceProvisionedController; @@ -82,7 +81,7 @@ public final class ShadeControllerImpl extends BaseShadeControllerImpl { WindowRootViewVisibilityInteractor windowRootViewVisibilityInteractor, KeyguardStateController keyguardStateController, StatusBarStateController statusBarStateController, - StatusBarKeyguardViewManager statusBarKeyguardViewManager, + KeyguardViewController keyguardViewController, StatusBarWindowControllerStore statusBarWindowControllerStore, DeviceProvisionedController deviceProvisionedController, NotificationShadeWindowController notificationShadeWindowController, @@ -93,7 +92,7 @@ public final class ShadeControllerImpl extends BaseShadeControllerImpl { Lazy<NotificationGutsManager> gutsManager ) { super(commandQueue, - statusBarKeyguardViewManager, + keyguardViewController, notificationShadeWindowController, assistManagerLazy); SceneContainerFlag.assertInLegacyMode(); @@ -107,7 +106,6 @@ public final class ShadeControllerImpl extends BaseShadeControllerImpl { mGutsManager = gutsManager; mNotificationShadeWindowController = notificationShadeWindowController; mNotifShadeWindowViewController = notificationShadeWindowViewController; - mStatusBarKeyguardViewManager = statusBarKeyguardViewManager; mDisplayId = displayId; mKeyguardStateController = keyguardStateController; mAssistManagerLazy = assistManagerLazy; @@ -396,7 +394,7 @@ public final class ShadeControllerImpl extends BaseShadeControllerImpl { @Override public void collapseShadeForActivityStart() { - if (isExpandedVisible() && !mStatusBarKeyguardViewManager.isBouncerShowing()) { + if (isExpandedVisible() && !getKeyguardViewController().isBouncerShowing()) { animateCollapseShadeForcedDelayed(); } else { // Do it after DismissAction has been processed to conserve the 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 1ab0b93da175..8f4e8701cad8 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,7 +22,6 @@ 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 @@ -33,7 +32,6 @@ 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 /** @@ -91,14 +89,10 @@ constructor( ) : ShadeModeInteractor { private val isDualShadeEnabled: Flow<Boolean> = - if (SceneContainerFlag.isEnabled) { - secureSettingsRepository.boolSetting( - Settings.Secure.DUAL_SHADE, - defaultValue = DUAL_SHADE_ENABLED_DEFAULT, - ) - } else { - flowOf(false) - } + secureSettingsRepository.boolSetting( + Settings.Secure.DUAL_SHADE, + defaultValue = DUAL_SHADE_ENABLED_DEFAULT, + ) override val isShadeLayoutWide: StateFlow<Boolean> = repository.isShadeLayoutWide diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/BatteryStatusChip.kt b/packages/SystemUI/src/com/android/systemui/statusbar/BatteryStatusChip.kt index a58ce4162ddc..02cec13d2ce8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/BatteryStatusChip.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/BatteryStatusChip.kt @@ -26,6 +26,7 @@ import com.android.settingslib.flags.Flags.newStatusBarIcons import com.android.systemui.battery.BatteryMeterView import com.android.systemui.battery.unified.BatteryColors import com.android.systemui.res.R +import com.android.systemui.statusbar.core.NewStatusBarIcons import com.android.systemui.statusbar.events.BackgroundAnimatableView class BatteryStatusChip @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : @@ -37,6 +38,8 @@ class BatteryStatusChip @JvmOverloads constructor(context: Context, attrs: Attri get() = batteryMeterView init { + NewStatusBarIcons.assertInLegacyMode() + inflate(context, R.layout.battery_status_chip, this) roundedContainer = requireViewById(R.id.rounded_container) batteryMeterView = requireViewById(R.id.battery_meter_view) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt index 3e761079bc49..6aa2fe29e768 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt @@ -38,7 +38,6 @@ import com.android.systemui.Flags import com.android.systemui.Flags.spatialModelAppPushback import com.android.systemui.animation.ShadeInterpolation import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dump.DumpManager import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.plugins.statusbar.StatusBarStateController @@ -61,8 +60,6 @@ import java.util.Optional import javax.inject.Inject import kotlin.math.max import kotlin.math.sign -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch /** * Responsible for blurring the notification shade window, and applying a zoom effect to the @@ -84,7 +81,6 @@ constructor( @ShadeDisplayAware private val context: Context, private val splitShadeStateController: SplitShadeStateController, private val windowRootViewBlurInteractor: WindowRootViewBlurInteractor, - @Application private val applicationScope: CoroutineScope, private val appZoomOutOptional: Optional<AppZoomOut>, dumpManager: DumpManager, configurationController: ConfigurationController, @@ -394,18 +390,15 @@ constructor( private fun initBlurListeners() { if (!Flags.bouncerUiRevamp()) return - applicationScope.launch { - Log.d(TAG, "Starting coroutines for window root view blur") - windowRootViewBlurInteractor.onBlurAppliedEvent.collect { appliedBlurRadius -> - if (updateScheduled) { - // Process the blur applied event only if we scheduled the update - TrackTracer.instantForGroup("shade", "shade_blur_radius", appliedBlurRadius) - updateScheduled = false - onBlurApplied(appliedBlurRadius, zoomOutCalculatedFromShadeRadius) - } else { - // Try scheduling an update now, maybe our blur request will be scheduled now. - scheduleUpdate() - } + windowRootViewBlurInteractor.registerBlurAppliedListener { appliedBlurRadius -> + if (updateScheduled) { + // Process the blur applied event only if we scheduled the update + TrackTracer.instantForGroup("shade", "shade_blur_radius", appliedBlurRadius) + updateScheduled = false + onBlurApplied(appliedBlurRadius, zoomOutCalculatedFromShadeRadius) + } else { + // Try scheduling an update now, maybe our blur request will be scheduled now. + scheduleUpdate() } } } 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 a9338885d4c2..b1af811178e4 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 @@ -105,17 +105,12 @@ constructor( */ val notificationChip: Flow<NotificationChipModel?> = combine(_notificationModel, isAppVisible) { notif, isAppVisible -> - if (isAppVisible) { - // If the app that posted this notification is visible, we want to hide the chip - // because information between the status bar chip and the app itself could be - // out-of-sync (like a timer that's slightly off) - null - } else { - notif.toNotificationChipModel() - } + notif.toNotificationChipModel(isAppVisible) } - private fun ActiveNotificationModel.toNotificationChipModel(): NotificationChipModel? { + private fun ActiveNotificationModel.toNotificationChipModel( + isVisible: Boolean + ): NotificationChipModel? { val promotedContent = this.promotedContent if (promotedContent == null) { logger.w({ @@ -138,7 +133,13 @@ constructor( } } - return NotificationChipModel(key, appName, statusBarChipIconView, promotedContent) + return NotificationChipModel( + key, + appName, + statusBarChipIconView, + promotedContent, + isVisible, + ) } @AssistedFactory 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 9463db57585b..c26d10311f1e 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 @@ -142,10 +142,10 @@ constructor( } /** - * A flow modeling the notifications that should be shown as chips in the status bar. Emits an - * empty list if there are no notifications that should show a status bar chip. + * 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. */ - val notificationChips: Flow<List<NotificationChipModel>> = + private val allNotificationChips: Flow<List<NotificationChipModel>> = if (StatusBarNotifChips.isEnabled) { // For all our current interactors... promotedNotificationInteractors.flatMapLatest { intrs -> @@ -172,4 +172,13 @@ constructor( } else { flowOf(emptyList()) } + + /** Emits the notifications that should actually be *shown* as chips in the status bar. */ + val shownNotificationChips: Flow<List<NotificationChipModel>> = + allNotificationChips.map { chipsList -> + // If the app that posted this notification is visible, we want to hide the chip + // because information between the status bar chip and the app itself could be + // out-of-sync (like a timer that's slightly off) + chipsList.filter { !it.isAppVisible } + } } 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 e7a90804a768..97c37628f2e1 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 @@ -22,8 +22,10 @@ import com.android.systemui.statusbar.notification.promoted.shared.model.Promote /** Modeling all the data needed to render a status bar notification chip. */ data class NotificationChipModel( val key: String, - /** The user-readable name of the app that posted the call notification. */ + /** The user-readable name of the app that posted this notification. */ val appName: String, val statusBarChipIconView: StatusBarIconView?, val promotedContent: PromotedNotificationContentModel, + /** True if the app managing this notification is currently visible to the user. */ + val isAppVisible: Boolean, ) 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 2d6102e310f2..3ecbdf82f2cb 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 @@ -58,7 +58,7 @@ constructor( */ val chips: Flow<List<OngoingActivityChipModel.Active>> = combine( - notifChipsInteractor.notificationChips, + notifChipsInteractor.shownNotificationChips, headsUpNotificationInteractor.statusBarHeadsUpState, ) { notifications, headsUpState -> notifications.map { it.toActivityChipModel(headsUpState) } 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 8443d106dfb1..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 @@ -19,8 +19,6 @@ package com.android.systemui.statusbar.chips.ui.compose import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.Measurable @@ -39,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 @@ -121,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/compose/OngoingActivityChip.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt index 1cdf6800fb97..4a999d5f5e0e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChip.kt @@ -49,11 +49,18 @@ import com.android.systemui.animation.Expandable import com.android.systemui.common.ui.compose.Icon import com.android.systemui.common.ui.compose.load import com.android.systemui.res.R +import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.chips.ui.model.ColorsModel import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel +import com.android.systemui.statusbar.core.StatusBarConnectedDisplays +import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder @Composable -fun OngoingActivityChip(model: OngoingActivityChipModel.Active, modifier: Modifier = Modifier) { +fun OngoingActivityChip( + model: OngoingActivityChipModel.Active, + iconViewStore: NotificationIconContainerViewBinder.IconViewStore?, + modifier: Modifier = Modifier, +) { when (val clickBehavior = model.clickBehavior) { is OngoingActivityChipModel.ClickBehavior.ExpandAction -> { // Wrap the chip in an Expandable so we can animate the expand transition. @@ -65,15 +72,15 @@ fun OngoingActivityChip(model: OngoingActivityChipModel.Active, modifier: Modifi ), modifier = modifier, ) { expandable -> - ChipBody(model, onClick = { clickBehavior.onClick(expandable) }) + ChipBody(model, iconViewStore, onClick = { clickBehavior.onClick(expandable) }) } } is OngoingActivityChipModel.ClickBehavior.ShowHeadsUpNotification -> { - ChipBody(model, onClick = { clickBehavior.onClick() }) + ChipBody(model, iconViewStore, onClick = { clickBehavior.onClick() }) } is OngoingActivityChipModel.ClickBehavior.None -> { - ChipBody(model, modifier = modifier) + ChipBody(model, iconViewStore, modifier = modifier) } } } @@ -81,12 +88,15 @@ fun OngoingActivityChip(model: OngoingActivityChipModel.Active, modifier: Modifi @Composable private fun ChipBody( model: OngoingActivityChipModel.Active, + iconViewStore: NotificationIconContainerViewBinder.IconViewStore?, modifier: Modifier = Modifier, onClick: (() -> Unit)? = null, ) { val context = LocalContext.current val isClickable = onClick != null - val hasEmbeddedIcon = model.icon is OngoingActivityChipModel.ChipIcon.StatusBarView + val hasEmbeddedIcon = + model.icon is OngoingActivityChipModel.ChipIcon.StatusBarView || + model.icon is OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon val contentDescription = when (val icon = model.icon) { is OngoingActivityChipModel.ChipIcon.StatusBarView -> icon.contentDescription.load() @@ -156,7 +166,9 @@ private fun ChipBody( } ), ) { - model.icon?.let { ChipIcon(viewModel = it, colors = model.colors) } + model.icon?.let { + ChipIcon(viewModel = it, iconViewStore = iconViewStore, colors = model.colors) + } val isIconOnly = model is OngoingActivityChipModel.Active.IconOnly if (!isIconOnly) { @@ -169,6 +181,7 @@ private fun ChipBody( @Composable private fun ChipIcon( viewModel: OngoingActivityChipModel.ChipIcon, + iconViewStore: NotificationIconContainerViewBinder.IconViewStore?, colors: ColorsModel, modifier: Modifier = Modifier, ) { @@ -176,22 +189,16 @@ private fun ChipIcon( when (viewModel) { is OngoingActivityChipModel.ChipIcon.StatusBarView -> { - // TODO(b/364653005): If the notification updates their small icon, ensure it's updated - // in the chip. - val originalIcon = viewModel.impl - val iconSizePx = - context.resources.getDimensionPixelSize( - R.dimen.ongoing_activity_chip_embedded_padding_icon_size - ) - AndroidView( - modifier = modifier, - factory = { _ -> - originalIcon.apply { - layoutParams = ViewGroup.LayoutParams(iconSizePx, iconSizePx) - imageTintList = ColorStateList.valueOf(colors.text(context)) - } - }, - ) + StatusBarConnectedDisplays.assertInLegacyMode() + StatusBarIcon(colors, viewModel.impl.notification?.key, modifier) { viewModel.impl } + } + is OngoingActivityChipModel.ChipIcon.StatusBarNotificationIcon -> { + StatusBarConnectedDisplays.assertInNewMode() + check(iconViewStore != null) + + StatusBarIcon(colors, viewModel.notificationKey, modifier) { + iconViewStore.iconView(viewModel.notificationKey) + } } is OngoingActivityChipModel.ChipIcon.SingleColorIcon -> { @@ -209,6 +216,31 @@ private fun ChipIcon( } } +/** A Compose wrapper around [StatusBarIconView]. */ +@Composable +private fun StatusBarIcon( + colors: ColorsModel, + notificationKey: String?, + modifier: Modifier = Modifier, + iconFactory: () -> StatusBarIconView?, +) { + val context = LocalContext.current + + val iconSizePx = + context.resources.getDimensionPixelSize( + R.dimen.ongoing_activity_chip_embedded_padding_icon_size + ) + AndroidView( + modifier = modifier, + factory = { _ -> + iconFactory.invoke()?.apply { + layoutParams = ViewGroup.LayoutParams(iconSizePx, iconSizePx) + imageTintList = ColorStateList.valueOf(colors.text(context)) + } ?: throw IllegalStateException("Missing StatusBarIconView for $notificationKey") + }, + ) +} + @Composable private fun ExpandableChip( color: () -> Color, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChips.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChips.kt index 64cad31f2150..4017c436151e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChips.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/OngoingActivityChips.kt @@ -26,18 +26,24 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.android.systemui.statusbar.chips.ui.model.MultipleOngoingActivityChipsModel +import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder @Composable -fun OngoingActivityChips(chips: MultipleOngoingActivityChipsModel, modifier: Modifier = Modifier) { +fun OngoingActivityChips( + chips: MultipleOngoingActivityChipsModel, + iconViewStore: NotificationIconContainerViewBinder.IconViewStore?, + modifier: Modifier = Modifier, +) { Row( // TODO(b/372657935): Remove magic numbers for padding and spacing. modifier = modifier.fillMaxHeight().padding(horizontal = 6.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp), ) { - // TODO(b/372657935): Make sure chips are only shown when there is enough horizontal space. chips.active .filter { !it.isHidden } - .forEach { key(it.key) { OngoingActivityChip(model = it) } } + .forEach { + key(it.key) { OngoingActivityChip(model = it, iconViewStore = iconViewStore) } + } } } 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/events/StatusEvent.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/StatusEvent.kt index ea1d7820c79c..5887eb6ad865 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/events/StatusEvent.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/StatusEvent.kt @@ -25,6 +25,8 @@ import com.android.systemui.privacy.OngoingPrivacyChip import com.android.systemui.privacy.PrivacyItem import com.android.systemui.statusbar.BatteryStatusChip import com.android.systemui.statusbar.ConnectedDisplayChip +import com.android.systemui.statusbar.core.NewStatusBarIcons +import com.android.systemui.statusbar.events.ui.view.BatteryStatusEventComposeChip typealias ViewCreator = (context: Context) -> BackgroundAnimatableView @@ -53,9 +55,7 @@ interface StatusEvent { } } -class BGView( - context: Context -) : View(context), BackgroundAnimatableView { +class BGView(context: Context) : View(context), BackgroundAnimatableView { override val view: View get() = this @@ -65,9 +65,7 @@ class BGView( } @SuppressLint("AppCompatCustomView") -class BGImageView( - context: Context -) : ImageView(context), BackgroundAnimatableView { +class BGImageView(context: Context) : ImageView(context), BackgroundAnimatableView { override val view: View get() = this @@ -84,8 +82,10 @@ class BatteryEvent(@IntRange(from = 0, to = 100) val batteryLevel: Int) : Status override val shouldAnnounceAccessibilityEvent: Boolean = false override val viewCreator: ViewCreator = { context -> - BatteryStatusChip(context).apply { - setBatteryLevel(batteryLevel) + if (NewStatusBarIcons.isEnabled) { + BatteryStatusEventComposeChip(batteryLevel, context) + } else { + BatteryStatusChip(context).apply { setBatteryLevel(batteryLevel) } } } @@ -103,9 +103,7 @@ class ConnectedDisplayEvent : StatusEvent { override var contentDescription: String? = "" override val shouldAnnounceAccessibilityEvent: Boolean = true - override val viewCreator: ViewCreator = { context -> - ConnectedDisplayChip(context) - } + override val viewCreator: ViewCreator = { context -> ConnectedDisplayChip(context) } override fun toString(): String { return javaClass.simpleName @@ -134,7 +132,8 @@ open class PrivacyEvent(override val showAnimation: Boolean = true) : StatusEven } override fun shouldUpdateFromEvent(other: StatusEvent?): Boolean { - return other is PrivacyEvent && (other.privacyItems != privacyItems || + return other is PrivacyEvent && + (other.privacyItems != privacyItems || other.contentDescription != contentDescription || (other.forceVisible && !forceVisible)) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/ui/view/BatteryStatusEventComposeChip.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/ui/view/BatteryStatusEventComposeChip.kt new file mode 100644 index 000000000000..a90e3ff4b2dc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/ui/view/BatteryStatusEventComposeChip.kt @@ -0,0 +1,100 @@ +/* + * 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.events.ui.view + +import android.annotation.SuppressLint +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.widget.FrameLayout +import android.widget.LinearLayout +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.ComposeView +import com.android.systemui.res.R +import com.android.systemui.statusbar.core.NewStatusBarIcons +import com.android.systemui.statusbar.events.BackgroundAnimatableView +import com.android.systemui.statusbar.pipeline.battery.domain.interactor.BatteryInteractor +import com.android.systemui.statusbar.pipeline.battery.shared.ui.BatteryColors.LightThemeChargingColors +import com.android.systemui.statusbar.pipeline.battery.shared.ui.BatteryFrame +import com.android.systemui.statusbar.pipeline.battery.shared.ui.BatteryGlyph +import com.android.systemui.statusbar.pipeline.battery.ui.composable.BatteryCanvas +import com.android.systemui.statusbar.pipeline.battery.ui.viewmodel.BatteryViewModel +import com.android.systemui.statusbar.pipeline.battery.ui.viewmodel.BatteryViewModel.Companion.glyphRepresentation + +/** + * [StatusEvent] chip for the battery plugged in status event. Shows the current battery level and + * charging state in the status bar via the system event animation. + * + * This chip will fully replace [BatteryStatusChip] when [NewStatusBarIcons] is rolled out + */ +@SuppressLint("ViewConstructor") +class BatteryStatusEventComposeChip +@JvmOverloads +constructor(level: Int, context: Context, attrs: AttributeSet? = null) : + FrameLayout(context, attrs), BackgroundAnimatableView { + private val roundedContainer: LinearLayout + private val composeInner: ComposeView + override val contentView: View + get() = composeInner + + init { + NewStatusBarIcons.assertInNewMode() + + inflate(context, R.layout.status_bar_event_chip_compose, this) + roundedContainer = requireViewById(R.id.rounded_container) + composeInner = requireViewById(R.id.compose_view) + composeInner.apply { + setContent { + val isFull = BatteryInteractor.isBatteryFull(level) + BatteryCanvas( + modifier = + Modifier.width(BatteryViewModel.STATUS_BAR_BATTERY_WIDTH) + .height(BatteryViewModel.STATUS_BAR_BATTERY_HEIGHT), + path = BatteryFrame.pathSpec, + // TODO(b/394659067): get a content description for this chip + contentDescription = "", + innerWidth = BatteryFrame.innerWidth, + innerHeight = BatteryFrame.innerHeight, + // This event only happens when plugged in, so we always show it as charging + glyphs = + if (isFull) listOf(BatteryGlyph.Bolt) + else level.glyphRepresentation() + BatteryGlyph.Bolt, + level = level, + isFull = isFull, + colorsProvider = { LightThemeChargingColors }, + ) + } + } + updateResources() + } + + /** + * When animating as a chip in the status bar, we want to animate the width for the rounded + * container. We have to subtract our own top and left offset because the bounds come to us as + * absolute on-screen bounds. + */ + override fun setBoundsForAnimation(l: Int, t: Int, r: Int, b: Int) { + roundedContainer.setLeftTopRightBottom(l - left, t - top, r - left, b - top) + } + + @SuppressLint("UseCompatLoadingForDrawables") + private fun updateResources() { + roundedContainer.background = mContext.getDrawable(R.drawable.statusbar_chip_bg) + } +} 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/icon/IconManager.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt index 31375cc4a03a..c512b43c91a3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/icon/IconManager.kt @@ -222,10 +222,6 @@ constructor( return@traceSection } - if (StatusBarConnectedDisplays.isEnabled) { - onIconUpdateRequiredListeners.onEach { it.onIconUpdateRequired(entry) } - } - if (usingCache && !Flags.notificationsBackgroundIcons()) { Log.wtf( TAG, @@ -238,6 +234,10 @@ constructor( entry.icons.peopleAvatarDescriptor = null } + if (StatusBarConnectedDisplays.isEnabled) { + onIconUpdateRequiredListeners.onEach { it.onIconUpdateRequired(entry) } + } + val (normalIconDescriptor, sensitiveIconDescriptor) = getIconDescriptors(entry) val notificationContentDescription = entry.sbn.notification?.let { iconBuilder.getIconContentDescription(it) } 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/promoted/AODPromotedNotification.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/promoted/AODPromotedNotification.kt index 777ffda8c87d..e5d2361e8524 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 @@ -23,6 +23,7 @@ import android.view.LayoutInflater import android.view.View import android.view.View.GONE import android.view.View.VISIBLE +import android.view.ViewGroup.MarginLayoutParams import android.view.ViewStub import android.widget.Chronometer import android.widget.DateTimeView @@ -38,7 +39,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.runtime.Composable import androidx.compose.runtime.key import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.unit.dp @@ -164,8 +164,8 @@ private class AODPromotedNotificationViewUpdater(root: View) { private var chronometerStub: ViewStub? = root.findViewById(R.id.chronometer) private var chronometer: Chronometer? = null private val closeButton: View? = root.findViewById(R.id.close_button) - private val conversationIconContainer: View? = - root.findViewById(R.id.conversation_icon_container) + private val conversationIconBadge: View? = root.findViewById(R.id.conversation_icon_badge) + private val conversationIcon: CachingIconView? = root.findViewById(R.id.conversation_icon) private val conversationText: TextView? = root.findViewById(R.id.conversation_text) private val expandButton: NotificationExpandButton? = root.findViewById(R.id.expand_button) private val headerText: TextView? = root.findViewById(R.id.header_text) @@ -175,12 +175,13 @@ private class AODPromotedNotificationViewUpdater(root: View) { root.findViewById(R.id.header_text_secondary_divider) private val icon: NotificationRowIconView? = root.findViewById(R.id.icon) private val leftIcon: ImageView? = root.findViewById(R.id.left_icon) - private val rightIcon: ImageView? = root.findViewById(R.id.right_icon) + private val mainColumn: View? = root.findViewById(R.id.notification_main_column) private val notificationProgressEndIcon: CachingIconView? = root.findViewById(R.id.notification_progress_end_icon) private val notificationProgressStartIcon: CachingIconView? = root.findViewById(R.id.notification_progress_start_icon) private val profileBadge: ImageView? = root.findViewById(R.id.profile_badge) + private val rightIcon: ImageView? = root.findViewById(R.id.right_icon) private val text: ImageFloatingTextView? = root.findViewById(R.id.text) private val time: DateTimeView? = root.findViewById(R.id.time) private val timeDivider: View? = root.findViewById(R.id.time_divider) @@ -198,7 +199,7 @@ private class AODPromotedNotificationViewUpdater(root: View) { alternateExpandTarget?.visibility = GONE bigPicture?.visibility = GONE closeButton?.visibility = GONE - conversationIconContainer?.visibility = GONE + conversationIconBadge?.visibility = GONE expandButton?.visibility = GONE leftIcon?.visibility = GONE notificationProgressEndIcon?.visibility = GONE @@ -209,6 +210,16 @@ private class AODPromotedNotificationViewUpdater(root: View) { ?.drawable ?.mutate() ?.setColorFilter(SecondaryText.colorInt, PorterDuff.Mode.SRC_IN) + + if (Flags.notificationsRedesignTemplates()) { + (mainColumn?.layoutParams as? MarginLayoutParams)?.let { mainColumnMargins -> + mainColumnMargins.topMargin = + Notification.Builder.getContentMarginTop( + root.context, + R.dimen.notification_2025_content_margin_top, + ) + } + } } fun update(content: PromotedNotificationContentModel, audiblyAlertedIconVisible: Boolean) { @@ -229,16 +240,11 @@ private class AODPromotedNotificationViewUpdater(root: View) { textView: ImageFloatingTextView? = null, showOldProgress: Boolean = true, ) { - // Icon binding must be called in this order - updateImageView(icon, content.smallIcon) - icon?.setImageLevel(content.iconLevel) - icon?.setBackgroundColor(Background.colorInt) - icon?.originalIconColor = PrimaryText.colorInt - updateHeader(content, hideTitle = true) updateTitle(title, content) updateText(textView ?: text, content) + updateSmallIcon(icon, content) updateImageView(rightIcon, content.skeletonLargeIcon) if (showOldProgress) { @@ -341,6 +347,8 @@ private class AODPromotedNotificationViewUpdater(root: View) { updateImageView(verificationIcon, content.verificationIcon) updateTextView(verificationText, content.verificationText) + + updateSmallIcon(conversationIcon, content) } private fun updateConversationHeaderDividers( @@ -398,6 +406,19 @@ private class AODPromotedNotificationViewUpdater(root: View) { } } + private fun updateSmallIcon( + smallIconView: CachingIconView?, + content: PromotedNotificationContentModel, + ) { + smallIconView ?: return + + // Icon binding must be called in this order + updateImageView(smallIconView, content.smallIcon) + smallIconView.setImageLevel(content.iconLevel) + smallIconView.setBackgroundColor(Background.colorInt) + smallIconView.originalIconColor = PrimaryText.colorInt + } + private fun inflateChronometer() { if (chronometer != null) { return @@ -480,14 +501,12 @@ private fun Notification.ProgressStyle.Point.toSkeleton(): Notification.Progress } } -private enum class AodPromotedNotificationColor(colorUInt: UInt) { - Background(0xFF000000u), - PrimaryText(0xFFFFFFFFu), - SecondaryText(0xFFCCCCCCu); +private enum class AodPromotedNotificationColor(val colorInt: Int) { + Background(android.graphics.Color.BLACK), + PrimaryText(android.graphics.Color.WHITE), + SecondaryText(android.graphics.Color.WHITE); - val colorInt = colorUInt.toInt() - val color = Color(colorInt) - val brush = SolidColor(color) + val brush = SolidColor(androidx.compose.ui.graphics.Color(colorInt)) } private val viewUpdaterTagId = systemuiR.id.aod_promoted_notification_view_updater_tag 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 0f21514fcc94..393f95d3ad77 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 @@ -17,8 +17,11 @@ 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 import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -26,9 +29,17 @@ import kotlinx.coroutines.flow.map @SysUISingleton class AODPromotedNotificationInteractor @Inject -constructor(activeNotificationsInteractor: ActiveNotificationsInteractor) { +constructor( + activeNotificationsInteractor: ActiveNotificationsInteractor, + dumpManager: DumpManager, +) : FlowDumperImpl(dumpManager) { val content: Flow<PromotedNotificationContentModel?> = activeNotificationsInteractor.topLevelRepresentativeNotifications.map { notifs -> notifs.firstNotNullOfOrNull { it.promotedContent } } + + val isPresent: Flow<Boolean> = + content + .map { (it != null) && (it.style != Style.Ineligible) } + .dumpWhileCollecting("isPresent") } 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..d1d1ea9b5ff4 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 @@ -2740,6 +2740,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) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionBackgroundDrawable.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionBackgroundDrawable.kt index 6aa5e405f29c..77135802eced 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionBackgroundDrawable.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionBackgroundDrawable.kt @@ -18,243 +18,126 @@ package com.android.systemui.statusbar.notification.row import android.animation.ValueAnimator import android.content.Context -import android.graphics.BlendMode import android.graphics.Canvas -import android.graphics.Color import android.graphics.ColorFilter -import android.graphics.LinearGradient import android.graphics.Paint import android.graphics.Path import android.graphics.PixelFormat +import android.graphics.drawable.Drawable +import android.graphics.LinearGradient +import android.graphics.Matrix import android.graphics.Rect -import android.graphics.RuntimeShader +import android.graphics.RectF import android.graphics.Shader -import android.graphics.drawable.Drawable -import android.widget.FrameLayout -import androidx.compose.foundation.layout.size -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp -import androidx.compose.ui.viewinterop.AndroidView -import com.android.internal.graphics.ColorUtils import com.android.systemui.res.R -import com.android.systemui.surfaceeffects.shaderutil.ShaderUtilLibrary import com.android.wm.shell.shared.animation.Interpolators -import kotlin.math.min -/** - * A background style for smarter-smart-actions. The style is composed by a simplex3d noise, - * overlaid with sparkles. - */ class MagicActionBackgroundDrawable( context: Context, - primaryContainer: Int? = null, - seed: Float = 0f, ) : Drawable() { - private val pixelDensity = context.resources.displayMetrics.density - private val cornerRadius = - context.resources.getDimensionPixelSize(R.dimen.smart_reply_button_corner_radius).toFloat() - private val outlineStrokeWidth = - context.resources - .getDimensionPixelSize(R.dimen.smart_action_button_outline_stroke_width) - .toFloat() - private val buttonShape = Path() - private val paddingVertical = - context.resources.getDimensionPixelSize(R.dimen.smart_action_button_icon_padding).toFloat() + private val cornerRadius = context.resources.getDimension(R.dimen.magic_action_button_corner_radius) + private val outlineStrokeWidth = context.resources.getDimension(R.dimen.magic_action_button_outline_stroke_width) + private val insetVertical = 8 * context.resources.displayMetrics.density - /** The color of the button background. */ - private val mainColor = - primaryContainer - ?: context.getColor(com.android.internal.R.color.materialColorPrimaryContainer) - - /** Slightly brighter version of [mainColor] used on the simplex noise. */ - private val effectColor: Int - get() { - val labColor = arrayOf(0.0, 0.0, 0.0).toDoubleArray() - ColorUtils.colorToLAB(mainColor, labColor) - val camColor = ColorUtils.colorToCAM(mainColor) - return ColorUtils.CAMToColor( - camColor.hue, - camColor.chroma, - min(100f, (labColor[0] + 10).toFloat()), + private val buttonShape = Path() + // Color and style + private val bgPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + val bgColor = + context.getColor( + com.android.internal.R.color.materialColorPrimaryContainer ) - } + color = bgColor + style = Paint.Style.FILL + } + private val outlinePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + val outlineColor = + context.getColor( + com.android.internal.R.color.materialColorOutlineVariant + ) + color = outlineColor + style = Paint.Style.STROKE + strokeWidth = outlineStrokeWidth + } + private val outlineStartColor = + context.getColor( + com.android.internal.R.color.materialColorTertiaryContainer + ) + private val outlineMiddleColor = + context.getColor( + com.android.internal.R.color.materialColorPrimaryFixedDim + ) + private val outlineEndColor = + context.getColor( + com.android.internal.R.color.materialColorPrimary + ) + // Animation + private var gradientAnimator: ValueAnimator + private var rotationAngle = 20f // Start rotation at 20 degrees - private val bgShader = MagicActionBackgroundShader() - private val bgPaint = Paint() - private val outlinePaint = Paint() - private val gradientAnimator = - ValueAnimator.ofFloat(0f, 1f).apply { - duration = 2500 - interpolator = Interpolators.LINEAR - addUpdateListener { invalidateSelf() } - } - private val turbulenceAnimator = - ValueAnimator.ofFloat(seed, seed + TURBULENCE_MOVEMENT).apply { - duration = ANIMATION_DURATION + init { + gradientAnimator = ValueAnimator.ofFloat(0f, 1f).apply { + duration = 5000 // 5 seconds interpolator = Interpolators.LINEAR - addUpdateListener { invalidateSelf() } + repeatCount = 1 + addUpdateListener { animator -> + val animatedValue = animator.animatedValue as Float + rotationAngle = 20f + animatedValue * 360f // Rotate in a spiral + invalidateSelf() + } + // TODO: Reset the outline color when animation ends. start() } - private val effectFadeAnimation = - ValueAnimator.ofFloat(0f, 1f).apply { - duration = 1000 - startDelay = ANIMATION_DURATION - 1000L - interpolator = Interpolators.STANDARD_DECELERATE - addUpdateListener { invalidateSelf() } - } - - init { - bgShader.setColorUniform("in_color", mainColor) - bgShader.setColorUniform("in_effectColor", effectColor) - bgPaint.shader = bgShader - outlinePaint.style = Paint.Style.STROKE - // Stroke is doubled in width and then clipped, to avoid anti-aliasing artifacts at the edge - // of the rectangle. - outlinePaint.strokeWidth = outlineStrokeWidth * 2 - outlinePaint.blendMode = BlendMode.SCREEN - outlinePaint.alpha = OUTLINE_ALPHA - - animate() - } - - private fun animate() { - turbulenceAnimator.start() - gradientAnimator.start() - effectFadeAnimation.start() } override fun draw(canvas: Canvas) { - updateShaders() + val boundsF = RectF(bounds) + boundsF.inset(0f, insetVertical) + buttonShape.reset() + buttonShape.addRoundRect(boundsF, cornerRadius, cornerRadius, Path.Direction.CW) - // We clip instead of drawing 2 rounded rects, otherwise there will be artifacts where - // around the button background and the outline. canvas.save() + // Draw background canvas.clipPath(buttonShape) canvas.drawPath(buttonShape, bgPaint) + // Apply gradient to outline canvas.drawPath(buttonShape, outlinePaint) + updateGradient(boundsF) canvas.restore() } - private fun updateShaders() { - val effectAlpha = 1f - effectFadeAnimation.animatedValue as Float - val turbulenceZ = turbulenceAnimator.animatedValue as Float - bgShader.setFloatUniform("in_sparkleMove", turbulenceZ * 1000) - bgShader.setFloatUniform("in_noiseMove", 0f, 0f, turbulenceZ) - bgShader.setFloatUniform("in_turbulenceAlpha", effectAlpha) - bgShader.setFloatUniform("in_spkarkleAlpha", SPARKLE_ALPHA * effectAlpha) - val gradientOffset = gradientAnimator.animatedValue as Float * bounds.width() - val outlineGradient = - LinearGradient( - gradientOffset + bounds.left.toFloat(), - 0f, - gradientOffset + bounds.right.toFloat(), - 0f, - mainColor, - ColorUtils.setAlphaComponent(mainColor, 0), - Shader.TileMode.MIRROR, - ) - outlinePaint.shader = outlineGradient + private fun updateGradient(boundsF: RectF) { + val gradient = LinearGradient( + boundsF.left, boundsF.top, + boundsF.right, boundsF.bottom, + intArrayOf(outlineStartColor, outlineMiddleColor, outlineEndColor), + null, + Shader.TileMode.CLAMP + ) + // Create a rotation matrix for the spiral effect + val matrix = Matrix() + matrix.setRotate(rotationAngle, boundsF.centerX(), boundsF.centerY()) + gradient.setLocalMatrix(matrix) + + outlinePaint.shader = gradient } override fun onBoundsChange(bounds: Rect) { super.onBoundsChange(bounds) - - val width = bounds.width().toFloat() - val height = bounds.height().toFloat() - if (width == 0f || height == 0f) return - - bgShader.setFloatUniform("in_gridNum", NOISE_SIZE) - bgShader.setFloatUniform("in_size", width, height) - bgShader.setFloatUniform("in_aspectRatio", width / height) - bgShader.setFloatUniform("in_pixelDensity", pixelDensity) - - buttonShape.reset() - buttonShape.addRoundRect( - bounds.left.toFloat(), - bounds.top + paddingVertical, - bounds.right.toFloat(), - bounds.bottom - paddingVertical, - cornerRadius, - cornerRadius, - Path.Direction.CW, - ) + invalidateSelf() // Redraw when size changes } override fun setAlpha(alpha: Int) { bgPaint.alpha = alpha + outlinePaint.alpha = alpha invalidateSelf() } override fun setColorFilter(colorFilter: ColorFilter?) { bgPaint.colorFilter = colorFilter + outlinePaint.colorFilter = colorFilter invalidateSelf() } override fun getOpacity(): Int = PixelFormat.TRANSLUCENT - - companion object { - /** Smoothness of the turbulence. Larger numbers yield more detail. */ - private const val NOISE_SIZE = 0.57f - /** Strength of the sparkles overlaid on the turbulence. */ - private const val SPARKLE_ALPHA = 0.15f - /** Alpha (0..255) of the button outline */ - private const val OUTLINE_ALPHA = 82 - /** Turbulence grid size */ - private const val TURBULENCE_MOVEMENT = 4.3f - /** Total animation duration in millis */ - private const val ANIMATION_DURATION = 5000L - } -} - -private class MagicActionBackgroundShader : RuntimeShader(SHADER) { - - // language=AGSL - companion object { - private const val UNIFORMS = - """ - uniform float in_gridNum; - uniform vec3 in_noiseMove; - uniform half in_sparkleMove; - uniform vec2 in_size; - uniform float in_aspectRatio; - uniform half in_pixelDensity; - uniform float in_turbulenceAlpha; - uniform float in_spkarkleAlpha; - layout(color) uniform vec4 in_color; - layout(color) uniform vec4 in_effectColor; - """ - private const val MAIN_SHADER = - """vec4 main(vec2 p) { - vec2 uv = p / in_size.xy; - uv.x *= in_aspectRatio; - vec3 noiseP = vec3(uv + in_noiseMove.xy, in_noiseMove.z) * in_gridNum; - half luma = getLuminosity(half3(simplex3d(noiseP))); - half4 turbulenceColor = mix(in_color, in_effectColor, luma * in_turbulenceAlpha); - float sparkle = sparkles(p - mod(p, in_pixelDensity * 0.8), in_sparkleMove); - sparkle = min(sparkle * in_spkarkleAlpha, in_spkarkleAlpha); - return saturate(turbulenceColor + half4(sparkle)); - } - """ - private const val SHADER = UNIFORMS + ShaderUtilLibrary.SHADER_LIB + MAIN_SHADER - } -} - -// @Preview -@Composable -fun DrawablePreview() { - AndroidView( - factory = { context -> - FrameLayout(context).apply { - background = - MagicActionBackgroundDrawable( - context = context, - primaryContainer = Color.parseColor("#c5eae2"), - seed = 0f, - ) - } - }, - modifier = Modifier.size(100.dp, 50.dp), - ) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionButton.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionButton.kt new file mode 100644 index 000000000000..d735360f1d4e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/MagicActionButton.kt @@ -0,0 +1,36 @@ +/* + * 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.annotation.SuppressLint +import android.content.Context +import android.util.AttributeSet +import android.widget.Button + +/** + * Custom Button for Magic Action Button, which includes the custom background and foreground. + */ +@SuppressLint("AppCompatCustomView") +class MagicActionButton @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0, +) : Button(context, attrs, defStyleAttr) { + init { + background = MagicActionBackgroundDrawable(context) + } +} 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/PromotedNotificationInfo.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PromotedNotificationInfo.java index 93b2a2d41691..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 @@ -40,6 +42,7 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry; public class PromotedNotificationInfo extends NotificationInfo { private static final String TAG = "PromotedNotifInfoGuts"; private INotificationManager mNotificationManager; + private NotificationGuts mGutsContainer; public PromotedNotificationInfo(Context context, AttributeSet attrs) { super(context, attrs); @@ -49,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, @@ -63,40 +68,35 @@ 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; - bindDismiss(entry.getSbn(), onCloseClick); bindDemote(entry.getSbn(), pkg); } - - protected void bindDismiss(StatusBarNotification sbn, - View.OnClickListener onCloseClick) { - View dismissButton = findViewById(R.id.promoted_dismiss); - - dismissButton.setOnClickListener(onCloseClick); - dismissButton.setVisibility(!sbn.isNonDismissable() - && dismissButton.hasOnClickListeners() ? VISIBLE : GONE); - - } - protected void bindDemote(StatusBarNotification sbn, String packageName) { View demoteButton = findViewById(R.id.promoted_demote); demoteButton.setOnClickListener(getDemoteClickListener(sbn, packageName)); demoteButton.setVisibility(demoteButton.hasOnClickListeners() ? VISIBLE : GONE); } + @Override + public void setGutsParent(NotificationGuts guts) { + mGutsContainer = guts; + super.setGutsParent(guts); + } + private OnClickListener getDemoteClickListener(StatusBarNotification sbn, String packageName) { - return ((View unusedView) -> { + return ((View v) -> { try { // TODO(b/391661009): Signal AutomaticPromotionCoordinator here mNotificationManager.setCanBePromoted(packageName, sbn.getUid(), false, true); + mGutsContainer.closeControls(v, true); } catch (RemoteException e) { Log.e(TAG, "Couldn't revoke live update permission", e); } 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/row/wrapper/NotificationHeaderViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java index e9eecdd8a26f..e8affaa4b60f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationHeaderViewWrapper.java @@ -31,6 +31,7 @@ import android.view.ViewGroup; import android.view.animation.Interpolator; import android.view.animation.PathInterpolator; import android.widget.DateTimeView; +import android.widget.FrameLayout; import android.widget.ImageButton; import android.widget.ImageView; import android.widget.TextView; @@ -41,6 +42,7 @@ import com.android.app.animation.Interpolators; import com.android.internal.widget.CachingIconView; import com.android.internal.widget.NotificationCloseButton; import com.android.internal.widget.NotificationExpandButton; +import com.android.systemui.Flags; import com.android.systemui.res.R; import com.android.systemui.statusbar.TransformableView; import com.android.systemui.statusbar.ViewTransformationHelper; @@ -67,6 +69,7 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper imple private CachingIconView mIcon; private NotificationCloseButton mCloseButton; private NotificationExpandButton mExpandButton; + private FrameLayout mExpandButtonSpacer; private View mAltExpandTarget; private View mIconContainer; protected NotificationHeaderView mNotificationHeader; @@ -154,6 +157,10 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper imple mHeaderText = mView.findViewById(com.android.internal.R.id.header_text); mAppNameText = mView.findViewById(com.android.internal.R.id.app_name_text); mExpandButton = mView.findViewById(com.android.internal.R.id.expand_button); + if (Flags.uiRichOngoingForceExpanded()) { + mExpandButtonSpacer = + mView.findViewById(com.android.internal.R.id.expand_button_spacer); + } mAltExpandTarget = mView.findViewById(com.android.internal.R.id.alternate_expand_target); mIconContainer = mView.findViewById(com.android.internal.R.id.conversation_icon_container); mWorkProfileImage = mView.findViewById(com.android.internal.R.id.profile_badge); @@ -295,6 +302,9 @@ public class NotificationHeaderViewWrapper extends NotificationViewWrapper imple boolean expandable, View.OnClickListener onClickListener, boolean requestLayout) { + if (Flags.uiRichOngoingForceExpanded() && mExpandButtonSpacer != null) { + mExpandButtonSpacer.setVisibility(expandable ? GONE : VISIBLE); + } mExpandButton.setVisibility(expandable ? VISIBLE : GONE); mExpandButton.setOnClickListener(expandable ? onClickListener : null); if (mAltExpandTarget != null) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt index 5a29a699a7e6..a507c4ceecd2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/MagneticNotificationRowManagerImpl.kt @@ -190,7 +190,8 @@ constructor( currentMagneticListeners.forEachIndexed { i, target -> target?.let { if (i != currentMagneticListeners.size / 2) { - snapBack(it, velocity) + val velocityMultiplier = MAGNETIC_TRANSLATION_MULTIPLIERS[i] + snapBack(it, velocity?.times(velocityMultiplier)) } } } 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 42d02e10ab8d..b9352bf64be4 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 @@ -52,6 +52,9 @@ import android.graphics.Outline; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Rect; +import android.graphics.RenderEffect; +import android.graphics.RenderNode; +import android.graphics.Shader; import android.os.Bundle; import android.os.SystemClock; import android.os.Trace; @@ -513,6 +516,13 @@ public class NotificationStackScrollLayout /** The clip path defining where we are NOT allowed to draw. */ private final Path mNegativeRoundedClipPath = new Path(); + /** RenderNode to blur notifications which will be reused (redrawn) whenever NSSL is drawn. */ + private final RenderNode mBlurNode = new RenderNode("BlurNode"); + + /** Radius of the blur effect applied to the content of the NSSL. */ + private float mBlurRadius = 0f; + @Nullable private RenderEffect mBlurEffect = null; + /** * The clip Path used to clip the launching notification. This may be different * from the normal path, as the views launch animation could start clipped. @@ -896,6 +906,7 @@ public class NotificationStackScrollLayout mOverflingDistance = configuration.getScaledOverflingDistance(); Resources res = context.getResources(); + mSwipeHelper.updateResourceProperties(res); final boolean isSmallScreenLandscape = res.getBoolean(R.bool.is_small_screen_landscape); boolean useSmallLandscapeLockscreenResources = mIsSmallLandscapeLockscreenEnabled && isSmallScreenLandscape; @@ -2073,8 +2084,6 @@ public class NotificationStackScrollLayout Resources res = getResources(); updateSplitNotificationShade(); mStatusBarHeight = SystemBarUtils.getStatusBarHeight(mContext); - float densityScale = res.getDisplayMetrics().density; - mSwipeHelper.setDensityScale(densityScale); float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop(); mSwipeHelper.setPagingTouchSlop(pagingTouchSlop); reinitView(); @@ -5974,6 +5983,24 @@ public class NotificationStackScrollLayout invalidate(); } + @Override + public void setBlurRadius(float blurRadius) { + if (mBlurRadius != blurRadius) { + mBlurRadius = blurRadius; + updateBlurEffect(); + invalidate(); + } + } + + private void updateBlurEffect() { + if (mBlurRadius > 0) { + mBlurEffect = + RenderEffect.createBlurEffect(mBlurRadius, mBlurRadius, Shader.TileMode.CLAMP); + } else { + mBlurEffect = null; + } + } + /** * Set rounded rect clipping bounds on this view. */ @@ -6144,26 +6171,64 @@ public class NotificationStackScrollLayout @Override protected void dispatchDraw(@NonNull Canvas canvas) { - if (!mLaunchingNotification) { - // When launching notifications, we're clipping the children individually instead of in - // dispatchDraw - if (mShouldUseRoundedRectClipping) { - // Let's clip rounded. - canvas.clipPath(mRoundedClipPath); + if (mBlurEffect != null) { + // reuse the cached RenderNode to blur + mBlurNode.setPosition(0, 0, canvas.getWidth(), canvas.getHeight()); + mBlurNode.setRenderEffect(mBlurEffect); + Canvas blurCanvas = mBlurNode.beginRecording(); + // draw all the children (except HUNs) on the blurred canvas + super.dispatchDraw(blurCanvas); + mBlurNode.endRecording(); + // apply clipping to the canvas + int saveCount = canvas.save(); + applyClipToCanvas(canvas); + // draw the blurred content to the clipped canvas + canvas.drawRenderNode(mBlurNode); + // restore the canvas, so it doesn't clip anymore + canvas.restoreToCount(saveCount); + // draw the children that were left out during the dispatchDraw phase + for (int i = 0; i < getChildCount(); i++) { + // TODO(b/388469101) draw these children in z-order + ExpandableView child = getChildAtIndex(i); + if (shouldSkipBlurForChild(child)) { + super.drawChild(canvas, child, getDrawingTime()); + } } - if (mShouldUseNegativeRoundedRectClipping) { - // subtract the negative path if it is defined - canvas.clipOutPath(mNegativeRoundedClipPath); + } else { + if (!mLaunchingNotification) { + // When launching notifications, we're clipping the children individually instead + // of in dispatchDraw + applyClipToCanvas(canvas); } + super.dispatchDraw(canvas); + } + } + + private void applyClipToCanvas(Canvas canvas) { + if (mShouldUseRoundedRectClipping) { + // clip by the positive path if it is defined + canvas.clipPath(mRoundedClipPath); + } + if (mShouldUseNegativeRoundedRectClipping) { + // subtract the negative path if it is defined + canvas.clipOutPath(mNegativeRoundedClipPath); } - super.dispatchDraw(canvas); } @Override protected boolean drawChild(Canvas canvas, View child, long drawingTime) { boolean shouldUseClipping = mShouldUseRoundedRectClipping || mShouldUseNegativeRoundedRectClipping; - if (mLaunchingNotification && shouldUseClipping) { + if (mBlurEffect != null) { + if (shouldSkipBlurForChild(child)) { + // skip drawing this child during the regular dispatchDraw pass + return false; + } else { + // draw the child as if nothing happened, non-blurred elements shouldn't be + // affected by clipping either + return super.drawChild(canvas, child, drawingTime); + } + } else if (mLaunchingNotification && shouldUseClipping) { // Let's clip children individually during notification launch canvas.save(); ExpandableView expandableView = (ExpandableView) child; @@ -6194,6 +6259,14 @@ public class NotificationStackScrollLayout } } + private boolean shouldSkipBlurForChild(View child) { + if (child instanceof ExpandableView row) { + return row.isHeadsUpState(); + } else { + return false; + } + } + /** * Calculate the total translation needed when dismissing. */ 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/NotificationSwipeHelper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java index 6f4047f48205..c5a846e1da05 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationSwipeHelper.java @@ -534,12 +534,6 @@ class NotificationSwipeHelper extends SwipeHelper implements NotificationSwipeAc } @Override - public void setDensityScale(float densityScale) { - super.setDensityScale(densityScale); - mCallback.onDensityScaleChange(densityScale); - } - - @Override public void resetTouchState() { super.resetTouchState(); mCallback.resetMagneticStates(); @@ -565,8 +559,6 @@ class NotificationSwipeHelper extends SwipeHelper implements NotificationSwipeAc */ float getTotalTranslationLength(View animView); - void onDensityScaleChange(float density); - boolean handleSwipeableViewTranslation(SwipeableView view, float translate); // Reset any ongoing magnetic interactions diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt index 5fec0965f6a0..a7305f7f27ab 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt @@ -58,6 +58,13 @@ interface NotificationScrollView { */ fun setNegativeClippingShape(shape: ShadeScrimShape?) + /** + * Sets a blur effect on the view. A radius of 0 means no blur. + * + * @param radius blur radius in pixels + */ + fun setBlurRadius(radius: Float) + /** set the y position in px of the top of the stack in this view's coordinates */ fun setStackTop(stackTop: Float) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt index 43a552b516ae..a4e39cbd8388 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt @@ -94,6 +94,7 @@ constructor( } } launch { viewModel.qsExpandFraction.collectTraced { view.setQsExpandFraction(it) } } + launch { viewModel.blurRadius(maxBlurRadius).collect(view::setBlurRadius) } launch { viewModel.isShowingStackOnLockscreen.collectTraced { view.setShowingStackOnLockscreen(it) @@ -146,6 +147,10 @@ constructor( } } + /** blur radius to be applied when the QS panel is fully expanded */ + private val maxBlurRadius: Flow<Int> = + configuration.getDimensionPixelSize(R.dimen.max_shade_content_blur_radius) + /** flow of the scrim clipping radius */ private val scrimRadius: Flow<Int> get() = configuration.getDimensionPixelOffset(R.dimen.notification_scrim_corner_radius) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt index 7f016a1cbc2e..08d98a1d53e5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt @@ -15,6 +15,8 @@ * */ +@file:OptIn(ExperimentalCoroutinesApi::class) + package com.android.systemui.statusbar.notification.stack.ui.viewmodel import com.android.compose.animation.scene.ContentKey @@ -46,11 +48,14 @@ import com.android.systemui.util.kotlin.ActivatableFlowDumperImpl import dagger.Lazy import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.combineTransform import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull @@ -193,6 +198,37 @@ constructor( val qsExpandFraction: Flow<Float> = shadeInteractor.qsExpansion.dumpWhileCollecting("qsExpandFraction") + /** Blur radius to be applied to Notifications. */ + fun blurRadius(maxBlurRadius: Flow<Int>) = + combine(blurFraction, maxBlurRadius) { fraction, maxRadius -> fraction * maxRadius } + + /** + * Scale of the blur effect that should be applied to Notifications. + * + * 0 -> don't blur (default, removes all blur render effects) 1 -> do the full blur (apply a + * render effect with the max blur radius) + */ + private val blurFraction: Flow<Float> = + if (SceneContainerFlag.isEnabled) { + shadeModeInteractor.shadeMode.flatMapLatest { shadeMode -> + when (shadeMode) { + ShadeMode.Dual -> + combineTransform( + shadeInteractor.shadeExpansion, + shadeInteractor.qsExpansion, + ) { notificationShadeExpansion, qsExpansion -> + if (notificationShadeExpansion == 0f) { + // Blur out notifications as the QS overlay panel expands + emit(qsExpansion) + } + } + else -> flowOf(0f) + } + } + } else { + flowOf(0f) + } + /** Whether we should close any open notification guts. */ val shouldCloseGuts: Flow<Boolean> = stackAppearanceInteractor.shouldCloseGuts 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 54efa4a2bcf2..2c8c7a1bdd44 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,6 +48,7 @@ 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 @@ -136,6 +137,7 @@ 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, @@ -572,6 +574,7 @@ 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/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java index 04dedb629a15..9aa4c54c4292 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java @@ -275,7 +275,8 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp DozeScrimController dozeScrimController, KeyguardViewMediator keyguardViewMediator, NotificationShadeWindowController notificationShadeWindowController, - KeyguardStateController keyguardStateController, Handler handler, + KeyguardStateController keyguardStateController, + @Main Handler handler, KeyguardUpdateMonitor keyguardUpdateMonitor, @Main Resources resources, KeyguardBypassController keyguardBypassController, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java index 258b7cafcdcf..66f0d4ad692e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java @@ -168,6 +168,27 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump */ @FloatRange(from = 0, to = 1) private float mBouncerHiddenFraction = KeyguardBouncerConstants.EXPANSION_HIDDEN; + private boolean mIsBlurSupported = false; + + private float getDefaultScrimAlpha(boolean ignoreCurrentState) { + if (Flags.bouncerUiRevamp() && mIsBlurSupported) { + // Hack to not make the shade transparent when shade blur is not enabled. + if (!Flags.notificationShadeBlur() && !ignoreCurrentState) { + // When we expand directly to full quick settings, shade state is KEYGUARD + if (mState == ScrimState.SHADE_LOCKED || (mState == ScrimState.KEYGUARD + && mQsExpansion == 1)) { + return BUSY_SCRIM_ALPHA; + } + } + return TRANSPARENT_BOUNCER_SCRIM_ALPHA; + } else { + return BUSY_SCRIM_ALPHA; + } + } + + private float getDefaultScrimAlpha() { + return getDefaultScrimAlpha(false); + } @IntDef(prefix = {"VISIBILITY_"}, value = { TRANSPARENT, @@ -230,7 +251,6 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump private float mScrimBehindAlphaKeyguard = KEYGUARD_SCRIM_ALPHA; static final float TRANSPARENT_BOUNCER_SCRIM_ALPHA = 0.54f; - private float mDefaultScrimAlpha; private float mRawPanelExpansionFraction; private float mPanelScrimMinFraction; @@ -329,7 +349,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump DozeParameters dozeParameters, KeyguardStateController keyguardStateController, DelayedWakeLock.Factory delayedWakeLockFactory, - Handler handler, + @Main Handler handler, KeyguardUpdateMonitor keyguardUpdateMonitor, DockManager dockManager, ConfigurationController configurationController, @@ -350,7 +370,6 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump mLargeScreenShadeInterpolator = largeScreenShadeInterpolator; mBlurConfig = blurConfig; mWindowRootViewBlurInteractor = windowRootViewBlurInteractor; - mDefaultScrimAlpha = BUSY_SCRIM_ALPHA; mKeyguardStateController = keyguardStateController; mDarkenWhileDragging = !mKeyguardStateController.canDismissLockScreen(); @@ -414,7 +433,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump states[i].init(mScrimInFront, mScrimBehind, mDozeParameters, mDockManager, this::isBlurCurrentlySupported); states[i].setScrimBehindAlphaKeyguard(mScrimBehindAlphaKeyguard); - states[i].setDefaultScrimAlpha(mDefaultScrimAlpha); + states[i].setDefaultScrimAlpha(getDefaultScrimAlpha()); } mTransparentScrimBackground = notificationsScrim.getResources() @@ -498,10 +517,9 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump } } - private void updateDefaultScrimAlpha(float alpha) { - mDefaultScrimAlpha = alpha; + private void updateDefaultScrimAlphas() { for (ScrimState state : ScrimState.values()) { - state.setDefaultScrimAlpha(mDefaultScrimAlpha); + state.setDefaultScrimAlpha(getDefaultScrimAlpha(true)); } applyAndDispatchState(); } @@ -513,13 +531,13 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump } private void handleBlurSupportedChanged(boolean isBlurSupported) { + this.mIsBlurSupported = isBlurSupported; if (Flags.bouncerUiRevamp()) { + updateDefaultScrimAlphas(); if (isBlurSupported) { - updateDefaultScrimAlpha(TRANSPARENT_BOUNCER_SCRIM_ALPHA); ScrimState.BOUNCER_SCRIMMED.setNotifBlurRadius(mBlurConfig.getMaxBlurRadiusPx()); } else { ScrimState.BOUNCER_SCRIMMED.setNotifBlurRadius(0f); - updateDefaultScrimAlpha(BUSY_SCRIM_ALPHA); } } if (Flags.notificationShadeBlur()) { @@ -1003,7 +1021,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump float behindFraction = getInterpolatedFraction(); behindFraction = (float) Math.pow(behindFraction, 0.8f); mBehindAlpha = 1; - mNotificationsAlpha = behindFraction * mDefaultScrimAlpha; + mNotificationsAlpha = behindFraction * getDefaultScrimAlpha(); } else { if (Flags.notificationShadeBlur()) { // TODO (b/390730594): match any spec for controlling alpha based on shade @@ -1014,7 +1032,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump mNotificationsTint = mState.getNotifTint(); } else { mBehindAlpha = mLargeScreenShadeInterpolator.getBehindScrimAlpha( - mPanelExpansionFraction * mDefaultScrimAlpha); + mPanelExpansionFraction * getDefaultScrimAlpha()); mNotificationsAlpha = mLargeScreenShadeInterpolator.getNotificationScrimAlpha( mPanelExpansionFraction); @@ -1030,7 +1048,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump final float interpolatedFraction = BouncerPanelExpansionCalculator.aboutToShowBouncerProgress( mBouncerHiddenFraction); - mBehindAlpha = MathUtils.lerp(mDefaultScrimAlpha, mBehindAlpha, + mBehindAlpha = MathUtils.lerp(getDefaultScrimAlpha(), mBehindAlpha, interpolatedFraction); mBehindTint = ColorUtils.blendARGB(ScrimState.BOUNCER.getBehindTint(), mBehindTint, @@ -1120,7 +1138,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump float behindAlpha; int behindTint = state.getBehindTint(); if (mDarkenWhileDragging) { - behindAlpha = MathUtils.lerp(mDefaultScrimAlpha, stateBehind, + behindAlpha = MathUtils.lerp(getDefaultScrimAlpha(), stateBehind, interpolatedFract); } else { behindAlpha = MathUtils.lerp(0 /* start */, stateBehind, @@ -1136,7 +1154,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump } } if (mQsExpansion > 0) { - behindAlpha = MathUtils.lerp(behindAlpha, mDefaultScrimAlpha, mQsExpansion); + behindAlpha = MathUtils.lerp(behindAlpha, getDefaultScrimAlpha(), mQsExpansion); float tintProgress = mQsExpansion; if (mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()) { // this is case of - on lockscreen - going from expanded QS to bouncer. @@ -1653,7 +1671,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump pw.println(mTransitionToLockScreenFullShadeNotificationsProgress); pw.print(" mDefaultScrimAlpha="); - pw.println(mDefaultScrimAlpha); + pw.println(getDefaultScrimAlpha()); pw.print(" mPanelExpansionFraction="); pw.println(mPanelExpansionFraction); pw.print(" mExpansionAffectsAlpha="); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java index d47c0f8d59d8..2282b9702ad5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java @@ -151,7 +151,9 @@ public enum ScrimState { @Override public void prepare(ScrimState previousState) { if (Flags.bouncerUiRevamp()) { - if (previousState == SHADE_LOCKED) { + // Add unlocked here because scrim state is unlocked when there is an app on top of + // the lockscreen and shade is pulled over it. + if (previousState == SHADE_LOCKED || previousState == UNLOCKED) { mBehindAlpha = previousState.getBehindAlpha(); mNotifAlpha = previousState.getNotifAlpha(); } else { @@ -195,6 +197,15 @@ public enum ScrimState { mNotifAlpha = Color.alpha(mNotifTint) / 255.0f; mFrontAlpha = 0.0f; } else { + if (Flags.bouncerUiRevamp()) { + // This is only required until shade blur flag is fully enabled, shade is always + // opaque when shade blur is not enabled, and mClipQsScrim is always false. + mBehindAlpha = 1f; + mNotifAlpha = 1f; + mFrontAlpha = 0f; + mBehindTint = mBackgroundColor; + return; + } mBehindAlpha = mClipQsScrim ? 1 : mDefaultScrimAlpha; mNotifAlpha = 1f; mFrontAlpha = 0f; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java index 01de925f3d78..bc297699c41a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java @@ -46,7 +46,6 @@ import androidx.annotation.VisibleForTesting; import com.android.internal.util.LatencyTracker; import com.android.internal.widget.LockPatternUtils; -import com.android.keyguard.KeyguardMessageAreaController; import com.android.keyguard.KeyguardSecurityModel; import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.KeyguardUpdateMonitorCallback; @@ -56,7 +55,6 @@ import com.android.keyguard.ViewMediatorCallback; import com.android.systemui.DejankUtils; import com.android.systemui.Flags; import com.android.systemui.animation.back.FlingOnBackAnimationCallback; -import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor; import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.bouncer.domain.interactor.BouncerInteractor; import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerCallbackInteractor; @@ -158,7 +156,6 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb private final ConfigurationController mConfigurationController; private final NavigationModeController mNavigationModeController; private final NotificationShadeWindowController mNotificationShadeWindowController; - private final KeyguardMessageAreaController.Factory mKeyguardMessageAreaFactory; private final DreamOverlayStateController mDreamOverlayStateController; @Nullable private final FoldAodAnimationController mFoldAodAnimationController; @@ -328,7 +325,6 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb private float mQsExpansion; final Set<KeyguardViewManagerCallback> mCallbacks = new HashSet<>(); - private final UdfpsOverlayInteractor mUdfpsOverlayInteractor; private final ActivityStarter mActivityStarter; private OnDismissAction mAfterKeyguardGoneAction; @@ -386,7 +382,6 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb DockManager dockManager, NotificationShadeWindowController notificationShadeWindowController, KeyguardStateController keyguardStateController, - KeyguardMessageAreaController.Factory keyguardMessageAreaFactory, Optional<SysUIUnfoldComponent> sysUIUnfoldComponent, Lazy<ShadeController> shadeController, LatencyTracker latencyTracker, @@ -395,7 +390,6 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb PrimaryBouncerInteractor primaryBouncerInteractor, BouncerView primaryBouncerView, AlternateBouncerInteractor alternateBouncerInteractor, - UdfpsOverlayInteractor udfpsOverlayInteractor, ActivityStarter activityStarter, KeyguardTransitionInteractor keyguardTransitionInteractor, KeyguardDismissTransitionInteractor keyguardDismissTransitionInteractor, @@ -423,7 +417,6 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb mKeyguardUpdateManager = keyguardUpdateMonitor; mStatusBarStateController = sysuiStatusBarStateController; mDockManager = dockManager; - mKeyguardMessageAreaFactory = keyguardMessageAreaFactory; mShadeController = shadeController; mLatencyTracker = latencyTracker; mKeyguardSecurityModel = keyguardSecurityModel; @@ -434,7 +427,6 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb .map(SysUIUnfoldComponent::getFoldAodAnimationController).orElse(null); mAlternateBouncerInteractor = alternateBouncerInteractor; mBouncerInteractor = bouncerInteractor; - mUdfpsOverlayInteractor = udfpsOverlayInteractor; mActivityStarter = activityStarter; mKeyguardTransitionInteractor = keyguardTransitionInteractor; mKeyguardDismissTransitionInteractor = keyguardDismissTransitionInteractor; @@ -1581,6 +1573,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb && mPrimaryBouncerView.getDelegate().dispatchBackKeyEventPreIme(); } + @Override public void readyForKeyguardDone() { mViewMediatorCallback.readyForKeyguardDone(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java index ded964d8a1cc..6c8e1825ea0a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarter.java @@ -57,6 +57,7 @@ import com.android.systemui.animation.ActivityTransitionAnimator; import com.android.systemui.assist.AssistManager; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.power.domain.interactor.PowerInteractor; import com.android.systemui.settings.UserTracker; @@ -155,7 +156,7 @@ public class StatusBarNotificationActivityStarter implements NotificationActivit @Inject StatusBarNotificationActivityStarter( @ShadeDisplayAware Context context, - Handler mainThreadHandler, + @Main Handler mainThreadHandler, @Background Executor uiBgExecutor, NotificationVisibilityProvider visibilityProvider, HeadsUpManager headsUpManager, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt index 76f10b1c2a8b..0d43789e95a8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationController.kt @@ -20,6 +20,7 @@ import com.android.internal.jank.InteractionJankMonitor.CUJ_SCREEN_OFF_SHOW_AOD import com.android.systemui.DejankUtils import com.android.systemui.Flags.lightRevealMigration import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyguard.KeyguardViewMediator import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.shade.ShadeViewController @@ -71,7 +72,7 @@ constructor( private val powerManager: PowerManager, private val shadeLockscreenInteractorLazy: Lazy<ShadeLockscreenInteractor>, private val panelExpansionInteractorLazy: Lazy<PanelExpansionInteractor>, - private val handler: Handler = Handler(), + @Main private val handler: Handler, ) : WakefulnessLifecycle.Observer, ScreenOffAnimation { private lateinit var centralSurfaces: CentralSurfaces /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/domain/interactor/BatteryInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/domain/interactor/BatteryInteractor.kt index 8fdb6ee57587..d53cbabb1d19 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/domain/interactor/BatteryInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/battery/domain/interactor/BatteryInteractor.kt @@ -29,7 +29,7 @@ class BatteryInteractor @Inject constructor(repo: BatteryRepository) { val level = repo.level.filterNotNull() /** Whether the battery has been fully charged */ - val isFull = level.map { it >= 100 } + val isFull = level.map { isBatteryFull(it) } /** * For the sake of battery views, consider it to be "charging" if plugged in. This allows users @@ -82,6 +82,8 @@ class BatteryInteractor @Inject constructor(repo: BatteryRepository) { companion object { /** Level below which we consider to be critically low */ private const val CRITICAL_LEVEL = 20 + + fun isBatteryFull(level: Int) = level >= 100 } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/BindableIconsRegistry.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/BindableIconsRegistry.kt index 8400fb08e147..701dae1594f5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/BindableIconsRegistry.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/icons/shared/BindableIconsRegistry.kt @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.pipeline.icons.shared import com.android.systemui.dagger.SysUISingleton import com.android.systemui.statusbar.pipeline.icons.shared.model.BindableIcon +import com.android.systemui.statusbar.pipeline.mobile.ui.StackedMobileBindableIcon import com.android.systemui.statusbar.pipeline.satellite.ui.DeviceBasedSatelliteBindableIcon import javax.inject.Inject @@ -40,11 +41,12 @@ class BindableIconsRegistryImpl @Inject constructor( /** Bindables go here */ - oemSatellite: DeviceBasedSatelliteBindableIcon + oemSatellite: DeviceBasedSatelliteBindableIcon, + stackedMobile: StackedMobileBindableIcon, ) : BindableIconsRegistry { /** * Adding the injected bindables to this list will get them registered with * StatusBarIconController */ - override val bindableIcons: List<BindableIcon> = listOf(oemSatellite) + override val bindableIcons: List<BindableIcon> = listOf(oemSatellite, stackedMobile) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt index ac3728d9dcaf..c52536d2b312 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconsInteractor.kt @@ -21,6 +21,7 @@ import android.telephony.CarrierConfigManager import android.telephony.SubscriptionManager import android.telephony.SubscriptionManager.PROFILE_CLASS_PROVISIONING import com.android.settingslib.SignalIcon.MobileIconGroup +import com.android.settingslib.flags.Flags import com.android.settingslib.mobile.TelephonyIcons import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background @@ -28,6 +29,7 @@ import com.android.systemui.flags.FeatureFlagsClassic import com.android.systemui.flags.Flags.FILTER_PROVISIONING_NETWORK_SUBSCRIPTIONS import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.log.table.logDiffsForTable +import com.android.systemui.statusbar.core.StatusBarRootModernization import com.android.systemui.statusbar.pipeline.dagger.MobileSummaryLog import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionRepository @@ -39,6 +41,7 @@ import com.android.systemui.util.CarrierConfigTracker import java.lang.ref.WeakReference import javax.inject.Inject import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted @@ -79,6 +82,9 @@ interface MobileIconsInteractor { */ val icons: StateFlow<List<MobileIconInteractor>> + /** Whether the mobile icons can be stacked vertically. */ + val isStackable: StateFlow<Boolean> + /** True if the active mobile data subscription has data enabled */ val activeDataConnectionHasDataEnabled: StateFlow<Boolean> @@ -126,6 +132,7 @@ interface MobileIconsInteractor { fun getMobileConnectionInteractorForSubId(subId: Int): MobileIconInteractor } +@OptIn(ExperimentalCoroutinesApi::class) @Suppress("EXPERIMENTAL_IS_NOT_ENABLED") @SysUISingleton class MobileIconsInteractorImpl @@ -290,6 +297,18 @@ constructor( } .stateIn(scope, SharingStarted.WhileSubscribed(), emptyList()) + override val isStackable = + if (Flags.newStatusBarIcons() && StatusBarRootModernization.isEnabled) { + icons.flatMapLatest { icons -> + combine(icons.map { it.isNonTerrestrial }) { + it.size == 2 && it.none { isNonTerrestrial -> isNonTerrestrial } + } + } + } else { + flowOf(false) + } + .stateIn(scope, SharingStarted.WhileSubscribed(), false) + /** * Copied from the old pipeline. We maintain a 2s period of time where we will keep the * validated bit from the old active network (A) while data is changing to the new one (B). diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt index 30cc2c5da994..abd543d78687 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileUiAdapter.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.pipeline.mobile.ui +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application @@ -27,7 +28,7 @@ import java.io.PrintWriter import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.collectLatest -import com.android.app.tracing.coroutines.launchTraced as launch +import kotlinx.coroutines.flow.combine /** * This class is intended to provide a context to collect on the @@ -56,12 +57,23 @@ constructor( // Start notifying the icon controller of subscriptions scope.launch { isCollecting = true - mobileIconsViewModel.subscriptionIdsFlow.collectLatest { - logger.logUiAdapterSubIdsSentToIconController(it) - lastValue = it - iconController.setNewMobileIconSubIds(it) - shadeCarrierGroupController?.updateModernMobileIcons(it) - } + combine( + mobileIconsViewModel.subscriptionIdsFlow, + mobileIconsViewModel.isStackable, + ::Pair, + ) + .collectLatest { (subIds, isStackable) -> + logger.logUiAdapterSubIdsSentToIconController(subIds, isStackable) + lastValue = subIds + if (isStackable) { + // Passing an empty list to remove pre-existing mobile icons. + // StackedMobileBindableIcon will show the stacked icon instead. + iconController.setNewMobileIconSubIds(emptyList()) + } else { + iconController.setNewMobileIconSubIds(subIds) + } + shadeCarrierGroupController?.updateModernMobileIcons(subIds) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileViewLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileViewLogger.kt index 2af6795b39c4..4c2849de34ee 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileViewLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/MobileViewLogger.kt @@ -31,22 +31,24 @@ import javax.inject.Inject @SysUISingleton class MobileViewLogger @Inject -constructor( - @MobileViewLog private val buffer: LogBuffer, - dumpManager: DumpManager, -) : Dumpable { +constructor(@MobileViewLog private val buffer: LogBuffer, dumpManager: DumpManager) : Dumpable { init { dumpManager.registerNormalDumpable(this) } private val collectionStatuses = mutableMapOf<String, Boolean>() - fun logUiAdapterSubIdsSentToIconController(subs: List<Int>) { + fun logUiAdapterSubIdsSentToIconController(subs: List<Int>, isStackable: Boolean) { buffer.log( TAG, LogLevel.INFO, - { str1 = subs.toString() }, - { "Sub IDs in MobileUiAdapter being sent to icon controller: $str1" }, + { + str1 = subs.toString() + bool1 = isStackable + }, + { + "Sub IDs in MobileUiAdapter being sent to icon controller: $str1, isStackable=$bool1" + }, ) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/StackedMobileBindableIcon.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/StackedMobileBindableIcon.kt new file mode 100644 index 000000000000..fa9fa4c1366f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/StackedMobileBindableIcon.kt @@ -0,0 +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. + */ + +package com.android.systemui.statusbar.pipeline.mobile.ui + +import android.content.Context +import com.android.settingslib.flags.Flags +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.statusbar.core.StatusBarRootModernization +import com.android.systemui.statusbar.pipeline.icons.shared.model.BindableIcon +import com.android.systemui.statusbar.pipeline.icons.shared.model.ModernStatusBarViewCreator +import com.android.systemui.statusbar.pipeline.mobile.ui.binder.StackedMobileIconBinder +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.StackedMobileIconViewModel +import com.android.systemui.statusbar.pipeline.shared.ui.view.SingleBindableStatusBarComposeIconView +import javax.inject.Inject + +@SysUISingleton +class StackedMobileBindableIcon +@Inject +constructor( + context: Context, + mobileIconsViewModel: MobileIconsViewModel, + viewModelFactory: StackedMobileIconViewModel.Factory, +) : BindableIcon { + override val slot: String = + context.getString(com.android.internal.R.string.status_bar_stacked_mobile) + + override val initializer = ModernStatusBarViewCreator { context -> + SingleBindableStatusBarComposeIconView.createView(context).also { view -> + view.initView(slot) { + StackedMobileIconBinder.bind(view, mobileIconsViewModel, viewModelFactory) + } + } + } + + override val shouldBindIcon: Boolean = + Flags.newStatusBarIcons() && StatusBarRootModernization.isEnabled +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/StackedMobileIconBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/StackedMobileIconBinder.kt new file mode 100644 index 000000000000..c9fc53ecadc0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/StackedMobileIconBinder.kt @@ -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.systemui.statusbar.pipeline.mobile.ui.binder + +import androidx.compose.material3.LocalContentColor +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.lifecycle.rememberViewModel +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.StackedMobileIconViewModel +import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewBinding +import com.android.systemui.statusbar.pipeline.shared.ui.composable.StackedMobileIcon +import com.android.systemui.statusbar.pipeline.shared.ui.view.SingleBindableStatusBarComposeIconView + +object StackedMobileIconBinder { + fun bind( + view: SingleBindableStatusBarComposeIconView, + mobileIconsViewModel: MobileIconsViewModel, + viewModelFactory: StackedMobileIconViewModel.Factory, + ): ModernStatusBarViewBinding { + return SingleBindableStatusBarComposeIconView.withDefaultBinding( + view = view, + shouldBeVisible = { mobileIconsViewModel.isStackable.value }, + ) { _, tint -> + view.repeatWhenAttached { + repeatOnLifecycle(Lifecycle.State.STARTED) { + view.composeView.apply { + setViewCompositionStrategy( + ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed + ) + setContent { + val viewModel = + rememberViewModel("StackedMobileIconBinder") { + viewModelFactory.create() + } + if (viewModel.isIconVisible) { + CompositionLocalProvider(LocalContentColor provides Color(tint())) { + StackedMobileIcon(viewModel) + } + } + } + } + } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt index 22feb7ce77c8..6176a3e9e281 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconsViewModel.kt @@ -70,15 +70,20 @@ constructor( } .stateIn(scope, SharingStarted.WhileSubscribed(), listOf()) - private val firstMobileSubViewModel: StateFlow<MobileIconViewModelCommon?> = + val mobileSubViewModels: StateFlow<List<MobileIconViewModelCommon>> = subscriptionIdsFlow + .map { ids -> ids.map { commonViewModelForSub(it) } } + .stateIn(scope, SharingStarted.WhileSubscribed(), emptyList()) + + private val firstMobileSubViewModel: StateFlow<MobileIconViewModelCommon?> = + mobileSubViewModels .map { if (it.isEmpty()) { null } else { // Mobile icons get reversed by [StatusBarIconController], so the last element // in this list will show up visually first. - commonViewModelForSub(it.last()) + it.last() } } .stateIn(scope, SharingStarted.WhileSubscribed(), null) @@ -94,6 +99,8 @@ constructor( } .stateIn(scope, SharingStarted.WhileSubscribed(), false) + val isStackable: StateFlow<Boolean> = interactor.isStackable + init { scope.launch { subscriptionIdsFlow.collect { invalidateCaches(it) } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModel.kt new file mode 100644 index 000000000000..a2c2a3cd1507 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/StackedMobileIconViewModel.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.pipeline.mobile.ui.viewmodel + +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import com.android.systemui.common.shared.model.Icon +import com.android.systemui.lifecycle.ExclusiveActivatable +import com.android.systemui.lifecycle.Hydrator +import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf + +@OptIn(ExperimentalCoroutinesApi::class) +class StackedMobileIconViewModel +@AssistedInject +constructor(mobileIconsViewModel: MobileIconsViewModel) : ExclusiveActivatable() { + private val hydrator = Hydrator("StackedMobileIconViewModel") + + private val isStackable: Boolean by + hydrator.hydratedStateOf( + traceName = "isStackable", + source = mobileIconsViewModel.isStackable, + initialValue = false, + ) + + private val iconViewModelFlow: StateFlow<List<MobileIconViewModelCommon>> = + mobileIconsViewModel.mobileSubViewModels + + val dualSim: DualSim? by + hydrator.hydratedStateOf( + traceName = "dualSim", + source = + iconViewModelFlow.flatMapLatest { viewModels -> + combine(viewModels.map { it.icon }) { icons -> + icons + .toList() + .filterIsInstance<SignalIconModel.Cellular>() + .takeIf { it.size == 2 } + ?.let { DualSim(it[0], it[1]) } + } + }, + initialValue = null, + ) + + val networkTypeIcon: Icon.Resource? by + hydrator.hydratedStateOf( + traceName = "networkTypeIcon", + source = + iconViewModelFlow.flatMapLatest { viewModels -> + viewModels.firstOrNull()?.networkTypeIcon ?: flowOf(null) + }, + initialValue = null, + ) + + val isIconVisible: Boolean by derivedStateOf { isStackable && dualSim != null } + + override suspend fun onActivated(): Nothing { + hydrator.activate() + } + + @AssistedFactory + interface Factory { + fun create(): StackedMobileIconViewModel + } + + data class DualSim( + val primary: SignalIconModel.Cellular, + val secondary: SignalIconModel.Cellular, + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt index 9c9d41e975e3..cd320a12d577 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/binder/HomeStatusBarViewBinder.kt @@ -93,10 +93,11 @@ constructor( // CollapsedStatusBarFragment doesn't need this if (StatusBarRootModernization.isEnabled) { - primaryChipView.isVisible = false - systemInfoView.isVisible = false - clockView.isVisible = false - notificationIconsArea.isVisible = false + // GONE because this shouldn't take space in the layout + primaryChipView.hideInitially(state = View.GONE) + systemInfoView.hideInitially() + clockView.hideInitially() + notificationIconsArea.hideInitially() } view.repeatWhenAttached { @@ -365,6 +366,17 @@ constructor( } } + /** + * Hide the view for initialization, but skip if it's already hidden and does not cancel + * animations. + */ + private fun View.hideInitially(state: Int = View.INVISIBLE) { + if (visibility == View.INVISIBLE || visibility == View.GONE) { + return + } + visibility = state + } + // See CollapsedStatusBarFragment#hide. private fun View.hide(state: Int = View.INVISIBLE, shouldAnimateChange: Boolean) { if (visibility == View.INVISIBLE || visibility == View.GONE) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StackedMobileIcon.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StackedMobileIcon.kt new file mode 100644 index 000000000000..465a43fbfb9e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StackedMobileIcon.kt @@ -0,0 +1,180 @@ +/* + * 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.pipeline.shared.ui.composable + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.LocalContentColor +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.android.compose.modifiers.height +import com.android.compose.modifiers.width +import com.android.systemui.common.ui.compose.Icon +import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.StackedMobileIconViewModel +import com.android.systemui.statusbar.pipeline.shared.ui.composable.StackedMobileIconDimensions.BarBaseHeightFiveBarsSp +import com.android.systemui.statusbar.pipeline.shared.ui.composable.StackedMobileIconDimensions.BarBaseHeightFourBarsSp +import com.android.systemui.statusbar.pipeline.shared.ui.composable.StackedMobileIconDimensions.BarsLevelIncrementSp +import com.android.systemui.statusbar.pipeline.shared.ui.composable.StackedMobileIconDimensions.BarsVerticalPaddingSp +import com.android.systemui.statusbar.pipeline.shared.ui.composable.StackedMobileIconDimensions.HorizontalPaddingFiveBarsSp +import com.android.systemui.statusbar.pipeline.shared.ui.composable.StackedMobileIconDimensions.HorizontalPaddingFourBarsSp +import com.android.systemui.statusbar.pipeline.shared.ui.composable.StackedMobileIconDimensions.IconHeightSp +import com.android.systemui.statusbar.pipeline.shared.ui.composable.StackedMobileIconDimensions.IconWidthFiveBarsSp +import com.android.systemui.statusbar.pipeline.shared.ui.composable.StackedMobileIconDimensions.IconWidthFourBarsSp +import com.android.systemui.statusbar.pipeline.shared.ui.composable.StackedMobileIconDimensions.SecondaryBarHeightSp +import kotlin.math.max + +/** + * The dual sim icon that shows both connections stacked vertically with the active connection on + * top + */ +@Composable +fun StackedMobileIcon(viewModel: StackedMobileIconViewModel, modifier: Modifier = Modifier) { + val dualSim = viewModel.dualSim ?: return + + val contentColor = LocalContentColor.current + + Row(verticalAlignment = Alignment.CenterVertically, modifier = modifier) { + viewModel.networkTypeIcon?.let { + Icon( + it, + tint = contentColor, + modifier = + Modifier.height { IconHeightSp.roundToPx() }.padding(start = 1.dp, end = 2.dp), + ) + } + + StackedMobileIcon(dualSim, contentColor) + } +} + +@Composable +private fun StackedMobileIcon( + viewModel: StackedMobileIconViewModel.DualSim, + color: Color, + modifier: Modifier = Modifier, +) { + val maxNumberOfLevels = + max(viewModel.primary.numberOfLevels, viewModel.secondary.numberOfLevels) + val dimensions = if (maxNumberOfLevels == 6) FiveBarsDimensions else FourBarsDimensions + val iconSize = + with(LocalDensity.current) { dimensions.totalWidth.toDp() to IconHeightSp.toDp() } + + Canvas(modifier.size(width = iconSize.first, height = iconSize.second)) { + val verticalPaddingPx = BarsVerticalPaddingSp.roundToPx() + val horizontalPaddingPx = dimensions.barsHorizontalPadding.roundToPx() + val totalPaddingWidthPx = horizontalPaddingPx * (maxNumberOfLevels - 1) + + val barWidthPx = (size.width - totalPaddingWidthPx) / maxNumberOfLevels + val dotHeightPx = SecondaryBarHeightSp.toPx() + val baseBarHeightPx = dimensions.barBaseHeight.toPx() + + var xOffsetPx = 0f + for (bar in 1..maxNumberOfLevels) { + // Bottom dots representing secondary sim + val dotYOffsetPx = size.height - dotHeightPx + if (bar <= viewModel.secondary.numberOfLevels) { + drawMobileIconBar( + level = viewModel.secondary.level, + bar = bar, + topLeft = Offset(xOffsetPx, dotYOffsetPx), + size = Size(barWidthPx, dotHeightPx), + activeColor = color, + ) + } + + // Top bars representing primary sim + if (bar <= viewModel.primary.numberOfLevels) { + val barHeightPx = baseBarHeightPx + (BarsLevelIncrementSp.toPx() * (bar - 1)) + val barYOffsetPx = dotYOffsetPx - verticalPaddingPx - barHeightPx + drawMobileIconBar( + level = viewModel.primary.level, + bar = bar, + topLeft = Offset(xOffsetPx, barYOffsetPx), + size = Size(barWidthPx, barHeightPx), + activeColor = color, + ) + } + + xOffsetPx += barWidthPx + horizontalPaddingPx + } + } +} + +private fun DrawScope.drawMobileIconBar( + level: Int, + bar: Int, + topLeft: Offset, + size: Size, + activeColor: Color, + inactiveColor: Color = activeColor.copy(alpha = .3f), + cornerRadius: CornerRadius = CornerRadius(size.width / 2), +) { + drawRoundRect( + color = if (level >= bar) activeColor else inactiveColor, + topLeft = topLeft, + size = size, + cornerRadius = cornerRadius, + ) +} + +private abstract class BarsDependentDimensions( + val totalWidth: TextUnit, + val barsHorizontalPadding: TextUnit, + val barBaseHeight: TextUnit, +) + +private object FourBarsDimensions : + BarsDependentDimensions( + IconWidthFourBarsSp, + HorizontalPaddingFourBarsSp, + BarBaseHeightFourBarsSp, + ) + +private object FiveBarsDimensions : + BarsDependentDimensions( + IconWidthFiveBarsSp, + HorizontalPaddingFiveBarsSp, + BarBaseHeightFiveBarsSp, + ) + +private object StackedMobileIconDimensions { + // Common dimensions + val IconHeightSp = 12.sp + val BarsVerticalPaddingSp = 1.5.sp + val BarsLevelIncrementSp = 1.sp + val SecondaryBarHeightSp = 3.sp + + // Dimensions dependant on the number of total bars + val IconWidthFiveBarsSp = 18.5.sp + val IconWidthFourBarsSp = 16.sp + val HorizontalPaddingFiveBarsSp = 1.5.sp + val HorizontalPaddingFourBarsSp = 2.sp + val BarBaseHeightFiveBarsSp = 3.5.sp + val BarBaseHeightFourBarsSp = 4.5.sp +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt index c34fa464cc3a..a961713230c8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/composable/StatusBarRoot.kt @@ -41,12 +41,15 @@ import com.android.systemui.plugins.DarkIconDispatcher import com.android.systemui.res.R import com.android.systemui.statusbar.chips.ui.compose.OngoingActivityChips import com.android.systemui.statusbar.core.NewStatusBarIcons +import com.android.systemui.statusbar.core.StatusBarConnectedDisplays import com.android.systemui.statusbar.core.StatusBarRootModernization import com.android.systemui.statusbar.data.repository.DarkIconDispatcherStore import com.android.systemui.statusbar.events.domain.interactor.SystemStatusEventAnimationInteractor import com.android.systemui.statusbar.featurepods.popups.StatusBarPopupChips import com.android.systemui.statusbar.featurepods.popups.ui.compose.StatusBarPopupChipsContainer +import com.android.systemui.statusbar.notification.icon.ui.viewbinder.ConnectedDisplaysStatusBarNotificationIconViewStore import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerStatusBarViewBinder +import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder import com.android.systemui.statusbar.phone.NotificationIconContainer import com.android.systemui.statusbar.phone.PhoneStatusBarView import com.android.systemui.statusbar.phone.StatusBarLocation @@ -72,6 +75,7 @@ constructor( private val homeStatusBarViewModelFactory: HomeStatusBarViewModelFactory, private val homeStatusBarViewBinder: HomeStatusBarViewBinder, private val notificationIconsBinder: NotificationIconContainerStatusBarViewBinder, + private val iconViewStoreFactory: ConnectedDisplaysStatusBarNotificationIconViewStore.Factory, private val darkIconManagerFactory: DarkIconManager.Factory, private val iconController: StatusBarIconController, private val ongoingCallController: OngoingCallController, @@ -89,6 +93,7 @@ constructor( statusBarViewModelFactory = homeStatusBarViewModelFactory, statusBarViewBinder = homeStatusBarViewBinder, notificationIconsBinder = notificationIconsBinder, + iconViewStoreFactory = iconViewStoreFactory, darkIconManagerFactory = darkIconManagerFactory, iconController = iconController, ongoingCallController = ongoingCallController, @@ -119,6 +124,7 @@ fun StatusBarRoot( statusBarViewModelFactory: HomeStatusBarViewModelFactory, statusBarViewBinder: HomeStatusBarViewBinder, notificationIconsBinder: NotificationIconContainerStatusBarViewBinder, + iconViewStoreFactory: ConnectedDisplaysStatusBarNotificationIconViewStore.Factory, darkIconManagerFactory: DarkIconManager.Factory, iconController: StatusBarIconController, ongoingCallController: OngoingCallController, @@ -129,6 +135,14 @@ fun StatusBarRoot( val displayId = parent.context.displayId val statusBarViewModel = rememberViewModel("HomeStatusBar") { statusBarViewModelFactory.create(displayId) } + val iconViewStore: NotificationIconContainerViewBinder.IconViewStore? = + if (StatusBarConnectedDisplays.isEnabled) { + rememberViewModel("HomeStatusBar.IconViewStore[$displayId]") { + iconViewStoreFactory.create(displayId) + } + } else { + null + } Box(Modifier.fillMaxSize()) { // TODO(b/364360986): remove this before rolling the flag forward @@ -174,7 +188,10 @@ fun StatusBarRoot( val chips by statusBarViewModel.ongoingActivityChips .collectAsStateWithLifecycle() - OngoingActivityChips(chips = chips) + OngoingActivityChips( + chips = chips, + iconViewStore = iconViewStore, + ) } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarComposeIconView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarComposeIconView.kt new file mode 100644 index 000000000000..8076040564fb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/view/SingleBindableStatusBarComposeIconView.kt @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.pipeline.shared.ui.view + +import android.content.Context +import android.graphics.Color +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import androidx.compose.ui.platform.ComposeView +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import com.android.app.tracing.coroutines.launchTraced as launch +import com.android.systemui.lifecycle.repeatWhenAttached +import com.android.systemui.res.R +import com.android.systemui.statusbar.StatusBarIconView +import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN +import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewBinding +import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewVisibilityHelper +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.flow.MutableStateFlow + +/** Compose view that is bound to bindable_status_bar_compose_icon.xml */ +class SingleBindableStatusBarComposeIconView(context: Context, attrs: AttributeSet?) : + ModernStatusBarView(context, attrs) { + + internal lateinit var composeView: ComposeView + internal lateinit var dotView: StatusBarIconView + + override fun toString(): String { + return "SingleBindableStatusBarComposeIcon(" + + "slot='$slot', " + + "isCollecting=${binding.isCollecting()}, " + + "visibleState=${StatusBarIconView.getVisibleStateString(visibleState)}); " + + "viewString=${super.toString()}" + } + + override fun initView(slot: String, bindingCreator: () -> ModernStatusBarViewBinding) { + super.initView(slot, bindingCreator) + + composeView = requireViewById(R.id.compose_view) + dotView = requireViewById(R.id.status_bar_dot) + } + + companion object { + fun createView(context: Context): SingleBindableStatusBarComposeIconView { + return LayoutInflater.from(context) + .inflate(R.layout.bindable_status_bar_compose_icon, null) + as SingleBindableStatusBarComposeIconView + } + + /** + * Using a given binding [block], create the necessary scaffolding to handle the general + * case of a single status bar icon. This includes eliding into a dot view when there is not + * enough space, and handling tint. + * + * [block] should be a simple [launch] call that handles updating the single icon view with + * its new view. Currently there is no simple way to e.g., extend to handle multiple tints + * for dual-layered icons, and any more complex logic should probably find a way to return + * its own version of [ModernStatusBarViewBinding]. + */ + fun withDefaultBinding( + view: SingleBindableStatusBarComposeIconView, + shouldBeVisible: () -> Boolean, + block: suspend LifecycleOwner.(View, () -> Int) -> Unit, + ): ModernStatusBarViewBinding { + @StatusBarIconView.VisibleState + val visibilityState: MutableStateFlow<Int> = MutableStateFlow(STATE_HIDDEN) + + val iconTint: MutableStateFlow<Int> = MutableStateFlow(Color.WHITE) + val decorTint: MutableStateFlow<Int> = MutableStateFlow(Color.WHITE) + + var isCollecting: Boolean = false + + view.repeatWhenAttached { + // Child binding + block(view) { iconTint.value } + + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + // isVisible controls the visibility state of the outer group, and thus it + // needs to run in the CREATED lifecycle so it can continue to watch while + // invisible. See (b/291031862) for details + launch { + visibilityState.collect { visibilityState -> + // for b/296864006, we can not hide all the child views if + // visibilityState is STATE_HIDDEN. Because hiding all child views + // would cause the getWidth() of this view return 0, and that would + // cause the translation calculation fails in StatusIconContainer. + // Therefore, like class MobileIconBinder, instead of set the child + // views visibility to View.GONE, we set their visibility to + // View.INVISIBLE to make them invisible but keep the width. + ModernStatusBarViewVisibilityHelper.setVisibilityState( + visibilityState, + view.composeView, + view.dotView, + ) + } + } + + launch { iconTint.collect { tint -> view.dotView.setDecorColor(tint) } } + + launch { + decorTint.collect { decorTint -> view.dotView.setDecorColor(decorTint) } + } + + try { + awaitCancellation() + } finally { + isCollecting = false + } + } + } + } + + return object : ModernStatusBarViewBinding { + override fun getShouldIconBeVisible(): Boolean { + return shouldBeVisible() + } + + override fun onVisibilityStateChanged(state: Int) { + visibilityState.value = state + } + + override fun onIconTintChanged(newTint: Int, contrastTint: Int) { + iconTint.value = newTint + } + + override fun onDecorTintChanged(newTint: Int) { + decorTint.value = newTint + } + + override fun isCollecting(): Boolean { + return isCollecting + } + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java index ccd75602aa13..dd742ff8f1cc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java @@ -55,6 +55,7 @@ import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver; import com.android.systemui.res.R; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.CommandQueue; +import com.android.systemui.statusbar.core.StatusBarRootModernization; import com.android.systemui.statusbar.phone.ui.StatusBarIconController; import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener; import com.android.systemui.tuner.TunerService; @@ -216,7 +217,9 @@ public class Clock extends TextView implements // Make sure we update to the current time updateClock(); - updateClockVisibility(); + if (!StatusBarRootModernization.isEnabled()) { + updateClockVisibility(); + } updateShowSeconds(); } @@ -275,19 +278,25 @@ public class Clock extends TextView implements @Override public void setVisibility(int visibility) { - if (visibility == View.VISIBLE && !shouldBeVisible()) { - return; + if (!StatusBarRootModernization.isEnabled()) { + if (visibility == View.VISIBLE && !shouldBeVisible()) { + return; + } } super.setVisibility(visibility); } - public void setClockVisibleByUser(boolean visible) { + private void setClockVisibleByUser(boolean visible) { + StatusBarRootModernization.assertInLegacyMode(); + mClockVisibleByUser = visible; updateClockVisibility(); } - public void setClockVisibilityByPolicy(boolean visible) { + private void setClockVisibilityByPolicy(boolean visible) { + StatusBarRootModernization.assertInLegacyMode(); + mClockVisibleByPolicy = visible; updateClockVisibility(); } @@ -297,6 +306,8 @@ public class Clock extends TextView implements } private void updateClockVisibility() { + StatusBarRootModernization.assertInLegacyMode(); + boolean visible = shouldBeVisible(); int visibility = visible ? View.VISIBLE : View.GONE; super.setVisibility(visibility); @@ -346,15 +357,23 @@ public class Clock extends TextView implements if (CLOCK_SECONDS.equals(key)) { mShowSeconds = TunerService.parseIntegerSwitch(newValue, false); updateShowSeconds(); - } else if (StatusBarIconController.ICON_HIDE_LIST.equals(key)) { - setClockVisibleByUser(!StatusBarIconController.getIconHideList(getContext(), newValue) - .contains("clock")); - updateClockVisibility(); + } else if (!StatusBarRootModernization.isEnabled()) { + if (StatusBarIconController.ICON_HIDE_LIST.equals(key)) { + setClockVisibleByUser( + !StatusBarIconController + .getIconHideList(getContext(), newValue) + .contains("clock")); + updateClockVisibility(); + } } } @Override public void disable(int displayId, int state1, int state2, boolean animate) { + if (StatusBarRootModernization.isEnabled()) { + return; + } + if (displayId != getDisplay().getDisplayId()) { return; } diff --git a/packages/SystemUI/src/com/android/systemui/util/concurrency/GlobalConcurrencyModule.java b/packages/SystemUI/src/com/android/systemui/util/concurrency/GlobalConcurrencyModule.java index 70774f13fe6b..c43e7637998c 100644 --- a/packages/SystemUI/src/com/android/systemui/util/concurrency/GlobalConcurrencyModule.java +++ b/packages/SystemUI/src/com/android/systemui/util/concurrency/GlobalConcurrencyModule.java @@ -65,15 +65,6 @@ public abstract class GlobalConcurrencyModule { } /** - * @deprecated Use @Main Handler. - */ - @Deprecated - @Provides - public static Handler provideHandler() { - return new Handler(); - } - - /** * Provide an Executor specifically for running UI operations on a separate thread. * * Keep submitted runnables short and to the point, just as with any other UI code. diff --git a/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.kt b/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.kt index a7abb6b5f1d3..5b5f85c12efc 100644 --- a/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.kt +++ b/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.kt @@ -53,6 +53,14 @@ object SysUIConcurrencyModule { private const val NOTIFICATION_INFLATION_SLOW_DISPATCH_THRESHOLD = 1000L private const val NOTIFICATION_INFLATION_SLOW_DELIVERY_THRESHOLD = 1000L + /** + * Choreographer instance for the main thread. + * TODO(b/395887935): Lets move this to @Main and provide thread-local references + */ + @Provides + @SysUISingleton + fun providesChoreographer(): Choreographer = Choreographer.getInstance() + /** Background Looper */ @Provides @SysUISingleton @@ -99,10 +107,7 @@ object SysUIConcurrencyModule { @Provides @SysUISingleton @NotifInflation - fun provideNotifInflationLooper(@Background bgLooper: Looper): Looper { - if (!Flags.dedicatedNotifInflationThread()) { - return bgLooper - } + fun provideNotifInflationLooper(): Looper { val thread = HandlerThread("NotifInflation", Process.THREAD_PRIORITY_BACKGROUND) thread.start() val looper = thread.getLooper() diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/module/VolumeDialogModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/module/VolumeDialogModule.kt index 025e269b70b6..7b08317d5265 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/module/VolumeDialogModule.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/dagger/module/VolumeDialogModule.kt @@ -18,8 +18,13 @@ package com.android.systemui.volume.dialog.dagger.module import com.android.systemui.volume.dialog.ringer.data.repository.VolumeDialogRingerFeedbackRepository import com.android.systemui.volume.dialog.ringer.data.repository.VolumeDialogRingerFeedbackRepositoryImpl +import com.android.systemui.volume.dialog.ringer.ui.binder.VolumeDialogRingerViewBinder +import com.android.systemui.volume.dialog.settings.ui.binder.VolumeDialogSettingsButtonViewBinder +import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogSlidersViewBinder +import com.android.systemui.volume.dialog.ui.binder.ViewBinder import dagger.Binds import dagger.Module +import dagger.Provides /** Dagger module for volume dialog code in the volume package */ @Module @@ -29,4 +34,14 @@ interface VolumeDialogModule { fun bindVolumeDialogRingerFeedbackRepository( ringerFeedbackRepository: VolumeDialogRingerFeedbackRepositoryImpl ): VolumeDialogRingerFeedbackRepository + + companion object { + + @Provides + fun provideViewBinders( + slidersViewBinder: VolumeDialogSlidersViewBinder, + ringerViewBinder: VolumeDialogRingerViewBinder, + settingsButtonViewBinder: VolumeDialogSettingsButtonViewBinder, + ): List<ViewBinder> = listOf(slidersViewBinder, ringerViewBinder, settingsButtonViewBinder) + } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt index eb2b2f68a6e2..ef750574830a 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt @@ -39,6 +39,7 @@ import com.android.systemui.volume.dialog.ringer.ui.viewmodel.RingerDrawerState import com.android.systemui.volume.dialog.ringer.ui.viewmodel.RingerViewModel import com.android.systemui.volume.dialog.ringer.ui.viewmodel.RingerViewModelState import com.android.systemui.volume.dialog.ringer.ui.viewmodel.VolumeDialogRingerDrawerViewModel +import com.android.systemui.volume.dialog.ui.binder.ViewBinder import com.android.systemui.volume.dialog.ui.utils.suspendAnimate import com.android.systemui.volume.dialog.ui.viewmodel.VolumeDialogViewModel import javax.inject.Inject @@ -60,7 +61,7 @@ class VolumeDialogRingerViewBinder constructor( private val viewModel: VolumeDialogRingerDrawerViewModel, private val dialogViewModel: VolumeDialogViewModel, -) { +) : ViewBinder { private val roundnessSpringForce = SpringForce(1F).apply { stiffness = 800F @@ -73,10 +74,11 @@ constructor( } private val rgbEvaluator = ArgbEvaluator() - fun CoroutineScope.bind(view: View) { + override fun CoroutineScope.bind(view: View) { val volumeDialogBackgroundView = view.requireViewById<View>(R.id.volume_dialog_background) val ringerBackgroundView = view.requireViewById<View>(R.id.ringer_buttons_background) val drawerContainer = view.requireViewById<MotionLayout>(R.id.volume_ringer_drawer) + val unselectedButtonUiModel = RingerButtonUiModel.getUnselectedButton(view.context) val selectedButtonUiModel = RingerButtonUiModel.getSelectedButton(view.context) val volumeDialogBgSmallRadius = diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/settings/ui/binder/VolumeDialogSettingsButtonViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/settings/ui/binder/VolumeDialogSettingsButtonViewBinder.kt index a330685fc6bb..54f04e274c03 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/settings/ui/binder/VolumeDialogSettingsButtonViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/settings/ui/binder/VolumeDialogSettingsButtonViewBinder.kt @@ -21,6 +21,7 @@ import android.widget.ImageButton import com.android.systemui.res.R import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope import com.android.systemui.volume.dialog.settings.ui.viewmodel.VolumeDialogSettingsButtonViewModel +import com.android.systemui.volume.dialog.ui.binder.ViewBinder import com.android.systemui.volume.dialog.ui.viewmodel.VolumeDialogViewModel import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -34,9 +35,9 @@ class VolumeDialogSettingsButtonViewBinder constructor( private val viewModel: VolumeDialogSettingsButtonViewModel, private val dialogViewModel: VolumeDialogViewModel, -) { +) : ViewBinder { - fun CoroutineScope.bind(view: View) { + override fun CoroutineScope.bind(view: View) { val button = view.requireViewById<ImageButton>(R.id.volume_dialog_settings) launch { dialogViewModel.addTouchableBounds(button) } viewModel.isVisible diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinder.kt index 7f58c835094f..0d970e5117c2 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinder.kt @@ -25,6 +25,7 @@ import com.android.systemui.res.R import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope import com.android.systemui.volume.dialog.sliders.dagger.VolumeDialogSliderComponent import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSlidersViewModel +import com.android.systemui.volume.dialog.ui.binder.ViewBinder import com.android.systemui.volume.dialog.ui.viewmodel.VolumeDialogViewModel import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -38,16 +39,16 @@ class VolumeDialogSlidersViewBinder constructor( private val viewModel: VolumeDialogSlidersViewModel, private val dialogViewModel: VolumeDialogViewModel, -) { +) : ViewBinder { - fun CoroutineScope.bind(view: View) { + override fun CoroutineScope.bind(view: View) { val floatingSlidersContainer: ViewGroup = view.requireViewById(R.id.volume_dialog_floating_sliders_container) val mainSliderContainer: View = view.requireViewById(R.id.volume_dialog_main_slider_container) val background: View = view.requireViewById(R.id.volume_dialog_background) - val settingsButton: View = view.requireViewById(R.id.volume_dialog_settings) - val ringerDrawer: View = view.requireViewById(R.id.volume_ringer_drawer) + val bottomSection: View = view.requireViewById(R.id.volume_dialog_bottom_section_container) + val topSection: View = view.requireViewById(R.id.volume_dialog_top_section_container) launch { dialogViewModel.addTouchableBounds(mainSliderContainer, floatingSlidersContainer) } viewModel.sliders @@ -55,7 +56,7 @@ constructor( bindSlider( uiModel.sliderComponent, mainSliderContainer, - arrayOf(mainSliderContainer, background, settingsButton, ringerDrawer), + arrayOf(mainSliderContainer, background, bottomSection, topSection), ) val floatingSliderViewBinders = uiModel.floatingSliderComponent diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/ViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/ViewBinder.kt new file mode 100644 index 000000000000..4d1af0d71108 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/ViewBinder.kt @@ -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. + */ + +package com.android.systemui.volume.dialog.ui.binder + +import android.view.View +import kotlinx.coroutines.CoroutineScope + +interface ViewBinder { + + fun CoroutineScope.bind(view: View) +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt index 7cc4bcc4e11c..98042d5022f9 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinder.kt @@ -17,28 +17,28 @@ package com.android.systemui.volume.dialog.ui.binder import android.app.Dialog -import android.content.res.Resources import android.view.View import android.view.ViewTreeObserver import android.view.WindowInsets +import androidx.compose.ui.util.lerp import androidx.constraintlayout.motion.widget.MotionLayout import androidx.core.view.updatePadding +import androidx.dynamicanimation.animation.DynamicAnimation +import androidx.dynamicanimation.animation.FloatValueHolder +import androidx.dynamicanimation.animation.SpringAnimation +import androidx.dynamicanimation.animation.SpringForce import com.android.internal.view.RotationPolicy import com.android.systemui.common.ui.view.onApplyWindowInsets -import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.res.R import com.android.systemui.util.kotlin.awaitCancellationThenDispose -import com.android.systemui.volume.SystemUIInterpolators import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope -import com.android.systemui.volume.dialog.ringer.ui.binder.VolumeDialogRingerViewBinder -import com.android.systemui.volume.dialog.settings.ui.binder.VolumeDialogSettingsButtonViewBinder import com.android.systemui.volume.dialog.shared.model.VolumeDialogVisibilityModel -import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogSlidersViewBinder import com.android.systemui.volume.dialog.ui.utils.JankListenerFactory import com.android.systemui.volume.dialog.ui.utils.suspendAnimate import com.android.systemui.volume.dialog.ui.viewmodel.VolumeDialogViewModel import com.android.systemui.volume.dialog.utils.VolumeTracer import javax.inject.Inject +import kotlin.math.ceil import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow @@ -50,26 +50,25 @@ import kotlinx.coroutines.flow.scan import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine +private const val SPRING_STIFFNESS = 700f +private const val SPRING_DAMPING_RATIO = 0.9f + +private const val FRACTION_HIDE = 0f +private const val FRACTION_SHOW = 1f +private const val ANIMATION_MINIMUM_VISIBLE_CHANGE = 0.01f + /** Binds the root view of the Volume Dialog. */ @OptIn(ExperimentalCoroutinesApi::class) @VolumeDialogScope class VolumeDialogViewBinder @Inject constructor( - @Main resources: Resources, private val viewModel: VolumeDialogViewModel, private val jankListenerFactory: JankListenerFactory, private val tracer: VolumeTracer, - private val volumeDialogRingerViewBinder: VolumeDialogRingerViewBinder, - private val slidersViewBinder: VolumeDialogSlidersViewBinder, - private val settingsButtonViewBinder: VolumeDialogSettingsButtonViewBinder, + private val viewBinders: List<@JvmSuppressWildcards ViewBinder>, ) { - private val dialogShowAnimationDurationMs = - resources.getInteger(R.integer.config_dialogShowAnimationDurationMs).toLong() - private val dialogHideAnimationDurationMs = - resources.getInteger(R.integer.config_dialogHideAnimationDurationMs).toLong() - fun CoroutineScope.bind(dialog: Dialog) { val insets: MutableStateFlow<WindowInsets> = MutableStateFlow(WindowInsets.Builder().build()) @@ -105,9 +104,9 @@ constructor( .awaitCancellationThenDispose() } - with(volumeDialogRingerViewBinder) { bind(root) } - with(slidersViewBinder) { bind(root) } - with(settingsButtonViewBinder) { bind(root) } + for (viewBinder in viewBinders) { + with(viewBinder) { bind(root) } + } } private fun CoroutineScope.animateVisibility( @@ -115,22 +114,35 @@ constructor( dialog: Dialog, visibilityModel: Flow<VolumeDialogVisibilityModel>, ) { + view.applyAnimationProgress(FRACTION_HIDE) + val animationValueHolder = FloatValueHolder(FRACTION_HIDE) + val animation: SpringAnimation = + SpringAnimation(animationValueHolder) + .setSpring( + SpringForce() + .setStiffness(SPRING_STIFFNESS) + .setDampingRatio(SPRING_DAMPING_RATIO) + ) + .setMinimumVisibleChange(ANIMATION_MINIMUM_VISIBLE_CHANGE) + .addUpdateListener { _, value, _ -> view.applyAnimationProgress(value) } + var junkListener: DynamicAnimation.OnAnimationUpdateListener? = null + visibilityModel .mapLatest { when (it) { is VolumeDialogVisibilityModel.Visible -> { tracer.traceVisibilityEnd(it) - view.animateShow( - duration = dialogShowAnimationDurationMs, - translationX = calculateTranslationX(view), - ) + junkListener?.let(animation::removeUpdateListener) + junkListener = + jankListenerFactory.show(view).also(animation::addUpdateListener) + animation.suspendAnimate(FRACTION_SHOW) } is VolumeDialogVisibilityModel.Dismissed -> { tracer.traceVisibilityEnd(it) - view.animateHide( - duration = dialogHideAnimationDurationMs, - translationX = calculateTranslationX(view), - ) + junkListener?.let(animation::removeUpdateListener) + junkListener = + jankListenerFactory.dismiss(view).also(animation::addUpdateListener) + animation.suspendAnimate(FRACTION_HIDE) dialog.dismiss() } is VolumeDialogVisibilityModel.Invisible -> { @@ -141,37 +153,21 @@ constructor( .launchIn(this) } - private fun calculateTranslationX(view: View): Float? { - return if (view.display.rotation == RotationPolicy.NATURAL_ROTATION) { - if (view.isLayoutRtl) { - -1 + /** + * @param fraction in range [0, 1]. 0 corresponds to the dialog being hidden and 1 - visible. + */ + private fun View.applyAnimationProgress(fraction: Float) { + alpha = ceil(fraction) + if (display.rotation == RotationPolicy.NATURAL_ROTATION) { + if (isLayoutRtl) { + -1 + } else { + 1 + } * width / 2f } else { - 1 - } * view.width / 2f - } else { - null - } - } - - private suspend fun View.animateShow(duration: Long, translationX: Float?) { - translationX?.let { setTranslationX(translationX) } - alpha = 0f - animate() - .alpha(1f) - .translationX(0f) - .setDuration(duration) - .setInterpolator(SystemUIInterpolators.LogDecelerateInterpolator()) - .suspendAnimate(jankListenerFactory.show(this, duration)) - } - - private suspend fun View.animateHide(duration: Long, translationX: Float?) { - val animator = - animate() - .alpha(0f) - .setDuration(duration) - .setInterpolator(SystemUIInterpolators.LogAccelerateInterpolator()) - translationX?.let { animator.translationX(it) } - animator.suspendAnimate(jankListenerFactory.dismiss(this, duration)) + null + } + ?.let { maxTranslationX -> translationX = lerp(maxTranslationX, 0f, fraction) } } private suspend fun ViewTreeObserver.listenToComputeInternalInsets() = diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/JankListenerFactory.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/JankListenerFactory.kt index 9fcd77716fb6..803911a9cf77 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/JankListenerFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/JankListenerFactory.kt @@ -17,8 +17,8 @@ package com.android.systemui.volume.dialog.ui.utils import android.animation.Animator -import android.animation.AnimatorListenerAdapter import android.view.View +import androidx.dynamicanimation.animation.DynamicAnimation import com.android.internal.jank.Cuj import com.android.internal.jank.InteractionJankMonitor import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPluginScope @@ -30,35 +30,36 @@ class JankListenerFactory @Inject constructor(private val interactionJankMonitor: InteractionJankMonitor) { - fun show(view: View, timeout: Long) = getJunkListener(view, "show", timeout) - - fun update(view: View, timeout: Long) = getJunkListener(view, "update", timeout) + fun show(view: View): DynamicAnimation.OnAnimationUpdateListener { + return createJunkListener(view, "show") + } - fun dismiss(view: View, timeout: Long) = getJunkListener(view, "dismiss", timeout) + fun dismiss(view: View): DynamicAnimation.OnAnimationUpdateListener { + return createJunkListener(view, "dismiss") + } - private fun getJunkListener( + private fun createJunkListener( view: View, type: String, - timeout: Long, - ): Animator.AnimatorListener { - return object : AnimatorListenerAdapter() { - override fun onAnimationStart(animation: Animator) { + ): DynamicAnimation.OnAnimationUpdateListener { + var trackedStart = false + return DynamicAnimation.OnAnimationUpdateListener { animation, _, _ -> + if (!trackedStart) { + trackedStart = true interactionJankMonitor.begin( InteractionJankMonitor.Configuration.Builder.withView( Cuj.CUJ_VOLUME_CONTROL, view, ) .setTag(type) - .setTimeout(timeout) ) - } - - override fun onAnimationEnd(animation: Animator) { - interactionJankMonitor.end(Cuj.CUJ_VOLUME_CONTROL) - } - - override fun onAnimationCancel(animation: Animator) { - interactionJankMonitor.cancel(Cuj.CUJ_VOLUME_CONTROL) + animation.addEndListener { _, canceled, _, _ -> + if (canceled) { + interactionJankMonitor.cancel(Cuj.CUJ_VOLUME_CONTROL) + } else { + interactionJankMonitor.end(Cuj.CUJ_VOLUME_CONTROL) + } + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/SuspendAnimators.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/SuspendAnimators.kt index 52a19e0903e2..31e596f0278d 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/SuspendAnimators.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/SuspendAnimators.kt @@ -96,21 +96,23 @@ suspend fun <T> ValueAnimator.suspendAnimate(onValueChanged: (T) -> Unit) { * Starts spring animation and suspends until it's finished. Cancels the animation if the running * coroutine is cancelled. */ -suspend fun SpringAnimation.suspendAnimate(onAnimationUpdate: (Float) -> Unit) = - suspendCancellableCoroutine { continuation -> - val updateListener = - DynamicAnimation.OnAnimationUpdateListener { _, value, _ -> onAnimationUpdate(value) } - val endListener = - DynamicAnimation.OnAnimationEndListener { _, _, _, _ -> continuation.resumeIfCan(Unit) } - addUpdateListener(updateListener) - addEndListener(endListener) - animateToFinalPosition(1F) - continuation.invokeOnCancellation { - removeUpdateListener(updateListener) - removeEndListener(endListener) - cancel() - } +suspend fun SpringAnimation.suspendAnimate( + finalPosition: Float = 1f, + onAnimationUpdate: (Float) -> Unit = {}, +) = suspendCancellableCoroutine { continuation -> + val updateListener = + DynamicAnimation.OnAnimationUpdateListener { _, value, _ -> onAnimationUpdate(value) } + val endListener = + DynamicAnimation.OnAnimationEndListener { _, _, _, _ -> continuation.resumeIfCan(Unit) } + addUpdateListener(updateListener) + addEndListener(endListener) + animateToFinalPosition(finalPosition) + continuation.invokeOnCancellation { + removeUpdateListener(updateListener) + removeEndListener(endListener) + cancel() } +} /** * Starts the animation and suspends until it's finished. Cancels the animation if the running 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/window/data/repository/NoopWindowRootViewBlurRepository.kt b/packages/SystemUI/src/com/android/systemui/window/data/repository/NoopWindowRootViewBlurRepository.kt index 80aa11a71569..f1dd374d3e1d 100644 --- a/packages/SystemUI/src/com/android/systemui/window/data/repository/NoopWindowRootViewBlurRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/window/data/repository/NoopWindowRootViewBlurRepository.kt @@ -25,4 +25,5 @@ class NoopWindowRootViewBlurRepository @Inject constructor() : WindowRootViewBlu override val blurRadius: MutableStateFlow<Int> = MutableStateFlow(0) override val isBlurOpaque: MutableStateFlow<Boolean> = MutableStateFlow(true) override val isBlurSupported: StateFlow<Boolean> = MutableStateFlow(false) -}
\ No newline at end of file + override var blurAppliedListener: BlurAppliedListener? = null +} diff --git a/packages/SystemUI/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepository.kt b/packages/SystemUI/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepository.kt index 41ceda033bdf..f98efe1e3c33 100644 --- a/packages/SystemUI/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepository.kt @@ -26,6 +26,7 @@ import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import com.android.systemui.window.data.repository.WindowRootViewBlurRepository.Companion.isDisableBlurSysPropSet import java.util.concurrent.Executor +import java.util.function.Consumer import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose @@ -34,6 +35,8 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.stateIn +typealias BlurAppliedListener = Consumer<Int> + /** Repository that maintains state for the window blur effect. */ interface WindowRootViewBlurRepository { val blurRadius: MutableStateFlow<Int> @@ -42,6 +45,8 @@ interface WindowRootViewBlurRepository { /** Is blur supported based on settings toggle and battery power saver mode. */ val isBlurSupported: StateFlow<Boolean> + var blurAppliedListener: BlurAppliedListener? + companion object { /** * Whether the `persist.sysui.disableBlur` is set, this is used to disable blur for tests. @@ -82,6 +87,8 @@ constructor( } // stateIn because this is backed by a binder call. .stateIn(scope, SharingStarted.WhileSubscribed(), false) + override var blurAppliedListener: BlurAppliedListener? = null + private fun isBlurAllowed(): Boolean { return ActivityManager.isHighEndGfx() && !isDisableBlurSysPropSet() } diff --git a/packages/SystemUI/src/com/android/systemui/window/domain/interactor/WindowRootViewBlurInteractor.kt b/packages/SystemUI/src/com/android/systemui/window/domain/interactor/WindowRootViewBlurInteractor.kt index 7a88a2ef966b..5197242e8079 100644 --- a/packages/SystemUI/src/com/android/systemui/window/domain/interactor/WindowRootViewBlurInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/window/domain/interactor/WindowRootViewBlurInteractor.kt @@ -16,7 +16,6 @@ package com.android.systemui.window.domain.interactor -import android.annotation.SuppressLint import android.util.Log import com.android.systemui.Flags import com.android.systemui.communal.domain.interactor.CommunalInteractor @@ -25,11 +24,11 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER +import com.android.systemui.window.data.repository.BlurAppliedListener import com.android.systemui.window.data.repository.WindowRootViewBlurRepository import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -54,8 +53,6 @@ constructor( private val communalInteractor: CommunalInteractor, private val repository: WindowRootViewBlurRepository, ) { - @SuppressLint("SharedFlowCreation") private val _onBlurAppliedEvent = MutableSharedFlow<Int>() - private var isBouncerTransitionInProgress: StateFlow<Boolean> = if (Flags.bouncerUiRevamp()) { keyguardTransitionInteractor @@ -71,8 +68,16 @@ constructor( * Invoked by the view after blur of [appliedBlurRadius] was successfully applied on the window * root view. */ - suspend fun onBlurApplied(appliedBlurRadius: Int) { - _onBlurAppliedEvent.emit(appliedBlurRadius) + fun onBlurApplied(appliedBlurRadius: Int) { + repository.blurAppliedListener?.accept(appliedBlurRadius) + } + + /** + * Register a listener that gets invoked whenever blur is applied, clears the listener if the + * passed in value is null + */ + fun registerBlurAppliedListener(listener: BlurAppliedListener?) { + repository.blurAppliedListener = listener } /** @@ -84,11 +89,6 @@ constructor( /** Radius of blur to be applied on the window root view. */ val blurRadius: StateFlow<Int> = repository.blurRadius.asStateFlow() - /** - * Emits the applied blur radius whenever blur is successfully applied to the window root view. - */ - val onBlurAppliedEvent: Flow<Int> = _onBlurAppliedEvent - /** Whether the blur applied is opaque or transparent. */ val isBlurOpaque: Flow<Boolean> = combine( 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/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModel.kt index 1b42352f1736..e60e85335f7a 100644 --- a/packages/SystemUI/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/window/ui/viewmodel/WindowRootViewModel.kt @@ -18,18 +18,13 @@ package com.android.systemui.window.ui.viewmodel import android.os.Build import android.util.Log -import com.android.app.tracing.coroutines.launchTraced import com.android.systemui.Flags import com.android.systemui.keyguard.ui.transitions.GlanceableHubTransition import com.android.systemui.keyguard.ui.transitions.PrimaryBouncerTransition -import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.window.domain.interactor.WindowRootViewBlurInteractor import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.awaitCancellation -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest @@ -38,8 +33,6 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onEach -typealias BlurAppliedUiEvent = Int - /** View model for window root view. */ @OptIn(ExperimentalCoroutinesApi::class) class WindowRootViewModel @@ -48,9 +41,7 @@ constructor( primaryBouncerTransitions: Set<@JvmSuppressWildcards PrimaryBouncerTransition>, glanceableHubTransitions: Set<@JvmSuppressWildcards GlanceableHubTransition>, private val blurInteractor: WindowRootViewBlurInteractor, -) : ExclusiveActivatable() { - - private val blurEvents = Channel<BlurAppliedUiEvent>(Channel.BUFFERED) +) { private val bouncerBlurRadiusFlows = if (Flags.bouncerUiRevamp()) @@ -88,22 +79,11 @@ constructor( } } - override suspend fun onActivated(): Nothing { - coroutineScope { - launchTraced("WindowRootViewModel#blurEvents") { - for (event in blurEvents) { - if (isLoggable) { - Log.d(TAG, "blur applied for $event") - } - blurInteractor.onBlurApplied(event) - } - } - } - awaitCancellation() - } - fun onBlurApplied(blurRadius: Int) { - blurEvents.trySend(blurRadius) + if (isLoggable) { + Log.d(TAG, "blur applied for radius $blurRadius") + } + blurInteractor.onBlurApplied(blurRadius) } @AssistedFactory @@ -120,9 +100,3 @@ constructor( } } } - -/** - * @property radius Radius of blur to be applied on the window root view. - * @property isOpaque Whether the blur applied is opaque or transparent. - */ -data class BlurState(val radius: Int, val isOpaque: Boolean) 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/bluetooth/qsdialog/BluetoothDetailsContentManagerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsContentManagerTest.kt index 6ed990d513cb..9c932695f295 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsContentManagerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsContentManagerTest.kt @@ -87,7 +87,7 @@ class BluetoothDetailsContentManagerTest : SysuiTestCase() { private val fakeSystemClock = FakeSystemClock() private val uiProperties = - BluetoothTileDialogViewModel.UiProperties.build( + BluetoothDetailsContentViewModel.UiProperties.build( isBluetoothEnabled = ENABLED, isAutoOnToggleFeatureAvailable = ENABLED, ) @@ -314,7 +314,7 @@ class BluetoothDetailsContentManagerTest : SysuiTestCase() { val cachedHeight = Int.MAX_VALUE val contentManager = BluetoothDetailsContentManager( - BluetoothTileDialogViewModel.UiProperties.build(ENABLED, ENABLED), + BluetoothDetailsContentViewModel.UiProperties.build(ENABLED, ENABLED), cachedHeight, bluetoothTileDialogCallback, /* isInDialog= */ true, @@ -339,7 +339,7 @@ class BluetoothDetailsContentManagerTest : SysuiTestCase() { testScope.runTest { val contentManager = BluetoothDetailsContentManager( - BluetoothTileDialogViewModel.UiProperties.build(ENABLED, ENABLED), + BluetoothDetailsContentViewModel.UiProperties.build(ENABLED, ENABLED), MATCH_PARENT, bluetoothTileDialogCallback, /* isInDialog= */ true, @@ -364,7 +364,7 @@ class BluetoothDetailsContentManagerTest : SysuiTestCase() { testScope.runTest { val contentManager = BluetoothDetailsContentManager( - BluetoothTileDialogViewModel.UiProperties.build(ENABLED, ENABLED), + BluetoothDetailsContentViewModel.UiProperties.build(ENABLED, ENABLED), MATCH_PARENT, bluetoothTileDialogCallback, /* isInDialog= */ true, diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsContentViewModelTest.kt index 47a834be9b9c..bfc5361b6129 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothDetailsContentViewModelTest.kt @@ -67,14 +67,14 @@ import org.mockito.junit.MockitoRule @RunWith(AndroidJUnit4::class) @TestableLooper.RunWithLooper(setAsMainLooper = true) @EnableFlags(Flags.FLAG_BLUETOOTH_QS_TILE_DIALOG_AUTO_ON_TOGGLE) -class BluetoothTileDialogViewModelTest : SysuiTestCase() { +class BluetoothDetailsContentViewModelTest : SysuiTestCase() { @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule() private val kosmos = testKosmos() private val fakeSystemClock = FakeSystemClock() private val backgroundExecutor = FakeExecutor(fakeSystemClock) - private lateinit var bluetoothTileDialogViewModel: BluetoothTileDialogViewModel + private lateinit var bluetoothDetailsContentViewModel: BluetoothDetailsContentViewModel @Mock private lateinit var bluetoothDeviceMetadataInteractor: BluetoothDeviceMetadataInteractor @@ -126,8 +126,8 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { testScope = kosmos.testScope // TODO(b/364515243): use real object instead of mock whenever(kosmos.deviceItemInteractor.deviceItemUpdate).thenReturn(MutableSharedFlow()) - bluetoothTileDialogViewModel = - BluetoothTileDialogViewModel( + bluetoothDetailsContentViewModel = + BluetoothDetailsContentViewModel( deviceItemInteractor, deviceItemActionInteractor, BluetoothStateInteractor( @@ -194,7 +194,7 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { @Test fun testShowDetailsContent_noAnimation() { testScope.runTest { - bluetoothTileDialogViewModel.showDetailsContent(null, null) + bluetoothDetailsContentViewModel.showDetailsContent(null, null) runCurrent() verify(mDialogTransitionAnimator, never()).show(any(), any(), any()) @@ -204,7 +204,7 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { @Test fun testShowDetailsContent_animated() { testScope.runTest { - bluetoothTileDialogViewModel.showDetailsContent(expandable, null) + bluetoothDetailsContentViewModel.showDetailsContent(expandable, null) runCurrent() verify(mDialogTransitionAnimator).show(any(), any(), anyBoolean()) @@ -214,7 +214,7 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { @Test fun testShowDetailsContent_animated_inDetailsView() { testScope.runTest { - bluetoothTileDialogViewModel.showDetailsContent(expandable, mockView) + bluetoothDetailsContentViewModel.showDetailsContent(expandable, mockView) runCurrent() verify(bluetoothDetailsContentManager).bind(mockView) @@ -226,7 +226,7 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { fun testShowDetailsContent_animated_callInBackgroundThread() { testScope.runTest { backgroundExecutor.execute { - bluetoothTileDialogViewModel.showDetailsContent(expandable, null) + bluetoothDetailsContentViewModel.showDetailsContent(expandable, null) runCurrent() verify(mDialogTransitionAnimator).show(any(), any(), anyBoolean()) @@ -238,7 +238,7 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { fun testShowDetailsContent_animated_callInBackgroundThread_inDetailsView() { testScope.runTest { backgroundExecutor.execute { - bluetoothTileDialogViewModel.showDetailsContent(expandable, mockView) + bluetoothDetailsContentViewModel.showDetailsContent(expandable, mockView) runCurrent() verify(bluetoothDetailsContentManager).bind(mockView) @@ -250,7 +250,7 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { @Test fun testShowDetailsContent_fetchDeviceItem() { testScope.runTest { - bluetoothTileDialogViewModel.showDetailsContent(null, null) + bluetoothDetailsContentViewModel.showDetailsContent(null, null) runCurrent() verify(deviceItemInteractor).deviceItemUpdate @@ -261,11 +261,11 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { fun testStartSettingsActivity_activityLaunched_dialogDismissed() { testScope.runTest { whenever(deviceItem.cachedBluetoothDevice).thenReturn(cachedBluetoothDevice) - bluetoothTileDialogViewModel.showDetailsContent(null, null) + bluetoothDetailsContentViewModel.showDetailsContent(null, null) runCurrent() val clickedView = View(context) - bluetoothTileDialogViewModel.onPairNewDeviceClicked(clickedView) + bluetoothDetailsContentViewModel.onPairNewDeviceClicked(clickedView) verify(uiEventLogger).log(BluetoothTileDialogUiEvent.PAIR_NEW_DEVICE_CLICKED) verify(activityStarter).postStartActivityDismissingKeyguard(any(), anyInt(), nullable()) @@ -276,7 +276,7 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { fun testBuildUiProperties_bluetoothOn_shouldHideAutoOn() { testScope.runTest { val actual = - BluetoothTileDialogViewModel.UiProperties.build( + BluetoothDetailsContentViewModel.UiProperties.build( isBluetoothEnabled = true, isAutoOnToggleFeatureAvailable = true, ) @@ -288,7 +288,7 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { fun testBuildUiProperties_bluetoothOff_shouldShowAutoOn() { testScope.runTest { val actual = - BluetoothTileDialogViewModel.UiProperties.build( + BluetoothDetailsContentViewModel.UiProperties.build( isBluetoothEnabled = false, isAutoOnToggleFeatureAvailable = true, ) @@ -300,7 +300,7 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { fun testBuildUiProperties_bluetoothOff_autoOnFeatureUnavailable_shouldHideAutoOn() { testScope.runTest { val actual = - BluetoothTileDialogViewModel.UiProperties.build( + BluetoothDetailsContentViewModel.UiProperties.build( isBluetoothEnabled = false, isAutoOnToggleFeatureAvailable = false, ) @@ -313,7 +313,7 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { testScope.runTest { whenever(bluetoothAdapter.isAutoOnSupported).thenReturn(true) - val actual = bluetoothTileDialogViewModel.isAutoOnToggleFeatureAvailable() + val actual = bluetoothDetailsContentViewModel.isAutoOnToggleFeatureAvailable() assertThat(actual).isTrue() } } @@ -323,7 +323,7 @@ class BluetoothTileDialogViewModelTest : SysuiTestCase() { testScope.runTest { whenever(bluetoothAdapter.isAutoOnSupported).thenReturn(false) - val actual = bluetoothTileDialogViewModel.isAutoOnToggleFeatureAvailable() + val actual = bluetoothDetailsContentViewModel.isAutoOnToggleFeatureAvailable() assertThat(actual).isFalse() } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt index 2788f1d95382..7f75b8f03533 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegateTest.kt @@ -80,7 +80,7 @@ class BluetoothTileDialogDelegateTest : SysuiTestCase() { @Mock private lateinit var dialogTransitionAnimator: DialogTransitionAnimator private val uiProperties = - BluetoothTileDialogViewModel.UiProperties.build( + BluetoothDetailsContentViewModel.UiProperties.build( isBluetoothEnabled = ENABLED, isAutoOnToggleFeatureAvailable = ENABLED, ) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java index 4ccfa29d4ba0..e8054c07eac8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java @@ -206,6 +206,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { private @Mock ShadeInteractor mShadeInteractor; private @Mock ShadeWindowLogger mShadeWindowLogger; private @Mock SelectedUserInteractor mSelectedUserInteractor; + private @Mock UserTracker.Callback mUserTrackerCallback; private @Mock KeyguardInteractor mKeyguardInteractor; private @Mock KeyguardTransitionBootInteractor mKeyguardTransitionBootInteractor; private @Captor ArgumentCaptor<KeyguardStateController.Callback> @@ -280,7 +281,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { () -> mShadeInteractor, mShadeWindowLogger, () -> mSelectedUserInteractor, - mUserTracker, + mock(UserTracker.class), mKosmos.getNotificationShadeWindowModel(), mSecureSettings, mKosmos::getCommunalInteractor, @@ -318,7 +319,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { } catch (Exception e) { // Just so we don't have to add the exception signature to every test. - fail(); + fail(e.getMessage()); } } @@ -330,18 +331,156 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { /* First test the default behavior: handleUserSwitching() is not invoked */ when(mUserTracker.isUserSwitching()).thenReturn(false); - mViewMediator.mUpdateCallback = mock(KeyguardUpdateMonitorCallback.class); mViewMediator.onSystemReady(); TestableLooper.get(this).processAllMessages(); - verify(mViewMediator.mUpdateCallback, never()).onUserSwitching(userId); + verify(mUserTrackerCallback, never()).onUserChanging(eq(userId), eq(mContext), + any(Runnable.class)); /* Next test user switching is already in progress when started */ when(mUserTracker.isUserSwitching()).thenReturn(true); mViewMediator.onSystemReady(); TestableLooper.get(this).processAllMessages(); - verify(mViewMediator.mUpdateCallback).onUserSwitching(userId); + verify(mUserTrackerCallback).onUserChanging(eq(userId), eq(mContext), + any(Runnable.class)); + } + + @Test + @TestableLooper.RunWithLooper(setAsMainLooper = true) + public void testGoingAwayFollowedByBeforeUserSwitchDoesNotHideKeyguard() { + setCurrentUser(/* userId= */1099, /* isSecure= */false); + + // Setup keyguard + mViewMediator.onSystemReady(); + processAllMessagesAndBgExecutorMessages(); + mViewMediator.setShowingLocked(true, ""); + + // Request keyguard going away + when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(true); + mViewMediator.mKeyguardGoingAwayRunnable.run(); + + // After the request, begin a switch to a new secure user + int nextUserId = 500; + setCurrentUser(nextUserId, /* isSecure= */true); + Runnable result = mock(Runnable.class); + mViewMediator.handleBeforeUserSwitching(nextUserId, result); + processAllMessagesAndBgExecutorMessages(); + verify(result).run(); + + // After that request has begun, have WM tell us to exit keyguard + RemoteAnimationTarget[] apps = new RemoteAnimationTarget[]{ + mock(RemoteAnimationTarget.class) + }; + RemoteAnimationTarget[] wallpapers = new RemoteAnimationTarget[]{ + mock(RemoteAnimationTarget.class) + }; + IRemoteAnimationFinishedCallback callback = mock(IRemoteAnimationFinishedCallback.class); + mViewMediator.startKeyguardExitAnimation(TRANSIT_OLD_KEYGUARD_GOING_AWAY, apps, wallpapers, + null, callback); + processAllMessagesAndBgExecutorMessages(); + + // The call to exit should be rejected, and keyguard should still be visible + verify(mKeyguardUnlockAnimationController, never()).notifyStartSurfaceBehindRemoteAnimation( + any(), any(), any(), anyLong(), anyBoolean()); + try { + assertATMSLockScreenShowing(true); + } catch (Exception e) { + fail(e.getMessage()); + } + assertTrue(mViewMediator.isShowingAndNotOccluded()); + } + + @Test + @TestableLooper.RunWithLooper(setAsMainLooper = true) + public void testUserSwitchToSecureUserShowsBouncer() { + setCurrentUser(/* userId= */1099, /* isSecure= */true); + + // Setup keyguard + mViewMediator.onSystemReady(); + processAllMessagesAndBgExecutorMessages(); + mViewMediator.setShowingLocked(true, ""); + + // After the request, begin a switch to a new secure user + int nextUserId = 500; + setCurrentUser(nextUserId, /* isSecure= */true); + + Runnable beforeResult = mock(Runnable.class); + mViewMediator.handleBeforeUserSwitching(nextUserId, beforeResult); + processAllMessagesAndBgExecutorMessages(); + verify(beforeResult).run(); + + // Dismiss should not be called while user switch is in progress + Runnable onSwitchResult = mock(Runnable.class); + mViewMediator.handleUserSwitching(nextUserId, onSwitchResult); + processAllMessagesAndBgExecutorMessages(); + verify(onSwitchResult).run(); + verify(mStatusBarKeyguardViewManager, never()).dismissAndCollapse(); + + // The attempt to dismiss only comes on user switch complete, which will trigger a call to + // show the bouncer in StatusBarKeyguardViewManager + mViewMediator.handleUserSwitchComplete(nextUserId); + TestableLooper.get(this).moveTimeForward(600); + processAllMessagesAndBgExecutorMessages(); + + verify(mStatusBarKeyguardViewManager).dismissAndCollapse(); + } + + @Test + @TestableLooper.RunWithLooper(setAsMainLooper = true) + public void testUserSwitchToInsecureUserDismissesKeyguard() { + int userId = 1099; + when(mUserTracker.getUserId()).thenReturn(userId); + + // Setup keyguard + mViewMediator.onSystemReady(); + processAllMessagesAndBgExecutorMessages(); + mViewMediator.setShowingLocked(true, ""); + + // After the request, begin a switch to an insecure user + int nextUserId = 500; + when(mLockPatternUtils.isSecure(nextUserId)).thenReturn(false); + + Runnable beforeResult = mock(Runnable.class); + mViewMediator.handleBeforeUserSwitching(nextUserId, beforeResult); + processAllMessagesAndBgExecutorMessages(); + verify(beforeResult).run(); + + // The call to dismiss comes during the user switch + Runnable onSwitchResult = mock(Runnable.class); + mViewMediator.handleUserSwitching(nextUserId, onSwitchResult); + processAllMessagesAndBgExecutorMessages(); + verify(onSwitchResult).run(); + + verify(mStatusBarKeyguardViewManager).dismissAndCollapse(); + } + + @Test + @TestableLooper.RunWithLooper(setAsMainLooper = true) + public void testUserSwitchToSecureUserWhileKeyguardNotVisibleShowsKeyguard() { + setCurrentUser(/* userId= */1099, /* isSecure= */true); + + // Setup keyguard as not visible + mViewMediator.onSystemReady(); + processAllMessagesAndBgExecutorMessages(); + mViewMediator.setShowingLocked(false, ""); + processAllMessagesAndBgExecutorMessages(); + + // Begin a switch to a new secure user + int nextUserId = 500; + setCurrentUser(nextUserId, /* isSecure= */true); + + Runnable beforeResult = mock(Runnable.class); + mViewMediator.handleBeforeUserSwitching(nextUserId, beforeResult); + processAllMessagesAndBgExecutorMessages(); + verify(beforeResult).run(); + + try { + assertATMSLockScreenShowing(true); + } catch (Exception e) { + fail(); + } + assertTrue(mViewMediator.isShowingAndNotOccluded()); } @Test @@ -1105,7 +1244,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { processAllMessagesAndBgExecutorMessages(); verify(mStatusBarKeyguardViewManager, never()).reset(anyBoolean()); - assertATMSAndKeyguardViewMediatorStatesMatch(); + } @Test @@ -1149,6 +1288,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { IRemoteAnimationFinishedCallback callback = mock(IRemoteAnimationFinishedCallback.class); when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(true); + mViewMediator.mKeyguardGoingAwayRunnable.run(); mViewMediator.startKeyguardExitAnimation(TRANSIT_OLD_KEYGUARD_GOING_AWAY, apps, wallpapers, null, callback); processAllMessagesAndBgExecutorMessages(); @@ -1203,13 +1343,6 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { // The captor will have the most recent setLockScreenShown call's value. assertEquals(showing, showingCaptor.getValue()); - - // We're now just after the last setLockScreenShown call. If we expect the lockscreen to be - // showing, ensure that we didn't subsequently ask for it to go away. - if (showing) { - orderedSetLockScreenShownCalls.verify(mActivityTaskManagerService, never()) - .keyguardGoingAway(anyInt()); - } } /** @@ -1371,6 +1504,7 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { mKeyguardTransitionBootInteractor, mKosmos::getCommunalSceneInteractor, mock(WindowManagerOcclusionManager.class)); + mViewMediator.mUserChangedCallback = mUserTrackerCallback; mViewMediator.start(); mViewMediator.registerCentralSurfaces(mCentralSurfaces, null, null, null, null); @@ -1384,4 +1518,10 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { private void captureKeyguardUpdateMonitorCallback() { verify(mUpdateMonitor).registerCallback(mKeyguardUpdateMonitorCallbackCaptor.capture()); } + + private void setCurrentUser(int userId, boolean isSecure) { + when(mUserTracker.getUserId()).thenReturn(userId); + when(mSelectedUserInteractor.getSelectedUserId()).thenReturn(userId); + when(mLockPatternUtils.isSecure(userId)).thenReturn(isSecure); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BluetoothTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BluetoothTileTest.kt index 1305b0c4c499..cfe34f446f36 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BluetoothTileTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/BluetoothTileTest.kt @@ -16,7 +16,7 @@ import com.android.internal.telephony.flags.Flags import com.android.settingslib.Utils import com.android.settingslib.bluetooth.CachedBluetoothDevice import com.android.systemui.SysuiTestCase -import com.android.systemui.bluetooth.qsdialog.BluetoothTileDialogViewModel +import com.android.systemui.bluetooth.qsdialog.BluetoothDetailsContentViewModel import com.android.systemui.classifier.FalsingManagerFake import com.android.systemui.flags.FeatureFlagsClassic import com.android.systemui.plugins.ActivityStarter @@ -71,7 +71,7 @@ class BluetoothTileTest(flags: FlagsParameterization) : SysuiTestCase() { @Mock private lateinit var bluetoothController: BluetoothController @Mock private lateinit var uiEventLogger: QsEventLogger @Mock private lateinit var featureFlags: FeatureFlagsClassic - @Mock private lateinit var bluetoothTileDialogViewModel: BluetoothTileDialogViewModel + @Mock private lateinit var bluetoothDetailsContentViewModel: BluetoothDetailsContentViewModel @Mock private lateinit var clickJob: Job private lateinit var testableLooper: TestableLooper private lateinit var tile: FakeBluetoothTile @@ -96,7 +96,7 @@ class BluetoothTileTest(flags: FlagsParameterization) : SysuiTestCase() { qsLogger, bluetoothController, featureFlags, - bluetoothTileDialogViewModel, + bluetoothDetailsContentViewModel, ) tile.initialize() @@ -238,7 +238,7 @@ class BluetoothTileTest(flags: FlagsParameterization) : SysuiTestCase() { tile.handleClick(null) - verify(bluetoothTileDialogViewModel) + verify(bluetoothDetailsContentViewModel) .showDetailsContent(/* expandable= */ null, /* view= */ null) } @@ -308,7 +308,7 @@ class BluetoothTileTest(flags: FlagsParameterization) : SysuiTestCase() { qsLogger: QSLogger, bluetoothController: BluetoothController, featureFlags: FeatureFlagsClassic, - bluetoothTileDialogViewModel: BluetoothTileDialogViewModel, + bluetoothDetailsContentViewModel: BluetoothDetailsContentViewModel, ) : BluetoothTile( qsHost, @@ -322,7 +322,7 @@ class BluetoothTileTest(flags: FlagsParameterization) : SysuiTestCase() { qsLogger, bluetoothController, featureFlags, - bluetoothTileDialogViewModel, + bluetoothDetailsContentViewModel, ) { var restrictionChecked: String? = null 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/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java index 9137215f8c61..14a1233045bb 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,7 +63,6 @@ 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; @@ -119,8 +118,10 @@ public class ScrimControllerTest extends SysuiTestCase { @Rule public Expect mExpect = Expect.create(); private final KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this); - private FakeConfigurationController mConfigurationController; - private LargeScreenShadeInterpolator mLinearLargeScreenShadeInterpolator; + private final FakeConfigurationController mConfigurationController = + new FakeConfigurationController(); + private final LargeScreenShadeInterpolator + mLinearLargeScreenShadeInterpolator = new LinearLargeScreenShadeInterpolator(); private final TestScope mTestScope = mKosmos.getTestScope(); private final JavaAdapter mJavaAdapter = new JavaAdapter(mTestScope.getBackgroundScope()); @@ -136,7 +137,6 @@ 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,11 +149,12 @@ 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; @@ -237,9 +238,6 @@ 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); @@ -272,9 +270,6 @@ public class ScrimControllerTest extends SysuiTestCase { when(mAlternateBouncerToGoneTransitionViewModel.getScrimAlpha()) .thenReturn(emptyFlow()); - mKeyguardTransitionRepository = mKosmos.getKeyguardTransitionRepository(); - mKeyguardTransitionInteractor = mKosmos.getKeyguardTransitionInteractor(); - mScrimController = new ScrimController( mLightBarController, mDozeParameters, @@ -327,7 +322,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void transitionToKeyguard() { mScrimController.legacyTransitionTo(ScrimState.KEYGUARD); finishAnimationsImmediately(); @@ -343,7 +337,7 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test -@DisableSceneContainer void transitionToShadeLocked() { + public void transitionToShadeLocked() { mScrimController.legacyTransitionTo(SHADE_LOCKED); mScrimController.setQsPosition(1f, 0); finishAnimationsImmediately(); @@ -379,7 +373,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void transitionToShadeLocked_clippingQs() { mScrimController.setClipsQsScrim(true); mScrimController.legacyTransitionTo(SHADE_LOCKED); @@ -398,7 +391,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void transitionToOff() { mScrimController.legacyTransitionTo(ScrimState.OFF); finishAnimationsImmediately(); @@ -414,7 +406,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void transitionToAod_withRegularWallpaper() { mScrimController.legacyTransitionTo(ScrimState.AOD); finishAnimationsImmediately(); @@ -430,7 +421,6 @@ 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); @@ -475,7 +465,6 @@ 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); @@ -517,7 +506,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void transitionToPulsing_withFrontAlphaUpdates() { // Pre-condition // Need to go to AoD first because PULSING doesn't change @@ -563,7 +551,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void transitionToKeyguardBouncer() { mScrimController.legacyTransitionTo(BOUNCER); finishAnimationsImmediately(); @@ -584,7 +571,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void lockscreenToHubTransition_setsBehindScrimAlpha() { // Start on lockscreen. mScrimController.legacyTransitionTo(ScrimState.KEYGUARD); @@ -631,7 +617,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void hubToLockscreenTransition_setsViewAlpha() { // Start on glanceable hub. mScrimController.legacyTransitionTo(ScrimState.GLANCEABLE_HUB); @@ -678,7 +663,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void transitionToHub() { mScrimController.setRawPanelExpansionFraction(0f); mScrimController.setBouncerHiddenFraction(KeyguardBouncerConstants.EXPANSION_HIDDEN); @@ -693,7 +677,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void openBouncerOnHub() { mScrimController.legacyTransitionTo(ScrimState.GLANCEABLE_HUB); @@ -723,7 +706,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void openShadeOnHub() { mScrimController.legacyTransitionTo(ScrimState.GLANCEABLE_HUB); @@ -752,7 +734,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void transitionToHubOverDream() { mScrimController.setRawPanelExpansionFraction(0f); mScrimController.setBouncerHiddenFraction(KeyguardBouncerConstants.EXPANSION_HIDDEN); @@ -767,7 +748,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void openBouncerOnHubOverDream() { mScrimController.legacyTransitionTo(ScrimState.GLANCEABLE_HUB_OVER_DREAM); @@ -797,7 +777,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void openShadeOnHubOverDream() { mScrimController.legacyTransitionTo(ScrimState.GLANCEABLE_HUB_OVER_DREAM); @@ -826,7 +805,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void onThemeChange_bouncerBehindTint_isUpdatedToSurfaceColor() { assertEquals(BOUNCER.getBehindTint(), 0x112233); mSurfaceColor = 0x223344; @@ -835,7 +813,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void onThemeChangeWhileClipQsScrim_bouncerBehindTint_remainsBlack() { mScrimController.setClipsQsScrim(true); mScrimController.legacyTransitionTo(BOUNCER); @@ -848,7 +825,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void transitionToKeyguardBouncer_clippingQs() { mScrimController.setClipsQsScrim(true); mScrimController.legacyTransitionTo(BOUNCER); @@ -869,7 +845,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void disableClipQsScrimWithoutStateTransition_updatesTintAndAlpha() { mScrimController.setClipsQsScrim(true); mScrimController.legacyTransitionTo(BOUNCER); @@ -892,7 +867,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void enableClipQsScrimWithoutStateTransition_updatesTintAndAlpha() { mScrimController.setClipsQsScrim(false); mScrimController.legacyTransitionTo(BOUNCER); @@ -915,7 +889,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void transitionToBouncer() { mScrimController.legacyTransitionTo(ScrimState.BOUNCER_SCRIMMED); finishAnimationsImmediately(); @@ -929,7 +902,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void transitionToUnlocked_clippedQs() { mScrimController.setClipsQsScrim(true); mScrimController.setRawPanelExpansionFraction(0f); @@ -988,7 +960,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void transitionToUnlocked_nonClippedQs_followsLargeScreensInterpolator() { mScrimController.setClipsQsScrim(false); mScrimController.setRawPanelExpansionFraction(0f); @@ -1028,7 +999,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void scrimStateCallback() { mScrimController.legacyTransitionTo(ScrimState.UNLOCKED); finishAnimationsImmediately(); @@ -1044,7 +1014,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void panelExpansion() { mScrimController.setRawPanelExpansionFraction(0f); mScrimController.setRawPanelExpansionFraction(0.5f); @@ -1067,7 +1036,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void qsExpansion() { reset(mScrimBehind); mScrimController.setQsPosition(1f, 999 /* value doesn't matter */); @@ -1080,7 +1048,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void qsExpansion_clippingQs() { reset(mScrimBehind); mScrimController.setClipsQsScrim(true); @@ -1094,7 +1061,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void qsExpansion_half_clippingQs() { reset(mScrimBehind); mScrimController.setClipsQsScrim(true); @@ -1108,7 +1074,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void panelExpansionAffectsAlpha() { mScrimController.setRawPanelExpansionFraction(0f); mScrimController.setRawPanelExpansionFraction(0.5f); @@ -1131,7 +1096,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void transitionToUnlockedFromOff() { // Simulate unlock with fingerprint without AOD mScrimController.legacyTransitionTo(ScrimState.OFF); @@ -1154,7 +1118,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void transitionToUnlockedFromAod() { // Simulate unlock with fingerprint mScrimController.legacyTransitionTo(ScrimState.AOD); @@ -1177,7 +1140,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void scrimBlanksBeforeLeavingAod() { // Simulate unlock with fingerprint mScrimController.legacyTransitionTo(ScrimState.AOD); @@ -1201,7 +1163,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void scrimBlankCallbackWhenUnlockingFromPulse() { boolean[] blanked = {false}; // Simulate unlock with fingerprint @@ -1220,7 +1181,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void blankingNotRequired_leavingAoD() { // GIVEN display does NOT need blanking when(mDozeParameters.getDisplayNeedsBlanking()).thenReturn(false); @@ -1276,7 +1236,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void testScrimCallback() { int[] callOrder = {0, 0, 0}; int[] currentCall = {0}; @@ -1303,14 +1262,12 @@ 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() { @@ -1324,7 +1281,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void testHoldsWakeLock_whenAOD() { mScrimController.legacyTransitionTo(ScrimState.AOD); verify(mWakeLock).acquire(anyString()); @@ -1334,7 +1290,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void testDoesNotHoldWakeLock_whenUnlocking() { mScrimController.legacyTransitionTo(ScrimState.UNLOCKED); finishAnimationsImmediately(); @@ -1342,7 +1297,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void testCallbackInvokedOnSameStateTransition() { mScrimController.legacyTransitionTo(ScrimState.UNLOCKED); finishAnimationsImmediately(); @@ -1352,7 +1306,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void testConservesExpansionOpacityAfterTransition() { mScrimController.legacyTransitionTo(ScrimState.UNLOCKED); mScrimController.setRawPanelExpansionFraction(0.5f); @@ -1370,7 +1323,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void testCancelsOldAnimationBeforeBlanking() { mScrimController.legacyTransitionTo(ScrimState.AOD); finishAnimationsImmediately(); @@ -1383,7 +1335,6 @@ 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()); @@ -1392,7 +1343,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void testEatsTouchEvent() { HashSet<ScrimState> eatsTouches = new HashSet<>(Collections.singletonList(ScrimState.AOD)); @@ -1409,7 +1359,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void testAnimatesTransitionToAod() { when(mDozeParameters.shouldControlScreenOff()).thenReturn(false); ScrimState.AOD.prepare(ScrimState.KEYGUARD); @@ -1424,7 +1373,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void testIsLowPowerMode() { HashSet<ScrimState> lowPowerModeStates = new HashSet<>(Arrays.asList( ScrimState.OFF, ScrimState.AOD, ScrimState.PULSING)); @@ -1442,7 +1390,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void testScrimsOpaque_whenShadeFullyExpanded() { mScrimController.legacyTransitionTo(ScrimState.UNLOCKED); mScrimController.setRawPanelExpansionFraction(1); @@ -1457,7 +1404,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void testScrimsVisible_whenShadeVisible() { mScrimController.setClipsQsScrim(true); mScrimController.legacyTransitionTo(ScrimState.UNLOCKED); @@ -1473,7 +1419,6 @@ 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); @@ -1494,7 +1439,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void testScrimsVisible_whenShadeVisible_clippingQs() { mScrimController.setClipsQsScrim(true); mScrimController.legacyTransitionTo(ScrimState.UNLOCKED); @@ -1510,7 +1454,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void testScrimsVisible_whenShadeVisibleOnLockscreen() { mScrimController.legacyTransitionTo(ScrimState.KEYGUARD); mScrimController.setQsPosition(0.25f, 300); @@ -1522,7 +1465,6 @@ 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 @@ -1535,7 +1477,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void testNotificationScrimVisible_afterOpeningShadeFromLockscreen() { mScrimController.setRawPanelExpansionFraction(1); mScrimController.legacyTransitionTo(SHADE_LOCKED); @@ -1547,7 +1488,6 @@ 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 @@ -1564,7 +1504,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void expansionNotificationAlpha_shadeLocked_bouncerActive_usesBouncerInterpolator() { when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(true); @@ -1581,7 +1520,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void expansionNotificationAlpha_shadeLocked_bouncerNotActive_usesShadeInterpolator() { when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(false); @@ -1597,7 +1535,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void notificationAlpha_unnocclusionAnimating_bouncerNotActive_usesKeyguardNotifAlpha() { when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(false); @@ -1617,7 +1554,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void notificationAlpha_inKeyguardState_bouncerActive_usesInvertedBouncerInterpolator() { when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(true); mScrimController.setClipsQsScrim(true); @@ -1638,7 +1574,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void notificationAlpha_inKeyguardState_bouncerNotActive_usesInvertedShadeInterpolator() { when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(false); mScrimController.setClipsQsScrim(true); @@ -1659,7 +1594,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void behindTint_inKeyguardState_bouncerNotActive_usesKeyguardBehindTint() { when(mStatusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(false); mScrimController.setClipsQsScrim(false); @@ -1671,7 +1605,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void testNotificationTransparency_followsTransitionToFullShade() { mScrimController.setClipsQsScrim(true); @@ -1713,7 +1646,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void notificationTransparency_followsNotificationScrimProgress() { mScrimController.legacyTransitionTo(SHADE_LOCKED); mScrimController.setRawPanelExpansionFraction(1.0f); @@ -1730,7 +1662,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void notificationAlpha_qsNotClipped_alphaMatchesNotificationExpansionProgress() { mScrimController.setClipsQsScrim(false); mScrimController.legacyTransitionTo(ScrimState.KEYGUARD); @@ -1766,7 +1697,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void setNotificationsOverScrollAmount_setsTranslationYOnNotificationsScrim() { int overScrollAmount = 10; @@ -1776,7 +1706,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void setNotificationsOverScrollAmount_doesNotSetTranslationYOnBehindScrim() { int overScrollAmount = 10; @@ -1786,7 +1715,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void setNotificationsOverScrollAmount_doesNotSetTranslationYOnFrontScrim() { int overScrollAmount = 10; @@ -1796,7 +1724,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void notificationBoundsTopGetsPassedToKeyguard() { mScrimController.legacyTransitionTo(SHADE_LOCKED); mScrimController.setQsPosition(1f, 0); @@ -1807,7 +1734,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void notificationBoundsTopDoesNotGetPassedToKeyguardWhenNotifScrimIsNotVisible() { mScrimController.setKeyguardOccluded(true); mScrimController.legacyTransitionTo(ScrimState.KEYGUARD); @@ -1818,7 +1744,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void transitionToDreaming() { mScrimController.setRawPanelExpansionFraction(0f); mScrimController.setBouncerHiddenFraction(KeyguardBouncerConstants.EXPANSION_HIDDEN); @@ -1838,7 +1763,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void keyguardGoingAwayUpdateScrims() { when(mKeyguardStateController.isKeyguardGoingAway()).thenReturn(true); mScrimController.updateScrims(); @@ -1848,7 +1772,6 @@ public class ScrimControllerTest extends SysuiTestCase { @Test - @DisableSceneContainer public void setUnOccludingAnimationKeyguard() { mScrimController.legacyTransitionTo(ScrimState.KEYGUARD); finishAnimationsImmediately(); @@ -1863,7 +1786,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void testHidesScrimFlickerInActivity() { mScrimController.setKeyguardOccluded(true); mScrimController.legacyTransitionTo(ScrimState.KEYGUARD); @@ -1882,7 +1804,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void notificationAlpha_inKeyguardState_bouncerNotActive_clipsQsScrimFalse() { mScrimController.setClipsQsScrim(false); mScrimController.legacyTransitionTo(ScrimState.KEYGUARD); @@ -1892,7 +1813,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void aodStateSetsFrontScrimToNotBlend() { mScrimController.legacyTransitionTo(ScrimState.AOD); assertFalse("Front scrim should not blend with main color", @@ -1900,7 +1820,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void applyState_unlocked_bouncerShowing() { mScrimController.legacyTransitionTo(ScrimState.UNLOCKED); mScrimController.setBouncerHiddenFraction(0.99f); @@ -1910,7 +1829,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void ignoreTransitionRequestWhileKeyguardTransitionRunning() { mScrimController.legacyTransitionTo(ScrimState.UNLOCKED); mScrimController.mBouncerToGoneTransition.accept( @@ -1923,7 +1841,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void primaryBouncerToGoneOnFinishCallsKeyguardFadedAway() { when(mKeyguardStateController.isKeyguardFadingAway()).thenReturn(true); mScrimController.mBouncerToGoneTransition.accept( @@ -1934,7 +1851,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void primaryBouncerToGoneOnFinishCallsLightBarController() { reset(mLightBarController); mScrimController.mBouncerToGoneTransition.accept( @@ -1946,7 +1862,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void testDoNotAnimateChangeIfOccludeAnimationPlaying() { mScrimController.setOccludeAnimationPlaying(true); mScrimController.legacyTransitionTo(ScrimState.UNLOCKED); @@ -1955,7 +1870,6 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test - @DisableSceneContainer public void testNotifScrimAlpha_1f_afterUnlockFinishedAndExpanded() { mScrimController.legacyTransitionTo(ScrimState.KEYGUARD); when(mKeyguardUnlockAnimationController.isPlayingCannedUnlockAnimation()).thenReturn(true); diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorKosmos.kt index e6e59e1a523e..75c4b6f5366b 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorKosmos.kt @@ -22,6 +22,7 @@ import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.power.domain.interactor.powerInteractor val Kosmos.communalSceneTransitionInteractor: CommunalSceneTransitionInteractor by Kosmos.Fixture { @@ -33,5 +34,6 @@ val Kosmos.communalSceneTransitionInteractor: CommunalSceneTransitionInteractor sceneInteractor = communalSceneInteractor, repository = communalSceneTransitionRepository, keyguardInteractor = keyguardInteractor, + powerInteractor = powerInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractorKosmos.kt index 7b0d208298d0..38372acff113 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractorKosmos.kt @@ -18,6 +18,8 @@ package com.android.systemui.keyguard.domain.interactor import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor import com.android.systemui.communal.domain.interactor.communalInteractor +import com.android.systemui.communal.domain.interactor.communalSceneInteractor +import com.android.systemui.communal.domain.interactor.communalSettingsInteractor import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope @@ -39,5 +41,7 @@ val Kosmos.fromAlternateBouncerTransitionInteractor by powerInteractor = powerInteractor, keyguardOcclusionInteractor = keyguardOcclusionInteractor, primaryBouncerInteractor = primaryBouncerInteractor, + communalSceneInteractor = communalSceneInteractor, + communalSettingsInteractor = communalSettingsInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorKosmos.kt index 3c369d7d954f..6b240b5f36ac 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorKosmos.kt @@ -18,6 +18,7 @@ package com.android.systemui.keyguard.domain.interactor import com.android.keyguard.keyguardSecurityModel import com.android.systemui.communal.domain.interactor.communalSceneInteractor +import com.android.systemui.communal.domain.interactor.communalSettingsInteractor import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope @@ -37,6 +38,7 @@ var Kosmos.fromPrimaryBouncerTransitionInteractor by mainDispatcher = testDispatcher, keyguardInteractor = keyguardInteractor, communalSceneInteractor = communalSceneInteractor, + communalSettingsInteractor = communalSettingsInteractor, keyguardSecurityModel = keyguardSecurityModel, selectedUserInteractor = selectedUserInteractor, powerInteractor = powerInteractor, 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 8844eb040f02..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,8 +20,9 @@ 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 import com.android.systemui.wallpapers.domain.interactor.wallpaperFocalAreaInteractor @@ -30,7 +31,8 @@ val Kosmos.keyguardClockInteractor by KeyguardClockInteractor( mediaCarouselInteractor = mediaCarouselInteractor, activeNotificationsInteractor = activeNotificationsInteractor, - shadeInteractor = shadeInteractor, + aodPromotedNotificationInteractor = aodPromotedNotificationInteractor, + 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/KeyguardRootViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt index ea3feeaf0854..78356318cbb4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt @@ -17,6 +17,7 @@ package com.android.systemui.keyguard.ui.viewmodel import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor +import com.android.systemui.dump.dumpManager import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.keyguard.domain.interactor.pulseExpansionInteractor @@ -26,6 +27,7 @@ import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.shade.ui.viewmodel.notificationShadeWindowModel import com.android.systemui.statusbar.notification.icon.ui.viewmodel.notificationIconContainerAlwaysOnDisplayViewModel +import com.android.systemui.statusbar.notification.promoted.domain.interactor.aodPromotedNotificationInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationsKeyguardInteractor import com.android.systemui.statusbar.phone.dozeParameters import com.android.systemui.statusbar.phone.screenOffAnimationController @@ -41,6 +43,7 @@ val Kosmos.keyguardRootViewModel by Fixture { keyguardTransitionInteractor = keyguardTransitionInteractor, notificationsKeyguardInteractor = notificationsKeyguardInteractor, pulseExpansionInteractor = pulseExpansionInteractor, + aodPromotedNotificationInteractor = aodPromotedNotificationInteractor, aodNotificationIconViewModel = notificationIconContainerAlwaysOnDisplayViewModel, notificationShadeWindowModel = notificationShadeWindowModel, alternateBouncerToAodTransitionViewModel = alternateBouncerToAodTransitionViewModel, @@ -90,5 +93,6 @@ val Kosmos.keyguardRootViewModel by Fixture { aodBurnInViewModel = aodBurnInViewModel, shadeInteractor = shadeInteractor, wallpaperFocalAreaInteractor = wallpaperFocalAreaInteractor, + dumpManager = dumpManager, ) } 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/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/statusbar/notification/collection/EntryUtil.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/EntryUtil.kt index 8b4de2bcc26f..05f1c0bac982 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/EntryUtil.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/EntryUtil.kt @@ -33,3 +33,6 @@ inline fun NotificationEntry.modifyEntry( fun getAttachState(entry: ListEntry): ListAttachState { return entry.attachState } + +fun buildEntry(block: NotificationEntryBuilder.() -> Unit) = + NotificationEntryBuilder().apply(block).build() 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/communal/domain/interactor/CommunalBackActionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractorKosmos.kt index 57c8fd066ea8..df1c82278bc2 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalBackActionInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/promoted/domain/interactor/AODPromotedNotificationInteractorKosmos.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2024 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,16 +14,16 @@ * limitations under the License. */ -package com.android.systemui.communal.domain.interactor +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.scene.domain.interactor.sceneInteractor +import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor -val Kosmos.communalBackActionInteractor by +val Kosmos.aodPromotedNotificationInteractor by Kosmos.Fixture { - CommunalBackActionInteractor( - communalInteractor = communalInteractor, - communalSceneInteractor = communalSceneInteractor, - sceneInteractor = sceneInteractor, + AODPromotedNotificationInteractor( + activeNotificationsInteractor = activeNotificationsInteractor, + dumpManager = dumpManager, ) } 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 7a2b7c24252b..047bd13f0c27 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,6 +29,7 @@ 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 @@ -81,6 +82,7 @@ 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/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt index 352f6cf011e1..9b6f205fba72 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconsInteractor.kt @@ -26,6 +26,7 @@ import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel import com.android.systemui.statusbar.pipeline.mobile.util.MobileMappingsProxy import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow class FakeMobileIconsInteractor( mobileMappings: MobileMappingsProxy, @@ -73,6 +74,8 @@ class FakeMobileIconsInteractor( override val icons: MutableStateFlow<List<MobileIconInteractor>> = MutableStateFlow(emptyList()) + override val isStackable: StateFlow<Boolean> = MutableStateFlow(false) + private val _defaultMobileIconMapping = MutableStateFlow(TEST_MAPPING) override val defaultMobileIconMapping = _defaultMobileIconMapping diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinderKosmos.kt index dc09e3233b1e..386e0feb3b3a 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinderKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogViewBinderKosmos.kt @@ -16,7 +16,6 @@ package com.android.systemui.volume.dialog.ui.binder -import android.content.applicationContext import com.android.systemui.kosmos.Kosmos import com.android.systemui.volume.dialog.ringer.volumeDialogRingerViewBinder import com.android.systemui.volume.dialog.settings.ui.binder.volumeDialogSettingsButtonViewBinder @@ -28,12 +27,13 @@ import com.android.systemui.volume.dialog.utils.volumeTracer val Kosmos.volumeDialogViewBinder by Kosmos.Fixture { VolumeDialogViewBinder( - applicationContext.resources, volumeDialogViewModel, jankListenerFactory, volumeTracer, - volumeDialogRingerViewBinder, - volumeDialogSlidersViewBinder, - volumeDialogSettingsButtonViewBinder, + listOf( + volumeDialogSlidersViewBinder, + volumeDialogRingerViewBinder, + volumeDialogSettingsButtonViewBinder, + ), ) } 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/window/data/repository/WindowRootViewBlurRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepositoryKosmos.kt index 96992233375d..b619e2d70724 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/window/data/repository/WindowRootViewBlurRepositoryKosmos.kt @@ -29,4 +29,5 @@ class FakeWindowRootViewBlurRepository : WindowRootViewBlurRepository { override val blurRadius: MutableStateFlow<Int> = MutableStateFlow(0) override val isBlurOpaque: MutableStateFlow<Boolean> = MutableStateFlow(false) override val isBlurSupported: MutableStateFlow<Boolean> = MutableStateFlow(false) + override var blurAppliedListener: BlurAppliedListener? = null } 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/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java index 5283df5ca7e1..4fa0d506f09e 100644 --- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java +++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickController.java @@ -538,6 +538,11 @@ public class AutoclickController extends BaseEventStreamTransformation { return mActive; } + @VisibleForTesting + long getScheduledClickTimeForTesting() { + return mScheduledClickTime; + } + /** * Updates delay that should be used when scheduling clicks. The delay will be used only for * clicks scheduled after this point (pending click tasks are not affected). diff --git a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java index cf928e2f3fa4..614b2285d6e0 100644 --- a/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java +++ b/services/accessibility/java/com/android/server/accessibility/autoclick/AutoclickTypePanel.java @@ -168,6 +168,10 @@ public class AutoclickTypePanel { } public void hide() { + // Sets the button background to unselected styling, this is necessary to make sure the + // button background styling is correct when the panel shows up next time. + toggleSelectedButtonStyle(mSelectedButton, /* isSelected= */ false); + mWindowManager.removeView(mContentView); } diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java index d47aab061788..e0f2939a2083 100644 --- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java +++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java @@ -16,7 +16,6 @@ package com.android.server.appwidget; -import static android.appwidget.flags.Flags.checkRemoteViewsUriPermission; import static android.appwidget.flags.Flags.remoteAdapterConversion; import static android.appwidget.flags.Flags.remoteViewsProto; import static android.appwidget.flags.Flags.removeAppWidgetServiceIoFromCriticalPath; @@ -2560,9 +2559,7 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku // Make sure the package runs under the caller uid. mSecurityPolicy.enforceCallFromPackage(callingPackage); // Make sure RemoteViews do not contain URIs that the caller cannot access. - if (checkRemoteViewsUriPermission()) { - checkRemoteViewsUris(views); - } + checkRemoteViewsUris(views); synchronized (mLock) { ensureGroupStateLoadedLocked(userId); 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/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index f34016905502..6e3d7bd19b41 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -8315,14 +8315,6 @@ public class ActivityManagerService extends IActivityManager.Stub setThreadScheduler(proc.getRenderThreadTid(), SCHED_FIFO | SCHED_RESET_ON_FORK, 1); } else { - if (Flags.resetOnForkEnabled()) { - if (Process.getThreadScheduler(proc.getRenderThreadTid()) - == Process.SCHED_OTHER) { - Process.setThreadScheduler(proc.getRenderThreadTid(), - Process.SCHED_OTHER | Process.SCHED_RESET_ON_FORK, - 0); - } - } setThreadPriority(proc.getRenderThreadTid(), THREAD_PRIORITY_TOP_APP_BOOST); } diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java index cd40905b01da..61c5501a7b5a 100644 --- a/services/core/java/com/android/server/am/OomAdjuster.java +++ b/services/core/java/com/android/server/am/OomAdjuster.java @@ -471,13 +471,6 @@ public class OomAdjuster { } void setThreadPriority(int tid, int priority) { - if (Flags.resetOnForkEnabled()) { - if (Process.getThreadScheduler(tid) == Process.SCHED_OTHER) { - Process.setThreadScheduler(tid, - Process.SCHED_OTHER | Process.SCHED_RESET_ON_FORK, - 0); - } - } Process.setThreadPriority(tid, priority); } } diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java index 27e9e44f1090..e0fbaf43ea43 100644 --- a/services/core/java/com/android/server/am/UserController.java +++ b/services/core/java/com/android/server/am/UserController.java @@ -31,6 +31,7 @@ import static android.app.ActivityManagerInternal.ALLOW_FULL_ONLY; import static android.app.ActivityManagerInternal.ALLOW_NON_FULL; import static android.app.ActivityManagerInternal.ALLOW_NON_FULL_IN_PROFILE; import static android.app.ActivityManagerInternal.ALLOW_PROFILES_OR_NON_FULL; +import static android.app.KeyguardManager.LOCK_ON_USER_SWITCH_CALLBACK; import static android.os.PowerWhitelistManager.REASON_BOOT_COMPLETED; import static android.os.PowerWhitelistManager.REASON_LOCKED_BOOT_COMPLETED; import static android.os.PowerWhitelistManager.TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED; @@ -3904,10 +3905,6 @@ class UserController implements Handler.Callback { return mService.mWindowManager; } - ActivityTaskManagerInternal getActivityTaskManagerInternal() { - return mService.mAtmInternal; - } - void activityManagerOnUserStopped(@UserIdInt int userId) { LocalServices.getService(ActivityTaskManagerInternal.class).onUserStopped(userId); } @@ -4122,40 +4119,25 @@ class UserController implements Handler.Callback { } void lockDeviceNowAndWaitForKeyguardShown() { - if (getWindowManager().isKeyguardLocked()) { - Slogf.w(TAG, "Not locking the device since the keyguard is already locked"); - return; - } - final TimingsTraceAndSlog t = new TimingsTraceAndSlog(); t.traceBegin("lockDeviceNowAndWaitForKeyguardShown"); final CountDownLatch latch = new CountDownLatch(1); - ActivityTaskManagerInternal.ScreenObserver screenObserver = - new ActivityTaskManagerInternal.ScreenObserver() { - @Override - public void onAwakeStateChanged(boolean isAwake) { - - } - - @Override - public void onKeyguardStateChanged(boolean isShowing) { - if (isShowing) { - latch.countDown(); - } - } - }; - - getActivityTaskManagerInternal().registerScreenObserver(screenObserver); - getWindowManager().lockDeviceNow(); + Bundle bundle = new Bundle(); + bundle.putBinder(LOCK_ON_USER_SWITCH_CALLBACK, new IRemoteCallback.Stub() { + public void sendResult(Bundle data) { + latch.countDown(); + } + }); + getWindowManager().lockNow(bundle); try { if (!latch.await(20, TimeUnit.SECONDS)) { - throw new RuntimeException("Keyguard is not shown in 20 seconds"); + throw new RuntimeException("User controller expected a callback while waiting " + + "to show the keyguard. Timed out after 20 seconds."); } } catch (InterruptedException e) { throw new RuntimeException(e); } finally { - getActivityTaskManagerInternal().unregisterScreenObserver(screenObserver); t.traceEnd(); } } diff --git a/services/core/java/com/android/server/am/flags.aconfig b/services/core/java/com/android/server/am/flags.aconfig index e89889e794ba..27c384a22fb6 100644 --- a/services/core/java/com/android/server/am/flags.aconfig +++ b/services/core/java/com/android/server/am/flags.aconfig @@ -205,13 +205,6 @@ flag { } flag { - name: "reset_on_fork_enabled" - namespace: "system_performance" - description: "Set reset_on_fork flag." - bug: "370988407" -} - -flag { name: "push_global_state_to_oomadjuster" namespace: "backstage_power" description: "Migrate OomAdjuster pulled device state to a push model" @@ -319,3 +312,14 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "lower_sms_oom_importance" + namespace: "backstage_power" + description: "Lower messaging app process oom importance to PERCEPTIBLE_APP_ADJ + 1." + bug: "372511805" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/services/core/java/com/android/server/appbinding/AppBindingConstants.java b/services/core/java/com/android/server/appbinding/AppBindingConstants.java index 71847694b1d8..561cc0ef0a3b 100644 --- a/services/core/java/com/android/server/appbinding/AppBindingConstants.java +++ b/services/core/java/com/android/server/appbinding/AppBindingConstants.java @@ -102,9 +102,16 @@ public class AppBindingConstants { boolean smsServiceEnabled = parser.getBoolean(SMS_SERVICE_ENABLED_KEY, true); - int smsAppBindFlags = parser.getInt( - SMS_APP_BIND_FLAGS_KEY, - Context.BIND_NOT_VISIBLE | Context.BIND_FOREGROUND_SERVICE); + int smsAppBindFlags; + if (com.android.server.am.Flags.lowerSmsOomImportance()) { + smsAppBindFlags = parser.getInt( + SMS_APP_BIND_FLAGS_KEY, + Context.BIND_ALMOST_PERCEPTIBLE | Context.BIND_FOREGROUND_SERVICE); + } else { + smsAppBindFlags = parser.getInt( + SMS_APP_BIND_FLAGS_KEY, + Context.BIND_NOT_VISIBLE | Context.BIND_FOREGROUND_SERVICE); + } long serviceStableConnectionThresholdSec = parser.getLong( SERVICE_STABLE_CONNECTION_THRESHOLD_SEC_KEY, TimeUnit.MINUTES.toSeconds(2)); diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java index 4ec813827cbc..6d6e1fb6bfb3 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java +++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java @@ -1030,38 +1030,10 @@ public class AudioDeviceBroker { } } - BtDeviceInfo createBtDeviceInfo(@NonNull BtDeviceChangedData d, @NonNull BluetoothDevice device, - int state) { - int audioDevice; - int codec = AudioSystem.AUDIO_FORMAT_DEFAULT; - switch (d.mInfo.getProfile()) { - case BluetoothProfile.A2DP_SINK: - audioDevice = AudioSystem.DEVICE_IN_BLUETOOTH_A2DP; - break; - case BluetoothProfile.A2DP: - audioDevice = AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP; - break; - case BluetoothProfile.HEARING_AID: - audioDevice = AudioSystem.DEVICE_OUT_HEARING_AID; - break; - case BluetoothProfile.LE_AUDIO: - if (d.mInfo.isLeOutput()) { - audioDevice = AudioSystem.DEVICE_OUT_BLE_HEADSET; - } else { - audioDevice = AudioSystem.DEVICE_IN_BLE_HEADSET; - } - break; - case BluetoothProfile.LE_AUDIO_BROADCAST: - audioDevice = AudioSystem.DEVICE_OUT_BLE_BROADCAST; - break; - case BluetoothProfile.HEADSET: - // the actual device type is not important at this point and - // will be set by BtHelper.handleBtScoActiveDeviceChange() - audioDevice = AudioSystem.DEVICE_OUT_BLUETOOTH_SCO; - break; - default: throw new IllegalArgumentException("Invalid profile " + d.mInfo.getProfile()); - } - return new BtDeviceInfo(d, device, state, audioDevice, codec); + /*package*/ static BtDeviceInfo createBtDeviceInfo(@NonNull BtDeviceChangedData d, + @NonNull BluetoothDevice device, int state) { + int audioDevice = BtHelper.getTypeFromProfile(d.mInfo.getProfile(), d.mInfo.isLeOutput()); + return new BtDeviceInfo(d, device, state, audioDevice, AudioSystem.AUDIO_FORMAT_DEFAULT); } private void btMediaMetricRecord(@NonNull BluetoothDevice device, String state, @@ -1728,8 +1700,8 @@ public class AudioDeviceBroker { } // must be called synchronized on mConnectedDevices - /*package*/ boolean hasScheduledA2dpConnection(BluetoothDevice btDevice) { - final BtDeviceInfo devInfoToCheck = new BtDeviceInfo(btDevice, BluetoothProfile.A2DP); + /*package*/ boolean hasScheduledA2dpConnection(BluetoothDevice btDevice, int profile) { + final BtDeviceInfo devInfoToCheck = new BtDeviceInfo(btDevice, profile); return mBrokerHandler.hasEqualMessages(MSG_L_SET_BT_ACTIVE_DEVICE, devInfoToCheck); } diff --git a/services/core/java/com/android/server/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java index 37ef9a0648a6..ef10793fd955 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java +++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java @@ -987,8 +987,10 @@ public class AudioDeviceInventory { "onBluetoothDeviceConfigChange addr=" + address + " event=" + BtHelper.deviceEventToString(event))); + int deviceType = BtHelper.getTypeFromProfile(btInfo.mProfile, btInfo.mIsLeOutput); + synchronized (mDevicesLock) { - if (mDeviceBroker.hasScheduledA2dpConnection(btDevice)) { + if (mDeviceBroker.hasScheduledA2dpConnection(btDevice, btInfo.mProfile)) { AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( "A2dp config change ignored (scheduled connection change)") .printSlog(EventLogger.Event.ALOGI, TAG)); @@ -996,8 +998,7 @@ public class AudioDeviceInventory { .record(); return delayMs; } - final String key = DeviceInfo.makeDeviceListKey( - AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP, address); + final String key = DeviceInfo.makeDeviceListKey(deviceType, address); final DeviceInfo di = mConnectedDevices.get(key); if (di == null) { Log.e(TAG, "invalid null DeviceInfo in onBluetoothDeviceConfigChange"); @@ -1022,7 +1023,7 @@ public class AudioDeviceInventory { BtHelper.getName(btDevice), codec); if (res != AudioSystem.AUDIO_STATUS_OK) { AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( - "APM handleDeviceConfigChange failed for A2DP device addr=" + "APM handleDeviceConfigChange failed for device addr=" + address + " codec=" + AudioSystem.audioFormatToString(codec)) .printSlog(EventLogger.Event.ALOGE, TAG)); @@ -1033,7 +1034,7 @@ public class AudioDeviceInventory { BluetoothProfile.STATE_DISCONNECTED)); } else { AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( - "APM handleDeviceConfigChange success for A2DP device addr=" + "APM handleDeviceConfigChange success for device addr=" + address + " codec=" + AudioSystem.audioFormatToString(codec)) .printSlog(EventLogger.Event.ALOGI, TAG)); diff --git a/services/core/java/com/android/server/audio/BtHelper.java b/services/core/java/com/android/server/audio/BtHelper.java index 0479c70656b7..922116999bc7 100644 --- a/services/core/java/com/android/server/audio/BtHelper.java +++ b/services/core/java/com/android/server/audio/BtHelper.java @@ -1291,6 +1291,29 @@ public class BtHelper { return 0; // 0 is not a valid profile } + /*package */ static int getTypeFromProfile(int profile, boolean isLeOutput) { + switch (profile) { + case BluetoothProfile.A2DP_SINK: + return AudioSystem.DEVICE_IN_BLUETOOTH_A2DP; + case BluetoothProfile.A2DP: + return AudioSystem.DEVICE_OUT_BLUETOOTH_A2DP; + case BluetoothProfile.HEARING_AID: + return AudioSystem.DEVICE_OUT_HEARING_AID; + case BluetoothProfile.LE_AUDIO: + if (isLeOutput) { + return AudioSystem.DEVICE_OUT_BLE_HEADSET; + } else { + return AudioSystem.DEVICE_IN_BLE_HEADSET; + } + case BluetoothProfile.LE_AUDIO_BROADCAST: + return AudioSystem.DEVICE_OUT_BLE_BROADCAST; + case BluetoothProfile.HEADSET: + return AudioSystem.DEVICE_OUT_BLUETOOTH_SCO; + default: + throw new IllegalArgumentException("Invalid profile " + profile); + } + } + /*package */ static Bundle getPreferredAudioProfiles(String address) { BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); return adapter.getPreferredAudioProfiles(adapter.getRemoteDevice(address)); 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/DisplayDevice.java b/services/core/java/com/android/server/display/DisplayDevice.java index 2bdb5c25d0d5..a749a4013cdc 100644 --- a/services/core/java/com/android/server/display/DisplayDevice.java +++ b/services/core/java/com/android/server/display/DisplayDevice.java @@ -68,6 +68,8 @@ abstract class DisplayDevice { private int mCurrentLayerStack = -1; private int mCurrentFlags = 0; private int mCurrentOrientation = -1; + private int mLastDisplayWidth; + private int mLastDisplayHeight; private Rect mCurrentLayerStackRect; private Rect mCurrentDisplayRect; private final Context mContext; @@ -216,9 +218,9 @@ abstract class DisplayDevice { } /** - * Gives the display device a chance to update its properties while in a transaction. + * Updates the surface for the display. */ - public void performTraversalLocked(SurfaceControl.Transaction t) { + public void configureSurfaceLocked(SurfaceControl.Transaction t) { } /** @@ -374,6 +376,29 @@ abstract class DisplayDevice { } /** + * Configure transaction with the display size. + */ + public void configureDisplaySizeLocked(SurfaceControl.Transaction t) { + DisplayDeviceInfo info = getDisplayDeviceInfoLocked(); + boolean isInstalledRotated = info.installOrientation == ROTATION_90 + || info.installOrientation == ROTATION_270; + int displayWidth = isInstalledRotated ? info.height : info.width; + int displayHeight = isInstalledRotated ? info.width : info.height; + setDisplaySizeLocked(t, displayWidth, displayHeight); + } + + /** + * Sets display size while in a transaction. + */ + public final void setDisplaySizeLocked(SurfaceControl.Transaction t, int width, int height) { + if (width != mLastDisplayWidth && height != mLastDisplayHeight) { + mLastDisplayWidth = width; + mLastDisplayHeight = height; + t.setDisplaySize(mDisplayToken, width, height); + } + } + + /** * Sets the display surface while in a transaction. */ public final void setSurfaceLocked(SurfaceControl.Transaction t, Surface surface) { diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index 854b0dd7676b..b6a3f4041b13 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -3122,7 +3122,6 @@ public final class DisplayManagerService extends SystemService { displayTransactions.get(display.getDisplayIdLocked(), t); if (device != null) { configureDisplayLocked(displayTransaction, device); - device.performTraversalLocked(displayTransaction); } }); diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java index 83ca563e0534..324f95a5974b 100644 --- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java +++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java @@ -35,6 +35,7 @@ import android.os.Looper; import android.os.PowerManager; import android.os.SystemProperties; import android.os.Trace; +import android.util.DisplayMetrics; import android.util.DisplayUtils; import android.util.LongSparseArray; import android.util.Slog; @@ -84,6 +85,10 @@ final class LocalDisplayAdapter extends DisplayAdapter { private static final String PROPERTY_EMULATOR_CIRCULAR = "ro.boot.emulator.circular"; private static final double DEFAULT_DISPLAY_SIZE = 24.0; + // Touch target size 10.4mm in inches (divided by mm per inch 25.4) + private static final double EXTERNAL_DISPLAY_BASE_TOUCH_TARGET_SIZE_IN_INCHES = 10.4 / 25.4; + + private static final double BASE_TOUCH_TARGET_SIZE_DP = 48.0; private final LongSparseArray<LocalDisplayDevice> mDevices = new LongSparseArray<>(); @@ -530,17 +535,20 @@ final class LocalDisplayAdapter extends DisplayAdapter { if (densityMapping == null) { if (getFeatureFlags().isBaseDensityForExternalDisplaysEnabled() && !mStaticDisplayInfo.isInternal) { - double dpi; + double ppi; if (mActiveSfDisplayMode.xDpi > 0 && mActiveSfDisplayMode.yDpi > 0) { - dpi = Math.sqrt((Math.pow(mActiveSfDisplayMode.xDpi, 2) + ppi = Math.sqrt((Math.pow(mActiveSfDisplayMode.xDpi, 2) + Math.pow(mActiveSfDisplayMode.yDpi, 2)) / 2); } else { // xDPI and yDPI is missing, calculate DPI from display resolution and // default display size - dpi = Math.sqrt(Math.pow(mInfo.width, 2) + Math.pow(mInfo.height, 2)) + ppi = Math.sqrt(Math.pow(mInfo.width, 2) + Math.pow(mInfo.height, 2)) / DEFAULT_DISPLAY_SIZE; } + double pixels = ppi * EXTERNAL_DISPLAY_BASE_TOUCH_TARGET_SIZE_IN_INCHES; + double dpi = + pixels * DisplayMetrics.DENSITY_DEFAULT / BASE_TOUCH_TARGET_SIZE_DP; return (int) (dpi + 0.5); } return (int) (mStaticDisplayInfo.density * 160 + 0.5); diff --git a/services/core/java/com/android/server/display/LogicalDisplay.java b/services/core/java/com/android/server/display/LogicalDisplay.java index f9d413732e3e..b2b9ef17ec8d 100644 --- a/services/core/java/com/android/server/display/LogicalDisplay.java +++ b/services/core/java/com/android/server/display/LogicalDisplay.java @@ -228,14 +228,17 @@ final class LogicalDisplay { */ private final boolean mIsAnisotropyCorrectionEnabled; + private final boolean mSyncedResolutionSwitchEnabled; + private boolean mCanHostTasks; LogicalDisplay(int displayId, int layerStack, DisplayDevice primaryDisplayDevice) { - this(displayId, layerStack, primaryDisplayDevice, false, false); + this(displayId, layerStack, primaryDisplayDevice, false, false, false); } LogicalDisplay(int displayId, int layerStack, DisplayDevice primaryDisplayDevice, - boolean isAnisotropyCorrectionEnabled, boolean isAlwaysRotateDisplayDeviceEnabled) { + boolean isAnisotropyCorrectionEnabled, boolean isAlwaysRotateDisplayDeviceEnabled, + boolean isSyncedResolutionSwitchEnabled) { mDisplayId = displayId; mLayerStack = layerStack; mPrimaryDisplayDevice = primaryDisplayDevice; @@ -248,6 +251,7 @@ final class LogicalDisplay { mBaseDisplayInfo.thermalBrightnessThrottlingDataId = mThermalBrightnessThrottlingDataId; mIsAnisotropyCorrectionEnabled = isAnisotropyCorrectionEnabled; mAlwaysRotateDisplayDeviceEnabled = isAlwaysRotateDisplayDeviceEnabled; + mSyncedResolutionSwitchEnabled = isSyncedResolutionSwitchEnabled; mCanHostTasks = (mDisplayId == Display.DEFAULT_DISPLAY); } @@ -791,7 +795,12 @@ final class LogicalDisplay { } mDisplayPosition.set(mTempDisplayRect.left, mTempDisplayRect.top); + + if (mSyncedResolutionSwitchEnabled || displayDeviceInfo.type == Display.TYPE_VIRTUAL) { + device.configureDisplaySizeLocked(t); + } device.setProjectionLocked(t, orientation, mTempLayerStackRect, mTempDisplayRect); + device.configureSurfaceLocked(t); } /** diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java index ecc8896b69c6..02db051dff57 100644 --- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java +++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java @@ -1248,7 +1248,8 @@ class LogicalDisplayMapper implements DisplayDeviceRepository.Listener { final int layerStack = assignLayerStackLocked(displayId); final LogicalDisplay display = new LogicalDisplay(displayId, layerStack, device, mFlags.isPixelAnisotropyCorrectionInLogicalDisplayEnabled(), - mFlags.isAlwaysRotateDisplayDeviceEnabled()); + mFlags.isAlwaysRotateDisplayDeviceEnabled(), + mFlags.isSyncedResolutionSwitchEnabled()); display.updateLocked(mDisplayDeviceRepo, mSyntheticModeManager); final DisplayInfo info = display.getDisplayInfoLocked(); diff --git a/services/core/java/com/android/server/display/OverlayDisplayAdapter.java b/services/core/java/com/android/server/display/OverlayDisplayAdapter.java index 382c88327523..b5a9b19bc5c5 100644 --- a/services/core/java/com/android/server/display/OverlayDisplayAdapter.java +++ b/services/core/java/com/android/server/display/OverlayDisplayAdapter.java @@ -341,7 +341,7 @@ final class OverlayDisplayAdapter extends DisplayAdapter { } @Override - public void performTraversalLocked(SurfaceControl.Transaction t) { + public void configureSurfaceLocked(SurfaceControl.Transaction t) { if (mSurfaceTexture != null) { if (mSurface == null) { mSurface = new Surface(mSurfaceTexture); diff --git a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java index abbdeb9da364..4779b690adfb 100644 --- a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java +++ b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java @@ -484,14 +484,19 @@ public class VirtualDisplayAdapter extends DisplayAdapter { } @Override - public void performTraversalLocked(SurfaceControl.Transaction t) { - if ((mPendingChanges & PENDING_RESIZE) != 0) { - t.setDisplaySize(getDisplayTokenLocked(), mWidth, mHeight); - } + public void configureSurfaceLocked(SurfaceControl.Transaction t) { if ((mPendingChanges & PENDING_SURFACE_CHANGE) != 0) { setSurfaceLocked(t, mSurface); + mPendingChanges &= ~PENDING_SURFACE_CHANGE; + } + } + + @Override + public void configureDisplaySizeLocked(SurfaceControl.Transaction t) { + if ((mPendingChanges & PENDING_RESIZE) != 0) { + setDisplaySizeLocked(t, mWidth, mHeight); + mPendingChanges &= ~PENDING_RESIZE; } - mPendingChanges = 0; } @Override diff --git a/services/core/java/com/android/server/display/WifiDisplayAdapter.java b/services/core/java/com/android/server/display/WifiDisplayAdapter.java index 607c5d6a88bc..902eefa824b5 100644 --- a/services/core/java/com/android/server/display/WifiDisplayAdapter.java +++ b/services/core/java/com/android/server/display/WifiDisplayAdapter.java @@ -640,7 +640,7 @@ final class WifiDisplayAdapter extends DisplayAdapter { } @Override - public void performTraversalLocked(SurfaceControl.Transaction t) { + public void configureSurfaceLocked(SurfaceControl.Transaction t) { if (mSurface != null) { setSurfaceLocked(t, mSurface); } 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 aab2760dbc66..bc5d90599b41 100644 --- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java +++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java @@ -86,6 +86,11 @@ public class DisplayManagerFlags { com.android.graphics.surfaceflinger.flags.Flags.FLAG_DISPLAY_CONFIG_ERROR_HAL, com.android.graphics.surfaceflinger.flags.Flags::displayConfigErrorHal); + private final FlagState mSyncedResolutionSwitch = new FlagState( + com.android.graphics.surfaceflinger.flags.Flags.FLAG_SYNCED_RESOLUTION_SWITCH, + com.android.graphics.surfaceflinger.flags.Flags::syncedResolutionSwitch + ); + private final FlagState mBrightnessIntRangeUserPerceptionFlagState = new FlagState( Flags.FLAG_BRIGHTNESS_INT_RANGE_USER_PERCEPTION, Flags::brightnessIntRangeUserPerception); @@ -359,6 +364,10 @@ public class DisplayManagerFlags { return mDisplayConfigErrorHalFlagState.isEnabled(); } + public boolean isSyncedResolutionSwitchEnabled() { + return mSyncedResolutionSwitch.isEnabled(); + } + public boolean isBrightnessIntRangeUserPerceptionEnabled() { return mBrightnessIntRangeUserPerceptionFlagState.isEnabled(); } @@ -611,6 +620,7 @@ public class DisplayManagerFlags { pw.println(" " + mEvenDimmerFlagState); pw.println(" " + mSmallAreaDetectionFlagState); pw.println(" " + mDisplayConfigErrorHalFlagState); + pw.println(" " + mSyncedResolutionSwitch); pw.println(" " + mBrightnessIntRangeUserPerceptionFlagState); pw.println(" " + mRestrictDisplayModes); pw.println(" " + mBrightnessWearBedtimeModeClamperFlagState); diff --git a/services/core/java/com/android/server/dreams/DreamManagerService.java b/services/core/java/com/android/server/dreams/DreamManagerService.java index 7e8bb28b6a37..2af74f620c95 100644 --- a/services/core/java/com/android/server/dreams/DreamManagerService.java +++ b/services/core/java/com/android/server/dreams/DreamManagerService.java @@ -569,7 +569,8 @@ public final class DreamManagerService extends SystemService { } private void requestDreamInternal() { - if (isDreamingInternal() && !dreamIsFrontmost() && mController.bringDreamToFront()) { + if (isDreamingInternal() && !dreamIsFrontmost() && mController.bringDreamToFront() + && !isDozingInternal()) { return; } diff --git a/services/core/java/com/android/server/flags/people.aconfig b/services/core/java/com/android/server/flags/people.aconfig new file mode 100644 index 000000000000..532e95848a06 --- /dev/null +++ b/services/core/java/com/android/server/flags/people.aconfig @@ -0,0 +1,12 @@ +package: "com.android.server.flags" +container: "system" + +flag { + name: "early_data_manager_init" + namespace: "system_performance" + description: "Initialize DataManager earlier in boot" + bug: "394601872" + metadata { + purpose: PURPOSE_BUGFIX + } +} 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/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 484b47022f04..334e7b5240ce 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -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; } 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..4cf439611852 100644 --- a/services/core/java/com/android/server/media/quality/MediaQualityService.java +++ b/services/core/java/com/android/server/media/quality/MediaQualityService.java @@ -127,17 +127,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,16 +166,18 @@ 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()); } @@ -225,7 +227,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 +234,6 @@ public class MediaQualityService extends SystemService { pp.getPackageName(), pp.getInputId(), pp.getParameters()); - updateDatabaseOnPictureProfileAndNotifyManagerAndHal(values, pp.getParameters()); } } @@ -792,7 +792,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 +808,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; } @@ -1112,8 +1117,6 @@ public class MediaQualityService extends SystemService { private final class MqDatabaseUtils { - MediaQualityDbHelper mMediaQualityDbHelper; - private PictureProfile getPictureProfile(Long dbId) { String selection = BaseParameters.PARAMETER_ID + " = ?"; String[] selectionArguments = {Long.toString(dbId)}; @@ -1205,8 +1208,7 @@ public class MediaQualityService extends SystemService { /*groupBy=*/ null, /*having=*/ null, /*orderBy=*/ null); } - private MqDatabaseUtils(Context context) { - mMediaQualityDbHelper = new MediaQualityDbHelper(context); + private MqDatabaseUtils() { } } @@ -1404,11 +1406,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 +1433,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 +1495,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 +1546,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 +1597,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 3a3deb00562e..09c8b5ba823e 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; @@ -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)) { @@ -5244,6 +5248,21 @@ public class NotificationManagerService extends SystemService { } @Override + @FlaggedApi(android.app.Flags.FLAG_NM_BINDER_PERF_GET_APPS_WITH_CHANNELS) + public List<String> getPackagesWithAnyChannels(int userId) throws RemoteException { + checkCallerIsSystem(); + UserHandle user = UserHandle.of(userId); + List<String> packages = mPreferencesHelper.getPackagesWithAnyChannels(userId); + for (int i = packages.size() - 1; i >= 0; i--) { + String pkg = packages.get(i); + if (!areNotificationsEnabledForPackage(pkg, getUidForPackageAndUser(pkg, user))) { + packages.remove(i); + } + } + return packages; + } + + @Override public void clearData(String packageName, int uid, boolean fromApp) throws RemoteException { boolean packagesChanged = false; checkCallerIsSystem(); @@ -5715,12 +5734,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); @@ -5747,16 +5767,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 { @@ -6534,6 +6554,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) @@ -6938,7 +6965,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 @@ -13150,7 +13178,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. @@ -13790,7 +13819,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; } @@ -13818,7 +13847,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; } @@ -13874,7 +13903,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/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java index a171ffc2ed98..3974c839fd38 100644 --- a/services/core/java/com/android/server/notification/PreferencesHelper.java +++ b/services/core/java/com/android/server/notification/PreferencesHelper.java @@ -2006,6 +2006,29 @@ public class PreferencesHelper implements RankingConfig { } /** + * Gets all apps for this user that have a nonzero number of channels. This count does not + * include deleted channels. + */ + @FlaggedApi(android.app.Flags.FLAG_NM_BINDER_PERF_GET_APPS_WITH_CHANNELS) + public @NonNull List<String> getPackagesWithAnyChannels(@UserIdInt int userId) { + List<String> pkgs = new ArrayList<>(); + synchronized (mLock) { + for (PackagePreferences p : mPackagePreferences.values()) { + if (UserHandle.getUserId(p.uid) != userId) { + continue; + } + for (NotificationChannel c : p.channels.values()) { + if (!c.isDeleted()) { + pkgs.add(p.pkg); + break; + } + } + } + } + return pkgs; + } + + /** * True for pre-O apps that only have the default channel, or pre O apps that have no * channels yet. This method will create the default channel for pre-O apps that don't have it. * Should never be true for O+ targeting apps, but that's enforced on boot/when an app 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/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index 55d2508d085e..15688c0f7366 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -1021,7 +1021,7 @@ final class InstallPackageHelper { * * Failure at any phase will result in a full failure to install all packages. */ - void installPackagesTraced(List<InstallRequest> requests) { + void installPackagesTraced(List<InstallRequest> requests, MoveInfo moveInfo) { Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "installPackages"); boolean success = false; final Map<String, Boolean> createdAppId = new ArrayMap<>(requests.size()); @@ -1049,10 +1049,37 @@ final class InstallPackageHelper { } finally { completeInstallProcess(requests, createdAppId, success); Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER); + doPostInstall(requests, moveInfo); releaseWakeLock(acquireTime, requests.size()); } } + private void doPostInstall(List<InstallRequest> requests, MoveInfo moveInfo) { + for (InstallRequest request : requests) { + doPostInstallCleanUp(request, moveInfo); + } + + for (InstallRequest request : requests) { + restoreAndPostInstall(request); + } + } + + private void doPostInstallCleanUp(InstallRequest request, MoveInfo moveInfo) { + if (moveInfo != null) { + if (request.getReturnCode() == PackageManager.INSTALL_SUCCEEDED) { + mRemovePackageHelper.cleanUpForMoveInstall(moveInfo.mFromUuid, + moveInfo.mPackageName, moveInfo.mFromCodePath); + } else { + mRemovePackageHelper.cleanUpForMoveInstall(moveInfo.mToUuid, + moveInfo.mPackageName, moveInfo.mFromCodePath); + } + } else { + if (request.getReturnCode() != PackageManager.INSTALL_SUCCEEDED) { + mRemovePackageHelper.removeCodePath(request.getCodeFile()); + } + } + } + private long acquireWakeLock(int count) { if (!mPm.isSystemReady()) { return -1; diff --git a/services/core/java/com/android/server/pm/InstallingSession.java b/services/core/java/com/android/server/pm/InstallingSession.java index 6a2bf83ba368..3d8f2bbf4e7f 100644 --- a/services/core/java/com/android/server/pm/InstallingSession.java +++ b/services/core/java/com/android/server/pm/InstallingSession.java @@ -550,31 +550,11 @@ class InstallingSession { cleanUpForFailedInstall(request); } } - } else { - mPm.installPackagesTraced(installRequests); - for (InstallRequest request : installRequests) { - doPostInstall(request); - } - } - for (InstallRequest request : installRequests) { - mPm.restoreAndPostInstall(request); - } - } - - private void doPostInstall(InstallRequest request) { - if (mMoveInfo != null) { - if (request.getReturnCode() == PackageManager.INSTALL_SUCCEEDED) { - mPm.cleanUpForMoveInstall(mMoveInfo.mFromUuid, - mMoveInfo.mPackageName, mMoveInfo.mFromCodePath); - } else { - mPm.cleanUpForMoveInstall(mMoveInfo.mToUuid, - mMoveInfo.mPackageName, mMoveInfo.mFromCodePath); + mPm.restoreAndPostInstall(request); } } else { - if (request.getReturnCode() != PackageManager.INSTALL_SUCCEEDED) { - mPm.removeCodePath(request.getCodeFile()); - } + mPm.installPackagesTraced(installRequests, mMoveInfo); } } diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 8343935425cd..2464a291b4dd 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -8214,8 +8214,8 @@ public class PackageManagerService implements PackageSender, TestUtilityService return mInstallPackageHelper.enableCompressedPackage(stubPkg, stubPs); } - void installPackagesTraced(List<InstallRequest> requests) { - mInstallPackageHelper.installPackagesTraced(requests); + void installPackagesTraced(List<InstallRequest> requests, MoveInfo moveInfo) { + mInstallPackageHelper.installPackagesTraced(requests, moveInfo); } void restoreAndPostInstall(InstallRequest request) { diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 76c5240ab623..4153cd1be0a6 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -1163,6 +1163,15 @@ 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 @@ -1261,9 +1270,10 @@ public class PhoneWindowManager implements WindowManagerPolicy { // show hub. boolean keyguardAvailable = !mLockPatternUtils.isLockScreenDisabled( mCurrentUserId); - if (mUserManagerInternal.isUserUnlocked(mCurrentUserId) && hubEnabled - && keyguardAvailable && mDreamManagerInternal.dreamConditionActive()) { - // If the hub can be launched, send a message to keyguard. + 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. Bundle options = new Bundle(); options.putBoolean(EXTRA_TRIGGER_HUB, true); lockNow(options); @@ -1324,14 +1334,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 void attemptToDreamFromShortPowerButtonPress( + private boolean 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; + return false; } final DreamManagerInternal dreamManagerInternal = getDreamManagerInternal(); @@ -1339,7 +1349,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; + return false; } synchronized (mLock) { @@ -1350,6 +1360,8 @@ public class PhoneWindowManager implements WindowManagerPolicy { } dreamManagerInternal.requestDream(); + + return true; } /** @@ -6398,6 +6410,17 @@ 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/policy/keyguard/KeyguardServiceDelegate.java b/services/core/java/com/android/server/policy/keyguard/KeyguardServiceDelegate.java index 587447b8af26..9d7e9f53a1aa 100644 --- a/services/core/java/com/android/server/policy/keyguard/KeyguardServiceDelegate.java +++ b/services/core/java/com/android/server/policy/keyguard/KeyguardServiceDelegate.java @@ -92,6 +92,8 @@ public class KeyguardServiceDelegate { public boolean bootCompleted; public int screenState; public int interactiveState; + boolean doKeyguardTimeoutRequested; + Bundle doKeyguardTimeoutRequestedOptions; private void reset() { // Assume keyguard is showing and secure until we know for sure. This is here in @@ -225,6 +227,12 @@ public class KeyguardServiceDelegate { if (mKeyguardState.dreaming) { mKeyguardService.onDreamingStarted(); } + if (mKeyguardState.doKeyguardTimeoutRequested) { + mKeyguardService.doKeyguardTimeout( + mKeyguardState.doKeyguardTimeoutRequestedOptions); + mKeyguardState.doKeyguardTimeoutRequested = false; + mKeyguardState.doKeyguardTimeoutRequestedOptions = null; + } } @Override @@ -410,6 +418,11 @@ public class KeyguardServiceDelegate { public void doKeyguardTimeout(Bundle options) { if (mKeyguardService != null) { mKeyguardService.doKeyguardTimeout(options); + } else { + mKeyguardState.doKeyguardTimeoutRequested = true; + if (options != null) { + mKeyguardState.doKeyguardTimeoutRequestedOptions = options; + } } } 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/stats/pull/StatsPullAtomService.java b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java index 7f2c68ff60b1..889b494ef538 100644 --- a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java +++ b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java @@ -3661,16 +3661,17 @@ public class StatsPullAtomService extends SystemService { if (!packageNames.isEmpty()) { for (String packageName : packageNames) { - PackageInfo pkg; + int uid = INVALID_UID; try { - pkg = pm.getPackageInfoAsUser(packageName, 0, userId); + PackageInfo pkg = pm.getPackageInfoAsUser(packageName, 0, userId); + uid = pkg.applicationInfo.uid; } catch (PackageManager.NameNotFoundException e) { - Slog.w(TAG, "Role holder " + packageName + " not found"); - return StatsManager.PULL_SKIP; + Slog.w(TAG, "Role holder " + packageName + " not found for user " + + userId); } pulledData.add(FrameworkStatsLog.buildStatsEvent( - atomTag, pkg.applicationInfo.uid, packageName, roleName)); + atomTag, uid, packageName, roleName)); } } else { // Ensure that roles set to None are logged with an empty state. @@ -3679,6 +3680,9 @@ public class StatsPullAtomService extends SystemService { } } } + } catch (Throwable t) { + Log.e(TAG, "Could not read role holders", t); + return StatsManager.PULL_SKIP; } finally { Binder.restoreCallingIdentity(callingToken); } diff --git a/services/core/java/com/android/server/updates/CertPinInstallReceiver.java b/services/core/java/com/android/server/updates/CertPinInstallReceiver.java index c03fbc3d0aed..250e99b47b1a 100644 --- a/services/core/java/com/android/server/updates/CertPinInstallReceiver.java +++ b/services/core/java/com/android/server/updates/CertPinInstallReceiver.java @@ -16,9 +16,19 @@ package com.android.server.updates; +import android.content.Context; +import android.content.Intent; + public class CertPinInstallReceiver extends ConfigUpdateInstallReceiver { public CertPinInstallReceiver() { super("/data/misc/keychain/", "pins", "metadata/", "version"); } + + @Override + public void onReceive(final Context context, final Intent intent) { + if (!com.android.server.flags.Flags.certpininstallerRemoval()) { + super.onReceive(context, intent); + } + } } diff --git a/services/core/java/com/android/server/wearable/WearableSensingManagerPerUserService.java b/services/core/java/com/android/server/wearable/WearableSensingManagerPerUserService.java index 395816902592..d06827ab0529 100644 --- a/services/core/java/com/android/server/wearable/WearableSensingManagerPerUserService.java +++ b/services/core/java/com/android/server/wearable/WearableSensingManagerPerUserService.java @@ -273,6 +273,9 @@ final class WearableSensingManagerPerUserService @Override public void onError() { + synchronized (mLock) { + ensureRemoteServiceInitiated(); + } synchronized (mSecureChannelLock) { if (mSecureChannel != null && mSecureChannel diff --git a/services/core/java/com/android/server/wearable/WearableSensingSecureChannel.java b/services/core/java/com/android/server/wearable/WearableSensingSecureChannel.java index a16ff51e2d20..9f14ab7a70d3 100644 --- a/services/core/java/com/android/server/wearable/WearableSensingSecureChannel.java +++ b/services/core/java/com/android/server/wearable/WearableSensingSecureChannel.java @@ -156,6 +156,7 @@ final class WearableSensingSecureChannel { new AssociationRequest.Builder() .setDisplayName(CDM_ASSOCIATION_DISPLAY_NAME) .setSelfManaged(true) + .setDeviceProfile(AssociationRequest.DEVICE_PROFILE_WEARABLE_SENSING) .build(), mLightWeightExecutor, new CompanionDeviceManager.Callback() { @@ -195,7 +196,8 @@ final class WearableSensingSecureChannel { mCompanionDeviceManager.attachSystemDataTransport( associationId, new AutoCloseInputStream(mUnderlyingTransport), - new AutoCloseOutputStream(mUnderlyingTransport)); + new AutoCloseOutputStream(mUnderlyingTransport), + CompanionDeviceManager.TRANSPORT_FLAG_EXTEND_PATCH_DIFF); } } diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index cf9c57aa634a..c37b5a055140 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -46,9 +46,6 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.app.WindowConfiguration.activityTypeToString; -import static android.app.admin.DevicePolicyResources.Drawables.Source.PROFILE_SWITCH_ANIMATION; -import static android.app.admin.DevicePolicyResources.Drawables.Style.OUTLINE; -import static android.app.admin.DevicePolicyResources.Drawables.WORK_PROFILE_ICON; import static android.content.Context.CONTEXT_RESTRICTED; import static android.content.Intent.ACTION_MAIN; import static android.content.Intent.CATEGORY_HOME; @@ -189,7 +186,6 @@ import static com.android.server.wm.ActivityRecordProto.STARTING_DISPLAYED; import static com.android.server.wm.ActivityRecordProto.STARTING_MOVED; import static com.android.server.wm.ActivityRecordProto.STARTING_WINDOW; import static com.android.server.wm.ActivityRecordProto.STATE; -import static com.android.server.wm.ActivityRecordProto.THUMBNAIL; import static com.android.server.wm.ActivityRecordProto.TRANSLUCENT; import static com.android.server.wm.ActivityRecordProto.VISIBLE; import static com.android.server.wm.ActivityRecordProto.VISIBLE_REQUESTED; @@ -265,7 +261,6 @@ import android.app.PictureInPictureParams; import android.app.ResultInfo; import android.app.WaitResult; import android.app.WindowConfiguration; -import android.app.admin.DevicePolicyManager; import android.app.assist.ActivityId; import android.app.compat.CompatChanges; import android.app.servertransaction.ActivityConfigurationChangeItem; @@ -300,7 +295,6 @@ import android.graphics.Insets; import android.graphics.PixelFormat; import android.graphics.Point; import android.graphics.Rect; -import android.graphics.drawable.Drawable; import android.gui.DropInputMode; import android.hardware.HardwareBuffer; import android.net.Uri; @@ -333,7 +327,6 @@ import android.view.IAppTransitionAnimationSpecsFuture; import android.view.InputApplicationHandle; import android.view.RemoteAnimationAdapter; import android.view.RemoteAnimationDefinition; -import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; import android.view.SurfaceControl.Transaction; import android.view.WindowInsets; @@ -341,7 +334,6 @@ import android.view.WindowInsets.Type; import android.view.WindowManager; import android.view.WindowManager.LayoutParams; import android.view.WindowManager.TransitionOldType; -import android.view.animation.Animation; import android.window.ActivityWindowInfo; import android.window.ITaskFragmentOrganizer; import android.window.RemoteTransition; @@ -380,7 +372,6 @@ import com.android.server.uri.UriPermissionOwner; import com.android.server.wm.ActivityMetricsLogger.TransitionInfoSnapshot; import com.android.server.wm.SurfaceAnimator.AnimationType; import com.android.server.wm.WindowManagerService.H; -import com.android.server.wm.utils.InsetUtils; import com.android.window.flags.Flags; import dalvik.annotation.optimization.NeverCompile; @@ -4701,8 +4692,6 @@ final class ActivityRecord extends WindowToken { return true; } - // TODO: Transfer thumbnail - return false; } @@ -5707,7 +5696,21 @@ final class ActivityRecord extends WindowToken { displayContent.getInputMonitor().updateInputWindowsLw(false /*force*/); mTransitionChangeFlags = 0; - postApplyAnimation(visible, fromTransition); + // Set client visibility if: + // 1. The activity is becoming visible. This is usually no-op because assume that + // setVisibility(true) should have been called. Just in case if that was missed. + // 2. The activity is becoming invisible and not RESUMED state (it is usually PAUSED unless + // the activity is transient-hide). If the state is RESUMED, setVisibility(false) will be + // called until activityStopped. This is to avoid crashing apps that assume its view root + // won't be invisible before the activity is paused. + if (visible || mState != RESUMED) { + setClientVisible(visible); + } + // Notify the visibility change outside of transition in case onTransitionFinish is not + // called for updating snapshot states. + if (!fromTransition) { + mWmService.mSnapshotController.notifyAppVisibilityChanged(this, visible); + } } void commitVisibility(boolean visible, boolean performLayout) { @@ -5726,75 +5729,6 @@ final class ActivityRecord extends WindowToken { return mNeedsLetterboxedAnimation && isAnimating(); } - /** - * Post process after applying an app transition animation. - * - * <p class="note"><strong>Note: </strong> This function must be called after the animations - * have been applied and {@link #commitVisibility}.</p> - * - * @param visible {@code true} if this {@link ActivityRecord} has become visible, otherwise - * this has become invisible. - * @param fromTransition {@code true} if this call is part of finishing a transition. This is - * needed because the shell transition is no-longer active by the time - * commitVisibility is called. - */ - private void postApplyAnimation(boolean visible, boolean fromTransition) { - final boolean usingShellTransitions = mTransitionController.isShellTransitionsEnabled(); - final boolean delayed = !usingShellTransitions && isAnimating(PARENTS | CHILDREN, - ANIMATION_TYPE_APP_TRANSITION | ANIMATION_TYPE_WINDOW_ANIMATION); - if (!delayed && !usingShellTransitions) { - // We aren't delayed anything, but exiting windows rely on the animation finished - // callback being called in case the ActivityRecord was pretending to be delayed, - // which we might have done because we were in closing/opening apps list. - onAnimationFinished(ANIMATION_TYPE_APP_TRANSITION, null /* AnimationAdapter */); - if (visible) { - // The token was made immediately visible, there will be no entrance animation. - // We need to inform the client the enter animation was finished. - mEnteringAnimation = true; - mWmService.mActivityManagerAppTransitionNotifier.onAppTransitionFinishedLocked( - token); - } - } - - // If we're becoming visible, immediately change client visibility as well. there seem - // to be some edge cases where we change our visibility but client visibility never gets - // updated. - // If we're becoming invisible, update the client visibility if we are not running an - // animation and aren't in RESUMED state. Otherwise, we'll update client visibility in - // onAnimationFinished or activityStopped. - if (visible || (mState != RESUMED && (usingShellTransitions || !isAnimating( - PARENTS, ANIMATION_TYPE_APP_TRANSITION)))) { - setClientVisible(visible); - } - - final DisplayContent displayContent = getDisplayContent(); - if (!displayContent.mClosingApps.contains(this) - && !displayContent.mOpeningApps.contains(this) - && !fromTransition) { - // Take the screenshot before possibly hiding the WSA, otherwise the screenshot - // will not be taken. - mWmService.mSnapshotController.notifyAppVisibilityChanged(this, visible); - } - - // If we are hidden but there is no delay needed we immediately - // apply the Surface transaction so that the ActivityManager - // can have some guarantee on the Surface state following - // setting the visibility. This captures cases like dismissing - // the docked or root pinned task where there is no app transition. - // - // In the case of a "Null" animation, there will be - // no animation but there will still be a transition set. - // We still need to delay hiding the surface such that it - // can be synchronized with showing the next surface in the transition. - if (!usingShellTransitions && !isVisible() && !delayed - && !displayContent.mAppTransition.isTransitionSet()) { - forAllWindows(win -> { - win.mWinAnimator.hide(getPendingTransaction(), "immediately hidden"); - }, true); - scheduleAnimation(); - } - } - /** Updates draw state and shows drawn windows. */ void commitFinishDrawing(SurfaceControl.Transaction t) { boolean committed = false; @@ -7597,9 +7531,6 @@ final class ActivityRecord extends WindowToken { mActivityRecordInputSink.applyChangesToSurfaceIfChanged(getPendingTransaction()); } } - if (mThumbnail != null) { - mThumbnail.setShowing(getPendingTransaction(), show); - } mLastSurfaceShowing = show; super.prepareSurfaces(); } @@ -7611,84 +7542,6 @@ final class ActivityRecord extends WindowToken { return mLastSurfaceShowing; } - void attachThumbnailAnimation() { - if (!isAnimating(PARENTS, ANIMATION_TYPE_APP_TRANSITION)) { - return; - } - final HardwareBuffer thumbnailHeader = - getDisplayContent().mAppTransition.getAppTransitionThumbnailHeader(task); - if (thumbnailHeader == null) { - ProtoLog.d(WM_DEBUG_APP_TRANSITIONS, "No thumbnail header bitmap for: %s", task); - return; - } - clearThumbnail(); - final Transaction transaction = getAnimatingContainer().getPendingTransaction(); - mThumbnail = new WindowContainerThumbnail(transaction, getAnimatingContainer(), - thumbnailHeader); - mThumbnail.startAnimation(transaction, loadThumbnailAnimation(thumbnailHeader)); - } - - /** - * Attaches a surface with a thumbnail for the - * {@link android.app.ActivityOptions#ANIM_OPEN_CROSS_PROFILE_APPS} animation. - */ - void attachCrossProfileAppsThumbnailAnimation() { - if (!isAnimating(PARENTS, ANIMATION_TYPE_APP_TRANSITION)) { - return; - } - clearThumbnail(); - - final WindowState win = findMainWindow(); - if (win == null) { - return; - } - final Rect frame = win.getRelativeFrame(); - final Context context = mAtmService.getUiContext(); - final Drawable thumbnailDrawable; - if (task.mUserId == mWmService.mCurrentUserId) { - thumbnailDrawable = context.getDrawable(R.drawable.ic_account_circle); - } else { - final DevicePolicyManager dpm = context.getSystemService(DevicePolicyManager.class); - thumbnailDrawable = dpm.getResources().getDrawable( - WORK_PROFILE_ICON, OUTLINE, PROFILE_SWITCH_ANIMATION, - () -> context.getDrawable(R.drawable.ic_corp_badge)); - } - final HardwareBuffer thumbnail = getDisplayContent().mAppTransition - .createCrossProfileAppsThumbnail(thumbnailDrawable, frame); - if (thumbnail == null) { - return; - } - final Transaction transaction = getPendingTransaction(); - mThumbnail = new WindowContainerThumbnail(transaction, getTask(), thumbnail); - final Animation animation = - getDisplayContent().mAppTransition.createCrossProfileAppsThumbnailAnimationLocked( - frame); - mThumbnail.startAnimation(transaction, animation, new Point(frame.left, frame.top)); - } - - private Animation loadThumbnailAnimation(HardwareBuffer thumbnailHeader) { - final DisplayInfo displayInfo = mDisplayContent.getDisplayInfo(); - - // If this is a multi-window scenario, we use the windows frame as - // destination of the thumbnail header animation. If this is a full screen - // window scenario, we use the whole display as the target. - WindowState win = findMainWindow(); - Rect insets; - Rect appRect; - if (win != null) { - insets = win.getInsetsStateWithVisibilityOverride().calculateInsets( - win.getFrame(), Type.systemBars(), false /* ignoreVisibility */).toRect(); - appRect = new Rect(win.getFrame()); - appRect.inset(insets); - } else { - insets = null; - appRect = new Rect(0, 0, displayInfo.appWidth, displayInfo.appHeight); - } - final Configuration displayConfig = mDisplayContent.getConfiguration(); - return getDisplayContent().mAppTransition.createThumbnailAspectScaleAnimationLocked( - appRect, insets, thumbnailHeader, task, displayConfig.orientation); - } - @Override public void onAnimationLeashLost(Transaction t) { super.onAnimationLeashLost(t); @@ -7715,7 +7568,6 @@ final class ActivityRecord extends WindowToken { setAppLayoutChanges(FINISH_LAYOUT_REDO_ANIM | FINISH_LAYOUT_REDO_WALLPAPER, "ActivityRecord"); - clearThumbnail(); setClientVisible(isVisible() || mVisibleRequested); getDisplayContent().computeImeTargetIfNeeded(this); @@ -7725,12 +7577,6 @@ final class ActivityRecord extends WindowToken { this, reportedVisible, okToDisplay(), okToAnimate(), isStartingWindowDisplayed()); - // clean up thumbnail window - if (mThumbnail != null) { - mThumbnail.destroy(); - mThumbnail = null; - } - // WindowState.onExitAnimationDone might modify the children list, so make a copy and then // traverse the copy. final ArrayList<WindowState> children = new ArrayList<>(mChildren); @@ -7769,20 +7615,6 @@ final class ActivityRecord extends WindowToken { } } - @Override - void cancelAnimation() { - super.cancelAnimation(); - clearThumbnail(); - } - - private void clearThumbnail() { - if (mThumbnail == null) { - return; - } - mThumbnail.destroy(); - mThumbnail = null; - } - public @TransitionOldType int getTransit() { return mTransit; } @@ -8219,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. @@ -8247,6 +8080,9 @@ final class ActivityRecord extends WindowToken { resolvedConfig.windowConfiguration.setMaxBounds(mTmpBounds); } + mAppCompatController.getSandboxingPolicy().sandboxBoundsIfNeeded(resolvedConfig, + parentWindowingMode); + applySizeOverrideIfNeeded( mDisplayContent, info.applicationInfo, @@ -9728,9 +9564,6 @@ final class ActivityRecord extends WindowToken { proto.write(IS_WAITING_FOR_TRANSITION_START, isWaitingForTransitionStart()); proto.write(IS_ANIMATING, isAnimating(TRANSITION | PARENTS | CHILDREN, ANIMATION_TYPE_APP_TRANSITION | ANIMATION_TYPE_WINDOW_ANIMATION)); - if (mThumbnail != null){ - mThumbnail.dumpDebug(proto, THUMBNAIL); - } proto.write(FILLS_PARENT, fillsParent()); proto.write(APP_STOPPED, mAppStopped); proto.write(TRANSLUCENT, !occludesParent()); @@ -9834,31 +9667,6 @@ final class ActivityRecord extends WindowToken { } @Override - RemoteAnimationTarget createRemoteAnimationTarget( - RemoteAnimationController.RemoteAnimationRecord record) { - final WindowState mainWindow = findMainWindow(); - if (task == null || mainWindow == null) { - return null; - } - final Rect insets = mainWindow.getInsetsStateWithVisibilityOverride().calculateInsets( - task.getBounds(), Type.systemBars(), false /* ignoreVisibility */).toRect(); - InsetUtils.addInsets(insets, getLetterboxInsets()); - - final RemoteAnimationTarget target = new RemoteAnimationTarget(task.mTaskId, - record.getMode(), record.mAdapter.mCapturedLeash, !fillsParent(), - new Rect(), insets, - getPrefixOrderIndex(), record.mAdapter.mPosition, record.mAdapter.mLocalBounds, - record.mAdapter.mEndBounds, task.getWindowConfiguration(), - false /*isNotInRecents*/, - record.mThumbnailAdapter != null ? record.mThumbnailAdapter.mCapturedLeash : null, - record.mStartBounds, task.getTaskInfo(), checkEnterPictureInPictureAppOpsState()); - target.setShowBackdrop(record.mShowBackdrop); - target.setWillShowImeOnTarget(mStartingData != null && mStartingData.hasImeSurface()); - target.hasAnimatingParent = record.hasAnimatingParent(); - return target; - } - - @Override boolean canCreateRemoteAnimationTarget() { return true; } 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/AppTransition.java b/services/core/java/com/android/server/wm/AppTransition.java index 9c4b722feb47..d98ad8bb9e05 100644 --- a/services/core/java/com/android/server/wm/AppTransition.java +++ b/services/core/java/com/android/server/wm/AppTransition.java @@ -231,8 +231,6 @@ public class AppTransition implements Dump { private final int mDefaultWindowAnimationStyleResId; private boolean mOverrideTaskTransition; - private RemoteAnimationController mRemoteAnimationController; - final Handler mHandler; final Runnable mHandleAppTransitionTimeoutRunnable = () -> handleAppTransitionTimeout(); @@ -398,9 +396,7 @@ public class AppTransition implements Dump { : SystemClock.uptimeMillis(), AnimationAdapter.STATUS_BAR_TRANSITION_DURATION); - if (mRemoteAnimationController != null) { - mRemoteAnimationController.goodToGo(transit); - } else if ((isTaskOpenTransitOld(transit) || transit == TRANSIT_OLD_WALLPAPER_CLOSE) + if ((isTaskOpenTransitOld(transit) || transit == TRANSIT_OLD_WALLPAPER_CLOSE) && topOpeningAnim != null) { if (mDisplayContent.getDisplayPolicy().shouldAttachNavBarToAppDuringTransition()) { final NavBarFadeAnimationController controller = @@ -424,7 +420,6 @@ public class AppTransition implements Dump { mNextAppTransitionType = NEXT_TRANSIT_TYPE_NONE; mNextAppTransitionOverrideRequested = false; mNextAppTransitionAnimationsSpecs.clear(); - mRemoteAnimationController = null; mNextAppTransitionAnimationsSpecsFuture = null; mDefaultNextAppTransitionAnimationSpec = null; mAnimationFinishedCallback = null; @@ -442,13 +437,6 @@ public class AppTransition implements Dump { final boolean keyguardGoingAwayCancelled = mNextAppTransitionRequests.contains( TRANSIT_KEYGUARD_GOING_AWAY); - // The RemoteAnimationControl didn't register AppTransitionListener and - // only initialized the finish and timeout callback when goodToGo(). - // So cancel the remote animation here to prevent the animation can't do - // finish after transition state cleared. - if (mRemoteAnimationController != null) { - mRemoteAnimationController.cancelAnimation("freeze"); - } mNextAppTransitionRequests.clear(); clear(); setReady(); @@ -719,10 +707,6 @@ public class AppTransition implements Dump { && !mNextAppTransitionRequests.contains(TRANSIT_KEYGUARD_GOING_AWAY); } - RemoteAnimationController getRemoteAnimationController() { - return mRemoteAnimationController; - } - /** * * @param frame These are the bounds of the window when it finishes the animation. This is where @@ -1082,17 +1066,6 @@ public class AppTransition implements Dump { void overridePendingAppTransitionRemote(RemoteAnimationAdapter remoteAnimationAdapter, boolean sync, boolean isActivityEmbedding) { - ProtoLog.i(WM_DEBUG_APP_TRANSITIONS, "Override pending remote transitionSet=%b adapter=%s", - isTransitionSet(), remoteAnimationAdapter); - if (isTransitionSet() && !mNextAppTransitionIsSync) { - // ActivityEmbedding animation will run by the app process for which we want to respect - // the app override for whether or not to show background color. - clear(!isActivityEmbedding /* clearAppOverride */); - mNextAppTransitionType = NEXT_TRANSIT_TYPE_REMOTE; - mRemoteAnimationController = new RemoteAnimationController(mService, mDisplayContent, - remoteAnimationAdapter, mHandler, isActivityEmbedding); - mNextAppTransitionIsSync = sync; - } } void overrideInPlaceAppTransition(String packageName, int anim) { 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/NonAppWindowAnimationAdapter.java b/services/core/java/com/android/server/wm/NonAppWindowAnimationAdapter.java deleted file mode 100644 index 91598c5cc27a..000000000000 --- a/services/core/java/com/android/server/wm/NonAppWindowAnimationAdapter.java +++ /dev/null @@ -1,227 +0,0 @@ -/* - * Copyright (C) 2021 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server.wm; - -import static android.view.WindowManager.TRANSIT_OLD_KEYGUARD_GOING_AWAY; -import static android.view.WindowManager.TRANSIT_OLD_KEYGUARD_GOING_AWAY_ON_WALLPAPER; -import static android.view.WindowManager.TRANSIT_OLD_TASK_OPEN; -import static android.view.WindowManager.TRANSIT_OLD_TASK_TO_FRONT; -import static android.view.WindowManager.TRANSIT_OLD_WALLPAPER_CLOSE; - -import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_REMOTE_ANIMATIONS; -import static com.android.server.wm.AnimationAdapterProto.REMOTE; -import static com.android.server.wm.RemoteAnimationAdapterWrapperProto.TARGET; -import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_WINDOW_ANIMATION; - -import android.annotation.NonNull; -import android.graphics.Rect; -import android.os.SystemClock; -import android.util.proto.ProtoOutputStream; -import android.view.RemoteAnimationTarget; -import android.view.SurfaceControl; -import android.view.WindowManager; - -import com.android.internal.protolog.ProtoLog; -import com.android.server.policy.WindowManagerPolicy; - -import java.io.PrintWriter; -import java.util.ArrayList; - -class NonAppWindowAnimationAdapter implements AnimationAdapter { - - private final WindowContainer mWindowContainer; - private RemoteAnimationTarget mTarget; - private SurfaceControl mCapturedLeash; - private SurfaceAnimator.OnAnimationFinishedCallback mCapturedLeashFinishCallback; - private @SurfaceAnimator.AnimationType int mLastAnimationType; - - private long mDurationHint; - private long mStatusBarTransitionDelay; - - @Override - public boolean getShowWallpaper() { - return false; - } - - NonAppWindowAnimationAdapter(WindowContainer w, long durationHint, - long statusBarTransitionDelay) { - mWindowContainer = w; - mDurationHint = durationHint; - mStatusBarTransitionDelay = statusBarTransitionDelay; - } - - static RemoteAnimationTarget[] startNonAppWindowAnimations(WindowManagerService service, - DisplayContent displayContent, @WindowManager.TransitionOldType int transit, - long durationHint, long statusBarTransitionDelay, - ArrayList<NonAppWindowAnimationAdapter> adaptersOut) { - final ArrayList<RemoteAnimationTarget> targets = new ArrayList<>(); - if (shouldStartNonAppWindowAnimationsForKeyguardExit(transit)) { - startNonAppWindowAnimationsForKeyguardExit( - service, durationHint, statusBarTransitionDelay, targets, adaptersOut); - } else if (shouldAttachNavBarToApp(service, displayContent, transit)) { - startNavigationBarWindowAnimation( - displayContent, durationHint, statusBarTransitionDelay, targets, - adaptersOut); - } - return targets.toArray(new RemoteAnimationTarget[targets.size()]); - } - - static boolean shouldStartNonAppWindowAnimationsForKeyguardExit( - @WindowManager.TransitionOldType int transit) { - return transit == TRANSIT_OLD_KEYGUARD_GOING_AWAY - || transit == TRANSIT_OLD_KEYGUARD_GOING_AWAY_ON_WALLPAPER; - } - - static boolean shouldAttachNavBarToApp(WindowManagerService service, - DisplayContent displayContent, @WindowManager.TransitionOldType int transit) { - return (transit == TRANSIT_OLD_TASK_OPEN || transit == TRANSIT_OLD_TASK_TO_FRONT - || transit == TRANSIT_OLD_WALLPAPER_CLOSE) - && displayContent.getDisplayPolicy().shouldAttachNavBarToAppDuringTransition() - && displayContent.getAsyncRotationController() == null; - } - - /** - * Creates and starts remote animations for all the visible non app windows. - * - * @return RemoteAnimationTarget[] targets for all the visible non app windows - */ - private static void startNonAppWindowAnimationsForKeyguardExit(WindowManagerService service, - long durationHint, long statusBarTransitionDelay, - ArrayList<RemoteAnimationTarget> targets, - ArrayList<NonAppWindowAnimationAdapter> adaptersOut) { - - final WindowManagerPolicy policy = service.mPolicy; - service.mRoot.forAllWindows(nonAppWindow -> { - // Animation on the IME window is controlled via Insets. - if (nonAppWindow.mActivityRecord == null && nonAppWindow.canBeHiddenByKeyguard() - && nonAppWindow.wouldBeVisibleIfPolicyIgnored() && !nonAppWindow.isVisible() - && nonAppWindow != service.mRoot.getCurrentInputMethodWindow()) { - final NonAppWindowAnimationAdapter nonAppAdapter = new NonAppWindowAnimationAdapter( - nonAppWindow, durationHint, statusBarTransitionDelay); - adaptersOut.add(nonAppAdapter); - nonAppWindow.startAnimation(nonAppWindow.getPendingTransaction(), - nonAppAdapter, false /* hidden */, ANIMATION_TYPE_WINDOW_ANIMATION); - targets.add(nonAppAdapter.createRemoteAnimationTarget()); - } - }, true /* traverseTopToBottom */); - } - - /** - * Creates and starts remote animation for the navigation bar windows. - * - * @return RemoteAnimationTarget[] targets for all the visible non app windows - */ - private static void startNavigationBarWindowAnimation(DisplayContent displayContent, - long durationHint, long statusBarTransitionDelay, - ArrayList<RemoteAnimationTarget> targets, - ArrayList<NonAppWindowAnimationAdapter> adaptersOut) { - final WindowState navWindow = displayContent.getDisplayPolicy().getNavigationBar(); - final NonAppWindowAnimationAdapter nonAppAdapter = new NonAppWindowAnimationAdapter( - navWindow.mToken, durationHint, statusBarTransitionDelay); - adaptersOut.add(nonAppAdapter); - navWindow.mToken.startAnimation(navWindow.mToken.getPendingTransaction(), - nonAppAdapter, false /* hidden */, ANIMATION_TYPE_WINDOW_ANIMATION); - targets.add(nonAppAdapter.createRemoteAnimationTarget()); - } - - /** - * Create a remote animation target for this animation adapter. - */ - RemoteAnimationTarget createRemoteAnimationTarget() { - mTarget = new RemoteAnimationTarget(-1, -1, getLeash(), false, - new Rect(), null, mWindowContainer.getPrefixOrderIndex(), - mWindowContainer.getLastSurfacePosition(), mWindowContainer.getBounds(), null, - mWindowContainer.getWindowConfiguration(), true, null, null, null, false, - mWindowContainer.getWindowType()); - return mTarget; - } - - @Override - public void startAnimation(SurfaceControl animationLeash, SurfaceControl.Transaction t, - int type, @NonNull SurfaceAnimator.OnAnimationFinishedCallback finishCallback) { - ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, "startAnimation"); - mCapturedLeash = animationLeash; - mCapturedLeashFinishCallback = finishCallback; - mLastAnimationType = type; - } - - /** - * @return the callback to call to clean up when the animation has finished. - */ - SurfaceAnimator.OnAnimationFinishedCallback getLeashFinishedCallback() { - return mCapturedLeashFinishCallback; - } - - /** - * @return the type of animation. - */ - @SurfaceAnimator.AnimationType - int getLastAnimationType() { - return mLastAnimationType; - } - - WindowContainer getWindowContainer() { - return mWindowContainer; - } - - @Override - public long getDurationHint() { - return mDurationHint; - } - - @Override - public long getStatusBarTransitionsStartTime() { - return SystemClock.uptimeMillis() + mStatusBarTransitionDelay; - } - - /** - * @return the leash for this animation (only valid after the non app window surface animation - * has started). - */ - SurfaceControl getLeash() { - return mCapturedLeash; - } - - @Override - public void onAnimationCancelled(SurfaceControl animationLeash) { - ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, "onAnimationCancelled"); - } - - @Override - public void dump(PrintWriter pw, String prefix) { - pw.print(prefix); - pw.print("windowContainer="); - pw.println(mWindowContainer); - if (mTarget != null) { - pw.print(prefix); - pw.println("Target:"); - mTarget.dump(pw, prefix + " "); - } else { - pw.print(prefix); - pw.println("Target: null"); - } - } - - @Override - public void dumpDebug(ProtoOutputStream proto) { - final long token = proto.start(REMOTE); - if (mTarget != null) { - mTarget.dumpDebug(proto, TARGET); - } - proto.end(token); - } -} diff --git a/services/core/java/com/android/server/wm/RemoteAnimationController.java b/services/core/java/com/android/server/wm/RemoteAnimationController.java deleted file mode 100644 index b3b2c57550e4..000000000000 --- a/services/core/java/com/android/server/wm/RemoteAnimationController.java +++ /dev/null @@ -1,636 +0,0 @@ -/* - * Copyright (C) 2018 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 android.view.WindowManager.TRANSIT_OLD_KEYGUARD_UNOCCLUDE; - -import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_REMOTE_ANIMATIONS; -import static com.android.server.wm.AnimationAdapterProto.REMOTE; -import static com.android.server.wm.RemoteAnimationAdapterWrapperProto.TARGET; -import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME; -import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; - -import android.annotation.ColorInt; -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.graphics.Point; -import android.graphics.Rect; -import android.os.Binder; -import android.os.Handler; -import android.os.IBinder.DeathRecipient; -import android.os.RemoteException; -import android.os.SystemClock; -import android.util.Slog; -import android.util.proto.ProtoOutputStream; -import android.view.IRemoteAnimationFinishedCallback; -import android.view.RemoteAnimationAdapter; -import android.view.RemoteAnimationTarget; -import android.view.SurfaceControl; -import android.view.SurfaceControl.Transaction; -import android.view.WindowManager; - -import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.protolog.ProtoLog; -import com.android.internal.protolog.common.LogLevel; -import com.android.internal.util.FastPrintWriter; -import com.android.server.wm.SurfaceAnimator.AnimationType; -import com.android.server.wm.SurfaceAnimator.OnAnimationFinishedCallback; - -import java.io.PrintWriter; -import java.io.StringWriter; -import java.util.ArrayList; -import java.util.function.Consumer; - -/** - * Helper class to run app animations in a remote process. - */ -class RemoteAnimationController implements DeathRecipient { - private static final String TAG = TAG_WITH_CLASS_NAME - ? "RemoteAnimationController" : TAG_WM; - private static final long TIMEOUT_MS = 10000; - - private final WindowManagerService mService; - private final DisplayContent mDisplayContent; - private final RemoteAnimationAdapter mRemoteAnimationAdapter; - private final ArrayList<RemoteAnimationRecord> mPendingAnimations = new ArrayList<>(); - private final ArrayList<WallpaperAnimationAdapter> mPendingWallpaperAnimations = - new ArrayList<>(); - @VisibleForTesting - final ArrayList<NonAppWindowAnimationAdapter> mPendingNonAppAnimations = new ArrayList<>(); - private final Handler mHandler; - private final Runnable mTimeoutRunnable = () -> cancelAnimation("timeoutRunnable"); - private boolean mIsFinishing; - - private FinishedCallback mFinishedCallback; - private final boolean mIsActivityEmbedding; - private boolean mCanceled; - private boolean mLinkedToDeathOfRunner; - @Nullable - private Runnable mOnRemoteAnimationReady; - - RemoteAnimationController(WindowManagerService service, DisplayContent displayContent, - RemoteAnimationAdapter remoteAnimationAdapter, Handler handler, - boolean isActivityEmbedding) { - mService = service; - mDisplayContent = displayContent; - mRemoteAnimationAdapter = remoteAnimationAdapter; - mHandler = handler; - mIsActivityEmbedding = isActivityEmbedding; - } - - /** - * Creates an animation record for each individual {@link WindowContainer}. - * - * @param windowContainer The windows to animate. - * @param position The position app bounds relative to its parent. - * @param localBounds The bounds of the app relative to its parent. - * @param endBounds The end bounds after the transition, in screen coordinates. - * @param startBounds The start bounds before the transition, in screen coordinates. - * @param showBackdrop To show background behind a window during animation. - * @return The record representing animation(s) to run on the app. - */ - RemoteAnimationRecord createRemoteAnimationRecord(WindowContainer windowContainer, - Point position, Rect localBounds, Rect endBounds, Rect startBounds, - boolean showBackdrop) { - return createRemoteAnimationRecord(windowContainer, position, localBounds, endBounds, - startBounds, showBackdrop, startBounds != null /* shouldCreateSnapshot */); - } - - /** - * Creates an animation record for each individual {@link WindowContainer}. - * - * @param windowContainer The windows to animate. - * @param position The position app bounds relative to its parent. - * @param localBounds The bounds of the app relative to its parent. - * @param endBounds The end bounds after the transition, in screen coordinates. - * @param startBounds The start bounds before the transition, in screen coordinates. - * @param showBackdrop To show background behind a window during animation. - * @param shouldCreateSnapshot Whether this target should create a snapshot animation. - * @return The record representing animation(s) to run on the app. - */ - RemoteAnimationRecord createRemoteAnimationRecord(WindowContainer windowContainer, - Point position, Rect localBounds, Rect endBounds, Rect startBounds, - boolean showBackdrop, boolean shouldCreateSnapshot) { - ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, "createAnimationAdapter(): container=%s", - windowContainer); - final RemoteAnimationRecord adapters = new RemoteAnimationRecord(windowContainer, position, - localBounds, endBounds, startBounds, showBackdrop, shouldCreateSnapshot); - mPendingAnimations.add(adapters); - return adapters; - } - - /** Sets callback to run before starting remote animation. */ - void setOnRemoteAnimationReady(@Nullable Runnable onRemoteAnimationReady) { - mOnRemoteAnimationReady = onRemoteAnimationReady; - } - - /** - * We use isFromActivityEmbedding() in the server process to tell if we're running an - * Activity Embedding type remote animation, where animations are driven by the client. - * This is currently supporting features like showBackdrop where we need to load App XML. - */ - public boolean isFromActivityEmbedding() { - return mIsActivityEmbedding; - } - - /** - * Called when the transition is ready to be started, and all leashes have been set up. - */ - void goodToGo(@WindowManager.TransitionOldType int transit) { - ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, "goodToGo()"); - if (mCanceled) { - ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, - "goodToGo(): Animation canceled already"); - onAnimationFinished(); - invokeAnimationCancelled("already_cancelled"); - return; - } - - // Scale the timeout with the animator scale the controlling app is using. - mHandler.postDelayed(mTimeoutRunnable, - (long) (TIMEOUT_MS * mService.getCurrentAnimatorScale())); - mFinishedCallback = new FinishedCallback(this); - - // Create the app targets - final RemoteAnimationTarget[] appTargets = createAppAnimations(); - if (appTargets.length == 0 && !AppTransition.isKeyguardOccludeTransitOld(transit)) { - // Keyguard occlude transition can be executed before the occluding activity becomes - // visible. Even in this case, KeyguardService expects to receive binder call, so we - // don't cancel remote animation. - ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, - "goodToGo(): No apps to animate, mPendingAnimations=%d", - mPendingAnimations.size()); - onAnimationFinished(); - invokeAnimationCancelled("no_app_targets"); - return; - } - - if (mOnRemoteAnimationReady != null) { - mOnRemoteAnimationReady.run(); - mOnRemoteAnimationReady = null; - } - - // Create the remote wallpaper animation targets (if any) - final RemoteAnimationTarget[] wallpaperTargets = createWallpaperAnimations(); - - // Create the remote non app animation targets (if any) - final RemoteAnimationTarget[] nonAppTargets = createNonAppWindowAnimations(transit); - - mService.mAnimator.addAfterPrepareSurfacesRunnable(() -> { - try { - linkToDeathOfRunner(); - ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, "goodToGo(): onAnimationStart," - + " transit=%s, apps=%d, wallpapers=%d, nonApps=%d", - AppTransition.appTransitionOldToString(transit), appTargets.length, - wallpaperTargets.length, nonAppTargets.length); - if (AppTransition.isKeyguardOccludeTransitOld(transit)) { - EventLogTags.writeWmSetKeyguardOccluded( - transit == TRANSIT_OLD_KEYGUARD_UNOCCLUDE ? 0 : 1, - 1 /* animate */, - transit, - "onAnimationStart"); - } - mRemoteAnimationAdapter.getRunner().onAnimationStart(transit, appTargets, - wallpaperTargets, nonAppTargets, mFinishedCallback); - } catch (RemoteException e) { - Slog.e(TAG, "Failed to start remote animation", e); - onAnimationFinished(); - } - if (ProtoLog.isEnabled(WM_DEBUG_REMOTE_ANIMATIONS, LogLevel.DEBUG)) { - ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, "startAnimation(): Notify animation start:"); - writeStartDebugStatement(); - } - }); - setRunningRemoteAnimation(true); - } - - void cancelAnimation(String reason) { - ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, "cancelAnimation(): reason=%s", reason); - synchronized (mService.getWindowManagerLock()) { - if (mCanceled) { - return; - } - mCanceled = true; - } - onAnimationFinished(); - invokeAnimationCancelled(reason); - } - - private void writeStartDebugStatement() { - ProtoLog.i(WM_DEBUG_REMOTE_ANIMATIONS, "Starting remote animation"); - final StringWriter sw = new StringWriter(); - final FastPrintWriter pw = new FastPrintWriter(sw); - for (int i = mPendingAnimations.size() - 1; i >= 0; i--) { - mPendingAnimations.get(i).mAdapter.dump(pw, ""); - } - pw.close(); - ProtoLog.i(WM_DEBUG_REMOTE_ANIMATIONS, "%s", sw.toString()); - } - - private RemoteAnimationTarget[] createAppAnimations() { - ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, "createAppAnimations()"); - final ArrayList<RemoteAnimationTarget> targets = new ArrayList<>(); - for (int i = mPendingAnimations.size() - 1; i >= 0; i--) { - final RemoteAnimationRecord wrappers = mPendingAnimations.get(i); - final RemoteAnimationTarget target = wrappers.createRemoteAnimationTarget(); - if (target != null) { - ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, "\tAdd container=%s", - wrappers.mWindowContainer); - targets.add(target); - } else { - ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, "\tRemove container=%s", - wrappers.mWindowContainer); - - // We can't really start an animation but we still need to make sure to finish the - // pending animation that was started by SurfaceAnimator - if (wrappers.mAdapter != null - && wrappers.mAdapter.mCapturedFinishCallback != null) { - wrappers.mAdapter.mCapturedFinishCallback - .onAnimationFinished(wrappers.mAdapter.mAnimationType, - wrappers.mAdapter); - } - if (wrappers.mThumbnailAdapter != null - && wrappers.mThumbnailAdapter.mCapturedFinishCallback != null) { - wrappers.mThumbnailAdapter.mCapturedFinishCallback - .onAnimationFinished(wrappers.mThumbnailAdapter.mAnimationType, - wrappers.mThumbnailAdapter); - } - mPendingAnimations.remove(i); - } - } - return targets.toArray(new RemoteAnimationTarget[targets.size()]); - } - - private RemoteAnimationTarget[] createWallpaperAnimations() { - ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, "createWallpaperAnimations()"); - return WallpaperAnimationAdapter.startWallpaperAnimations(mDisplayContent, - mRemoteAnimationAdapter.getDuration(), - mRemoteAnimationAdapter.getStatusBarTransitionDelay(), - adapter -> { - synchronized (mService.mGlobalLock) { - // If the wallpaper animation is canceled, continue with the app animation - mPendingWallpaperAnimations.remove(adapter); - } - }, mPendingWallpaperAnimations); - } - - private RemoteAnimationTarget[] createNonAppWindowAnimations( - @WindowManager.TransitionOldType int transit) { - ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, "createNonAppWindowAnimations()"); - return NonAppWindowAnimationAdapter.startNonAppWindowAnimations(mService, - mDisplayContent, - transit, - mRemoteAnimationAdapter.getDuration(), - mRemoteAnimationAdapter.getStatusBarTransitionDelay(), - mPendingNonAppAnimations); - } - - private void onAnimationFinished() { - ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, "onAnimationFinished(): mPendingAnimations=%d", - mPendingAnimations.size()); - mHandler.removeCallbacks(mTimeoutRunnable); - synchronized (mService.mGlobalLock) { - mIsFinishing = true; - unlinkToDeathOfRunner(); - releaseFinishedCallback(); - try { - ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, - "onAnimationFinished(): Notify animation finished:"); - for (int i = mPendingAnimations.size() - 1; i >= 0; i--) { - final RemoteAnimationRecord adapters = mPendingAnimations.get(i); - if (adapters.mAdapter != null) { - adapters.mAdapter.mCapturedFinishCallback - .onAnimationFinished(adapters.mAdapter.mAnimationType, - adapters.mAdapter); - } - if (adapters.mThumbnailAdapter != null) { - adapters.mThumbnailAdapter.mCapturedFinishCallback - .onAnimationFinished(adapters.mThumbnailAdapter.mAnimationType, - adapters.mThumbnailAdapter); - } - mPendingAnimations.remove(i); - ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, "\tcontainer=%s", - adapters.mWindowContainer); - } - - for (int i = mPendingWallpaperAnimations.size() - 1; i >= 0; i--) { - final WallpaperAnimationAdapter adapter = mPendingWallpaperAnimations.get(i); - adapter.getLeashFinishedCallback().onAnimationFinished( - adapter.getLastAnimationType(), adapter); - mPendingWallpaperAnimations.remove(i); - ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, "\twallpaper=%s", adapter.getToken()); - } - - for (int i = mPendingNonAppAnimations.size() - 1; i >= 0; i--) { - final NonAppWindowAnimationAdapter adapter = mPendingNonAppAnimations.get(i); - adapter.getLeashFinishedCallback().onAnimationFinished( - adapter.getLastAnimationType(), adapter); - mPendingNonAppAnimations.remove(i); - ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, "\tnonApp=%s", - adapter.getWindowContainer()); - } - } catch (Exception e) { - Slog.e(TAG, "Failed to finish remote animation", e); - throw e; - } finally { - mIsFinishing = false; - } - // Reset input for all activities when the remote animation is finished. - final Consumer<ActivityRecord> updateActivities = - activity -> activity.setDropInputForAnimation(false); - mDisplayContent.forAllActivities(updateActivities); - } - setRunningRemoteAnimation(false); - ProtoLog.i(WM_DEBUG_REMOTE_ANIMATIONS, "Finishing remote animation"); - } - - private void invokeAnimationCancelled(String reason) { - ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, "cancelAnimation(): reason=%s", reason); - try { - mRemoteAnimationAdapter.getRunner().onAnimationCancelled(); - } catch (RemoteException e) { - Slog.e(TAG, "Failed to notify cancel", e); - } - mOnRemoteAnimationReady = null; - } - - private void releaseFinishedCallback() { - if (mFinishedCallback != null) { - mFinishedCallback.release(); - mFinishedCallback = null; - } - } - - private void setRunningRemoteAnimation(boolean running) { - final int pid = mRemoteAnimationAdapter.getCallingPid(); - final int uid = mRemoteAnimationAdapter.getCallingUid(); - - if (pid == 0) { - throw new RuntimeException("Calling pid of remote animation was null"); - } - final WindowProcessController wpc = mService.mAtmService.getProcessController(pid, uid); - if (wpc == null) { - Slog.w(TAG, "Unable to find process with pid=" + pid + " uid=" + uid); - return; - } - wpc.setRunningRemoteAnimation(running); - } - - private void linkToDeathOfRunner() throws RemoteException { - if (!mLinkedToDeathOfRunner) { - mRemoteAnimationAdapter.getRunner().asBinder().linkToDeath(this, 0); - mLinkedToDeathOfRunner = true; - } - } - - private void unlinkToDeathOfRunner() { - if (mLinkedToDeathOfRunner) { - mRemoteAnimationAdapter.getRunner().asBinder().unlinkToDeath(this, 0); - mLinkedToDeathOfRunner = false; - } - } - - @Override - public void binderDied() { - cancelAnimation("binderDied"); - } - - private static final class FinishedCallback extends IRemoteAnimationFinishedCallback.Stub { - - RemoteAnimationController mOuter; - - FinishedCallback(RemoteAnimationController outer) { - mOuter = outer; - } - - @Override - public void onAnimationFinished() throws RemoteException { - ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, "app-onAnimationFinished(): mOuter=%s", mOuter); - final long token = Binder.clearCallingIdentity(); - try { - if (mOuter != null) { - mOuter.onAnimationFinished(); - - // In case the client holds on to the finish callback, make sure we don't leak - // RemoteAnimationController which in turn would leak the runner on the client. - mOuter = null; - } - } finally { - Binder.restoreCallingIdentity(token); - } - } - - /** - * Marks this callback as not be used anymore by releasing the reference to the outer class - * to prevent memory leak. - */ - void release() { - ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, "app-release(): mOuter=%s", mOuter); - mOuter = null; - } - }; - - /** - * Contains information about a remote-animation for one WindowContainer. This keeps track of, - * potentially, multiple animating surfaces (AdapterWrappers) associated with one - * Window/Transition. For example, a change transition has an adapter controller for the - * main window and an adapter controlling the start-state snapshot. - * <p> - * This can be thought of as a bridge between the information that the remote animator sees (via - * {@link RemoteAnimationTarget}) and what the server sees (the - * {@link RemoteAnimationAdapterWrapper}(s) interfacing with the moving surfaces). - */ - public class RemoteAnimationRecord { - RemoteAnimationAdapterWrapper mAdapter; - RemoteAnimationAdapterWrapper mThumbnailAdapter = null; - RemoteAnimationTarget mTarget; - final WindowContainer mWindowContainer; - final Rect mStartBounds; - final boolean mShowBackdrop; - @ColorInt int mBackdropColor = 0; - private @RemoteAnimationTarget.Mode int mMode = RemoteAnimationTarget.MODE_CHANGING; - - RemoteAnimationRecord(WindowContainer windowContainer, Point endPos, Rect localBounds, - Rect endBounds, @Nullable Rect startBounds, boolean showBackdrop, - boolean shouldCreateSnapshot) { - mWindowContainer = windowContainer; - mShowBackdrop = showBackdrop; - if (startBounds != null) { - mStartBounds = new Rect(startBounds); - mAdapter = new RemoteAnimationAdapterWrapper(this, endPos, localBounds, endBounds, - mStartBounds, mShowBackdrop); - if (shouldCreateSnapshot && mRemoteAnimationAdapter.getChangeNeedsSnapshot()) { - final Rect thumbnailLocalBounds = new Rect(startBounds); - thumbnailLocalBounds.offsetTo(0, 0); - // Snapshot is located at (0,0) of the animation leash. It doesn't have size - // change, so the startBounds is its end bounds, and no start bounds for it. - mThumbnailAdapter = new RemoteAnimationAdapterWrapper(this, new Point(0, 0), - thumbnailLocalBounds, startBounds, new Rect(), mShowBackdrop); - } - } else { - mAdapter = new RemoteAnimationAdapterWrapper(this, endPos, localBounds, endBounds, - new Rect(), mShowBackdrop); - mStartBounds = null; - } - } - - void setBackDropColor(@ColorInt int backdropColor) { - mBackdropColor = backdropColor; - } - - RemoteAnimationTarget createRemoteAnimationTarget() { - if (mAdapter == null - || mAdapter.mCapturedFinishCallback == null - || mAdapter.mCapturedLeash == null) { - return null; - } - mTarget = mWindowContainer.createRemoteAnimationTarget(this); - return mTarget; - } - - void setMode(@RemoteAnimationTarget.Mode int mode) { - mMode = mode; - } - - int getMode() { - return mMode; - } - - /** Whether its parent is also an animation target in the same transition. */ - boolean hasAnimatingParent() { - // mOpeningApps and mClosingApps are only activities, so only need to check - // mChangingContainers. - for (int i = mDisplayContent.mChangingContainers.size() - 1; i >= 0; i--) { - if (mWindowContainer.isDescendantOf( - mDisplayContent.mChangingContainers.valueAt(i))) { - return true; - } - } - return false; - } - } - - class RemoteAnimationAdapterWrapper implements AnimationAdapter { - private final RemoteAnimationRecord mRecord; - SurfaceControl mCapturedLeash; - private OnAnimationFinishedCallback mCapturedFinishCallback; - private @AnimationType int mAnimationType; - final Point mPosition = new Point(); - final Rect mLocalBounds; - final Rect mEndBounds = new Rect(); - final Rect mStartBounds = new Rect(); - final boolean mShowBackdrop; - - RemoteAnimationAdapterWrapper(RemoteAnimationRecord record, Point position, - Rect localBounds, Rect endBounds, Rect startBounds, boolean showBackdrop) { - mRecord = record; - mPosition.set(position.x, position.y); - mLocalBounds = localBounds; - mEndBounds.set(endBounds); - mStartBounds.set(startBounds); - mShowBackdrop = showBackdrop; - } - - @Override - @ColorInt - public int getBackgroundColor() { - return mRecord.mBackdropColor; - } - - @Override - public boolean getShowBackground() { - return mShowBackdrop; - } - - @Override - public boolean getShowWallpaper() { - return false; - } - - @Override - public void startAnimation(SurfaceControl animationLeash, Transaction t, - @AnimationType int type, @NonNull OnAnimationFinishedCallback finishCallback) { - ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, "startAnimation"); - - if (mStartBounds.isEmpty()) { - // Restore position and stack crop until client has a chance to modify it. - t.setPosition(animationLeash, mPosition.x, mPosition.y); - t.setWindowCrop(animationLeash, mEndBounds.width(), mEndBounds.height()); - } else { - // Offset the change animation leash to the relative start position in parent. - // (mPosition) is the relative end position in parent container. - // (mStartBounds - mEndBounds) is the position difference between start and end. - // (mPosition + mStartBounds - mEndBounds) will be the relative start position. - t.setPosition(animationLeash, mPosition.x + mStartBounds.left - mEndBounds.left, - mPosition.y + mStartBounds.top - mEndBounds.top); - t.setWindowCrop(animationLeash, mStartBounds.width(), mStartBounds.height()); - } - mCapturedLeash = animationLeash; - mCapturedFinishCallback = finishCallback; - mAnimationType = type; - } - - @Override - public void onAnimationCancelled(SurfaceControl animationLeash) { - if (mIsFinishing) { - return; - } - if (mRecord.mAdapter == this) { - mRecord.mAdapter = null; - } else { - mRecord.mThumbnailAdapter = null; - } - if (mRecord.mAdapter == null && mRecord.mThumbnailAdapter == null) { - mPendingAnimations.remove(mRecord); - } - if (mPendingAnimations.isEmpty()) { - cancelAnimation("allAppAnimationsCanceled"); - } - } - - @Override - public long getDurationHint() { - return mRemoteAnimationAdapter.getDuration(); - } - - @Override - public long getStatusBarTransitionsStartTime() { - return SystemClock.uptimeMillis() - + mRemoteAnimationAdapter.getStatusBarTransitionDelay(); - } - - @Override - public void dump(PrintWriter pw, String prefix) { - pw.print(prefix); pw.print("container="); pw.println(mRecord.mWindowContainer); - if (mRecord.mTarget != null) { - pw.print(prefix); pw.println("Target:"); - mRecord.mTarget.dump(pw, prefix + " "); - } else { - pw.print(prefix); pw.println("Target: null"); - } - } - - @Override - public void dumpDebug(ProtoOutputStream proto) { - final long token = proto.start(REMOTE); - if (mRecord.mTarget != null) { - mRecord.mTarget.dumpDebug(proto, TARGET); - } - proto.end(token); - } - } -} diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java index ae3a015a690d..1966ecf57c73 100644 --- a/services/core/java/com/android/server/wm/TaskDisplayArea.java +++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java @@ -50,7 +50,6 @@ import android.graphics.Color; import android.os.UserHandle; import android.util.IntArray; import android.util.Slog; -import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; import android.view.WindowManager; @@ -776,13 +775,6 @@ final class TaskDisplayArea extends DisplayArea<WindowContainer> { return needsZBoost[0]; } - @Override - RemoteAnimationTarget createRemoteAnimationTarget( - RemoteAnimationController.RemoteAnimationRecord record) { - final ActivityRecord activity = getTopMostActivity(); - return activity != null ? activity.createRemoteAnimationTarget(record) : null; - } - void setBackgroundColor(@ColorInt int colorInt) { setBackgroundColor(colorInt, false /* restore */); } diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index 97a1a34336e9..74059c1cc9b1 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -99,7 +99,6 @@ import android.util.DisplayMetrics; import android.util.Slog; import android.util.proto.ProtoOutputStream; import android.view.DisplayInfo; -import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; import android.window.ITaskFragmentOrganizer; import android.window.TaskFragmentAnimationParams; @@ -2306,18 +2305,6 @@ class TaskFragment extends WindowContainer<WindowContainer> { } @Override - RemoteAnimationTarget createRemoteAnimationTarget( - RemoteAnimationController.RemoteAnimationRecord record) { - final ActivityRecord activity = record.getMode() == RemoteAnimationTarget.MODE_OPENING - // There may be a launching (e.g. trampoline or embedded) activity without a window - // on top of the existing task which is moving to front. Exclude finishing activity - // so the window of next activity can be chosen to create the animation target. - ? getActivity(r -> !r.finishing && r.hasChild()) - : getTopMostActivity(); - return activity != null ? activity.createRemoteAnimationTarget(record) : null; - } - - @Override boolean canCreateRemoteAnimationTarget() { return true; } 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/WallpaperAnimationAdapter.java b/services/core/java/com/android/server/wm/WallpaperAnimationAdapter.java deleted file mode 100644 index c3e85b1f8662..000000000000 --- a/services/core/java/com/android/server/wm/WallpaperAnimationAdapter.java +++ /dev/null @@ -1,191 +0,0 @@ -/* - * Copyright (C) 2019 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.internal.protolog.WmProtoLogGroups.WM_DEBUG_REMOTE_ANIMATIONS; -import static com.android.server.wm.AnimationAdapterProto.REMOTE; -import static com.android.server.wm.RemoteAnimationAdapterWrapperProto.TARGET; -import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_WINDOW_ANIMATION; - -import android.annotation.NonNull; -import android.graphics.Point; -import android.os.SystemClock; -import android.util.proto.ProtoOutputStream; -import android.view.RemoteAnimationTarget; -import android.view.SurfaceControl; - -import com.android.internal.protolog.ProtoLog; -import com.android.server.wm.SurfaceAnimator.AnimationType; - -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.function.Consumer; - -/** - * An animation adapter for wallpaper windows. - */ -class WallpaperAnimationAdapter implements AnimationAdapter { - private static final String TAG = "WallpaperAnimationAdapter"; - - private final WallpaperWindowToken mWallpaperToken; - private SurfaceControl mCapturedLeash; - private SurfaceAnimator.OnAnimationFinishedCallback mCapturedLeashFinishCallback; - private @AnimationType int mLastAnimationType; - - private long mDurationHint; - private long mStatusBarTransitionDelay; - - private Consumer<WallpaperAnimationAdapter> mAnimationCanceledRunnable; - private RemoteAnimationTarget mTarget; - - WallpaperAnimationAdapter(WallpaperWindowToken wallpaperToken, - long durationHint, long statusBarTransitionDelay, - Consumer<WallpaperAnimationAdapter> animationCanceledRunnable) { - mWallpaperToken = wallpaperToken; - mDurationHint = durationHint; - mStatusBarTransitionDelay = statusBarTransitionDelay; - mAnimationCanceledRunnable = animationCanceledRunnable; - } - - /** - * Creates and starts remote animations for all the visible wallpaper windows. - * - * @return RemoteAnimationTarget[] targets for all the visible wallpaper windows - */ - public static RemoteAnimationTarget[] startWallpaperAnimations(DisplayContent displayContent, - long durationHint, long statusBarTransitionDelay, - Consumer<WallpaperAnimationAdapter> animationCanceledRunnable, - ArrayList<WallpaperAnimationAdapter> adaptersOut) { - if (!shouldStartWallpaperAnimation(displayContent)) { - ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, - "\tWallpaper of display=%s is not visible", displayContent); - return new RemoteAnimationTarget[0]; - } - final ArrayList<RemoteAnimationTarget> targets = new ArrayList<>(); - displayContent.forAllWallpaperWindows(wallpaperWindow -> { - final WallpaperAnimationAdapter wallpaperAdapter = new WallpaperAnimationAdapter( - wallpaperWindow, durationHint, statusBarTransitionDelay, - animationCanceledRunnable); - wallpaperWindow.startAnimation(wallpaperWindow.getPendingTransaction(), - wallpaperAdapter, false /* hidden */, ANIMATION_TYPE_WINDOW_ANIMATION); - targets.add(wallpaperAdapter.createRemoteAnimationTarget()); - adaptersOut.add(wallpaperAdapter); - }); - return targets.toArray(new RemoteAnimationTarget[targets.size()]); - } - - static boolean shouldStartWallpaperAnimation(DisplayContent displayContent) { - return displayContent.mWallpaperController.isWallpaperVisible(); - } - - /** - * Create a remote animation target for this animation adapter. - */ - RemoteAnimationTarget createRemoteAnimationTarget() { - mTarget = new RemoteAnimationTarget(-1, -1, getLeash(), false, null, null, - mWallpaperToken.getPrefixOrderIndex(), new Point(), null, null, - mWallpaperToken.getWindowConfiguration(), true, null, null, null, false); - return mTarget; - } - - /** - * @return the leash for this animation (only valid after the wallpaper window surface animation - * has started). - */ - SurfaceControl getLeash() { - return mCapturedLeash; - } - - /** - * @return the callback to call to clean up when the animation has finished. - */ - SurfaceAnimator.OnAnimationFinishedCallback getLeashFinishedCallback() { - return mCapturedLeashFinishCallback; - } - - /** - * @return the type of animation. - */ - @AnimationType int getLastAnimationType() { - return mLastAnimationType; - } - - /** - * @return the wallpaper window - */ - WallpaperWindowToken getToken() { - return mWallpaperToken; - } - - @Override - public boolean getShowWallpaper() { - // Not used - return false; - } - - @Override - public void startAnimation(SurfaceControl animationLeash, SurfaceControl.Transaction t, - @AnimationType int type, - @NonNull SurfaceAnimator.OnAnimationFinishedCallback finishCallback) { - ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, "startAnimation"); - - // Restore z-layering until client has a chance to modify it. - t.setLayer(animationLeash, mWallpaperToken.getPrefixOrderIndex()); - mCapturedLeash = animationLeash; - mCapturedLeashFinishCallback = finishCallback; - mLastAnimationType = type; - } - - @Override - public void onAnimationCancelled(SurfaceControl animationLeash) { - ProtoLog.d(WM_DEBUG_REMOTE_ANIMATIONS, "onAnimationCancelled"); - mAnimationCanceledRunnable.accept(this); - } - - @Override - public long getDurationHint() { - return mDurationHint; - } - - @Override - public long getStatusBarTransitionsStartTime() { - return SystemClock.uptimeMillis() + mStatusBarTransitionDelay; - } - - @Override - public void dump(PrintWriter pw, String prefix) { - pw.print(prefix); - pw.print("token="); - pw.println(mWallpaperToken); - if (mTarget != null) { - pw.print(prefix); - pw.println("Target:"); - mTarget.dump(pw, prefix + " "); - } else { - pw.print(prefix); - pw.println("Target: null"); - } - } - - @Override - public void dumpDebug(ProtoOutputStream proto) { - final long token = proto.start(REMOTE); - if (mTarget != null) { - mTarget.dumpDebug(proto, TARGET); - } - proto.end(token); - } -} 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/java/com/android/server/wm/WallpaperWindowToken.java b/services/core/java/com/android/server/wm/WallpaperWindowToken.java index 3b79c54f1c73..7c88abcec7ec 100644 --- a/services/core/java/com/android/server/wm/WallpaperWindowToken.java +++ b/services/core/java/com/android/server/wm/WallpaperWindowToken.java @@ -34,7 +34,6 @@ import android.util.SparseArray; import com.android.internal.protolog.ProtoLog; import java.io.PrintWriter; -import java.util.function.Consumer; /** * A token that represents a set of wallpaper windows. @@ -250,11 +249,6 @@ class WallpaperWindowToken extends WindowToken { } @Override - void forAllWallpaperWindows(Consumer<WallpaperWindowToken> callback) { - callback.accept(this); - } - - @Override boolean fillsParent() { return true; } diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index 7af542f10127..95cdf46ea488 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -97,7 +97,6 @@ import android.view.InsetsSource; import android.view.InsetsState; import android.view.MagnificationSpec; import android.view.RemoteAnimationDefinition; -import android.view.RemoteAnimationTarget; import android.view.Surface; import android.view.SurfaceControl; import android.view.SurfaceControl.Builder; @@ -303,7 +302,6 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< * This gets used during some open/close transitions as well as during a change transition * where it represents the starting-state snapshot. */ - WindowContainerThumbnail mThumbnail; final Point mTmpPoint = new Point(); protected final Rect mTmpRect = new Rect(); final Rect mTmpPrevBounds = new Rect(); @@ -2097,12 +2095,6 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< return getActivity((r) -> !r.finishing && !r.isTaskOverlay()); } - void forAllWallpaperWindows(Consumer<WallpaperWindowToken> callback) { - for (int i = mChildren.size() - 1; i >= 0; --i) { - mChildren.get(i).forAllWallpaperWindows(callback); - } - } - /** * Calls the given {@param callback} for all tasks in depth-first top-down z-order at or below * this container. @@ -3174,61 +3166,10 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< getAnimationPosition(mTmpPoint); mTmpRect.offsetTo(0, 0); - final AppTransition appTransition = getDisplayContent().mAppTransition; - final RemoteAnimationController controller = appTransition.getRemoteAnimationController(); final boolean isChanging = AppTransition.isChangeTransitOld(transit) && enter && isChangingAppTransition(); - if (controller != null) { - // Here we load App XML in order to read com.android.R.styleable#Animation_showBackdrop. - boolean showBackdrop = false; - // Optionally set backdrop color if App explicitly provides it through - // {@link Activity#overridePendingTransition(int, int, int)}. - @ColorInt int backdropColor = 0; - if (controller.isFromActivityEmbedding()) { - if (isChanging) { - // When there are more than one changing containers, it may leave part of the - // screen empty. Show background color to cover that. - showBackdrop = getDisplayContent().mChangingContainers.size() > 1; - backdropColor = appTransition.getNextAppTransitionBackgroundColor(); - } else { - // Check whether the app has requested to show backdrop for open/close - // transition. - final Animation a = appTransition.getNextAppRequestedAnimation(enter); - if (a != null) { - showBackdrop = a.getShowBackdrop(); - backdropColor = a.getBackdropColor(); - } - } - } - final Rect localBounds = new Rect(mTmpRect); - localBounds.offsetTo(mTmpPoint.x, mTmpPoint.y); - final RemoteAnimationController.RemoteAnimationRecord adapters; - if (!isChanging && !enter && isClosingWhenResizing()) { - // Container that is closing while resizing. Pass in the closing start bounds, so - // the animation can start with the correct bounds, there won't be a snapshot. - // Cleanup the mClosingChangingContainers so that when the animation is finished, it - // will reset the surface. - final Rect closingStartBounds = getDisplayContent().mClosingChangingContainers - .remove(this); - adapters = controller.createRemoteAnimationRecord( - this, mTmpPoint, localBounds, screenBounds, closingStartBounds, - showBackdrop, false /* shouldCreateSnapshot */); - } else { - final Rect startBounds = null; - adapters = controller.createRemoteAnimationRecord( - this, mTmpPoint, localBounds, screenBounds, startBounds, showBackdrop); - } - if (backdropColor != 0) { - adapters.setBackDropColor(backdropColor); - } - if (!isChanging) { - adapters.setMode(enter - ? RemoteAnimationTarget.MODE_OPENING - : RemoteAnimationTarget.MODE_CLOSING); - } - resultAdapters = new Pair<>(adapters.mAdapter, adapters.mThumbnailAdapter); - } else if (isChanging) { + if (isChanging) { final float durationScale = mWmService.getTransitionAnimationScaleLocked(); final DisplayInfo displayInfo = getDisplayContent().getDisplayInfo(); mTmpRect.offsetTo(mTmpPoint.x, mTmpPoint.y); @@ -3417,11 +3358,6 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< return a; } - RemoteAnimationTarget createRemoteAnimationTarget( - RemoteAnimationController.RemoteAnimationRecord record) { - return null; - } - boolean canCreateRemoteAnimationTarget() { return false; } diff --git a/services/core/java/com/android/server/wm/WindowContainerThumbnail.java b/services/core/java/com/android/server/wm/WindowContainerThumbnail.java deleted file mode 100644 index ae477e1e22f0..000000000000 --- a/services/core/java/com/android/server/wm/WindowContainerThumbnail.java +++ /dev/null @@ -1,218 +0,0 @@ -/* - * 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.server.wm; - -import static android.view.SurfaceControl.METADATA_OWNER_UID; -import static android.view.SurfaceControl.METADATA_WINDOW_TYPE; - -import static com.android.internal.protolog.WmProtoLogGroups.WM_SHOW_TRANSACTIONS; -import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION; -import static com.android.server.wm.WindowContainerThumbnailProto.HEIGHT; -import static com.android.server.wm.WindowContainerThumbnailProto.SURFACE_ANIMATOR; -import static com.android.server.wm.WindowContainerThumbnailProto.WIDTH; -import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME; -import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; -import static com.android.server.wm.WindowManagerService.MAX_ANIMATION_DURATION; - -import android.graphics.ColorSpace; -import android.graphics.GraphicBuffer; -import android.graphics.PixelFormat; -import android.graphics.Point; -import android.hardware.HardwareBuffer; -import android.util.proto.ProtoOutputStream; -import android.view.SurfaceControl; -import android.view.SurfaceControl.Builder; -import android.view.SurfaceControl.Transaction; -import android.view.animation.Animation; - -import com.android.internal.protolog.ProtoLog; -import com.android.server.wm.SurfaceAnimator.Animatable; -import com.android.server.wm.SurfaceAnimator.AnimationType; - -/** - * Represents a surface that is displayed over a subclass of {@link WindowContainer} - */ -class WindowContainerThumbnail implements Animatable { - - private static final String TAG = TAG_WITH_CLASS_NAME ? "WindowContainerThumbnail" : TAG_WM; - - private final WindowContainer mWindowContainer; - private SurfaceControl mSurfaceControl; - private final SurfaceAnimator mSurfaceAnimator; - private final int mWidth; - private final int mHeight; - - /** - * @param t Transaction to create the thumbnail in. - * @param container The sub-class of {@link WindowContainer} to associate this thumbnail with. - * @param thumbnailHeader A thumbnail or placeholder for thumbnail to initialize with. - */ - WindowContainerThumbnail(Transaction t, WindowContainer container, - HardwareBuffer thumbnailHeader) { - this(t, container, thumbnailHeader, null /* animator */); - } - - WindowContainerThumbnail(Transaction t, WindowContainer container, - HardwareBuffer thumbnailHeader, SurfaceAnimator animator) { - mWindowContainer = container; - if (animator != null) { - mSurfaceAnimator = animator; - } else { - // We can't use a delegating constructor since we need to - // reference this::onAnimationFinished - mSurfaceAnimator = - new SurfaceAnimator(this, this::onAnimationFinished /* animationFinishedCallback */, - container.mWmService); - } - mWidth = thumbnailHeader.getWidth(); - mHeight = thumbnailHeader.getHeight(); - - // Create a new surface for the thumbnail - // TODO: This should be attached as a child to the app token, once the thumbnail animations - // use relative coordinates. Once we start animating task we can also consider attaching - // this to the task. - mSurfaceControl = mWindowContainer.makeChildSurface(mWindowContainer.getTopChild()) - .setName("thumbnail anim: " + mWindowContainer.toString()) - .setBLASTLayer() - .setFormat(PixelFormat.TRANSLUCENT) - .setMetadata(METADATA_WINDOW_TYPE, mWindowContainer.getWindowingMode()) - .setMetadata(METADATA_OWNER_UID, WindowManagerService.MY_UID) - .setCallsite("WindowContainerThumbnail") - .build(); - - ProtoLog.i(WM_SHOW_TRANSACTIONS, " THUMBNAIL %s: CREATE", mSurfaceControl); - - GraphicBuffer graphicBuffer = GraphicBuffer.createFromHardwareBuffer(thumbnailHeader); - t.setBuffer(mSurfaceControl, graphicBuffer); - t.setColorSpace(mSurfaceControl, ColorSpace.get(ColorSpace.Named.SRGB)); - t.show(mSurfaceControl); - - // We parent the thumbnail to the container, and just place it on top of anything else in - // the container. - t.setLayer(mSurfaceControl, Integer.MAX_VALUE); - } - - void startAnimation(Transaction t, Animation anim) { - startAnimation(t, anim, null /* position */); - } - - void startAnimation(Transaction t, Animation anim, Point position) { - anim.restrictDuration(MAX_ANIMATION_DURATION); - anim.scaleCurrentDuration(mWindowContainer.mWmService.getTransitionAnimationScaleLocked()); - mSurfaceAnimator.startAnimation(t, new LocalAnimationAdapter( - new WindowAnimationSpec(anim, position, - mWindowContainer.getDisplayContent().mAppTransition.canSkipFirstFrame(), - mWindowContainer.getDisplayContent().getWindowCornerRadius()), - mWindowContainer.mWmService.mSurfaceAnimationRunner), false /* hidden */, - ANIMATION_TYPE_APP_TRANSITION); - } - - private void onAnimationFinished(@AnimationType int type, AnimationAdapter anim) { - } - - void setShowing(Transaction pendingTransaction, boolean show) { - // TODO: Not needed anymore once thumbnail is attached to the app. - if (show) { - pendingTransaction.show(mSurfaceControl); - } else { - pendingTransaction.hide(mSurfaceControl); - } - } - - void destroy() { - mSurfaceAnimator.cancelAnimation(); - getPendingTransaction().remove(mSurfaceControl); - mSurfaceControl = null; - } - - /** - * Write to a protocol buffer output stream. Protocol buffer message definition is at {@link - * com.android.server.wm.WindowContainerThumbnailProto}. - * - * @param proto Stream to write the WindowContainerThumbnailProto object to. - * @param fieldId Field Id of the WindowContainerThumbnailProto as defined in the parent - * message. - * @hide - */ - void dumpDebug(ProtoOutputStream proto, long fieldId) { - final long token = proto.start(fieldId); - proto.write(WIDTH, mWidth); - proto.write(HEIGHT, mHeight); - if (mSurfaceAnimator.isAnimating()) { - mSurfaceAnimator.dumpDebug(proto, SURFACE_ANIMATOR); - } - proto.end(token); - } - - @Override - public Transaction getSyncTransaction() { - return mWindowContainer.getSyncTransaction(); - } - - @Override - public Transaction getPendingTransaction() { - return mWindowContainer.getPendingTransaction(); - } - - @Override - public void commitPendingTransaction() { - mWindowContainer.commitPendingTransaction(); - } - - @Override - public void onAnimationLeashCreated(Transaction t, SurfaceControl leash) { - t.setLayer(leash, Integer.MAX_VALUE); - } - - @Override - public void onAnimationLeashLost(Transaction t) { - - // TODO: Once attached to app token, we don't need to hide it immediately if thumbnail - // became visible. - t.hide(mSurfaceControl); - } - - @Override - public Builder makeAnimationLeash() { - return mWindowContainer.makeChildSurface(mWindowContainer.getTopChild()); - } - - @Override - public SurfaceControl getSurfaceControl() { - return mSurfaceControl; - } - - @Override - public SurfaceControl getAnimationLeashParent() { - return mWindowContainer.getAnimationLeashParent(); - } - - @Override - public SurfaceControl getParentSurfaceControl() { - return mWindowContainer.getParentSurfaceControl(); - } - - @Override - public int getSurfaceWidth() { - return mWidth; - } - - @Override - public int getSurfaceHeight() { - return mHeight; - } -} diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index 3b6a4dc6e1b0..4c53ba53a506 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -30,24 +30,25 @@ import static android.window.TaskFragmentOperation.OP_TYPE_CLEAR_ADJACENT_TASK_F import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE; import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_TASK_FRAGMENT; import static android.window.TaskFragmentOperation.OP_TYPE_DELETE_TASK_FRAGMENT; +import static android.window.TaskFragmentOperation.OP_TYPE_PRIVILEGED_REORDER_TO_BOTTOM_OF_TASK; +import static android.window.TaskFragmentOperation.OP_TYPE_PRIVILEGED_REORDER_TO_TOP_OF_TASK; +import static android.window.TaskFragmentOperation.OP_TYPE_PRIVILEGED_SET_CAN_AFFECT_SYSTEM_UI_FLAGS; +import static android.window.TaskFragmentOperation.OP_TYPE_PRIVILEGED_SET_MOVE_TO_BOTTOM_IF_CLEAR_WHEN_LAUNCH; import static android.window.TaskFragmentOperation.OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE; -import static android.window.TaskFragmentOperation.OP_TYPE_REORDER_TO_BOTTOM_OF_TASK; import static android.window.TaskFragmentOperation.OP_TYPE_REORDER_TO_FRONT; -import static android.window.TaskFragmentOperation.OP_TYPE_REORDER_TO_TOP_OF_TASK; import static android.window.TaskFragmentOperation.OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT; import static android.window.TaskFragmentOperation.OP_TYPE_REQUEST_FOCUS_ON_TASK_FRAGMENT; import static android.window.TaskFragmentOperation.OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS; import static android.window.TaskFragmentOperation.OP_TYPE_SET_ANIMATION_PARAMS; -import static android.window.TaskFragmentOperation.OP_TYPE_SET_CAN_AFFECT_SYSTEM_UI_FLAGS; import static android.window.TaskFragmentOperation.OP_TYPE_SET_COMPANION_TASK_FRAGMENT; import static android.window.TaskFragmentOperation.OP_TYPE_SET_DECOR_SURFACE_BOOSTED; import static android.window.TaskFragmentOperation.OP_TYPE_SET_DIM_ON_TASK; import static android.window.TaskFragmentOperation.OP_TYPE_SET_ISOLATED_NAVIGATION; -import static android.window.TaskFragmentOperation.OP_TYPE_SET_MOVE_TO_BOTTOM_IF_CLEAR_WHEN_LAUNCH; import static android.window.TaskFragmentOperation.OP_TYPE_SET_PINNED; import static android.window.TaskFragmentOperation.OP_TYPE_SET_RELATIVE_BOUNDS; import static android.window.TaskFragmentOperation.OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT; import static android.window.TaskFragmentOperation.OP_TYPE_UNKNOWN; +import static android.window.TaskFragmentOperation.PRIVILEGED_OP_START; import static android.window.WindowContainerTransaction.Change.CHANGE_FOCUSABLE; import static android.window.WindowContainerTransaction.Change.CHANGE_FORCE_TRANSLUCENT; import static android.window.WindowContainerTransaction.Change.CHANGE_HIDDEN; @@ -1786,7 +1787,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub taskFragment.setIsolatedNav(isolatedNav); break; } - case OP_TYPE_REORDER_TO_BOTTOM_OF_TASK: { + case OP_TYPE_PRIVILEGED_REORDER_TO_BOTTOM_OF_TASK: { final Task task = taskFragment.getTask(); if (task != null) { if (task.getBottomChild() != taskFragment) { @@ -1802,7 +1803,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub } break; } - case OP_TYPE_REORDER_TO_TOP_OF_TASK: { + case OP_TYPE_PRIVILEGED_REORDER_TO_TOP_OF_TASK: { final Task task = taskFragment.getTask(); if (task != null) { if (task.getTopChild() != taskFragment) { @@ -1852,7 +1853,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub : EMBEDDED_DIM_AREA_TASK_FRAGMENT); break; } - case OP_TYPE_SET_MOVE_TO_BOTTOM_IF_CLEAR_WHEN_LAUNCH: { + case OP_TYPE_PRIVILEGED_SET_MOVE_TO_BOTTOM_IF_CLEAR_WHEN_LAUNCH: { taskFragment.setMoveToBottomIfClearWhenLaunch(operation.getBooleanValue()); break; } @@ -1888,7 +1889,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub taskFragment.setPinned(pinned); break; } - case OP_TYPE_SET_CAN_AFFECT_SYSTEM_UI_FLAGS: { + case OP_TYPE_PRIVILEGED_SET_CAN_AFFECT_SYSTEM_UI_FLAGS: { taskFragment.setCanAffectSystemUiFlags(operation.getBooleanValue()); // Request to apply the flags. @@ -1938,45 +1939,23 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub return false; } final int opType = operation.getOpType(); - if (opType == OP_TYPE_CREATE_TASK_FRAGMENT) { - // No need to check TaskFragment. - return true; - } - - if (!validateTaskFragment(taskFragment, opType, errorCallbackToken, organizer)) { - return false; - } - if ((opType == OP_TYPE_REORDER_TO_BOTTOM_OF_TASK - || opType == OP_TYPE_REORDER_TO_TOP_OF_TASK) + if (opType >= PRIVILEGED_OP_START && !mTaskFragmentOrganizerController.isSystemOrganizer(organizer.asBinder())) { final Throwable exception = new SecurityException( - "Only a system organizer can perform OP_TYPE_REORDER_TO_BOTTOM_OF_TASK or " - + "OP_TYPE_REORDER_TO_TOP_OF_TASK." + "Only a system organizer can perform privileged operations. opType=" + opType ); sendTaskFragmentOperationFailure(organizer, errorCallbackToken, taskFragment, opType, exception); return false; } - if ((opType == OP_TYPE_SET_MOVE_TO_BOTTOM_IF_CLEAR_WHEN_LAUNCH) - && !mTaskFragmentOrganizerController.isSystemOrganizer(organizer.asBinder())) { - final Throwable exception = new SecurityException( - "Only a system organizer can perform " - + "OP_TYPE_SET_MOVE_TO_BOTTOM_IF_CLEAR_WHEN_LAUNCH." - ); - sendTaskFragmentOperationFailure(organizer, errorCallbackToken, taskFragment, - opType, exception); - return false; + if (opType == OP_TYPE_CREATE_TASK_FRAGMENT) { + // No need to check TaskFragment. + return true; } - if ((opType == OP_TYPE_SET_CAN_AFFECT_SYSTEM_UI_FLAGS) - && !mTaskFragmentOrganizerController.isSystemOrganizer(organizer.asBinder())) { - final Throwable exception = new SecurityException( - "Only a system organizer can perform OP_TYPE_SET_CAN_AFFECT_SYSTEM_UI_FLAGS." - ); - sendTaskFragmentOperationFailure(organizer, errorCallbackToken, taskFragment, - opType, exception); + if (!validateTaskFragment(taskFragment, opType, errorCallbackToken, organizer)) { return false; } diff --git a/services/people/java/com/android/server/people/PeopleService.java b/services/people/java/com/android/server/people/PeopleService.java index b9f00d7979c2..44a83541f0b9 100644 --- a/services/people/java/com/android/server/people/PeopleService.java +++ b/services/people/java/com/android/server/people/PeopleService.java @@ -16,6 +16,8 @@ package com.android.server.people; +import static com.android.server.flags.Flags.earlyDataManagerInit; + import android.Manifest; import android.annotation.NonNull; import android.annotation.Nullable; @@ -117,6 +119,18 @@ public class PeopleService extends SystemService { } @Override + public void onBootPhase(int phase) { + if (phase == PHASE_BOOT_COMPLETED) { + if (earlyDataManagerInit()) { + // Force initialization of DataManager before onUserUnlocked + // to avoid blocking the ActivityManagerService on + // shortcutHandleUnlockUser. + getDataManager(); + } + } + } + + @Override public void onUserUnlocked(@NonNull TargetUser user) { getDataManager().onUserUnlocked(user.getUserIdentifier()); } diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/AndroidManifest.xml b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/AndroidManifest.xml index b7de74987eb8..45523268c966 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/AndroidManifest.xml +++ b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/AndroidManifest.xml @@ -32,8 +32,8 @@ <uses-library android:name="android.test.runner" /> </application> - <!-- The "targetPackage" reference the instruments APK package, which is the FakeImeApk, while - the test package is "com.android.inputmethod.imetests" (FrameworksImeTests.apk).--> + <!-- The "targetPackage" reference the instruments APK package, which is the SimpleTestIme.apk, + while the test package is "com.android.inputmethod.imetests" (FrameworksImeTests.apk).--> <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" android:targetPackage="com.android.apps.inputmethod.simpleime" diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java index 72e9cc566497..5d64cb638702 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java @@ -92,6 +92,9 @@ public class InputMethodServiceTest { private static final String DISABLE_SHOW_IME_WITH_HARD_KEYBOARD_CMD = "settings put secure " + Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD + " 0"; + /** 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 GestureNavSwitchHelper mGestureNavSwitchHelper = new GestureNavSwitchHelper(); @@ -173,15 +176,14 @@ public class InputMethodServiceTest { Log.i(TAG, "Click on EditText"); verifyInputViewStatus( () -> clickOnViewCenter(mActivity.getEditText()), - EVENT_SHOW, - true /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); + EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); // Press home key to hide IME. Log.i(TAG, "Press home"); if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { assertWithMessage("Home key press was handled").that(mUiDevice.pressHome()).isTrue(); + // This doesn't call verifyInputViewStatus, as the refactor delays the events such that + // getCurrentInputStarted() would be false, as we would already be in launcher. // The IME visibility is only sent at the end of the animation. Therefore, we have to // wait until the visibility was sent to the server and the IME window hidden. eventually(() -> assertWithMessage("IME is not shown") @@ -190,11 +192,7 @@ public class InputMethodServiceTest { verifyInputViewStatus( () -> assertWithMessage("Home key press was handled") .that(mUiDevice.pressHome()).isTrue(), - EVENT_HIDE, - true /* expected */, - false /* inputViewStarted */); - assertWithMessage("IME is not shown") - .that(mInputMethodService.isInputViewShown()).isFalse(); + EVENT_HIDE, true /* eventExpected */, false /* shown */, "IME is not shown"); } } @@ -208,26 +206,12 @@ public class InputMethodServiceTest { // Triggers to show IME via public API. verifyInputViewStatusOnMainSync( () -> assertThat(mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(), - EVENT_SHOW, - true /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).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 /* expected */, - false /* inputViewStarted */); - if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { - // The IME visibility is only sent at the end of the animation. Therefore, we have to - // wait until the visibility was sent to the server and the IME window hidden. - eventually(() -> assertWithMessage("IME is not shown") - .that(mInputMethodService.isInputViewShown()).isFalse()); - } else { - assertWithMessage("IME is not shown") - .that(mInputMethodService.isInputViewShown()).isFalse(); - } + EVENT_HIDE, true /* eventExpected */, false /* shown */, "IME is not shown"); } /** @@ -240,26 +224,12 @@ public class InputMethodServiceTest { // Triggers to show IME via public API. verifyInputViewStatusOnMainSync( () -> mActivity.showImeWithWindowInsetsController(), - EVENT_SHOW, - true /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); + EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); // Triggers to hide IME via public API. verifyInputViewStatusOnMainSync( () -> mActivity.hideImeWithWindowInsetsController(), - EVENT_HIDE, - true /* expected */, - false /* inputViewStarted */); - if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { - // The IME visibility is only sent at the end of the animation. Therefore, we have to - // wait until the visibility was sent to the server and the IME window hidden. - eventually(() -> assertWithMessage("IME is not shown") - .that(mInputMethodService.isInputViewShown()).isFalse()); - } else { - assertWithMessage("IME is not shown") - .that(mInputMethodService.isInputViewShown()).isFalse(); - } + EVENT_HIDE, true /* eventExpected */, false /* shown */, "IME is not shown"); } /** @@ -277,10 +247,7 @@ public class InputMethodServiceTest { Log.i(TAG, "Call IMS#requestShowSelf(0)"); verifyInputViewStatusOnMainSync( () -> mInputMethodService.requestShowSelf(0 /* flags */), - EVENT_SHOW, - true /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); + 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). @@ -288,51 +255,31 @@ public class InputMethodServiceTest { verifyInputViewStatusOnMainSync( () -> mInputMethodService.requestHideSelf( InputMethodManager.HIDE_IMPLICIT_ONLY), - EVENT_HIDE, - false /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is still shown after HIDE_IMPLICIT_ONLY") - .that(mInputMethodService.isInputViewShown()).isTrue(); + 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 /* expected */, - false /* inputViewStarted */); - if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { - // The IME visibility is only sent at the end of the animation. Therefore, we have to - // wait until the visibility was sent to the server and the IME window hidden. - eventually(() -> assertWithMessage("IME is not shown") - .that(mInputMethodService.isInputViewShown()).isFalse()); - } else { - assertWithMessage("IME is not shown") - .that(mInputMethodService.isInputViewShown()).isFalse(); - } + 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 /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is shown with SHOW_IMPLICIT") - .that(mInputMethodService.isInputViewShown()).isTrue(); + 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 /* expected */, - false /* inputViewStarted */); - assertWithMessage("IME is not shown after HIDE_IMPLICIT_ONLY") - .that(mInputMethodService.isInputViewShown()).isFalse(); + EVENT_HIDE, true /* eventExpected */, false /* shown */, + "IME is not shown after HIDE_IMPLICIT_ONLY"); } } @@ -424,20 +371,14 @@ public class InputMethodServiceTest { verifyInputViewStatusOnMainSync(() -> assertThat( mActivity.showImeWithInputMethodManager( InputMethodManager.SHOW_IMPLICIT)).isTrue(), - EVENT_SHOW, - false /* expected */, - false /* inputViewStarted */); - assertWithMessage("IME is not shown after SHOW_IMPLICIT") - .that(mInputMethodService.isInputViewShown()).isFalse(); + EVENT_SHOW, false /* eventExpected */, false /* shown */, + "IME is not shown after SHOW_IMPLICIT"); verifyInputViewStatusOnMainSync( () -> assertThat(mActivity.showImeWithInputMethodManager(0 /* flags */)) .isTrue(), - EVENT_SHOW, - false /* expected */, - false /* inputViewStarted */); - assertWithMessage("IME is not shown after SHOW_EXPLICIT") - .that(mInputMethodService.isInputViewShown()).isFalse(); + EVENT_SHOW, false /* eventExpected */, false /* shown */, + "IME is not shown after SHOW_EXPLICIT"); } finally { mInputMethodService.getResources() .updateConfiguration(initialConfig, null /* metrics */, null /* compat */); @@ -455,10 +396,7 @@ public class InputMethodServiceTest { // IME should be shown. verifyInputViewStatusOnMainSync( () -> assertThat(mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(), - EVENT_SHOW, - true /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); + EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); } /** @@ -472,10 +410,7 @@ public class InputMethodServiceTest { // the IME should be shown. verifyInputViewStatusOnMainSync(() -> assertThat( mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT)).isTrue(), - EVENT_SHOW, - true /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); + EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); } /** @@ -492,9 +427,9 @@ public class InputMethodServiceTest { .that(mUiDevice.isNaturalOrientation()).isFalse()); // Wait for the TestActivity to be recreated. eventually(() -> assertWithMessage("Activity was re-created after rotation") - .that(TestActivity.getLastCreatedInstance()).isNotEqualTo(mActivity)); + .that(TestActivity.getInstance()).isNotEqualTo(mActivity)); // Get the new TestActivity. - mActivity = TestActivity.getLastCreatedInstance(); + 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") @@ -502,10 +437,7 @@ public class InputMethodServiceTest { verifyInputViewStatusOnMainSync(() -> assertThat( mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(), - EVENT_SHOW, - true /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); + EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); } /** @@ -527,9 +459,9 @@ public class InputMethodServiceTest { .that(mUiDevice.isNaturalOrientation()).isFalse()); // Wait for the TestActivity to be recreated. eventually(() -> assertWithMessage("Activity was re-created after rotation") - .that(TestActivity.getLastCreatedInstance()).isNotEqualTo(mActivity)); + .that(TestActivity.getInstance()).isNotEqualTo(mActivity)); // Get the new TestActivity. - mActivity = TestActivity.getLastCreatedInstance(); + 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") @@ -537,11 +469,7 @@ public class InputMethodServiceTest { verifyInputViewStatusOnMainSync(() -> assertThat( mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT)).isTrue(), - EVENT_SHOW, - false /* expected */, - false /* inputViewStarted */); - assertWithMessage("IME is not shown") - .that(mInputMethodService.isInputViewShown()).isFalse(); + EVENT_SHOW, false /* eventExpected */, false /* shown */, "IME is not shown"); } /** @@ -561,10 +489,7 @@ public class InputMethodServiceTest { verifyInputViewStatusOnMainSync(() -> assertThat( mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(), - EVENT_SHOW, - true /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); + EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); } finally { mInputMethodService.getResources() .updateConfiguration(initialConfig, null /* metrics */, null /* compat */); @@ -594,11 +519,7 @@ public class InputMethodServiceTest { verifyInputViewStatusOnMainSync(() ->assertThat( mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_IMPLICIT)) .isTrue(), - EVENT_SHOW, - false /* expected */, - false /* inputViewStarted */); - assertWithMessage("IME is not shown") - .that(mInputMethodService.isInputViewShown()).isFalse(); + EVENT_SHOW, false /* eventExpected */, false /* shown */, "IME is not shown"); } finally { mInputMethodService.getResources() .updateConfiguration(initialConfig, null /* metrics */, null /* compat */); @@ -623,10 +544,7 @@ public class InputMethodServiceTest { verifyInputViewStatusOnMainSync( () -> assertThat(mActivity.showImeWithInputMethodManager(0 /* flags */)) .isTrue(), - EVENT_SHOW, - true /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); + EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); // Simulate connecting a hardware keyboard. config.keyboard = Configuration.KEYBOARD_QWERTY; @@ -637,11 +555,8 @@ public class InputMethodServiceTest { verifyInputViewStatusOnMainSync( () -> mInputMethodService.onConfigurationChanged(config), - EVENT_CONFIG, - true /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is still shown after a configuration change") - .that(mInputMethodService.isInputViewShown()).isTrue(); + EVENT_CONFIG, true /* eventExpected */, true /* shown */, + "IME is still shown after a configuration change"); } finally { mInputMethodService.getResources() .updateConfiguration(initialConfig, null /* metrics */, null /* compat */); @@ -672,10 +587,7 @@ public class InputMethodServiceTest { verifyInputViewStatusOnMainSync(() -> assertThat( mActivity.showImeWithInputMethodManager( InputMethodManager.SHOW_IMPLICIT)).isTrue(), - EVENT_SHOW, - true /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); + EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); // Simulate connecting a hardware keyboard. config.keyboard = Configuration.KEYBOARD_QWERTY; @@ -692,11 +604,8 @@ public class InputMethodServiceTest { // still alive. verifyInputViewStatusOnMainSync( () -> mInputMethodService.onConfigurationChanged(config), - EVENT_CONFIG, - true /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is not shown after a configuration change") - .that(mInputMethodService.isInputViewShown()).isFalse(); + 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 */); @@ -722,31 +631,21 @@ public class InputMethodServiceTest { // Explicit show request. verifyInputViewStatusOnMainSync(() -> assertThat( mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(), - EVENT_SHOW, - true /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); + EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); // Implicit show request. verifyInputViewStatusOnMainSync(() -> assertThat( mActivity.showImeWithInputMethodManager( InputMethodManager.SHOW_IMPLICIT)).isTrue(), - EVENT_SHOW, - false /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is still shown") - .that(mInputMethodService.isInputViewShown()).isTrue(); + EVENT_SHOW, false /* eventExpected */, true /* shown */, "IME is still shown"); // Simulate a fake configuration change to avoid the recreation of TestActivity. // This should now consider the implicit show request, but keep the state from the // explicit show request, and thus not hide the IME. verifyInputViewStatusOnMainSync( () -> mInputMethodService.onConfigurationChanged(config), - EVENT_CONFIG, - true /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is still shown after a configuration change") - .that(mInputMethodService.isInputViewShown()).isTrue(); + EVENT_CONFIG, true /* eventExpected */, true /* shown */, + "IME is still shown after a configuration change"); } finally { mInputMethodService.getResources() .updateConfiguration(initialConfig, null /* metrics */, null /* compat */); @@ -769,27 +668,17 @@ public class InputMethodServiceTest { verifyInputViewStatusOnMainSync(() -> assertThat( mActivity.showImeWithInputMethodManager(InputMethodManager.SHOW_FORCED)).isTrue(), - EVENT_SHOW, - true /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); + EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); verifyInputViewStatusOnMainSync(() -> assertThat( mActivity.showImeWithInputMethodManager(0 /* flags */)).isTrue(), - EVENT_SHOW, - false /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is still shown") - .that(mInputMethodService.isInputViewShown()).isTrue(); + EVENT_SHOW, false /* eventExpected */, true /* shown */, "IME is still shown"); verifyInputViewStatusOnMainSync(() -> assertThat( mActivity.hideImeWithInputMethodManager(InputMethodManager.HIDE_NOT_ALWAYS)) .isTrue(), - EVENT_HIDE, - false /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is still shown after HIDE_NOT_ALWAYS") - .that(mInputMethodService.isInputViewShown()).isTrue(); + EVENT_HIDE, false /* eventExpected */, true /* shown */, + "IME is still shown after HIDE_NOT_ALWAYS"); } /** @@ -800,18 +689,15 @@ public class InputMethodServiceTest { setShowImeWithHardKeyboard(true /* enabled */); Log.i(TAG, "Set orientation natural"); - verifyFullscreenMode(() -> setOrientation(0), - false /* expected */, + verifyFullscreenMode(() -> setOrientation(0), false /* eventExpected */, true /* orientationPortrait */); Log.i(TAG, "Set orientation left"); - verifyFullscreenMode(() -> setOrientation(1), - true /* expected */, + verifyFullscreenMode(() -> setOrientation(1), true /* eventExpected */, false /* orientationPortrait */); Log.i(TAG, "Set orientation right"); - verifyFullscreenMode(() -> setOrientation(2), - false /* expected */, + verifyFullscreenMode(() -> setOrientation(2), false /* eventExpected */, false /* orientationPortrait */); } @@ -845,10 +731,7 @@ public class InputMethodServiceTest { setDrawsImeNavBarAndSwitcherButton(true /* enabled */); mActivity.showImeWithWindowInsetsController(); }, - EVENT_SHOW, - true /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); + EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); assertWithMessage("IME navigation bar is initially shown") .that(mInputMethodService.isImeNavigationBarShownForTesting()).isTrue(); @@ -883,10 +766,7 @@ public class InputMethodServiceTest { setDrawsImeNavBarAndSwitcherButton(false /* enabled */); mActivity.showImeWithWindowInsetsController(); }, - EVENT_SHOW, - true /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); + EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); assertWithMessage("IME navigation bar is initially not shown") .that(mInputMethodService.isImeNavigationBarShownForTesting()).isFalse(); @@ -917,24 +797,15 @@ public class InputMethodServiceTest { try (var ignored = mGestureNavSwitchHelper.withGestureNavigationMode()) { verifyInputViewStatusOnMainSync( () -> mActivity.showImeWithWindowInsetsController(), - EVENT_SHOW, - true /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); + EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); final var backButton = getUiObject(By.res(INPUT_METHOD_NAV_BACK_ID)); - backButton.click(); - mInstrumentation.waitForIdleSync(); - - if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { - // The IME visibility is only sent at the end of the animation. Therefore, we have - // to wait until the visibility was sent to the server and the IME window hidden. - eventually(() -> assertWithMessage("IME is not shown") - .that(mInputMethodService.isInputViewShown()).isFalse()); - } else { - assertWithMessage("IME is not shown") - .that(mInputMethodService.isInputViewShown()).isFalse(); - } + verifyInputViewStatus( + () -> { + backButton.click(); + mInstrumentation.waitForIdleSync(); + }, + EVENT_HIDE, true /* eventExpected */, false /* shown */, "IME is not shown"); } } @@ -952,30 +823,20 @@ public class InputMethodServiceTest { try (var ignored = mGestureNavSwitchHelper.withGestureNavigationMode()) { verifyInputViewStatusOnMainSync( () -> mActivity.showImeWithWindowInsetsController(), - EVENT_SHOW, - true /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); + EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); final var backButton = getUiObject(By.res(INPUT_METHOD_NAV_BACK_ID)); - backButton.longClick(); - mInstrumentation.waitForIdleSync(); - - if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { - // The IME visibility is only sent at the end of the animation. Therefore, we have - // to wait until the visibility was sent to the server and the IME window hidden. - eventually(() -> assertWithMessage("IME is not shown") - .that(mInputMethodService.isInputViewShown()).isFalse()); - } else { - assertWithMessage("IME is not shown") - .that(mInputMethodService.isInputViewShown()).isFalse(); - } + verifyInputViewStatus( + () -> { + backButton.longClick(); + mInstrumentation.waitForIdleSync(); + }, + EVENT_HIDE, true /* eventExpected */, false /* shown */, "IME is not shown"); } } /** - * Verifies that clicking on the IME switch button either shows the Input Method Switcher Menu, - * or switches the input method. + * Verifies that clicking on the IME switch button switches the input method subtype. */ @Test public void testImeSwitchButtonClick() throws Exception { @@ -985,34 +846,32 @@ public class InputMethodServiceTest { setShowImeWithHardKeyboard(true /* enabled */); + final var info = mImm.getCurrentInputMethodInfo(); + assertWithMessage("InputMethodInfo is not null").that(info).isNotNull(); + mImm.setExplicitlyEnabledInputMethodSubtypes(info.getId(), SUBTYPE_IDS); + + final var initialSubtype = mImm.getCurrentInputMethodSubtype(); + try (var ignored = mGestureNavSwitchHelper.withGestureNavigationMode()) { verifyInputViewStatusOnMainSync( () -> { setDrawsImeNavBarAndSwitcherButton(true /* enabled */); mActivity.showImeWithWindowInsetsController(); }, - EVENT_SHOW, - true /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); - - final var initialInfo = mImm.getCurrentInputMethodInfo(); + EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); final var imeSwitcherButton = getUiObject(By.res(INPUT_METHOD_NAV_IME_SWITCHER_ID)); imeSwitcherButton.click(); mInstrumentation.waitForIdleSync(); - final var newInfo = mImm.getCurrentInputMethodInfo(); + final var newSubtype = mImm.getCurrentInputMethodSubtype(); - assertWithMessage("Input Method Switcher Menu is shown or input method was switched") - .that(isInputMethodPickerShown(mImm) || !Objects.equals(initialInfo, newInfo)) + assertWithMessage("Input method subtype was switched") + .that(!Objects.equals(initialSubtype, newSubtype)) .isTrue(); assertWithMessage("IME is still shown after IME Switcher button was clicked") .that(mInputMethodService.isInputViewShown()).isTrue(); - - // Hide the IME Switcher Menu before finishing. - mUiDevice.pressBack(); } } @@ -1027,16 +886,17 @@ public class InputMethodServiceTest { setShowImeWithHardKeyboard(true /* enabled */); + final var info = mImm.getCurrentInputMethodInfo(); + assertWithMessage("InputMethodInfo is not null").that(info).isNotNull(); + mImm.setExplicitlyEnabledInputMethodSubtypes(info.getId(), SUBTYPE_IDS); + try (var ignored = mGestureNavSwitchHelper.withGestureNavigationMode()) { verifyInputViewStatusOnMainSync( () -> { setDrawsImeNavBarAndSwitcherButton(true /* enabled */); mActivity.showImeWithWindowInsetsController(); }, - EVENT_SHOW, - true /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); + EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); final var imeSwitcherButton = getUiObject(By.res(INPUT_METHOD_NAV_IME_SWITCHER_ID)); imeSwitcherButton.longClick(); @@ -1052,58 +912,78 @@ public class InputMethodServiceTest { } } - private void verifyInputViewStatus(@NonNull Runnable runnable, @Event int event, - boolean expected, boolean inputViewStarted) { - verifyInputViewStatusInternal(runnable, event, expected, inputViewStarted, - false /* runOnMainSync */); + private void verifyInputViewStatus(@NonNull Runnable runnable, @Event int eventType, + boolean eventExpected, boolean shown, @NonNull String message) { + verifyInputViewStatusInternal(runnable, eventType, eventExpected, + shown /* inputViewStarted */, shown, message, false /* runOnMainSync */); + } + + private void verifyInputViewStatusOnMainSync(@NonNull Runnable runnable, @Event int eventType, + boolean eventExpected, boolean shown, @NonNull String message) { + verifyInputViewStatusInternal(runnable, eventType, eventExpected, + shown /* inputViewStarted */, shown, message, true /* runOnMainSync */); } - private void verifyInputViewStatusOnMainSync(@NonNull Runnable runnable, @Event int event, - boolean expected, boolean inputViewStarted) { - verifyInputViewStatusInternal(runnable, event, expected, inputViewStarted, - true /* runOnMainSync */); + private void verifyInputViewStatusOnMainSync(@NonNull Runnable runnable, @Event int eventType, + boolean eventExpected, boolean inputViewStarted, boolean shown, + @NonNull String message) { + verifyInputViewStatusInternal(runnable, eventType, eventExpected, inputViewStarted, shown, + message, true /* runOnMainSync */); } /** * Verifies the status of the Input View after executing the given runnable, and waiting that - * the event was either triggered or not, based on the given expectation. + * the event was either called or not. * - * @param runnable the runnable to trigger the event - * @param event the event to await. - * @param expected whether the event is expected to be triggered. - * @param inputViewStarted the expected state of the Input View after executing the runnable. + * @param runnable the runnable to call the event. + * @param eventType the event type to wait for. + * @param eventExpected whether the event is expected to be called. + * @param inputViewStarted whether the input view is expected to be started. + * @param shown whether the input view is expected to be shown. + * @param message the message for the input view shown assertion. * @param runOnMainSync whether to execute the runnable on the main thread. */ - private void verifyInputViewStatusInternal(@NonNull Runnable runnable, @Event int event, - boolean expected, boolean inputViewStarted, boolean runOnMainSync) { - final boolean completed; + private void verifyInputViewStatusInternal(@NonNull Runnable runnable, @Event int eventType, + boolean eventExpected, boolean inputViewStarted, boolean shown, @NonNull String message, + boolean runOnMainSync) { + final boolean eventCalled; try { final var latch = new CountDownLatch(1); - mInputMethodService.setCountDownLatchForTesting(latch, event); + mInputMethodService.setCountDownLatchForTesting(latch, eventType); if (runOnMainSync) { mInstrumentation.runOnMainSync(runnable); } else { runnable.run(); } mInstrumentation.waitForIdleSync(); - completed = latch.await(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS); + eventCalled = latch.await(TIMEOUT_IN_SECONDS, TimeUnit.SECONDS); } catch (InterruptedException e) { fail("Interrupted while waiting for latch: " + e.getMessage()); return; } finally { - mInputMethodService.setCountDownLatchForTesting(null /* latch */, event); + mInputMethodService.setCountDownLatchForTesting(null /* latch */, eventType); } - if (expected && !completed) { - fail("Timed out waiting for " + eventToString(event)); - } else if (!expected && completed) { - fail("Unexpected call " + eventToString(event)); + if (eventExpected && !eventCalled) { + fail("Timed out waiting for " + eventToString(eventType)); + } else if (!eventExpected && eventCalled) { + fail("Unexpected call " + eventToString(eventType)); } - // Input is not finished. + // Input connection is not finished. assertWithMessage("Input connection is still started") .that(mInputMethodService.getCurrentInputStarted()).isTrue(); - assertWithMessage("IME visibility matches expected") + assertWithMessage("Input view started matches expected") .that(mInputMethodService.getCurrentInputViewStarted()).isEqualTo(inputViewStarted); + + if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { + // The IME visibility is only sent at the end of the hide animation. Therefore, we have + // to wait until the visibility was sent to the server and the IME window hidden. + eventually(() -> assertWithMessage(message).that(mInputMethodService.isInputViewShown()) + .isEqualTo(shown)); + } else { + assertWithMessage(message).that(mInputMethodService.isInputViewShown()) + .isEqualTo(shown); + } } private void setOrientation(int orientation) { @@ -1127,31 +1007,28 @@ public class InputMethodServiceTest { /** * Verifies the IME fullscreen mode state after executing the given runnable. * - * @param runnable the runnable to execute for setting the orientation. - * @param expected whether the runnable is expected to trigger the signal. + * @param runnable the runnable to set the orientation. + * @param eventExpected whether the event is expected to be called. * @param orientationPortrait whether the orientation is expected to be portrait. */ - private void verifyFullscreenMode(@NonNull Runnable runnable, boolean expected, + private void verifyFullscreenMode(@NonNull Runnable runnable, boolean eventExpected, boolean orientationPortrait) { - verifyInputViewStatus(runnable, EVENT_CONFIG, expected, false /* inputViewStarted */); - if (expected) { + 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.getLastCreatedInstance()).isNotEqualTo(mActivity)); + .that(TestActivity.getInstance()).isNotEqualTo(mActivity)); // Get the new TestActivity. - mActivity = TestActivity.getLastCreatedInstance(); + 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( - () -> mActivity.showImeWithWindowInsetsController(), - EVENT_SHOW, - true /* expected */, - true /* inputViewStarted */); - assertWithMessage("IME is shown").that(mInputMethodService.isInputViewShown()).isTrue(); + verifyInputViewStatusOnMainSync(() -> mActivity.showImeWithWindowInsetsController(), + EVENT_SHOW, true /* eventExpected */, true /* shown */, "IME is shown"); assertWithMessage("IME orientation matches expected") .that(mInputMethodService.getResources().getConfiguration().orientation) @@ -1172,21 +1049,8 @@ public class InputMethodServiceTest { .that(mInputMethodService.isFullscreenMode()).isEqualTo(!orientationPortrait); // Hide IME before finishing the run. - verifyInputViewStatusOnMainSync( - () -> mActivity.hideImeWithWindowInsetsController(), - EVENT_HIDE, - true /* expected */, - false /* inputViewStarted */); - - if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { - // The IME visibility is only sent at the end of the animation. Therefore, we have to - // wait until the visibility was sent to the server and the IME window hidden. - eventually(() -> assertWithMessage("IME is not shown") - .that(mInputMethodService.isInputViewShown()).isFalse()); - } else { - assertWithMessage("IME is not shown") - .that(mInputMethodService.isInputViewShown()).isFalse(); - } + verifyInputViewStatusOnMainSync(() -> mActivity.hideImeWithWindowInsetsController(), + EVENT_HIDE, true /* eventExpected */, false /* shown */, "IME is not shown"); } private void prepareIme() { diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java index 3af5db9841ea..6af4064b1288 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTestBase.java @@ -66,6 +66,7 @@ import com.android.internal.inputmethod.IInputMethodSession; import com.android.internal.inputmethod.IRemoteAccessibilityInputConnection; import com.android.internal.inputmethod.IRemoteInputConnection; import com.android.internal.inputmethod.InputBindResult; +import com.android.internal.protolog.IProtoLogConfigurationService; import com.android.internal.view.IInputMethodManager; import com.android.server.LocalServices; import com.android.server.ServiceThread; @@ -124,6 +125,7 @@ public class InputMethodManagerServiceTestBase { @Mock protected IBinder mMockInputMethodBinder; @Mock protected IInputManager mMockIInputManager; @Mock protected ImeTargetVisibilityPolicy mMockImeTargetVisibilityPolicy; + @Mock protected IProtoLogConfigurationService.Stub mMockProtoLogConfigurationService; protected Context mContext; protected MockitoSession mMockingSession; @@ -186,6 +188,9 @@ public class InputMethodManagerServiceTestBase { .when(() -> ServiceManager.getServiceOrThrow(Context.INPUT_METHOD_SERVICE)); doReturn(mMockIPlatformCompat) .when(() -> ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE)); + doReturn(mMockProtoLogConfigurationService) + .when(() -> ServiceManager.getServiceOrThrow( + Context.PROTOLOG_CONFIGURATION_SERVICE)); // Stubbing out context related methods to avoid the system holding strong references to // InputMethodManagerService. diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/xml/method.xml b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/xml/method.xml index 872b0688814a..3dd4a97f464f 100644 --- a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/xml/method.xml +++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/res/xml/method.xml @@ -17,7 +17,14 @@ <input-method xmlns:android="http://schemas.android.com/apk/res/android"> <subtype - android:label="FakeIme" + android:label="SimpleIme" android:imeSubtypeLocale="en_US" - android:imeSubtypeMode="keyboard"/> + android:imeSubtypeMode="keyboard" + android:subtypeId="1" /> + + <subtype + android:label="SimpleIme French" + android:imeSubtypeLocale="fr_FR" + android:imeSubtypeMode="keyboard" + android:subtypeId="2" /> </input-method>
\ No newline at end of file diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/SimpleKeyboardView.java b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/SimpleKeyboardView.java index 55d1b451c6b0..1840cdea4c20 100644 --- a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/SimpleKeyboardView.java +++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/SimpleKeyboardView.java @@ -23,6 +23,7 @@ import android.util.Log; import android.util.SparseArray; import android.view.KeyEvent; import android.view.LayoutInflater; +import android.view.WindowInsets; import android.widget.FrameLayout; import android.widget.TextView; @@ -107,6 +108,15 @@ final class SimpleKeyboardView extends FrameLayout { mapSoftKeys(); } + @Override + public WindowInsets onApplyWindowInsets(WindowInsets insets) { + // Handle edge to edge for navigationBars insets (system nav bar) + // and captionBars insets (IME navigation bar). + final int insetBottom = insets.getInsets(WindowInsets.Type.systemBars()).bottom; + setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), insetBottom); + return insets.inset(0, 0, 0, insetBottom); + } + /** * Sets the key press listener. * 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 3a7abbb167ec..558d1a7c4e8b 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 @@ -27,6 +27,7 @@ import androidx.annotation.Nullable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.lang.ref.WeakReference; import java.util.concurrent.CountDownLatch; /** Wrapper of {@link InputMethodService} to expose interfaces for testing purpose. */ @@ -35,8 +36,8 @@ public class InputMethodServiceWrapper extends InputMethodService { private static final String TAG = "InputMethodServiceWrapper"; /** Last created instance of this wrapper. */ - @Nullable - private static InputMethodServiceWrapper sInstance; + @NonNull + private static WeakReference<InputMethodServiceWrapper> sInstance = new WeakReference<>(null); /** IME show event ({@link #onStartInputView}). */ public static final int EVENT_SHOW = 0; @@ -68,32 +69,11 @@ public class InputMethodServiceWrapper extends InputMethodService { @Nullable private CountDownLatch mCountDownLatch; - /** Gets the last created instance of this wrapper, if available. */ - @Nullable - public static InputMethodServiceWrapper getInstance() { - return sInstance; - } - - public boolean getCurrentInputViewStarted() { - return mInputViewStarted; - } - - /** - * Sets the latch used to wait for the IME event. - * - * @param latch the latch to wait on. - * @param latchEvent the event to set the latch on. - */ - public void setCountDownLatchForTesting(@Nullable CountDownLatch latch, @Event int latchEvent) { - mCountDownLatch = latch; - mLatchEvent = latchEvent; - } - @Override public void onCreate() { Log.i(TAG, "onCreate()"); super.onCreate(); - sInstance = this; + sInstance = new WeakReference<>(this); } @Override @@ -103,6 +83,12 @@ public class InputMethodServiceWrapper extends InputMethodService { } @Override + public void onFinishInput() { + Log.i(TAG, "onFinishInput()"); + super.onFinishInput(); + } + + @Override public void onStartInputView(EditorInfo info, boolean restarting) { Log.i(TAG, "onStartInputView() editor=" + dumpEditorInfo(info) + ", restarting=" + restarting); @@ -114,12 +100,6 @@ public class InputMethodServiceWrapper extends InputMethodService { } @Override - public void onFinishInput() { - Log.i(TAG, "onFinishInput()"); - super.onFinishInput(); - } - - @Override public void onFinishInputView(boolean finishingInput) { Log.i(TAG, "onFinishInputView()"); super.onFinishInputView(finishingInput); @@ -146,14 +126,35 @@ public class InputMethodServiceWrapper extends InputMethodService { } } + public boolean getCurrentInputViewStarted() { + return mInputViewStarted; + } + + /** + * Sets the latch used to wait for the IME event. + * + * @param latch the latch to wait on. + * @param latchEvent the event to set the latch on. + */ + public void setCountDownLatchForTesting(@Nullable CountDownLatch latch, @Event int latchEvent) { + mCountDownLatch = latch; + mLatchEvent = latchEvent; + } + + /** Gets the last created instance of this wrapper, if available. */ + @Nullable + public static InputMethodServiceWrapper getInstance() { + return sInstance.get(); + } + /** * Gets the string representation of the IME event that is being waited on. * - * @param event the IME event. + * @param eventType the IME event type. */ @NonNull - public static String eventToString(@Event int event) { - return switch (event) { + public static String eventToString(@Event int eventType) { + return switch (eventType) { case EVENT_SHOW -> "onStartInputView"; case EVENT_HIDE -> "onFinishInputView"; case EVENT_CONFIG -> "onConfigurationChanged"; diff --git a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/testing/TestActivity.java b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/testing/TestActivity.java index eadbac3ca74b..ad90b32848b0 100644 --- a/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/testing/TestActivity.java +++ b/services/tests/InputMethodSystemServerTests/test-apps/SimpleTestIme/src/com/android/apps/inputmethod/simpleime/testing/TestActivity.java @@ -45,30 +45,13 @@ import java.lang.ref.WeakReference; public final class TestActivity extends Activity { private static final String TAG = "TestActivity"; - private static WeakReference<TestActivity> sLastCreatedInstance = new WeakReference<>(null); - /** - * Start a new test activity with an editor and wait for it to begin running before returning. - * - * @param instrumentation application instrumentation - * @return the newly started activity - */ + /** Last created instance of this activity. */ @NonNull - public static TestActivity startSync(@NonNull Instrumentation instrumentation) { - final var intent = new Intent() - .setAction(Intent.ACTION_MAIN) - .setClass(instrumentation.getTargetContext(), TestActivity.class) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); - return (TestActivity) instrumentation.startActivitySync(intent); - } + private static WeakReference<TestActivity> sInstance = new WeakReference<>(null); private EditText mEditText; - public EditText getEditText() { - return mEditText; - } - @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -80,13 +63,11 @@ public final class TestActivity extends Activity { rootView.setFitsSystemWindows(true); setContentView(rootView); mEditText.requestFocus(); - sLastCreatedInstance = new WeakReference<>(this); + sInstance = new WeakReference<>(this); } - /** Get the last created TestActivity instance, if available. */ - @Nullable - public static TestActivity getLastCreatedInstance() { - return sLastCreatedInstance.get(); + public EditText getEditText() { + return mEditText; } /** Shows soft keyboard via InputMethodManager. */ @@ -118,4 +99,26 @@ public final class TestActivity extends Activity { controller.hide(WindowInsets.Type.ime()); Log.i(TAG, "hideIme() via WindowInsetsController"); } + + /** Gets the last created instance of this activity, if available. */ + @Nullable + public static TestActivity getInstance() { + return sInstance.get(); + } + + /** + * Start a new test activity with an editor and wait for it to begin running before returning. + * + * @param instrumentation application instrumentation. + * @return the newly started activity. + */ + @NonNull + public static TestActivity startSync(@NonNull Instrumentation instrumentation) { + final var intent = new Intent() + .setAction(Intent.ACTION_MAIN) + .setClass(instrumentation.getTargetContext(), TestActivity.class) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + return (TestActivity) instrumentation.startActivitySync(intent); + } } 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/DisplayDeviceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceTest.java index 00296745a81f..38586155993e 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceTest.java @@ -23,10 +23,17 @@ import static android.view.Surface.ROTATION_90; import static com.google.common.truth.Truth.assertThat; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + import android.graphics.Point; import android.graphics.Rect; import android.platform.test.annotations.Presubmit; import android.view.Display; +import android.view.Surface; import android.view.SurfaceControl; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -37,6 +44,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.MockitoAnnotations; /** @@ -154,6 +162,65 @@ public class DisplayDeviceTest { assertThat(displayDevice.getDisplaySurfaceDefaultSizeLocked()).isEqualTo(LANDSCAPE_SIZE); } + @Test + public void testSetDisplaySize_landscapeInstallRotation() { + DisplayDevice displayDevice = new FakeDisplayDevice(mDisplayDeviceInfo, + mMockDisplayAdapter); + mDisplayDeviceInfo.installOrientation = Surface.ROTATION_0; + mDisplayDeviceInfo.width = 100; + mDisplayDeviceInfo.height = 200; + displayDevice.configureDisplaySizeLocked(mMockTransaction); + verify(mMockTransaction).setDisplaySize(isNull(), eq(100), eq(200)); + + Mockito.clearInvocations(mMockTransaction); + + mDisplayDeviceInfo.installOrientation = Surface.ROTATION_180; + mDisplayDeviceInfo.width = 300; + mDisplayDeviceInfo.height = 400; + displayDevice.configureDisplaySizeLocked(mMockTransaction); + verify(mMockTransaction).setDisplaySize(isNull(), eq(300), eq(400)); + } + + @Test + public void testSetDisplaySize_portraitInstallRotation() { + DisplayDevice displayDevice = new FakeDisplayDevice(mDisplayDeviceInfo, + mMockDisplayAdapter); + mDisplayDeviceInfo.installOrientation = Surface.ROTATION_90; + mDisplayDeviceInfo.width = 100; + mDisplayDeviceInfo.height = 200; + displayDevice.configureDisplaySizeLocked(mMockTransaction); + verify(mMockTransaction).setDisplaySize(isNull(), eq(200), eq(100)); + + Mockito.clearInvocations(mMockTransaction); + + mDisplayDeviceInfo.installOrientation = Surface.ROTATION_270; + mDisplayDeviceInfo.width = 300; + mDisplayDeviceInfo.height = 400; + displayDevice.configureDisplaySizeLocked(mMockTransaction); + verify(mMockTransaction).setDisplaySize(isNull(), eq(400), eq(300)); + } + + @Test + public void testSetDisplaySize_invokedOnlyAfterResize() { + DisplayDevice displayDevice = new FakeDisplayDevice(mDisplayDeviceInfo, + mMockDisplayAdapter); + mDisplayDeviceInfo.installOrientation = Surface.ROTATION_90; + mDisplayDeviceInfo.width = 100; + mDisplayDeviceInfo.height = 200; + displayDevice.configureDisplaySizeLocked(mMockTransaction); + verify(mMockTransaction).setDisplaySize(isNull(), eq(200), eq(100)); + + Mockito.clearInvocations(mMockTransaction); + + displayDevice.configureDisplaySizeLocked(mMockTransaction); + verify(mMockTransaction, never()).setDisplaySize(isNull(), anyInt(), anyInt()); + + mDisplayDeviceInfo.width = 300; + mDisplayDeviceInfo.height = 400; + displayDevice.configureDisplaySizeLocked(mMockTransaction); + verify(mMockTransaction).setDisplaySize(isNull(), eq(400), eq(300)); + } + private static class FakeDisplayDevice extends DisplayDevice { private final DisplayDeviceInfo mDisplayDeviceInfo; diff --git a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java index b9cea0c72306..f8b4113a3c04 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/LocalDisplayAdapterTest.java @@ -550,10 +550,10 @@ public class LocalDisplayAdapterTest { assertDisplayDpi( mListener.addedDisplays.get(0).getDisplayDeviceInfoLocked(), PORT_A, 100, 100, - 100); + 136); assertDisplayDpi( mListener.addedDisplays.get(1).getDisplayDeviceInfoLocked(), PORT_B, 100, 100, - 100); + 136); } private static class DisplayModeWrapper { diff --git a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayTest.java b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayTest.java index 1a0ab252f128..37f8aba1fb6c 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayTest.java @@ -159,7 +159,8 @@ public class LogicalDisplayTest { mDisplayDeviceInfo.type = Display.TYPE_INTERNAL; mLogicalDisplay = new LogicalDisplay(DISPLAY_ID, LAYER_STACK, mDisplayDevice, /*isAnisotropyCorrectionEnabled=*/ true, - /*isAlwaysRotateDisplayDeviceEnabled=*/ true); + /*isAlwaysRotateDisplayDeviceEnabled=*/ true, + /*isSyncedResolutionSwitchEnabled=*/ true); // In case of Anisotropy of pixels, then the content should be rescaled so it would adjust // to using the whole screen. This is because display will rescale it back to fill the @@ -188,7 +189,8 @@ public class LogicalDisplayTest { mDisplayDeviceInfo.type = Display.TYPE_EXTERNAL; mLogicalDisplay = new LogicalDisplay(DISPLAY_ID, LAYER_STACK, mDisplayDevice, /*isAnisotropyCorrectionEnabled=*/ true, - /*isAlwaysRotateDisplayDeviceEnabled=*/ true); + /*isAlwaysRotateDisplayDeviceEnabled=*/ true, + /*isSyncedResolutionSwitchEnabled=*/ true); // In case of Anisotropy of pixels, then the content should be rescaled so it would adjust // to using the whole screen. This is because display will rescale it back to fill the @@ -217,7 +219,8 @@ public class LogicalDisplayTest { mDisplayDeviceInfo.type = Display.TYPE_EXTERNAL; mLogicalDisplay = new LogicalDisplay(DISPLAY_ID, LAYER_STACK, mDisplayDevice, /*isAnisotropyCorrectionEnabled=*/ true, - /*isAlwaysRotateDisplayDeviceEnabled=*/ true); + /*isAlwaysRotateDisplayDeviceEnabled=*/ true, + /*isSyncedResolutionSwitchEnabled=*/ true); DisplayInfo displayInfo = new DisplayInfo(); displayInfo.logicalWidth = DISPLAY_WIDTH; @@ -275,7 +278,8 @@ public class LogicalDisplayTest { mDisplayDeviceInfo.type = Display.TYPE_EXTERNAL; mLogicalDisplay = new LogicalDisplay(DISPLAY_ID, LAYER_STACK, mDisplayDevice, /*isAnisotropyCorrectionEnabled=*/ true, - /*isAlwaysRotateDisplayDeviceEnabled=*/ true); + /*isAlwaysRotateDisplayDeviceEnabled=*/ true, + /*isSyncedResolutionSwitchEnabled=*/ true); DisplayInfo displayInfo = new DisplayInfo(); displayInfo.logicalWidth = DISPLAY_WIDTH; @@ -304,7 +308,8 @@ public class LogicalDisplayTest { mDisplayDeviceInfo.type = Display.TYPE_EXTERNAL; mLogicalDisplay = new LogicalDisplay(DISPLAY_ID, LAYER_STACK, mDisplayDevice, /*isAnisotropyCorrectionEnabled=*/ true, - /*isAlwaysRotateDisplayDeviceEnabled=*/ true); + /*isAlwaysRotateDisplayDeviceEnabled=*/ true, + /*isSyncedResolutionSwitchEnabled=*/ true); // In case of Anisotropy of pixels, then the content should be rescaled so it would adjust // to using the whole screen. This is because display will rescale it back to fill the @@ -379,11 +384,24 @@ public class LogicalDisplayTest { } @Test + public void testSetDisplaySizeIsCalledDuringConfigureDisplayLocked() { + mLogicalDisplay = new LogicalDisplay(DISPLAY_ID, LAYER_STACK, mDisplayDevice, + /*isAnisotropyCorrectionEnabled=*/ true, + /*isAlwaysRotateDisplayDeviceEnabled=*/ true, + /*isSyncedResolutionSwitchEnabled=*/ true); + mLogicalDisplay.updateLocked(mDeviceRepo, mSyntheticModeManager); + SurfaceControl.Transaction t = mock(SurfaceControl.Transaction.class); + mLogicalDisplay.configureDisplayLocked(t, mDisplayDevice, false); + verify(mDisplayDevice).configureDisplaySizeLocked(eq(t)); + } + + @Test public void testGetDisplayPositionAlwaysRotateDisplayEnabled() { mDisplayDeviceInfo.type = Display.TYPE_EXTERNAL; mLogicalDisplay = new LogicalDisplay(DISPLAY_ID, LAYER_STACK, mDisplayDevice, /*isAnisotropyCorrectionEnabled=*/ true, - /*isAlwaysRotateDisplayDeviceEnabled=*/ true); + /*isAlwaysRotateDisplayDeviceEnabled=*/ true, + /*isSyncedResolutionSwitchEnabled=*/ true); mLogicalDisplay.updateLocked(mDeviceRepo, mSyntheticModeManager); Point expectedPosition = new Point(); 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/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/accessibility/autoclick/AutoclickControllerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java index 0227ef1d2dc0..7f60caaa569b 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/autoclick/AutoclickControllerTest.java @@ -326,6 +326,80 @@ public class AutoclickControllerTest { assertThat(mController.mClickScheduler.getIsActiveForTesting()).isTrue(); } + @Test + public void smallJitteryMovement_doesNotTriggerClick() { + // Initial hover move to set an anchor point. + MotionEvent initialHoverMove = MotionEvent.obtain( + /* downTime= */ 0, + /* eventTime= */ 100, + /* action= */ MotionEvent.ACTION_HOVER_MOVE, + /* x= */ 30f, + /* y= */ 40f, + /* metaState= */ 0); + initialHoverMove.setSource(InputDevice.SOURCE_MOUSE); + mController.onMotionEvent(initialHoverMove, initialHoverMove, /* policyFlags= */ 0); + + // Get the initial scheduled click time. + long initialScheduledTime = mController.mClickScheduler.getScheduledClickTimeForTesting(); + + // Simulate small, jittery movements (all within the default slop). + MotionEvent jitteryMove1 = MotionEvent.obtain( + /* downTime= */ 0, + /* eventTime= */ 150, + /* action= */ MotionEvent.ACTION_HOVER_MOVE, + /* x= */ 31f, // Small change in x + /* y= */ 41f, // Small change in y + /* metaState= */ 0); + jitteryMove1.setSource(InputDevice.SOURCE_MOUSE); + mController.onMotionEvent(jitteryMove1, jitteryMove1, /* policyFlags= */ 0); + + MotionEvent jitteryMove2 = MotionEvent.obtain( + /* downTime= */ 0, + /* eventTime= */ 200, + /* action= */ MotionEvent.ACTION_HOVER_MOVE, + /* x= */ 30.5f, // Small change in x + /* y= */ 39.8f, // Small change in y + /* metaState= */ 0); + jitteryMove2.setSource(InputDevice.SOURCE_MOUSE); + mController.onMotionEvent(jitteryMove2, jitteryMove2, /* policyFlags= */ 0); + + // Verify that the scheduled click time has NOT changed. + assertThat(mController.mClickScheduler.getScheduledClickTimeForTesting()) + .isEqualTo(initialScheduledTime); + } + + @Test + public void singleSignificantMovement_triggersClick() { + // Initial hover move to set an anchor point. + MotionEvent initialHoverMove = MotionEvent.obtain( + /* downTime= */ 0, + /* eventTime= */ 100, + /* action= */ MotionEvent.ACTION_HOVER_MOVE, + /* x= */ 30f, + /* y= */ 40f, + /* metaState= */ 0); + initialHoverMove.setSource(InputDevice.SOURCE_MOUSE); + mController.onMotionEvent(initialHoverMove, initialHoverMove, /* policyFlags= */ 0); + + // Get the initial scheduled click time. + long initialScheduledTime = mController.mClickScheduler.getScheduledClickTimeForTesting(); + + // Simulate a single, significant movement (greater than the default slop). + MotionEvent significantMove = MotionEvent.obtain( + /* downTime= */ 0, + /* eventTime= */ 150, + /* action= */ MotionEvent.ACTION_HOVER_MOVE, + /* x= */ 60f, // Significant change in x (30f difference) + /* y= */ 70f, // Significant change in y (30f difference) + /* metaState= */ 0); + significantMove.setSource(InputDevice.SOURCE_MOUSE); + mController.onMotionEvent(significantMove, significantMove, /* policyFlags= */ 0); + + // Verify that the scheduled click time has changed (click was rescheduled). + assertThat(mController.mClickScheduler.getScheduledClickTimeForTesting()) + .isNotEqualTo(initialScheduledTime); + } + private void injectFakeMouseActionHoverMoveEvent() { MotionEvent event = getFakeMotionHoverMoveEvent(); event.setSource(InputDevice.SOURCE_MOUSE); diff --git a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java index 06958b81d846..1627f683cd3e 100644 --- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java @@ -25,6 +25,7 @@ import static android.app.ActivityManagerInternal.ALLOW_FULL_ONLY; import static android.app.ActivityManagerInternal.ALLOW_NON_FULL; import static android.app.ActivityManagerInternal.ALLOW_NON_FULL_IN_PROFILE; import static android.app.ActivityManagerInternal.ALLOW_PROFILES_OR_NON_FULL; +import static android.app.KeyguardManager.LOCK_ON_USER_SWITCH_CALLBACK; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.testing.DexmakerShareClassLoaderRule.runWithDexmakerShareClassLoader; @@ -115,7 +116,6 @@ import com.android.server.pm.UserManagerInternal; import com.android.server.pm.UserManagerService; import com.android.server.pm.UserTypeDetails; import com.android.server.pm.UserTypeFactory; -import com.android.server.wm.ActivityTaskManagerInternal; import com.android.server.wm.WindowManagerService; import com.google.common.collect.Range; @@ -1563,11 +1563,11 @@ public class UserControllerTest { // and the thread is still alive assertTrue(threadStartUser.isAlive()); - // mock send the keyguard shown event - ArgumentCaptor<ActivityTaskManagerInternal.ScreenObserver> captor = ArgumentCaptor.forClass( - ActivityTaskManagerInternal.ScreenObserver.class); - verify(mInjector.mActivityTaskManagerInternal).registerScreenObserver(captor.capture()); - captor.getValue().onKeyguardStateChanged(true); + // mock the binder response for the user switch completion + ArgumentCaptor<Bundle> captor = ArgumentCaptor.forClass(Bundle.class); + verify(mInjector.mWindowManagerMock).lockNow(captor.capture()); + IRemoteCallback.Stub.asInterface(captor.getValue().getBinder( + LOCK_ON_USER_SWITCH_CALLBACK)).sendResult(null); // verify the switch now moves on... Thread.sleep(1000); @@ -1757,7 +1757,6 @@ public class UserControllerTest { private final IStorageManager mStorageManagerMock; private final UserManagerInternal mUserManagerInternalMock; private final WindowManagerService mWindowManagerMock; - private final ActivityTaskManagerInternal mActivityTaskManagerInternal; private final PowerManagerInternal mPowerManagerInternal; private final AlarmManagerInternal mAlarmManagerInternal; private final KeyguardManager mKeyguardManagerMock; @@ -1779,7 +1778,6 @@ public class UserControllerTest { mUserManagerMock = mock(UserManagerService.class); mUserManagerInternalMock = mock(UserManagerInternal.class); mWindowManagerMock = mock(WindowManagerService.class); - mActivityTaskManagerInternal = mock(ActivityTaskManagerInternal.class); mStorageManagerMock = mock(IStorageManager.class); mPowerManagerInternal = mock(PowerManagerInternal.class); mAlarmManagerInternal = mock(AlarmManagerInternal.class); @@ -1843,11 +1841,6 @@ public class UserControllerTest { } @Override - ActivityTaskManagerInternal getActivityTaskManagerInternal() { - return mActivityTaskManagerInternal; - } - - @Override PowerManagerInternal getPowerManagerInternal() { return mPowerManagerInternal; } 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/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/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/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 0373eb6e9318..858dd3a605d8 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 @@ -5383,6 +5385,42 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + public void testGetPackagesWithChannels_blocked() throws Exception { + // While we mostly rely on the PreferencesHelper implementation of channels, we filter in + // NMS so that we do not return blocked packages. + // Three packages; all under user 1. + // pkg2 is blocked, but pkg1 and pkg3 are not. + String pkg1 = "com.package.one", pkg2 = "com.package.two", pkg3 = "com.package.three"; + int uid1 = UserHandle.getUid(1, 111); + int uid2 = UserHandle.getUid(1, 222); + int uid3 = UserHandle.getUid(1, 333); + + when(mPackageManager.getPackageUid(eq(pkg1), anyLong(), anyInt())).thenReturn(uid1); + when(mPackageManager.getPackageUid(eq(pkg2), anyLong(), anyInt())).thenReturn(uid2); + when(mPackageManager.getPackageUid(eq(pkg3), anyLong(), anyInt())).thenReturn(uid3); + when(mPermissionHelper.hasPermission(uid1)).thenReturn(true); + when(mPermissionHelper.hasPermission(uid2)).thenReturn(false); + when(mPermissionHelper.hasPermission(uid3)).thenReturn(true); + + NotificationChannel channel1 = new NotificationChannel("id1", "name1", + NotificationManager.IMPORTANCE_DEFAULT); + NotificationChannel channel2 = new NotificationChannel("id3", "name3", + NotificationManager.IMPORTANCE_DEFAULT); + NotificationChannel channel3 = new NotificationChannel("id4", "name3", + NotificationManager.IMPORTANCE_DEFAULT); + mService.mPreferencesHelper.createNotificationChannel(pkg1, uid1, channel1, true, false, + uid1, false); + mService.mPreferencesHelper.createNotificationChannel(pkg2, uid2, channel2, true, false, + uid2, false); + mService.mPreferencesHelper.createNotificationChannel(pkg3, uid3, channel3, true, false, + uid3, false); + + // Output should contain only the package with notification permissions (1, 3). + enableInteractAcrossUsers(); + assertThat(mBinderService.getPackagesWithAnyChannels(1)).containsExactly(pkg1, pkg3); + } + + @Test public void testHasCompanionDevice_failure() throws Exception { when(mCompanionMgr.getAssociations(anyString(), anyInt())).thenThrow( new IllegalArgumentException()); @@ -16287,6 +16325,20 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @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; @@ -16303,6 +16355,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; @@ -16321,6 +16392,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; @@ -16339,6 +16431,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; @@ -16356,6 +16469,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; @@ -16372,10 +16505,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; @@ -16394,7 +16547,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; @@ -16411,6 +16589,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); 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 3f26cd9258af..640de174ba20 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java @@ -3096,6 +3096,67 @@ public class PreferencesHelperTest extends UiServiceTestCase { } @Test + public void getPackagesWithAnyChannels_noChannels() { + assertThat(mHelper.getPackagesWithAnyChannels(UserHandle.getUserId(UID_O))).isEmpty(); + } + + @Test + public void getPackagesWithAnyChannels_someChannels() { + // 2 channels under PKG_N_MR1, 1 under PKG_O + NotificationChannel channel1 = new NotificationChannel("1", "something", + IMPORTANCE_DEFAULT); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel1, true, false, UID_N_MR1, + false); + NotificationChannel channel2 = new NotificationChannel("2", "another", IMPORTANCE_DEFAULT); + mHelper.createNotificationChannel(PKG_N_MR1, UID_N_MR1, channel2, true, false, UID_N_MR1, + false); + + NotificationChannel other = new NotificationChannel("3", "still another", + IMPORTANCE_DEFAULT); + mHelper.createNotificationChannel(PKG_O, UID_O, other, true, false, UID_O, false); + + assertThat(mHelper.getPackagesWithAnyChannels(USER.getIdentifier())).containsExactly( + PKG_N_MR1, PKG_O); + } + + @Test + public void getPackagesWithAnyChannels_onlyDeleted() { + NotificationChannel channel1 = new NotificationChannel("1", "something", + IMPORTANCE_DEFAULT); + channel1.setDeleted(true); + mHelper.createNotificationChannel(PKG_O, UID_O, channel1, true, false, UID_O, + false); + NotificationChannel channel2 = new NotificationChannel("2", "another", IMPORTANCE_DEFAULT); + channel2.setDeleted(true); + mHelper.createNotificationChannel(PKG_O, UID_O, channel2, true, false, UID_O, + false); + + assertThat(mHelper.getPackagesWithAnyChannels(UserHandle.getUserId(UID_O))).isEmpty(); + } + + @Test + public void getPackagesWithAnyChannels_distinguishesUsers() throws Exception { + // Set a package up for both users 0 and 10 + String pkgName = "test.package"; + int uid0 = UserHandle.getUid(0, 1234); + int uid10 = UserHandle.getUid(10, 1234); + setUpPackageWithUid(pkgName, uid0); + setUpPackageWithUid(pkgName, uid10); + + // but only user 10 has channels + NotificationChannel channel1 = new NotificationChannel("1", "something", + IMPORTANCE_DEFAULT); + mHelper.createNotificationChannel(pkgName, uid10, channel1, true, false, uid10, + false); + NotificationChannel channel2 = new NotificationChannel("2", "another", IMPORTANCE_DEFAULT); + mHelper.createNotificationChannel(pkgName, uid10, channel2, true, false, uid10, + false); + + assertThat(mHelper.getPackagesWithAnyChannels(0)).isEmpty(); + assertThat(mHelper.getPackagesWithAnyChannels(10)).containsExactly(pkgName); + } + + @Test public void testOnlyHasDefaultChannel() throws Exception { assertTrue(mHelper.onlyHasDefaultChannel(PKG_N_MR1, UID_N_MR1)); assertFalse(mHelper.onlyHasDefaultChannel(PKG_O, UID_O)); 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..e6c3fb369b91 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/DesktopModeHelperTest.java @@ -177,22 +177,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 +221,7 @@ public class DesktopModeHelperTest { eq(R.bool.config_isDesktopModeDevOptionSupported) ); - assertThat(DesktopModeHelper.isInternalDisplayEligibleToHostDesktops(mMockContext)).isTrue(); + assertThat(DesktopModeHelper.isDeviceEligibleForDesktopMode(mMockContext)).isTrue(); } private void resetEnforceDeviceRestriction() throws Exception { diff --git a/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java deleted file mode 100644 index 20381ba21758..000000000000 --- a/services/tests/wmtests/src/com/android/server/wm/RemoteAnimationControllerTest.java +++ /dev/null @@ -1,757 +0,0 @@ -/* - * Copyright (C) 2018 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 android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; -import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; -import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR; -import static android.view.WindowManager.TRANSIT_OLD_ACTIVITY_OPEN; -import static android.view.WindowManager.TRANSIT_OLD_KEYGUARD_GOING_AWAY; -import static android.view.WindowManager.TRANSIT_OLD_KEYGUARD_GOING_AWAY_ON_WALLPAPER; -import static android.view.WindowManager.TRANSIT_OLD_NONE; -import static android.view.WindowManager.TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE; -import static android.view.WindowManager.TRANSIT_OLD_TASK_OPEN; - -import static com.android.dx.mockito.inline.extended.ExtendedMockito.atLeast; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.never; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.verifyNoMoreInteractions; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; -import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_APP_TRANSITION; -import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_WINDOW_ANIMATION; - -import static junit.framework.Assert.fail; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; - -import android.graphics.Point; -import android.graphics.Rect; -import android.os.Binder; -import android.os.IBinder; -import android.os.IInterface; -import android.os.RemoteException; -import android.platform.test.annotations.Presubmit; -import android.view.IRemoteAnimationFinishedCallback; -import android.view.IRemoteAnimationRunner; -import android.view.RemoteAnimationAdapter; -import android.view.RemoteAnimationTarget; -import android.view.SurfaceControl; -import android.view.SurfaceControl.Transaction; - -import androidx.test.filters.SmallTest; - -import com.android.server.testutils.OffsettableClock; -import com.android.server.testutils.TestHandler; -import com.android.server.wm.RemoteAnimationController.RemoteAnimationRecord; -import com.android.server.wm.SurfaceAnimator.OnAnimationFinishedCallback; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.MockitoAnnotations; - -/** - * Build/Install/Run: - * atest WmTests:RemoteAnimationControllerTest - */ -@SmallTest -@Presubmit -@RunWith(WindowTestRunner.class) -public class RemoteAnimationControllerTest extends WindowTestsBase { - - @Mock - SurfaceControl mMockLeash; - @Mock - SurfaceControl mMockThumbnailLeash; - @Mock - Transaction mMockTransaction; - @Mock - OnAnimationFinishedCallback mFinishedCallback; - @Mock - OnAnimationFinishedCallback mThumbnailFinishedCallback; - @Mock - IRemoteAnimationRunner mMockRunner; - private RemoteAnimationAdapter mAdapter; - private RemoteAnimationController mController; - private final OffsettableClock mClock = new OffsettableClock.Stopped(); - private TestHandler mHandler; - - @Before - public void setUp() throws Exception { - MockitoAnnotations.initMocks(this); - - when(mMockRunner.asBinder()).thenReturn(new Binder()); - mAdapter = new RemoteAnimationAdapter(mMockRunner, 100, 50, true /* changeNeedsSnapshot */); - mAdapter.setCallingPidUid(123, 456); - runWithScissors(mWm.mH, () -> mHandler = new TestHandler(null, mClock), 0); - mController = new RemoteAnimationController(mWm, mDisplayContent, mAdapter, - mHandler, false /*isActivityEmbedding*/); - mWm.mAnimator.ready(); - } - - private WindowState createAppOverlayWindow() { - final WindowState win = newWindowBuilder("testOverlayWindow", - TYPE_APPLICATION_OVERLAY).build(); - win.mActivityRecord = null; - win.mHasSurface = true; - return win; - } - - @Test - public void testForwardsShowBackdrop() throws Exception { - final WindowState win = createTestWindow(); - mDisplayContent.mOpeningApps.add(win.mActivityRecord); - final WindowState overlayWin = createAppOverlayWindow(); - try { - final AnimationAdapter adapter = mController.createRemoteAnimationRecord( - win.mActivityRecord, - new Point(50, 100), null, new Rect(50, 100, 150, 150), null, - true /* showBackdrop */).mAdapter; - adapter.startAnimation(mMockLeash, mMockTransaction, ANIMATION_TYPE_APP_TRANSITION, - mFinishedCallback); - mController.goodToGo(TRANSIT_OLD_ACTIVITY_OPEN); - waitUntilWindowAnimatorIdle(); - final ArgumentCaptor<RemoteAnimationTarget[]> appsCaptor = - ArgumentCaptor.forClass(RemoteAnimationTarget[].class); - final ArgumentCaptor<RemoteAnimationTarget[]> wallpapersCaptor = - ArgumentCaptor.forClass(RemoteAnimationTarget[].class); - final ArgumentCaptor<RemoteAnimationTarget[]> nonAppsCaptor = - ArgumentCaptor.forClass(RemoteAnimationTarget[].class); - final ArgumentCaptor<IRemoteAnimationFinishedCallback> finishedCaptor = - ArgumentCaptor.forClass(IRemoteAnimationFinishedCallback.class); - verify(mMockRunner).onAnimationStart(eq(TRANSIT_OLD_ACTIVITY_OPEN), - appsCaptor.capture(), wallpapersCaptor.capture(), nonAppsCaptor.capture(), - finishedCaptor.capture()); - assertEquals(1, appsCaptor.getValue().length); - final RemoteAnimationTarget app = appsCaptor.getValue()[0]; - assertTrue(app.showBackdrop); - } finally { - mDisplayContent.mOpeningApps.clear(); - } - } - - @Test - public void testRun() throws Exception { - final WindowState win = createTestWindow(); - mDisplayContent.mOpeningApps.add(win.mActivityRecord); - final WindowState overlayWin = createAppOverlayWindow(); - try { - final AnimationAdapter adapter = mController.createRemoteAnimationRecord( - win.mActivityRecord, - new Point(50, 100), null, new Rect(50, 100, 150, 150), null, false).mAdapter; - adapter.startAnimation(mMockLeash, mMockTransaction, ANIMATION_TYPE_APP_TRANSITION, - mFinishedCallback); - mController.goodToGo(TRANSIT_OLD_ACTIVITY_OPEN); - waitUntilWindowAnimatorIdle(); - final ArgumentCaptor<RemoteAnimationTarget[]> appsCaptor = - ArgumentCaptor.forClass(RemoteAnimationTarget[].class); - final ArgumentCaptor<RemoteAnimationTarget[]> wallpapersCaptor = - ArgumentCaptor.forClass(RemoteAnimationTarget[].class); - final ArgumentCaptor<RemoteAnimationTarget[]> nonAppsCaptor = - ArgumentCaptor.forClass(RemoteAnimationTarget[].class); - final ArgumentCaptor<IRemoteAnimationFinishedCallback> finishedCaptor = - ArgumentCaptor.forClass(IRemoteAnimationFinishedCallback.class); - verify(mMockRunner).onAnimationStart(eq(TRANSIT_OLD_ACTIVITY_OPEN), - appsCaptor.capture(), wallpapersCaptor.capture(), nonAppsCaptor.capture(), - finishedCaptor.capture()); - assertEquals(1, appsCaptor.getValue().length); - final RemoteAnimationTarget app = appsCaptor.getValue()[0]; - assertEquals(new Point(50, 100), app.position); - assertEquals(new Rect(50, 100, 150, 150), app.sourceContainerBounds); - assertEquals(win.mActivityRecord.getPrefixOrderIndex(), app.prefixOrderIndex); - assertEquals(win.mActivityRecord.getTask().mTaskId, app.taskId); - assertEquals(mMockLeash, app.leash); - assertEquals(false, app.isTranslucent); - verify(mMockTransaction).setPosition(mMockLeash, app.position.x, app.position.y); - verify(mMockTransaction).setWindowCrop(mMockLeash, 100, 50); - - finishedCaptor.getValue().onAnimationFinished(); - verify(mFinishedCallback).onAnimationFinished(eq(ANIMATION_TYPE_APP_TRANSITION), - eq(adapter)); - assertEquals(0, nonAppsCaptor.getValue().length); - } finally { - mDisplayContent.mOpeningApps.clear(); - } - } - - @Test - public void testCancel() throws Exception { - final WindowState win = createTestWindow(); - final AnimationAdapter adapter = mController.createRemoteAnimationRecord( - win.mActivityRecord, - new Point(50, 100), null, new Rect(50, 100, 150, 150), null, false).mAdapter; - adapter.startAnimation(mMockLeash, mMockTransaction, ANIMATION_TYPE_APP_TRANSITION, - mFinishedCallback); - mController.goodToGo(TRANSIT_OLD_ACTIVITY_OPEN); - - adapter.onAnimationCancelled(mMockLeash); - verify(mMockRunner).onAnimationCancelled(); - } - - @Test - public void testTimeout() throws Exception { - final WindowState win = createTestWindow(); - final AnimationAdapter adapter = mController.createRemoteAnimationRecord( - win.mActivityRecord, - new Point(50, 100), null, new Rect(50, 100, 150, 150), null, false).mAdapter; - adapter.startAnimation(mMockLeash, mMockTransaction, ANIMATION_TYPE_APP_TRANSITION, - mFinishedCallback); - mController.goodToGo(TRANSIT_OLD_ACTIVITY_OPEN); - - mClock.fastForward(10500); - mHandler.timeAdvance(); - - verify(mMockRunner).onAnimationCancelled(); - verify(mFinishedCallback).onAnimationFinished(eq(ANIMATION_TYPE_APP_TRANSITION), - eq(adapter)); - } - - @Test - public void testTimeout_scaled() throws Exception { - try { - mWm.setAnimationScale(2, 5.0f); - final WindowState win = createTestWindow(); - final AnimationAdapter adapter = mController.createRemoteAnimationRecord( - win.mActivityRecord, new Point(50, 100), null, new Rect(50, 100, 150, 150), - null, false).mAdapter; - adapter.startAnimation(mMockLeash, mMockTransaction, ANIMATION_TYPE_APP_TRANSITION, - mFinishedCallback); - mController.goodToGo(TRANSIT_OLD_ACTIVITY_OPEN); - - mClock.fastForward(10500); - mHandler.timeAdvance(); - - verify(mMockRunner, never()).onAnimationCancelled(); - - mClock.fastForward(52500); - mHandler.timeAdvance(); - - verify(mMockRunner).onAnimationCancelled(); - verify(mFinishedCallback).onAnimationFinished(eq(ANIMATION_TYPE_APP_TRANSITION), - eq(adapter)); - } finally { - mWm.setAnimationScale(2, 1.0f); - } - } - - @Test - public void testZeroAnimations() throws Exception { - mController.goodToGo(TRANSIT_OLD_NONE); - verify(mMockRunner, never()).onAnimationStart(anyInt(), any(), any(), any(), any()); - verify(mMockRunner).onAnimationCancelled(); - } - - @Test - public void testNotReallyStarted() throws Exception { - final WindowState win = createTestWindow(); - mController.createRemoteAnimationRecord(win.mActivityRecord, - new Point(50, 100), null, new Rect(50, 100, 150, 150), null, false); - mController.goodToGo(TRANSIT_OLD_ACTIVITY_OPEN); - verify(mMockRunner, never()).onAnimationStart(anyInt(), any(), any(), any(), any()); - verify(mMockRunner).onAnimationCancelled(); - } - - @Test - public void testOneNotStarted() throws Exception { - final WindowState win1 = newWindowBuilder("testWin1", TYPE_BASE_APPLICATION).build(); - final WindowState win2 = newWindowBuilder("testWin2", TYPE_BASE_APPLICATION).build(); - mController.createRemoteAnimationRecord(win1.mActivityRecord, - new Point(50, 100), null, new Rect(50, 100, 150, 150), null, false); - final AnimationAdapter adapter = mController.createRemoteAnimationRecord( - win2.mActivityRecord, - new Point(50, 100), null, new Rect(50, 100, 150, 150), null, false).mAdapter; - adapter.startAnimation(mMockLeash, mMockTransaction, ANIMATION_TYPE_APP_TRANSITION, - mFinishedCallback); - mController.goodToGo(TRANSIT_OLD_ACTIVITY_OPEN); - waitUntilWindowAnimatorIdle(); - final ArgumentCaptor<RemoteAnimationTarget[]> appsCaptor = - ArgumentCaptor.forClass(RemoteAnimationTarget[].class); - final ArgumentCaptor<RemoteAnimationTarget[]> wallpapersCaptor = - ArgumentCaptor.forClass(RemoteAnimationTarget[].class); - final ArgumentCaptor<RemoteAnimationTarget[]> nonAppsCaptor = - ArgumentCaptor.forClass(RemoteAnimationTarget[].class); - final ArgumentCaptor<IRemoteAnimationFinishedCallback> finishedCaptor = - ArgumentCaptor.forClass(IRemoteAnimationFinishedCallback.class); - verify(mMockRunner).onAnimationStart(eq(TRANSIT_OLD_ACTIVITY_OPEN), - appsCaptor.capture(), wallpapersCaptor.capture(), nonAppsCaptor.capture(), - finishedCaptor.capture()); - assertEquals(1, appsCaptor.getValue().length); - assertEquals(mMockLeash, appsCaptor.getValue()[0].leash); - } - - @Test - public void testRemovedBeforeStarted() throws Exception { - final WindowState win = createTestWindow(); - final AnimationAdapter adapter = mController.createRemoteAnimationRecord( - win.mActivityRecord, - new Point(50, 100), null, new Rect(50, 100, 150, 150), null, false).mAdapter; - adapter.startAnimation(mMockLeash, mMockTransaction, ANIMATION_TYPE_APP_TRANSITION, - mFinishedCallback); - win.mActivityRecord.removeImmediately(); - mController.goodToGo(TRANSIT_OLD_ACTIVITY_OPEN); - verify(mMockRunner, never()).onAnimationStart(anyInt(), any(), any(), any(), any()); - verify(mMockRunner).onAnimationCancelled(); - verify(mFinishedCallback).onAnimationFinished(eq(ANIMATION_TYPE_APP_TRANSITION), - eq(adapter)); - } - - @Test - public void testOpeningTaskWithTopFinishingActivity() { - final WindowState win = createTestWindow(); - final Task task = win.getTask(); - final ActivityRecord topFinishing = new ActivityBuilder(mAtm).setTask(task).build(); - // Now the task contains: - // - Activity[1] (top, finishing, no window) - // - Activity[0] (has window) - topFinishing.finishing = true; - spyOn(mDisplayContent.mAppTransition); - doReturn(mController).when(mDisplayContent.mAppTransition).getRemoteAnimationController(); - task.applyAnimationUnchecked(null /* lp */, true /* enter */, TRANSIT_OLD_TASK_OPEN, - false /* isVoiceInteraction */, null /* sources */); - mController.goodToGo(TRANSIT_OLD_TASK_OPEN); - waitUntilWindowAnimatorIdle(); - final ArgumentCaptor<RemoteAnimationTarget[]> appsCaptor = - ArgumentCaptor.forClass(RemoteAnimationTarget[].class); - try { - verify(mMockRunner).onAnimationStart(eq(TRANSIT_OLD_TASK_OPEN), - appsCaptor.capture(), any(), any(), any()); - } catch (RemoteException ignored) { - } - assertEquals(1, appsCaptor.getValue().length); - assertEquals(RemoteAnimationTarget.MODE_OPENING, appsCaptor.getValue()[0].mode); - } - - @Test - public void testChangeToSmallerSize() throws Exception { - final WindowState win = createTestWindow(); - mDisplayContent.mChangingContainers.add(win.mActivityRecord); - try { - final RemoteAnimationRecord record = mController.createRemoteAnimationRecord( - win.mActivityRecord, new Point(50, 100), null, new Rect(50, 100, 150, 150), - new Rect(0, 0, 200, 200), false); - assertNotNull(record.mThumbnailAdapter); - ((AnimationAdapter) record.mAdapter) - .startAnimation(mMockLeash, mMockTransaction, ANIMATION_TYPE_WINDOW_ANIMATION, - mFinishedCallback); - ((AnimationAdapter) record.mThumbnailAdapter).startAnimation(mMockThumbnailLeash, - mMockTransaction, ANIMATION_TYPE_WINDOW_ANIMATION, mThumbnailFinishedCallback); - mController.goodToGo(TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE); - waitUntilWindowAnimatorIdle(); - final ArgumentCaptor<RemoteAnimationTarget[]> appsCaptor = - ArgumentCaptor.forClass(RemoteAnimationTarget[].class); - final ArgumentCaptor<RemoteAnimationTarget[]> wallpapersCaptor = - ArgumentCaptor.forClass(RemoteAnimationTarget[].class); - final ArgumentCaptor<RemoteAnimationTarget[]> nonAppsCaptor = - ArgumentCaptor.forClass(RemoteAnimationTarget[].class); - final ArgumentCaptor<IRemoteAnimationFinishedCallback> finishedCaptor = - ArgumentCaptor.forClass(IRemoteAnimationFinishedCallback.class); - verify(mMockRunner).onAnimationStart(eq(TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE), - appsCaptor.capture(), wallpapersCaptor.capture(), nonAppsCaptor.capture(), - finishedCaptor.capture()); - assertEquals(1, appsCaptor.getValue().length); - final RemoteAnimationTarget app = appsCaptor.getValue()[0]; - assertEquals(RemoteAnimationTarget.MODE_CHANGING, app.mode); - assertEquals(new Point(50, 100), app.position); - assertEquals(new Rect(50, 100, 150, 150), app.sourceContainerBounds); - assertEquals(new Rect(0, 0, 200, 200), app.startBounds); - assertEquals(mMockLeash, app.leash); - assertEquals(mMockThumbnailLeash, app.startLeash); - assertEquals(false, app.isTranslucent); - verify(mMockTransaction).setPosition( - mMockLeash, app.startBounds.left, app.startBounds.top); - verify(mMockTransaction).setWindowCrop( - mMockLeash, app.startBounds.width(), app.startBounds.height()); - verify(mMockTransaction).setPosition(mMockThumbnailLeash, 0, 0); - verify(mMockTransaction).setWindowCrop(mMockThumbnailLeash, app.startBounds.width(), - app.startBounds.height()); - - finishedCaptor.getValue().onAnimationFinished(); - verify(mFinishedCallback).onAnimationFinished(eq(ANIMATION_TYPE_WINDOW_ANIMATION), - eq(record.mAdapter)); - verify(mThumbnailFinishedCallback).onAnimationFinished( - eq(ANIMATION_TYPE_WINDOW_ANIMATION), eq(record.mThumbnailAdapter)); - } finally { - mDisplayContent.mChangingContainers.clear(); - } - } - - @Test - public void testChangeTolargerSize() throws Exception { - final WindowState win = createTestWindow(); - mDisplayContent.mChangingContainers.add(win.mActivityRecord); - try { - final RemoteAnimationRecord record = mController.createRemoteAnimationRecord( - win.mActivityRecord, new Point(0, 0), null, new Rect(0, 0, 200, 200), - new Rect(50, 100, 150, 150), false); - assertNotNull(record.mThumbnailAdapter); - ((AnimationAdapter) record.mAdapter) - .startAnimation(mMockLeash, mMockTransaction, ANIMATION_TYPE_WINDOW_ANIMATION, - mFinishedCallback); - ((AnimationAdapter) record.mThumbnailAdapter).startAnimation(mMockThumbnailLeash, - mMockTransaction, ANIMATION_TYPE_WINDOW_ANIMATION, mThumbnailFinishedCallback); - mController.goodToGo(TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE); - waitUntilWindowAnimatorIdle(); - final ArgumentCaptor<RemoteAnimationTarget[]> appsCaptor = - ArgumentCaptor.forClass(RemoteAnimationTarget[].class); - final ArgumentCaptor<RemoteAnimationTarget[]> wallpapersCaptor = - ArgumentCaptor.forClass(RemoteAnimationTarget[].class); - final ArgumentCaptor<RemoteAnimationTarget[]> nonAppsCaptor = - ArgumentCaptor.forClass(RemoteAnimationTarget[].class); - final ArgumentCaptor<IRemoteAnimationFinishedCallback> finishedCaptor = - ArgumentCaptor.forClass(IRemoteAnimationFinishedCallback.class); - verify(mMockRunner).onAnimationStart(eq(TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE), - appsCaptor.capture(), wallpapersCaptor.capture(), nonAppsCaptor.capture(), - finishedCaptor.capture()); - assertEquals(1, appsCaptor.getValue().length); - final RemoteAnimationTarget app = appsCaptor.getValue()[0]; - assertEquals(RemoteAnimationTarget.MODE_CHANGING, app.mode); - assertEquals(new Point(0, 0), app.position); - assertEquals(new Rect(0, 0, 200, 200), app.sourceContainerBounds); - assertEquals(new Rect(50, 100, 150, 150), app.startBounds); - assertEquals(mMockLeash, app.leash); - assertEquals(mMockThumbnailLeash, app.startLeash); - assertEquals(false, app.isTranslucent); - verify(mMockTransaction).setPosition( - mMockLeash, app.startBounds.left, app.startBounds.top); - verify(mMockTransaction).setWindowCrop( - mMockLeash, app.startBounds.width(), app.startBounds.height()); - verify(mMockTransaction).setPosition(mMockThumbnailLeash, 0, 0); - verify(mMockTransaction).setWindowCrop(mMockThumbnailLeash, app.startBounds.width(), - app.startBounds.height()); - - finishedCaptor.getValue().onAnimationFinished(); - verify(mFinishedCallback).onAnimationFinished(eq(ANIMATION_TYPE_WINDOW_ANIMATION), - eq(record.mAdapter)); - verify(mThumbnailFinishedCallback).onAnimationFinished( - eq(ANIMATION_TYPE_WINDOW_ANIMATION), eq(record.mThumbnailAdapter)); - } finally { - mDisplayContent.mChangingContainers.clear(); - } - } - - @Test - public void testChangeToDifferentPosition() throws Exception { - final WindowState win = createTestWindow(); - mDisplayContent.mChangingContainers.add(win.mActivityRecord); - try { - final RemoteAnimationRecord record = mController.createRemoteAnimationRecord( - win.mActivityRecord, new Point(100, 100), null, new Rect(150, 150, 400, 400), - new Rect(50, 100, 150, 150), false); - assertNotNull(record.mThumbnailAdapter); - ((AnimationAdapter) record.mAdapter) - .startAnimation(mMockLeash, mMockTransaction, ANIMATION_TYPE_WINDOW_ANIMATION, - mFinishedCallback); - ((AnimationAdapter) record.mThumbnailAdapter).startAnimation(mMockThumbnailLeash, - mMockTransaction, ANIMATION_TYPE_WINDOW_ANIMATION, mThumbnailFinishedCallback); - mController.goodToGo(TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE); - waitUntilWindowAnimatorIdle(); - final ArgumentCaptor<RemoteAnimationTarget[]> appsCaptor = - ArgumentCaptor.forClass(RemoteAnimationTarget[].class); - final ArgumentCaptor<RemoteAnimationTarget[]> wallpapersCaptor = - ArgumentCaptor.forClass(RemoteAnimationTarget[].class); - final ArgumentCaptor<RemoteAnimationTarget[]> nonAppsCaptor = - ArgumentCaptor.forClass(RemoteAnimationTarget[].class); - final ArgumentCaptor<IRemoteAnimationFinishedCallback> finishedCaptor = - ArgumentCaptor.forClass(IRemoteAnimationFinishedCallback.class); - verify(mMockRunner).onAnimationStart(eq(TRANSIT_OLD_TASK_CHANGE_WINDOWING_MODE), - appsCaptor.capture(), wallpapersCaptor.capture(), nonAppsCaptor.capture(), - finishedCaptor.capture()); - assertEquals(1, appsCaptor.getValue().length); - final RemoteAnimationTarget app = appsCaptor.getValue()[0]; - assertEquals(RemoteAnimationTarget.MODE_CHANGING, app.mode); - assertEquals(new Point(100, 100), app.position); - assertEquals(new Rect(150, 150, 400, 400), app.sourceContainerBounds); - assertEquals(new Rect(50, 100, 150, 150), app.startBounds); - assertEquals(mMockLeash, app.leash); - assertEquals(mMockThumbnailLeash, app.startLeash); - assertEquals(false, app.isTranslucent); - verify(mMockTransaction).setPosition( - mMockLeash, app.position.x + app.startBounds.left - app.screenSpaceBounds.left, - app.position.y + app.startBounds.top - app.screenSpaceBounds.top); - verify(mMockTransaction).setWindowCrop( - mMockLeash, app.startBounds.width(), app.startBounds.height()); - verify(mMockTransaction).setPosition(mMockThumbnailLeash, 0, 0); - verify(mMockTransaction).setWindowCrop(mMockThumbnailLeash, app.startBounds.width(), - app.startBounds.height()); - - finishedCaptor.getValue().onAnimationFinished(); - verify(mFinishedCallback).onAnimationFinished(eq(ANIMATION_TYPE_WINDOW_ANIMATION), - eq(record.mAdapter)); - verify(mThumbnailFinishedCallback).onAnimationFinished( - eq(ANIMATION_TYPE_WINDOW_ANIMATION), eq(record.mThumbnailAdapter)); - } finally { - mDisplayContent.mChangingContainers.clear(); - } - } - - @Test - public void testWallpaperIncluded_expectTarget() throws Exception { - final WindowToken wallpaperWindowToken = new WallpaperWindowToken(mWm, mock(IBinder.class), - true, mDisplayContent, true /* ownerCanManageAppTokens */); - spyOn(mDisplayContent.mWallpaperController); - doReturn(true).when(mDisplayContent.mWallpaperController).isWallpaperVisible(); - final WindowState win = createTestWindow(); - mDisplayContent.mOpeningApps.add(win.mActivityRecord); - try { - final AnimationAdapter adapter = mController.createRemoteAnimationRecord( - win.mActivityRecord, - new Point(50, 100), null, new Rect(50, 100, 150, 150), null, false).mAdapter; - adapter.startAnimation(mMockLeash, mMockTransaction, ANIMATION_TYPE_APP_TRANSITION, - mFinishedCallback); - mController.goodToGo(TRANSIT_OLD_ACTIVITY_OPEN); - waitUntilWindowAnimatorIdle(); - final ArgumentCaptor<RemoteAnimationTarget[]> appsCaptor = - ArgumentCaptor.forClass(RemoteAnimationTarget[].class); - final ArgumentCaptor<RemoteAnimationTarget[]> wallpapersCaptor = - ArgumentCaptor.forClass(RemoteAnimationTarget[].class); - final ArgumentCaptor<RemoteAnimationTarget[]> nonAppsCaptor = - ArgumentCaptor.forClass(RemoteAnimationTarget[].class); - final ArgumentCaptor<IRemoteAnimationFinishedCallback> finishedCaptor = - ArgumentCaptor.forClass(IRemoteAnimationFinishedCallback.class); - verify(mMockRunner).onAnimationStart(eq(TRANSIT_OLD_ACTIVITY_OPEN), - appsCaptor.capture(), wallpapersCaptor.capture(), nonAppsCaptor.capture(), - finishedCaptor.capture()); - assertEquals(1, wallpapersCaptor.getValue().length); - } finally { - mDisplayContent.mOpeningApps.clear(); - } - } - - @Test - public void testWallpaperAnimatorCanceled_expectAnimationKeepsRunning() throws Exception { - final WindowToken wallpaperWindowToken = new WallpaperWindowToken(mWm, mock(IBinder.class), - true, mDisplayContent, true /* ownerCanManageAppTokens */); - spyOn(mDisplayContent.mWallpaperController); - doReturn(true).when(mDisplayContent.mWallpaperController).isWallpaperVisible(); - final WindowState win = createTestWindow(); - mDisplayContent.mOpeningApps.add(win.mActivityRecord); - try { - final AnimationAdapter adapter = mController.createRemoteAnimationRecord( - win.mActivityRecord, - new Point(50, 100), null, new Rect(50, 100, 150, 150), null, false).mAdapter; - adapter.startAnimation(mMockLeash, mMockTransaction, ANIMATION_TYPE_APP_TRANSITION, - mFinishedCallback); - mController.goodToGo(TRANSIT_OLD_ACTIVITY_OPEN); - waitUntilWindowAnimatorIdle(); - final ArgumentCaptor<RemoteAnimationTarget[]> appsCaptor = - ArgumentCaptor.forClass(RemoteAnimationTarget[].class); - final ArgumentCaptor<RemoteAnimationTarget[]> wallpapersCaptor = - ArgumentCaptor.forClass(RemoteAnimationTarget[].class); - final ArgumentCaptor<RemoteAnimationTarget[]> nonAPpsCaptor = - ArgumentCaptor.forClass(RemoteAnimationTarget[].class); - final ArgumentCaptor<IRemoteAnimationFinishedCallback> finishedCaptor = - ArgumentCaptor.forClass(IRemoteAnimationFinishedCallback.class); - verify(mMockRunner).onAnimationStart(eq(TRANSIT_OLD_ACTIVITY_OPEN), - appsCaptor.capture(), wallpapersCaptor.capture(), nonAPpsCaptor.capture(), - finishedCaptor.capture()); - assertEquals(1, wallpapersCaptor.getValue().length); - - // Cancel the wallpaper window animator and ensure the runner is not canceled - wallpaperWindowToken.cancelAnimation(); - verify(mMockRunner, never()).onAnimationCancelled(); - } finally { - mDisplayContent.mOpeningApps.clear(); - } - } - - @Test - public void testNonAppIncluded_keygaurdGoingAway() throws Exception { - final WindowState win = createTestWindow(); - mDisplayContent.mOpeningApps.add(win.mActivityRecord); - // Add overlay window hidden by the keyguard. - final WindowState overlayWin = createAppOverlayWindow(); - overlayWin.hide(false /* doAnimation */, false /* requestAnim */); - try { - final AnimationAdapter adapter = mController.createRemoteAnimationRecord( - win.mActivityRecord, new Point(50, 100), null, - new Rect(50, 100, 150, 150), null, false).mAdapter; - adapter.startAnimation(mMockLeash, mMockTransaction, ANIMATION_TYPE_APP_TRANSITION, - mFinishedCallback); - mController.goodToGo(TRANSIT_OLD_KEYGUARD_GOING_AWAY); - waitUntilWindowAnimatorIdle(); - final ArgumentCaptor<RemoteAnimationTarget[]> appsCaptor = - ArgumentCaptor.forClass(RemoteAnimationTarget[].class); - final ArgumentCaptor<RemoteAnimationTarget[]> wallpapersCaptor = - ArgumentCaptor.forClass(RemoteAnimationTarget[].class); - final ArgumentCaptor<RemoteAnimationTarget[]> nonAppsCaptor = - ArgumentCaptor.forClass(RemoteAnimationTarget[].class); - final ArgumentCaptor<IRemoteAnimationFinishedCallback> finishedCaptor = - ArgumentCaptor.forClass(IRemoteAnimationFinishedCallback.class); - verify(mMockRunner).onAnimationStart(eq(TRANSIT_OLD_KEYGUARD_GOING_AWAY), - appsCaptor.capture(), wallpapersCaptor.capture(), nonAppsCaptor.capture(), - finishedCaptor.capture()); - assertEquals(1, appsCaptor.getValue().length); - final RemoteAnimationTarget app = appsCaptor.getValue()[0]; - assertEquals(new Point(50, 100), app.position); - assertEquals(new Rect(50, 100, 150, 150), app.sourceContainerBounds); - assertEquals(win.mActivityRecord.getPrefixOrderIndex(), app.prefixOrderIndex); - assertEquals(win.mActivityRecord.getTask().mTaskId, app.taskId); - assertEquals(mMockLeash, app.leash); - assertEquals(false, app.isTranslucent); - verify(mMockTransaction).setPosition(mMockLeash, app.position.x, app.position.y); - verify(mMockTransaction).setWindowCrop(mMockLeash, 100, 50); - - finishedCaptor.getValue().onAnimationFinished(); - verify(mFinishedCallback).onAnimationFinished(eq(ANIMATION_TYPE_APP_TRANSITION), - eq(adapter)); - assertEquals(1, nonAppsCaptor.getValue().length); - } finally { - mDisplayContent.mOpeningApps.clear(); - } - } - - @Test - public void testNonAppIncluded_keygaurdGoingAwayToWallpaper() throws Exception { - final WindowToken wallpaperWindowToken = new WallpaperWindowToken(mWm, mock(IBinder.class), - true, mDisplayContent, true /* ownerCanManageAppTokens */); - spyOn(mDisplayContent.mWallpaperController); - doReturn(true).when(mDisplayContent.mWallpaperController).isWallpaperVisible(); - final WindowState win = createTestWindow(); - mDisplayContent.mOpeningApps.add(win.mActivityRecord); - // Add overlay window hidden by the keyguard. - final WindowState overlayWin = createAppOverlayWindow(); - overlayWin.hide(false /* doAnimation */, false /* requestAnim */); - try { - final AnimationAdapter adapter = mController.createRemoteAnimationRecord( - win.mActivityRecord, new Point(50, 100), null, - new Rect(50, 100, 150, 150), null, false).mAdapter; - adapter.startAnimation(mMockLeash, mMockTransaction, ANIMATION_TYPE_APP_TRANSITION, - mFinishedCallback); - mController.goodToGo(TRANSIT_OLD_KEYGUARD_GOING_AWAY_ON_WALLPAPER); - waitUntilWindowAnimatorIdle(); - final ArgumentCaptor<RemoteAnimationTarget[]> appsCaptor = - ArgumentCaptor.forClass(RemoteAnimationTarget[].class); - final ArgumentCaptor<RemoteAnimationTarget[]> wallpapersCaptor = - ArgumentCaptor.forClass(RemoteAnimationTarget[].class); - final ArgumentCaptor<RemoteAnimationTarget[]> nonAppsCaptor = - ArgumentCaptor.forClass(RemoteAnimationTarget[].class); - final ArgumentCaptor<IRemoteAnimationFinishedCallback> finishedCaptor = - ArgumentCaptor.forClass(IRemoteAnimationFinishedCallback.class); - verify(mMockRunner).onAnimationStart(eq(TRANSIT_OLD_KEYGUARD_GOING_AWAY_ON_WALLPAPER), - appsCaptor.capture(), wallpapersCaptor.capture(), nonAppsCaptor.capture(), - finishedCaptor.capture()); - assertEquals(1, wallpapersCaptor.getValue().length); - assertEquals(1, nonAppsCaptor.getValue().length); - } finally { - mDisplayContent.mOpeningApps.clear(); - } - } - - @Test - public void testNonAppTarget_sendNavBar() throws Exception { - final int transit = TRANSIT_OLD_TASK_OPEN; - final AnimationAdapter adapter = setupForNonAppTargetNavBar(transit, true); - - final ArgumentCaptor<RemoteAnimationTarget[]> nonAppsCaptor = - ArgumentCaptor.forClass(RemoteAnimationTarget[].class); - final ArgumentCaptor<IRemoteAnimationFinishedCallback> finishedCaptor = - ArgumentCaptor.forClass(IRemoteAnimationFinishedCallback.class); - verify(mMockRunner).onAnimationStart(eq(transit), any(), any(), - nonAppsCaptor.capture(), finishedCaptor.capture()); - boolean containNavTarget = false; - for (int i = 0; i < nonAppsCaptor.getValue().length; i++) { - if (nonAppsCaptor.getValue()[0].windowType == TYPE_NAVIGATION_BAR) { - containNavTarget = true; - break; - } - } - assertTrue(containNavTarget); - assertEquals(1, mController.mPendingNonAppAnimations.size()); - final NonAppWindowAnimationAdapter nonAppAdapter = - mController.mPendingNonAppAnimations.get(0); - spyOn(nonAppAdapter.getLeashFinishedCallback()); - - finishedCaptor.getValue().onAnimationFinished(); - verify(mFinishedCallback).onAnimationFinished(eq(ANIMATION_TYPE_APP_TRANSITION), - eq(adapter)); - verify(nonAppAdapter.getLeashFinishedCallback()) - .onAnimationFinished(nonAppAdapter.getLastAnimationType(), nonAppAdapter); - } - - @Test - public void testNonAppTarget_notSendNavBar_notAttachToApp() throws Exception { - final int transit = TRANSIT_OLD_TASK_OPEN; - setupForNonAppTargetNavBar(transit, false); - - final ArgumentCaptor<RemoteAnimationTarget[]> nonAppsCaptor = - ArgumentCaptor.forClass(RemoteAnimationTarget[].class); - verify(mMockRunner).onAnimationStart(eq(transit), - any(), any(), nonAppsCaptor.capture(), any()); - for (int i = 0; i < nonAppsCaptor.getValue().length; i++) { - if (nonAppsCaptor.getValue()[0].windowType == TYPE_NAVIGATION_BAR) { - fail("Non-app animation target must not contain navbar"); - } - } - } - - @Test - public void testNonAppTarget_notSendNavBar_controlledByFadeRotation() throws Exception { - final AsyncRotationController mockController = - mock(AsyncRotationController.class); - doReturn(mockController).when(mDisplayContent).getAsyncRotationController(); - final int transit = TRANSIT_OLD_TASK_OPEN; - setupForNonAppTargetNavBar(transit, true); - - final ArgumentCaptor<RemoteAnimationTarget[]> nonAppsCaptor = - ArgumentCaptor.forClass(RemoteAnimationTarget[].class); - verify(mMockRunner).onAnimationStart(eq(transit), - any(), any(), nonAppsCaptor.capture(), any()); - for (int i = 0; i < nonAppsCaptor.getValue().length; i++) { - if (nonAppsCaptor.getValue()[0].windowType == TYPE_NAVIGATION_BAR) { - fail("Non-app animation target must not contain navbar"); - } - } - } - - private AnimationAdapter setupForNonAppTargetNavBar(int transit, boolean shouldAttachNavBar) { - final WindowState win = createTestWindow(); - mDisplayContent.mOpeningApps.add(win.mActivityRecord); - final WindowState navBar = newWindowBuilder("NavigationBar", TYPE_NAVIGATION_BAR).build(); - mDisplayContent.getDisplayPolicy().addWindowLw(navBar, navBar.mAttrs); - final DisplayPolicy policy = mDisplayContent.getDisplayPolicy(); - spyOn(policy); - doReturn(shouldAttachNavBar).when(policy).shouldAttachNavBarToAppDuringTransition(); - - final AnimationAdapter adapter = mController.createRemoteAnimationRecord( - win.mActivityRecord, new Point(50, 100), null, - new Rect(50, 100, 150, 150), null, false).mAdapter; - adapter.startAnimation(mMockLeash, mMockTransaction, ANIMATION_TYPE_APP_TRANSITION, - mFinishedCallback); - mController.goodToGo(transit); - waitUntilWindowAnimatorIdle(); - return adapter; - } - - private static void verifyNoMoreInteractionsExceptAsBinder(IInterface binder) { - verify(binder, atLeast(0)).asBinder(); - verifyNoMoreInteractions(binder); - } - - private WindowState createTestWindow() { - return newWindowBuilder("testWin", TYPE_BASE_APPLICATION).build(); - } -} 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/TaskFragmentOrganizerControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java index 546ecc6e38a5..ab76ae8e378a 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentOrganizerControllerTest.java @@ -29,14 +29,14 @@ import static android.view.WindowManager.TRANSIT_OPEN; import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_OR_MOVE_TASK_FRAGMENT_DECOR_SURFACE; import static android.window.TaskFragmentOperation.OP_TYPE_CREATE_TASK_FRAGMENT; import static android.window.TaskFragmentOperation.OP_TYPE_DELETE_TASK_FRAGMENT; +import static android.window.TaskFragmentOperation.OP_TYPE_PRIVILEGED_REORDER_TO_BOTTOM_OF_TASK; +import static android.window.TaskFragmentOperation.OP_TYPE_PRIVILEGED_REORDER_TO_TOP_OF_TASK; +import static android.window.TaskFragmentOperation.OP_TYPE_PRIVILEGED_SET_CAN_AFFECT_SYSTEM_UI_FLAGS; import static android.window.TaskFragmentOperation.OP_TYPE_REMOVE_TASK_FRAGMENT_DECOR_SURFACE; -import static android.window.TaskFragmentOperation.OP_TYPE_REORDER_TO_BOTTOM_OF_TASK; import static android.window.TaskFragmentOperation.OP_TYPE_REORDER_TO_FRONT; -import static android.window.TaskFragmentOperation.OP_TYPE_REORDER_TO_TOP_OF_TASK; import static android.window.TaskFragmentOperation.OP_TYPE_REPARENT_ACTIVITY_TO_TASK_FRAGMENT; import static android.window.TaskFragmentOperation.OP_TYPE_SET_ADJACENT_TASK_FRAGMENTS; import static android.window.TaskFragmentOperation.OP_TYPE_SET_ANIMATION_PARAMS; -import static android.window.TaskFragmentOperation.OP_TYPE_SET_CAN_AFFECT_SYSTEM_UI_FLAGS; import static android.window.TaskFragmentOperation.OP_TYPE_SET_DIM_ON_TASK; import static android.window.TaskFragmentOperation.OP_TYPE_START_ACTIVITY_IN_TASK_FRAGMENT; import static android.window.TaskFragmentOrganizer.KEY_ERROR_CALLBACK_OP_TYPE; @@ -1821,7 +1821,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { // Reorder TaskFragment to bottom final TaskFragmentOperation operation = new TaskFragmentOperation.Builder( - OP_TYPE_REORDER_TO_BOTTOM_OF_TASK).build(); + OP_TYPE_PRIVILEGED_REORDER_TO_BOTTOM_OF_TASK).build(); mTransaction.addTaskFragmentOperation(tf1.getFragmentToken(), operation); assertApplyTransactionAllowed(mTransaction); @@ -1858,7 +1858,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { // Reorder TaskFragment to top final TaskFragmentOperation operation = new TaskFragmentOperation.Builder( - OP_TYPE_REORDER_TO_TOP_OF_TASK).build(); + OP_TYPE_PRIVILEGED_REORDER_TO_TOP_OF_TASK).build(); mTransaction.addTaskFragmentOperation(tf0.getFragmentToken(), operation); assertApplyTransactionAllowed(mTransaction); @@ -1903,13 +1903,13 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { @Test public void testApplyTransaction_reorderToBottomOfTask_failsIfNotSystemOrganizer() { testApplyTransaction_reorder_failsIfNotSystemOrganizer_common( - OP_TYPE_REORDER_TO_BOTTOM_OF_TASK); + OP_TYPE_PRIVILEGED_REORDER_TO_BOTTOM_OF_TASK); } @Test public void testApplyTransaction_reorderToTopOfTask_failsIfNotSystemOrganizer() { testApplyTransaction_reorder_failsIfNotSystemOrganizer_common( - OP_TYPE_REORDER_TO_TOP_OF_TASK); + OP_TYPE_PRIVILEGED_REORDER_TO_TOP_OF_TASK); } @Test @@ -1922,7 +1922,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { // Setting the flag to false. TaskFragmentOperation operation = new TaskFragmentOperation.Builder( - OP_TYPE_SET_CAN_AFFECT_SYSTEM_UI_FLAGS).setBooleanValue(false).build(); + OP_TYPE_PRIVILEGED_SET_CAN_AFFECT_SYSTEM_UI_FLAGS).setBooleanValue(false).build(); mTransaction.addTaskFragmentOperation(tf.getFragmentToken(), operation); assertApplyTransactionAllowed(mTransaction); @@ -1931,7 +1931,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { // Setting the flag back to true. operation = new TaskFragmentOperation.Builder( - OP_TYPE_SET_CAN_AFFECT_SYSTEM_UI_FLAGS).setBooleanValue(true).build(); + OP_TYPE_PRIVILEGED_SET_CAN_AFFECT_SYSTEM_UI_FLAGS).setBooleanValue(true).build(); mTransaction.addTaskFragmentOperation(tf.getFragmentToken(), operation); assertApplyTransactionAllowed(mTransaction); @@ -1945,7 +1945,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { final TaskFragment tf = createTaskFragment(task); TaskFragmentOperation operation = new TaskFragmentOperation.Builder( - OP_TYPE_SET_CAN_AFFECT_SYSTEM_UI_FLAGS).setBooleanValue(false).build(); + OP_TYPE_PRIVILEGED_SET_CAN_AFFECT_SYSTEM_UI_FLAGS).setBooleanValue(false).build(); mTransaction .addTaskFragmentOperation(tf.getFragmentToken(), operation) .setErrorCallbackToken(mErrorToken); @@ -1955,7 +1955,7 @@ public class TaskFragmentOrganizerControllerTest extends WindowTestsBase { // The pending event will be dispatched on the handler (from requestTraversal). waitHandlerIdle(mWm.mAnimationHandler); - assertTaskFragmentErrorTransaction(OP_TYPE_SET_CAN_AFFECT_SYSTEM_UI_FLAGS, + assertTaskFragmentErrorTransaction(OP_TYPE_PRIVILEGED_SET_CAN_AFFECT_SYSTEM_UI_FLAGS, SecurityException.class); } diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowContainerThumbnailTest.java b/services/tests/wmtests/src/com/android/server/wm/WindowContainerThumbnailTest.java deleted file mode 100644 index 849072e133ae..000000000000 --- a/services/tests/wmtests/src/com/android/server/wm/WindowContainerThumbnailTest.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (C) 2019 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.dx.mockito.inline.extended.ExtendedMockito.mock; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; - -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.mockito.ArgumentMatchers.any; - -import android.hardware.HardwareBuffer; -import android.platform.test.annotations.Presubmit; - -import androidx.test.filters.SmallTest; - -import com.android.server.testutils.StubTransaction; - -import org.junit.Test; -import org.junit.runner.RunWith; - -/** - * Test class for {@link TaskSnapshotSurface}. - * - * Build/Install/Run: - * atest WmTests:WindowContainerThumbnailTest - * - */ -@SmallTest -@Presubmit -@RunWith(WindowTestRunner.class) -public class WindowContainerThumbnailTest extends WindowTestsBase { - private WindowContainerThumbnail buildThumbnail() { - final HardwareBuffer buffer = HardwareBuffer.create(1, 1, HardwareBuffer.RGBA_8888, - 1, HardwareBuffer.USAGE_CPU_READ_RARELY); - final ActivityRecord mockAr = mock(ActivityRecord.class); - when(mockAr.getPendingTransaction()).thenReturn(new StubTransaction()); - when(mockAr.makeChildSurface(any())).thenReturn(new MockSurfaceControlBuilder()); - when(mockAr.makeSurface()).thenReturn(new MockSurfaceControlBuilder()); - return new WindowContainerThumbnail(new StubTransaction(), mockAr, buffer, - mock(SurfaceAnimator.class)); - } - - @Test - public void testDestroy_nullsSurface() { - final WindowContainerThumbnail t = buildThumbnail(); - assertNotNull(t.getSurfaceControl()); - t.destroy(); - assertNull(t.getSurfaceControl()); - } -} diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java index a718c06cc2fa..59ee2f5c8e9f 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowStateTests.java @@ -1281,6 +1281,7 @@ public class WindowStateTests extends WindowTestsBase { // Simulate app plays closing transition to app2. app.mActivityRecord.commitVisibility(false, false); + mDisplayContent.computeImeTarget(true /* updateImeTarget */); assertTrue(app.mActivityRecord.mLastImeShown); // Verify the IME insets is visible on app, but not for app2 during app task switching. 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/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 b22e42d1ab89..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) @@ -358,6 +358,34 @@ class InputManagerServiceTests { verifyNoMoreInteractions(listener) } + @Test + fun testKeyEventsForwardedToFocusedWindow_whenWmAllows() { + service.systemRunning() + overrideSendActionKeyEventsToFocusedWindow( + /* hasPermission = */false, + /* hasPrivateFlag = */false + ) + whenever(wmCallbacks.interceptKeyBeforeDispatching(any(), any(), anyInt())).thenReturn(0) + + val event = KeyEvent( /* downTime= */0, /* eventTime= */0, KeyEvent.ACTION_DOWN, + KeyEvent.KEYCODE_SPACE, /* repeat= */0, KeyEvent.META_CTRL_ON) + assertEquals(0, service.interceptKeyBeforeDispatching(null, event, 0)) + } + + @Test + fun testKeyEventsNotForwardedToFocusedWindow_whenWmConsumes() { + service.systemRunning() + overrideSendActionKeyEventsToFocusedWindow( + /* hasPermission = */false, + /* hasPrivateFlag = */false + ) + whenever(wmCallbacks.interceptKeyBeforeDispatching(any(), any(), anyInt())).thenReturn(-1) + + val event = KeyEvent( /* downTime= */0, /* eventTime= */0, KeyEvent.ACTION_DOWN, + KeyEvent.KEYCODE_SPACE, /* repeat= */0, KeyEvent.META_CTRL_ON) + assertEquals(-1, service.interceptKeyBeforeDispatching(null, event, 0)) + } + private class AutoClosingVirtualDisplays(val displays: List<VirtualDisplay>) : AutoCloseable { operator fun get(i: Int): VirtualDisplay = displays[i] diff --git a/tests/Tracing/src/com/android/internal/protolog/ProtoLogConfigurationServiceTest.java b/tests/Tracing/src/com/android/internal/protolog/ProtoLogConfigurationServiceTest.java index a3d03a8278ed..3be725101252 100644 --- a/tests/Tracing/src/com/android/internal/protolog/ProtoLogConfigurationServiceTest.java +++ b/tests/Tracing/src/com/android/internal/protolog/ProtoLogConfigurationServiceTest.java @@ -38,6 +38,8 @@ import android.tools.traces.io.ResultReader; import android.tools.traces.io.ResultWriter; import android.tools.traces.monitors.PerfettoTraceMonitor; +import com.android.internal.protolog.IProtoLogConfigurationService.RegisterClientArgs; + import com.google.common.truth.Truth; import com.google.protobuf.InvalidProtocolBufferException; @@ -152,10 +154,9 @@ public class ProtoLogConfigurationServiceTest { public void canRegisterClientWithGroupsOnly() throws RemoteException { final ProtoLogConfigurationService service = new ProtoLogConfigurationServiceImpl(); - final ProtoLogConfigurationServiceImpl.RegisterClientArgs args = - new ProtoLogConfigurationServiceImpl.RegisterClientArgs() - .setGroups(new ProtoLogConfigurationServiceImpl.RegisterClientArgs - .GroupConfig(TEST_GROUP, true)); + final RegisterClientArgs args = new RegisterClientArgs(); + args.groups = new String[] { TEST_GROUP }; + args.groupsDefaultLogcatStatus = new boolean[] { true }; service.registerClient(mMockClient, args); Truth.assertThat(service.isLoggingToLogcat(TEST_GROUP)).isTrue(); @@ -167,11 +168,11 @@ public class ProtoLogConfigurationServiceTest { throws RemoteException, InvalidProtocolBufferException { final ProtoLogConfigurationService service = new ProtoLogConfigurationServiceImpl(); - final ProtoLogConfigurationServiceImpl.RegisterClientArgs args = - new ProtoLogConfigurationServiceImpl.RegisterClientArgs() - .setGroups(new ProtoLogConfigurationServiceImpl.RegisterClientArgs - .GroupConfig(TEST_GROUP, true)) - .setViewerConfigFile(mViewerConfigFile.getAbsolutePath()); + final RegisterClientArgs args = new RegisterClientArgs(); + args.groups = new String[] { TEST_GROUP }; + args.groupsDefaultLogcatStatus = new boolean[] { true }; + args.viewerConfigFile = mViewerConfigFile.getAbsolutePath(); + service.registerClient(mMockClient, args); service.registerClient(mSecondMockClient, args); @@ -204,11 +205,11 @@ public class ProtoLogConfigurationServiceTest { Mockito.mock(ProtoLogConfigurationServiceImpl.ViewerConfigFileTracer.class); final ProtoLogConfigurationService service = new ProtoLogConfigurationServiceImpl(tracer); - final ProtoLogConfigurationServiceImpl.RegisterClientArgs args = - new ProtoLogConfigurationServiceImpl.RegisterClientArgs() - .setGroups(new ProtoLogConfigurationServiceImpl.RegisterClientArgs - .GroupConfig(TEST_GROUP, true)) - .setViewerConfigFile(mViewerConfigFile.getAbsolutePath()); + final RegisterClientArgs args = new RegisterClientArgs(); + args.groups = new String[] { TEST_GROUP }; + args.groupsDefaultLogcatStatus = new boolean[] { true }; + args.viewerConfigFile = mViewerConfigFile.getAbsolutePath(); + service.registerClient(mMockClient, args); service.registerClient(mSecondMockClient, args); @@ -227,9 +228,9 @@ public class ProtoLogConfigurationServiceTest { public void sendEnableLoggingToLogcatToClient() throws RemoteException { final var service = new ProtoLogConfigurationServiceImpl(); - final var args = new ProtoLogConfigurationServiceImpl.RegisterClientArgs() - .setGroups(new ProtoLogConfigurationServiceImpl.RegisterClientArgs - .GroupConfig(TEST_GROUP, false)); + final RegisterClientArgs args = new RegisterClientArgs(); + args.groups = new String[] { TEST_GROUP }; + args.groupsDefaultLogcatStatus = new boolean[] { false }; service.registerClient(mMockClient, args); Truth.assertThat(service.isLoggingToLogcat(TEST_GROUP)).isFalse(); @@ -244,10 +245,9 @@ public class ProtoLogConfigurationServiceTest { public void sendDisableLoggingToLogcatToClient() throws RemoteException { final ProtoLogConfigurationService service = new ProtoLogConfigurationServiceImpl(); - final ProtoLogConfigurationServiceImpl.RegisterClientArgs args = - new ProtoLogConfigurationServiceImpl.RegisterClientArgs() - .setGroups(new ProtoLogConfigurationServiceImpl.RegisterClientArgs - .GroupConfig(TEST_GROUP, true)); + final RegisterClientArgs args = new RegisterClientArgs(); + args.groups = new String[] { TEST_GROUP }; + args.groupsDefaultLogcatStatus = new boolean[] { true }; service.registerClient(mMockClient, args); Truth.assertThat(service.isLoggingToLogcat(TEST_GROUP)).isTrue(); @@ -262,10 +262,10 @@ public class ProtoLogConfigurationServiceTest { public void doNotSendLoggingToLogcatToClientWithoutRegisteredGroup() throws RemoteException { final ProtoLogConfigurationService service = new ProtoLogConfigurationServiceImpl(); - final ProtoLogConfigurationServiceImpl.RegisterClientArgs args = - new ProtoLogConfigurationServiceImpl.RegisterClientArgs() - .setGroups(new ProtoLogConfigurationServiceImpl.RegisterClientArgs - .GroupConfig(TEST_GROUP, false)); + final RegisterClientArgs args = new RegisterClientArgs(); + args.groups = new String[] { TEST_GROUP }; + args.groupsDefaultLogcatStatus = new boolean[] { false }; + service.registerClient(mMockClient, args); Truth.assertThat(service.isLoggingToLogcat(TEST_GROUP)).isFalse(); @@ -283,10 +283,10 @@ public class ProtoLogConfigurationServiceTest { service.enableProtoLogToLogcat(TEST_GROUP); Truth.assertThat(service.isLoggingToLogcat(TEST_GROUP)).isTrue(); - final ProtoLogConfigurationServiceImpl.RegisterClientArgs args = - new ProtoLogConfigurationServiceImpl.RegisterClientArgs() - .setGroups(new ProtoLogConfigurationServiceImpl.RegisterClientArgs - .GroupConfig(TEST_GROUP, false)); + final RegisterClientArgs args = new RegisterClientArgs(); + args.groups = new String[] { TEST_GROUP }; + args.groupsDefaultLogcatStatus = new boolean[] { false }; + service.registerClient(mMockClient, args); Mockito.verify(mMockClient).toggleLogcat(eq(true), diff --git a/tests/testables/tests/src/android/animation/AnimatorTestRuleToolkitTest.kt b/tests/testables/tests/src/android/animation/AnimatorTestRuleToolkitTest.kt index 993c3fed9d59..2eb8ba1be811 100644 --- a/tests/testables/tests/src/android/animation/AnimatorTestRuleToolkitTest.kt +++ b/tests/testables/tests/src/android/animation/AnimatorTestRuleToolkitTest.kt @@ -16,6 +16,7 @@ package android.animation +import android.content.pm.PackageManager import android.graphics.Color import android.platform.test.annotations.MotionTest import android.view.ViewGroup @@ -23,11 +24,14 @@ import android.widget.FrameLayout import androidx.test.ext.junit.rules.ActivityScenarioRule import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import com.android.internal.dynamicanimation.animation.DynamicAnimation import com.android.internal.dynamicanimation.animation.SpringAnimation import com.android.internal.dynamicanimation.animation.SpringForce import kotlinx.coroutines.test.TestScope +import org.junit.Assume.assumeFalse +import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -65,6 +69,16 @@ class AnimatorTestRuleToolkitTest { bitmapDiffer = screenshotRule, ) + @Before + fun setUp() { + // Do not run on Automotive. + assumeFalse( + InstrumentationRegistry.getInstrumentation().context.packageManager.hasSystemFeature( + PackageManager.FEATURE_AUTOMOTIVE + ) + ) + } + @Test fun recordFilmstrip_withAnimator() { val animatedBox = createScene() @@ -188,12 +202,7 @@ class AnimatorTestRuleToolkitTest { null } return motionRule.recordMotion( - AnimatorRuleRecordingSpec( - container, - motionControl, - sampleIntervalMs, - visualCapture, - ) { + AnimatorRuleRecordingSpec(container, motionControl, sampleIntervalMs, visualCapture) { feature(ViewFeatureCaptures.alpha, "alpha") } ) diff --git a/tools/aapt2/cmd/Diff.cpp b/tools/aapt2/cmd/Diff.cpp index d3750a6100d3..b2b9179d4ffe 100644 --- a/tools/aapt2/cmd/Diff.cpp +++ b/tools/aapt2/cmd/Diff.cpp @@ -29,7 +29,8 @@ namespace aapt { class DiffContext : public IAaptContext { public: - DiffContext() : name_mangler_({}), symbol_table_(&name_mangler_) { + explicit DiffContext(bool ignore_id_shift) + : ignore_id_shift(ignore_id_shift), name_mangler_({}), symbol_table_(&name_mangler_) { } PackageType GetPackageType() override { @@ -71,6 +72,8 @@ class DiffContext : public IAaptContext { return empty; } + const bool ignore_id_shift; + private: std::string empty_; StdErrDiagnostics diagnostics_; @@ -79,7 +82,7 @@ class DiffContext : public IAaptContext { }; static void EmitDiffLine(const android::Source& source, StringPiece message) { - std::cerr << source << ": " << message << "\n"; + std::cout << source << ": " << message << "\n"; } static bool IsSymbolVisibilityDifferent(const Visibility& vis_a, const Visibility& vis_b) { @@ -95,8 +98,27 @@ static bool IsIdDiff(const Visibility::Level& level_a, const std::optional<Id>& return false; } +class ZeroingIdVisitor : public DescendingValueVisitor { + public: + using DescendingValueVisitor::Visit; + + void Visit(Reference* ref) override { + if (ref->name) { + ref->id.reset(); + } + } +}; + +static std::unique_ptr<Value> CloneAndClearIds(const Value* value, LoadedApk* apk) { + CloningValueTransformer cloner(&apk->GetResourceTable()->string_pool); + auto res = value->Transform(cloner); + ZeroingIdVisitor visitor; + res->Accept(&visitor); + return res; +} + static bool EmitResourceConfigValueDiff( - IAaptContext* context, LoadedApk* apk_a, const ResourceTablePackageView& pkg_a, + DiffContext* context, LoadedApk* apk_a, const ResourceTablePackageView& pkg_a, const ResourceTableTypeView& type_a, const ResourceTableEntryView& entry_a, const ResourceConfigValue* config_value_a, LoadedApk* apk_b, const ResourceTablePackageView& pkg_b, const ResourceTableTypeView& type_b, @@ -104,6 +126,14 @@ static bool EmitResourceConfigValueDiff( Value* value_a = config_value_a->value.get(); Value* value_b = config_value_b->value.get(); if (!value_a->Equals(value_b)) { + if (context->ignore_id_shift) { + // Check if the values are only different because of their IDs + auto cleared_a = CloneAndClearIds(value_a, apk_a); + auto cleared_b = CloneAndClearIds(value_b, apk_b); + if (cleared_a->Equals(cleared_b.get())) { + return false; + } + } std::stringstream str_stream; str_stream << "value " << pkg_a.name << ":" << type_a.named_type << "/" << entry_a.name << " config='" << config_value_a->config << "' does not match:\n"; @@ -116,7 +146,7 @@ static bool EmitResourceConfigValueDiff( return false; } -static bool EmitResourceEntryDiff(IAaptContext* context, LoadedApk* apk_a, +static bool EmitResourceEntryDiff(DiffContext* context, LoadedApk* apk_a, const ResourceTablePackageView& pkg_a, const ResourceTableTypeView& type_a, const ResourceTableEntryView& entry_a, LoadedApk* apk_b, @@ -180,90 +210,96 @@ static bool EmitResourceEntryDiff(IAaptContext* context, LoadedApk* apk_a, return diff; } -static bool EmitResourceTypeDiff(IAaptContext* context, LoadedApk* apk_a, +static const ResourceTableEntryView* findEntry(const ResourceTableTypeView& type, + const ResourceTableEntryView& entry) { + auto it = std::ranges::find_if(type.entries, [&](const ResourceTableEntryView& other_entry) { + return other_entry.name == entry.name; + }); + return it != std::end(type.entries) ? &*it : nullptr; +} + +static bool EmitResourceTypeDiff(DiffContext* context, LoadedApk* apk_a, const ResourceTablePackageView& pkg_a, const ResourceTableTypeView& type_a, LoadedApk* apk_b, const ResourceTablePackageView& pkg_b, const ResourceTableTypeView& type_b) { bool diff = false; - auto entry_a_iter = type_a.entries.begin(); - auto entry_b_iter = type_b.entries.begin(); - while (entry_a_iter != type_a.entries.end() || entry_b_iter != type_b.entries.end()) { - if (entry_b_iter == type_b.entries.end()) { + std::unordered_set<const ResourceTableEntryView*> found_b_entries; + for (auto&& entry_a : type_a.entries) { + auto entry_b_iter = findEntry(type_b, entry_a); + if (!entry_b_iter) { // Type A contains a type that type B does not have. std::stringstream str_stream; - str_stream << "missing " << pkg_a.name << ":" << type_a.named_type << "/" - << entry_a_iter->name; + str_stream << "missing " << pkg_a.name << ":" << type_a.named_type << "/" << entry_a.name; EmitDiffLine(apk_a->GetSource(), str_stream.str()); diff = true; - } else if (entry_a_iter == type_a.entries.end()) { - // Type B contains a type that type A does not have. + continue; + } + const auto& entry_b = *entry_b_iter; + found_b_entries.insert(&entry_b); + if (IsSymbolVisibilityDifferent(entry_a.visibility, entry_b.visibility)) { std::stringstream str_stream; - str_stream << "new entry " << pkg_b.name << ":" << type_b.named_type << "/" - << entry_b_iter->name; + str_stream << pkg_a.name << ":" << type_a.named_type << "/" << entry_a.name + << " has different visibility ("; + if (entry_b.visibility.staged_api) { + str_stream << "STAGED "; + } + if (entry_b.visibility.level == Visibility::Level::kPublic) { + str_stream << "PUBLIC"; + } else { + str_stream << "PRIVATE"; + } + str_stream << " vs "; + if (entry_a.visibility.staged_api) { + str_stream << "STAGED "; + } + if (entry_a.visibility.level == Visibility::Level::kPublic) { + str_stream << "PUBLIC"; + } else { + str_stream << "PRIVATE"; + } + str_stream << ")"; EmitDiffLine(apk_b->GetSource(), str_stream.str()); diff = true; - } else { - const auto& entry_a = *entry_a_iter; - const auto& entry_b = *entry_b_iter; - if (IsSymbolVisibilityDifferent(entry_a.visibility, entry_b.visibility)) { - std::stringstream str_stream; - str_stream << pkg_a.name << ":" << type_a.named_type << "/" << entry_a.name - << " has different visibility ("; - if (entry_b.visibility.staged_api) { - str_stream << "STAGED "; - } - if (entry_b.visibility.level == Visibility::Level::kPublic) { - str_stream << "PUBLIC"; - } else { - str_stream << "PRIVATE"; - } - str_stream << " vs "; - if (entry_a.visibility.staged_api) { - str_stream << "STAGED "; - } - if (entry_a.visibility.level == Visibility::Level::kPublic) { - str_stream << "PUBLIC"; - } else { - str_stream << "PRIVATE"; - } - str_stream << ")"; - EmitDiffLine(apk_b->GetSource(), str_stream.str()); - diff = true; - } else if (IsIdDiff(entry_a.visibility.level, entry_a.id, entry_b.visibility.level, - entry_b.id)) { - std::stringstream str_stream; - str_stream << pkg_a.name << ":" << type_a.named_type << "/" << entry_a.name - << " has different public ID ("; - if (entry_b.id) { - str_stream << "0x" << std::hex << entry_b.id.value(); - } else { - str_stream << "none"; - } - str_stream << " vs "; - if (entry_a.id) { - str_stream << "0x " << std::hex << entry_a.id.value(); - } else { - str_stream << "none"; - } - str_stream << ")"; - EmitDiffLine(apk_b->GetSource(), str_stream.str()); - diff = true; + } else if (!context->ignore_id_shift && IsIdDiff(entry_a.visibility.level, entry_a.id, + entry_b.visibility.level, entry_b.id)) { + std::stringstream str_stream; + str_stream << pkg_a.name << ":" << type_a.named_type << "/" << entry_a.name + << " has different public ID ("; + if (entry_b.id) { + str_stream << "0x" << std::hex << entry_b.id.value(); + } else { + str_stream << "none"; } - diff |= EmitResourceEntryDiff(context, apk_a, pkg_a, type_a, entry_a, apk_b, pkg_b, type_b, - entry_b); - } - if (entry_a_iter != type_a.entries.end()) { - ++entry_a_iter; + str_stream << " vs "; + if (entry_a.id) { + str_stream << "0x " << std::hex << entry_a.id.value(); + } else { + str_stream << "none"; + } + str_stream << ")"; + EmitDiffLine(apk_b->GetSource(), str_stream.str()); + diff = true; } - if (entry_b_iter != type_b.entries.end()) { - ++entry_b_iter; + diff |= EmitResourceEntryDiff(context, apk_a, pkg_a, type_a, entry_a, apk_b, pkg_b, type_b, + entry_b); + } + if (found_b_entries.size() < type_b.entries.size()) { + diff = true; + for (auto&& entry_b : type_b.entries) { + if (found_b_entries.contains(&entry_b)) { + continue; + } + // Type B contains a type that type A does not have. + std::stringstream str_stream; + str_stream << "new entry " << pkg_b.name << ":" << type_b.named_type << "/" << entry_b.name; + EmitDiffLine(apk_b->GetSource(), str_stream.str()); } } return diff; } -static bool EmitResourcePackageDiff(IAaptContext* context, LoadedApk* apk_a, +static bool EmitResourcePackageDiff(DiffContext* context, LoadedApk* apk_a, const ResourceTablePackageView& pkg_a, LoadedApk* apk_b, const ResourceTablePackageView& pkg_b) { bool diff = false; @@ -302,7 +338,8 @@ static bool EmitResourcePackageDiff(IAaptContext* context, LoadedApk* apk_a, str_stream << ")"; EmitDiffLine(apk_b->GetSource(), str_stream.str()); diff = true; - } else if (IsIdDiff(type_a.visibility_level, type_a.id, type_b.visibility_level, type_b.id)) { + } else if (!context->ignore_id_shift && + IsIdDiff(type_a.visibility_level, type_a.id, type_b.visibility_level, type_b.id)) { std::stringstream str_stream; str_stream << pkg_a.name << ":" << type_a.named_type << " has different public ID ("; if (type_b.id) { @@ -332,7 +369,7 @@ static bool EmitResourcePackageDiff(IAaptContext* context, LoadedApk* apk_a, return diff; } -static bool EmitResourceTableDiff(IAaptContext* context, LoadedApk* apk_a, LoadedApk* apk_b) { +static bool EmitResourceTableDiff(DiffContext* context, LoadedApk* apk_a, LoadedApk* apk_b) { const auto table_a = apk_a->GetResourceTable()->GetPartitionedView(); const auto table_b = apk_b->GetResourceTable()->GetPartitionedView(); @@ -355,7 +392,7 @@ static bool EmitResourceTableDiff(IAaptContext* context, LoadedApk* apk_a, Loade } else { const auto& package_a = *package_a_iter; const auto& package_b = *package_b_iter; - if (package_a.id != package_b.id) { + if (package_a.id != package_b.id && !context->ignore_id_shift) { std::stringstream str_stream; str_stream << "package '" << package_a.name << "' has different id ("; if (package_b.id) { @@ -405,7 +442,7 @@ static void ZeroOutAppReferences(ResourceTable* table) { } int DiffCommand::Action(const std::vector<std::string>& args) { - DiffContext context; + DiffContext context(ignore_id_shift_); if (args.size() != 2u) { std::cerr << "must have two apks as arguments.\n\n"; diff --git a/tools/aapt2/cmd/Diff.h b/tools/aapt2/cmd/Diff.h index c38888863be9..32e394416880 100644 --- a/tools/aapt2/cmd/Diff.h +++ b/tools/aapt2/cmd/Diff.h @@ -14,9 +14,7 @@ * limitations under the License. */ -#ifndef AAPT2_DIFF_H -#define AAPT2_DIFF_H - +#pragma once #include "Command.h" namespace aapt { @@ -25,11 +23,16 @@ class DiffCommand : public Command { public: explicit DiffCommand() : Command("diff") { SetDescription("Prints the differences in resources of two apks."); + AddOptionalSwitch("--ignore-id-shift", + "Match the resources when their IDs shift, e.g. because of the added\n" + "or deleted entries.", + &ignore_id_shift_); } int Action(const std::vector<std::string>& args) override; -}; -}// namespace aapt + private: + bool ignore_id_shift_ = false; +}; -#endif //AAPT2_DIFF_H +} // namespace aapt |