diff options
225 files changed, 5869 insertions, 1727 deletions
diff --git a/apex/jobscheduler/service/aconfig/job.aconfig b/apex/jobscheduler/service/aconfig/job.aconfig index 86ed06bf4e3d..29df80fda33d 100644 --- a/apex/jobscheduler/service/aconfig/job.aconfig +++ b/apex/jobscheduler/service/aconfig/job.aconfig @@ -105,4 +105,14 @@ flag { metadata { purpose: PURPOSE_BUGFIX } -}
\ No newline at end of file +} + +flag { + name: "include_trace_tag_in_job_name" + namespace: "backstage_power" + description: "Add the trace tag to the job name" + bug: "354795473" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java index aaf69864fe97..2d069f934d0d 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java @@ -674,6 +674,12 @@ public final class JobStatus { this.job = job; StringBuilder batteryName = new StringBuilder(); + if (com.android.server.job.Flags.includeTraceTagInJobName()) { + final String filteredTraceTag = this.getFilteredTraceTag(); + if (filteredTraceTag != null) { + batteryName.append("#").append(filteredTraceTag).append("#"); + } + } if (namespace != null) { batteryName.append("@").append(namespace).append("@"); } diff --git a/core/api/current.txt b/core/api/current.txt index d4ed533cad9e..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/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/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig index e6082d0df1f8..5c904c15e706 100644 --- a/core/java/android/content/pm/multiuser.aconfig +++ b/core/java/android/content/pm/multiuser.aconfig @@ -622,3 +622,10 @@ flag { description: "Add API to logout user" bug: "350045389" } + +flag { + name: "enable_moving_content_into_private_space" + namespace: "profile_experiences" + description: "Enable moving content into the Private Space" + bug: "360066001" +} diff --git a/core/java/android/content/res/ApkAssets.java b/core/java/android/content/res/ApkAssets.java index 075457885586..f538e9ffffdd 100644 --- a/core/java/android/content/res/ApkAssets.java +++ b/core/java/android/content/res/ApkAssets.java @@ -25,6 +25,7 @@ import android.content.res.loader.ResourcesProvider; import android.ravenwood.annotation.RavenwoodClassLoadHook; import android.ravenwood.annotation.RavenwoodKeepWholeClass; import android.text.TextUtils; +import android.util.Log; import com.android.internal.annotations.GuardedBy; @@ -50,6 +51,7 @@ import java.util.Objects; @RavenwoodKeepWholeClass @RavenwoodClassLoadHook(RavenwoodClassLoadHook.LIBANDROID_LOADING_HOOK) public final class ApkAssets { + private static final boolean DEBUG = false; /** * The apk assets contains framework resource values specified by the system. @@ -134,6 +136,17 @@ public final class ApkAssets { @Nullable private final AssetsProvider mAssets; + @NonNull + private String mName; + + private static final int UPTODATE_FALSE = 0; + private static final int UPTODATE_TRUE = 1; + private static final int UPTODATE_ALWAYS_TRUE = 2; + + // Start with the only value that may change later and would force a native call to + // double check it. + private int mPreviousUpToDateResult = UPTODATE_TRUE; + /** * Creates a new ApkAssets instance from the given path on disk. * @@ -304,7 +317,7 @@ public final class ApkAssets { private ApkAssets(@FormatType int format, @NonNull String path, @PropertyFlags int flags, @Nullable AssetsProvider assets) throws IOException { - this(format, flags, assets); + this(format, flags, assets, path); Objects.requireNonNull(path, "path"); mNativePtr = nativeLoad(format, path, flags, assets); mStringBlock = new StringBlock(nativeGetStringBlock(mNativePtr), true /*useSparse*/); @@ -313,7 +326,7 @@ public final class ApkAssets { private ApkAssets(@FormatType int format, @NonNull FileDescriptor fd, @NonNull String friendlyName, @PropertyFlags int flags, @Nullable AssetsProvider assets) throws IOException { - this(format, flags, assets); + this(format, flags, assets, friendlyName); Objects.requireNonNull(fd, "fd"); Objects.requireNonNull(friendlyName, "friendlyName"); mNativePtr = nativeLoadFd(format, fd, friendlyName, flags, assets); @@ -323,7 +336,7 @@ public final class ApkAssets { private ApkAssets(@FormatType int format, @NonNull FileDescriptor fd, @NonNull String friendlyName, long offset, long length, @PropertyFlags int flags, @Nullable AssetsProvider assets) throws IOException { - this(format, flags, assets); + this(format, flags, assets, friendlyName); Objects.requireNonNull(fd, "fd"); Objects.requireNonNull(friendlyName, "friendlyName"); mNativePtr = nativeLoadFdOffsets(format, fd, friendlyName, offset, length, flags, assets); @@ -331,16 +344,17 @@ public final class ApkAssets { } private ApkAssets(@PropertyFlags int flags, @Nullable AssetsProvider assets) { - this(FORMAT_APK, flags, assets); + this(FORMAT_APK, flags, assets, "empty"); mNativePtr = nativeLoadEmpty(flags, assets); mStringBlock = null; } private ApkAssets(@FormatType int format, @PropertyFlags int flags, - @Nullable AssetsProvider assets) { + @Nullable AssetsProvider assets, @NonNull String name) { mFlags = flags; mAssets = assets; mIsOverlay = format == FORMAT_IDMAP; + if (DEBUG) mName = name; } @UnsupportedAppUsage @@ -421,13 +435,41 @@ public final class ApkAssets { } } + private static double intervalMs(long beginNs, long endNs) { + return (endNs - beginNs) / 1000000.0; + } + /** * Returns false if the underlying APK was changed since this ApkAssets was loaded. */ public boolean isUpToDate() { + // This function is performance-critical - it's called multiple times on every Resources + // object creation, and on few other cache accesses - so it's important to avoid the native + // call when we know for sure what it will return (which is the case for both ALWAYS_TRUE + // and FALSE). + if (mPreviousUpToDateResult != UPTODATE_TRUE) { + return mPreviousUpToDateResult == UPTODATE_ALWAYS_TRUE; + } + final long beforeTs, afterLockTs, afterNativeTs, afterUnlockTs; + if (DEBUG) beforeTs = System.nanoTime(); + final int res; synchronized (this) { - return nativeIsUpToDate(mNativePtr); + if (DEBUG) afterLockTs = System.nanoTime(); + res = nativeIsUpToDate(mNativePtr); + if (DEBUG) afterNativeTs = System.nanoTime(); + } + if (DEBUG) { + afterUnlockTs = System.nanoTime(); + if (afterUnlockTs - beforeTs >= 10L * 1000000) { + Log.d("ApkAssets", "isUpToDate(" + mName + ") took " + + intervalMs(beforeTs, afterUnlockTs) + + " ms: " + intervalMs(beforeTs, afterLockTs) + + " / " + intervalMs(afterLockTs, afterNativeTs) + + " / " + intervalMs(afterNativeTs, afterUnlockTs)); + } } + mPreviousUpToDateResult = res; + return res != UPTODATE_FALSE; } public boolean isSystem() { @@ -487,7 +529,7 @@ public final class ApkAssets { private static native @NonNull String nativeGetAssetPath(long ptr); private static native @NonNull String nativeGetDebugName(long ptr); private static native long nativeGetStringBlock(long ptr); - @CriticalNative private static native boolean nativeIsUpToDate(long ptr); + @CriticalNative private static native int nativeIsUpToDate(long ptr); private static native long nativeOpenXml(long ptr, @NonNull String fileName) throws IOException; private static native @Nullable OverlayableInfo nativeGetOverlayableInfo(long ptr, String overlayableName) throws IOException; diff --git a/core/java/android/content/res/ResourceTimer.java b/core/java/android/content/res/ResourceTimer.java index d51f64ce8106..2d1bf4d9d296 100644 --- a/core/java/android/content/res/ResourceTimer.java +++ b/core/java/android/content/res/ResourceTimer.java @@ -17,13 +17,10 @@ package android.content.res; import android.annotation.NonNull; -import android.annotation.Nullable; - import android.app.AppProtoEnums; import android.os.Handler; import android.os.Looper; import android.os.Message; -import android.os.ParcelFileDescriptor; import android.os.Process; import android.os.SystemClock; import android.text.TextUtils; @@ -33,6 +30,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.util.FastPrintWriter; import com.android.internal.util.FrameworkStatsLog; +import java.io.FileDescriptor; import java.io.FileOutputStream; import java.io.PrintWriter; import java.util.Arrays; @@ -277,38 +275,40 @@ public final class ResourceTimer { * Update the metrics information and dump it. * @hide */ - public static void dumpTimers(@NonNull ParcelFileDescriptor pfd, @Nullable String[] args) { - FileOutputStream fout = new FileOutputStream(pfd.getFileDescriptor()); - PrintWriter pw = new FastPrintWriter(fout); - synchronized (sLock) { - if (!sEnabled || (sConfig == null)) { + public static void dumpTimers(@NonNull FileDescriptor fd, String... args) { + try (PrintWriter pw = new FastPrintWriter(new FileOutputStream(fd))) { + pw.println("\nDumping ResourceTimers"); + + final boolean enabled; + synchronized (sLock) { + enabled = sEnabled && sConfig != null; + } + if (!enabled) { pw.println(" Timers are not enabled in this process"); - pw.flush(); return; } - } - // Look for the --refresh switch. If the switch is present, then sTimers is updated. - // Otherwise, the current value of sTimers is displayed. - boolean refresh = Arrays.asList(args).contains("-refresh"); - - synchronized (sLock) { - update(refresh); - long runtime = sLastUpdated - sProcessStart; - pw.format(" config runtime=%d proc=%s\n", runtime, Process.myProcessName()); - for (int i = 0; i < sTimers.length; i++) { - Timer t = sTimers[i]; - if (t.count != 0) { - String name = sConfig.timers[i]; - pw.format(" stats timer=%s cnt=%d avg=%d min=%d max=%d pval=%s " - + "largest=%s\n", - name, t.count, t.total / t.count, t.mintime, t.maxtime, - packedString(t.percentile), - packedString(t.largest)); + // Look for the --refresh switch. If the switch is present, then sTimers is updated. + // Otherwise, the current value of sTimers is displayed. + boolean refresh = Arrays.asList(args).contains("-refresh"); + + synchronized (sLock) { + update(refresh); + long runtime = sLastUpdated - sProcessStart; + pw.format(" config runtime=%d proc=%s\n", runtime, Process.myProcessName()); + for (int i = 0; i < sTimers.length; i++) { + Timer t = sTimers[i]; + if (t.count != 0) { + String name = sConfig.timers[i]; + pw.format(" stats timer=%s cnt=%d avg=%d min=%d max=%d pval=%s " + + "largest=%s\n", + name, t.count, t.total / t.count, t.mintime, t.maxtime, + packedString(t.percentile), + packedString(t.largest)); + } } } } - pw.flush(); } // Enable (or disabled) the runtime timers. Note that timers are disabled by default. This diff --git a/core/java/android/content/res/XmlBlock.java b/core/java/android/content/res/XmlBlock.java index 2ead17588be4..40c532498fbc 100644 --- a/core/java/android/content/res/XmlBlock.java +++ b/core/java/android/content/res/XmlBlock.java @@ -29,8 +29,6 @@ import android.ravenwood.annotation.RavenwoodKeepWholeClass; import android.util.TypedValue; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.pm.pkg.component.AconfigFlags; -import com.android.internal.pm.pkg.parsing.ParsingPackageUtils; import com.android.internal.util.XmlUtils; import dalvik.annotation.optimization.CriticalNative; @@ -52,7 +50,6 @@ import java.io.Reader; @RavenwoodClassLoadHook(RavenwoodClassLoadHook.LIBANDROID_LOADING_HOOK) public final class XmlBlock implements AutoCloseable { private static final boolean DEBUG=false; - public static final String ANDROID_RESOURCES = "http://schemas.android.com/apk/res/android"; @UnsupportedAppUsage public XmlBlock(byte[] data) { @@ -346,23 +343,6 @@ public final class XmlBlock implements AutoCloseable { if (ev == ERROR_BAD_DOCUMENT) { throw new XmlPullParserException("Corrupt XML binary file"); } - if (Flags.layoutReadwriteFlags() && ev == START_TAG) { - AconfigFlags flags = ParsingPackageUtils.getAconfigFlags(); - if (flags.skipCurrentElement(/* pkg= */ null, this)) { - int depth = 1; - while (depth > 0) { - int ev2 = nativeNext(mParseState); - if (ev2 == ERROR_BAD_DOCUMENT) { - throw new XmlPullParserException("Corrupt XML binary file"); - } else if (ev2 == START_TAG) { - depth++; - } else if (ev2 == END_TAG) { - depth--; - } - } - return next(); - } - } if (mDecNextDepth) { mDepth--; mDecNextDepth = false; 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 08365c935626..33bf4a29ecc6 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -2776,7 +2776,7 @@ public class UserManager { } /** - * Returns whether logging out is currently allowed for the context user. + * Returns whether logging out is currently allowed for the specified user. * * <p>Logging out is not allowed in the following cases: * <ol> @@ -2794,11 +2794,10 @@ public class UserManager { * {@link #LOGOUTABILITY_STATUS_CANNOT_SWITCH}. * @hide */ - @UserHandleAware @RequiresPermission(Manifest.permission.MANAGE_USERS) - public @UserLogoutability int getUserLogoutability() { + public @UserLogoutability int getUserLogoutability(@UserIdInt int userId) { try { - return mService.getUserLogoutability(mUserId); + return mService.getUserLogoutability(userId); } catch (RemoteException re) { throw re.rethrowFromSystemServer(); } diff --git a/core/java/android/os/vibrator/VibratorFrequencyProfile.java b/core/java/android/os/vibrator/VibratorFrequencyProfile.java index 2b5f9bf2a22e..a8ed81846663 100644 --- a/core/java/android/os/vibrator/VibratorFrequencyProfile.java +++ b/core/java/android/os/vibrator/VibratorFrequencyProfile.java @@ -51,8 +51,7 @@ public final class VibratorFrequencyProfile { Preconditions.checkArgument(!frequencyProfile.isEmpty(), "Frequency profile must not be empty"); mFrequencyProfile = frequencyProfile; - mFrequenciesOutputAcceleration = generateFrequencyToAccelerationMap( - frequencyProfile.getFrequenciesHz(), frequencyProfile.getOutputAccelerationsGs()); + mFrequenciesOutputAcceleration = generateFrequencyToAccelerationMap(mFrequencyProfile); } /** @@ -133,18 +132,21 @@ public final class VibratorFrequencyProfile { } private static SparseArray<Float> generateFrequencyToAccelerationMap( - float[] frequencies, float[] accelerations) { - SparseArray<Float> sparseArray = new SparseArray<>(frequencies.length); - + VibratorInfo.FrequencyProfile frequencyProfile) { + float[] frequencies = frequencyProfile.getFrequenciesHz(); + SparseArray<Float> frequencyToAcceleration = new SparseArray<>(frequencies.length); + int lastFrequency = -1; for (int i = 0; i < frequencies.length; i++) { int frequency = (int) frequencies[i]; - float acceleration = accelerations[i]; - - sparseArray.put(frequency, - Math.min(acceleration, sparseArray.get(frequency, Float.MAX_VALUE))); + if (frequency == lastFrequency) { + continue; // Skip duplicate frequencies + } + float acceleration = frequencyProfile.getOutputAccelerationGs(frequency); + frequencyToAcceleration.put(frequency, acceleration); + lastFrequency = frequency; } - return sparseArray; + return frequencyToAcceleration; } } diff --git a/core/java/android/security/FileIntegrityManager.java b/core/java/android/security/FileIntegrityManager.java index 9e02ecd19aee..903f8170104e 100644 --- a/core/java/android/security/FileIntegrityManager.java +++ b/core/java/android/security/FileIntegrityManager.java @@ -65,13 +65,7 @@ public final class FileIntegrityManager { * other fs-verity APIs. */ public boolean isApkVeritySupported() { - try { - // Go through the service just to avoid exposing the vendor controlled system property - // to all apps. - return mService.isApkVeritySupported(); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } + return VerityUtils.isFsVeritySupported(); } /** diff --git a/core/java/android/security/IFileIntegrityService.aidl b/core/java/android/security/IFileIntegrityService.aidl index c6def239d59a..5a1a6a0ea6d9 100644 --- a/core/java/android/security/IFileIntegrityService.aidl +++ b/core/java/android/security/IFileIntegrityService.aidl @@ -24,8 +24,6 @@ import android.os.IInstalld; * @hide */ interface IFileIntegrityService { - boolean isApkVeritySupported(); - IInstalld.IFsveritySetupAuthToken createAuthToken(in ParcelFileDescriptor authFd); @EnforcePermission("SETUP_FSVERITY") diff --git a/core/java/android/security/intrusiondetection/IntrusionDetectionEvent.java b/core/java/android/security/intrusiondetection/IntrusionDetectionEvent.java index 76ee4480c222..f5f334891d2d 100644 --- a/core/java/android/security/intrusiondetection/IntrusionDetectionEvent.java +++ b/core/java/android/security/intrusiondetection/IntrusionDetectionEvent.java @@ -19,10 +19,10 @@ package android.security.intrusiondetection; import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; +import android.annotation.SystemApi; import android.app.admin.ConnectEvent; import android.app.admin.DnsEvent; import android.app.admin.SecurityLog.SecurityEvent; -import android.annotation.SystemApi; import android.os.Parcel; import android.os.Parcelable; import android.security.Flags; @@ -223,13 +223,13 @@ public final class IntrusionDetectionEvent implements Parcelable { out.writeInt(mType); switch (mType) { case SECURITY_EVENT: - out.writeParcelable(mSecurityEvent, flags); + mSecurityEvent.writeToParcel(out, flags); break; case NETWORK_EVENT_DNS: - out.writeParcelable(mNetworkEventDns, flags); + mNetworkEventDns.writeToParcel(out, flags); break; case NETWORK_EVENT_CONNECT: - out.writeParcelable(mNetworkEventConnect, flags); + mNetworkEventConnect.writeToParcel(out, flags); break; default: throw new IllegalArgumentException("Invalid event type: " + mType); diff --git a/core/java/android/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/TransitionInfo.java b/core/java/android/window/TransitionInfo.java index cf21e50e0a19..4f34aa36a204 100644 --- a/core/java/android/window/TransitionInfo.java +++ b/core/java/android/window/TransitionInfo.java @@ -29,7 +29,6 @@ import static android.view.Display.INVALID_DISPLAY; import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_UNSPECIFIED; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_CLOSE; -import static android.view.WindowManager.TRANSIT_FLAG_AOD_APPEARING; import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_APPEARING; import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY; import static android.view.WindowManager.TRANSIT_NONE; @@ -406,8 +405,7 @@ public final class TransitionInfo implements Parcelable { */ public boolean hasChangesOrSideEffects() { return !mChanges.isEmpty() || isKeyguardGoingAway() - || (mFlags & TRANSIT_FLAG_KEYGUARD_APPEARING) != 0 - || (mFlags & TRANSIT_FLAG_AOD_APPEARING) != 0; + || (mFlags & TRANSIT_FLAG_KEYGUARD_APPEARING) != 0; } /** diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig index 3266ad4d93ae..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." diff --git a/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java b/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java index 21d000dc5224..b57acf3d97fd 100644 --- a/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java +++ b/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java @@ -226,6 +226,21 @@ public class AconfigFlags { } private Boolean getFlagValueFromNewStorage(String flagPackageAndName) { + // We still need to check mFlagValues in case addFlagValuesForTesting() was called for + // testing purposes. + if (!mFlagValues.isEmpty() && mFlagValues.containsKey(flagPackageAndName)) { + Boolean value = mFlagValues.get(flagPackageAndName); + if (DEBUG) { + Slog.v( + LOG_TAG, + "Aconfig flag value (FOR TESTING) for " + + flagPackageAndName + + " = " + + value); + } + return value; + } + int index = flagPackageAndName.lastIndexOf('.'); if (index < 0) { Slog.e(LOG_TAG, "Unable to parse package name from " + flagPackageAndName); diff --git a/core/java/com/android/internal/security/VerityUtils.java b/core/java/com/android/internal/security/VerityUtils.java index 37500766a4ac..ac186d0a26b5 100644 --- a/core/java/com/android/internal/security/VerityUtils.java +++ b/core/java/com/android/internal/security/VerityUtils.java @@ -56,8 +56,7 @@ public abstract class VerityUtils { private static final int HASH_SIZE_BYTES = 32; public static boolean isFsVeritySupported() { - return Build.VERSION.DEVICE_INITIAL_SDK_INT >= Build.VERSION_CODES.R - || SystemProperties.getInt("ro.apk_verity.mode", 0) == 2; + return Build.VERSION.DEVICE_INITIAL_SDK_INT >= Build.VERSION_CODES.R; } /** Enables fs-verity for the file without signature. */ diff --git a/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java b/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java index ca355c41f7a9..b8503da2c09b 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java +++ b/core/java/com/android/internal/widget/remotecompose/core/CoreDocument.java @@ -65,7 +65,7 @@ public class CoreDocument implements Serializable { // We also keep a more fine-grained BUILD number, exposed as // ID_API_LEVEL = DOCUMENT_API_LEVEL + BUILD - static final float BUILD = 0.1f; + static final float BUILD = 0.2f; @NonNull ArrayList<Operation> mOperations = new ArrayList<>(); @@ -742,6 +742,7 @@ public class CoreDocument implements Serializable { if (op instanceof Component) { mComponentMap.put(((Component) op).getComponentId(), (Component) op); registerVariables(context, ((Component) op).getList()); + ((Component) op).registerVariables(context); } if (op instanceof ComponentValue) { ComponentValue v = (ComponentValue) op; diff --git a/core/java/com/android/internal/widget/remotecompose/core/Operations.java b/core/java/com/android/internal/widget/remotecompose/core/Operations.java index 9bb8d9f39975..09ec40271f4d 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/Operations.java +++ b/core/java/com/android/internal/widget/remotecompose/core/Operations.java @@ -35,6 +35,7 @@ import com.android.internal.widget.remotecompose.core.operations.DrawBitmapFontT import com.android.internal.widget.remotecompose.core.operations.DrawBitmapInt; import com.android.internal.widget.remotecompose.core.operations.DrawBitmapScaled; import com.android.internal.widget.remotecompose.core.operations.DrawCircle; +import com.android.internal.widget.remotecompose.core.operations.DrawContent; import com.android.internal.widget.remotecompose.core.operations.DrawLine; import com.android.internal.widget.remotecompose.core.operations.DrawOval; import com.android.internal.widget.remotecompose.core.operations.DrawPath; @@ -81,6 +82,7 @@ import com.android.internal.widget.remotecompose.core.operations.Theme; import com.android.internal.widget.remotecompose.core.operations.TimeAttribute; import com.android.internal.widget.remotecompose.core.operations.TouchExpression; import com.android.internal.widget.remotecompose.core.operations.layout.CanvasContent; +import com.android.internal.widget.remotecompose.core.operations.layout.CanvasOperations; import com.android.internal.widget.remotecompose.core.operations.layout.ClickModifierOperation; import com.android.internal.widget.remotecompose.core.operations.layout.ComponentStart; import com.android.internal.widget.remotecompose.core.operations.layout.ContainerEnd; @@ -105,6 +107,7 @@ import com.android.internal.widget.remotecompose.core.operations.layout.modifier import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.BorderModifierOperation; import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.ClipRectModifierOperation; import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.ComponentVisibilityOperation; +import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.DrawContentOperation; import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.GraphicsLayerModifierOperation; import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.HeightInModifierOperation; import com.android.internal.widget.remotecompose.core.operations.layout.modifiers.HeightModifierOperation; @@ -172,6 +175,7 @@ public class Operations { public static final int DATA_PATH = 123; public static final int DRAW_PATH = 124; public static final int DRAW_TWEEN_PATH = 125; + public static final int DRAW_CONTENT = 139; public static final int MATRIX_SCALE = 126; public static final int MATRIX_TRANSLATE = 127; public static final int MATRIX_SKEW = 128; @@ -215,6 +219,8 @@ public class Operations { public static final int ATTRIBUTE_TEXT = 170; public static final int ATTRIBUTE_IMAGE = 171; public static final int ATTRIBUTE_TIME = 172; + public static final int CANVAS_OPERATIONS = 173; + public static final int MODIFIER_DRAW_CONTENT = 174; ///////////////////////////////////////// ====================== @@ -366,6 +372,7 @@ public class Operations { map.put(MODIFIER_SCROLL, ScrollModifierOperation::read); map.put(MODIFIER_MARQUEE, MarqueeModifierOperation::read); map.put(MODIFIER_RIPPLE, RippleModifierOperation::read); + map.put(MODIFIER_DRAW_CONTENT, DrawContentOperation::read); map.put(CONTAINER_END, ContainerEnd::read); @@ -393,6 +400,7 @@ public class Operations { map.put(LAYOUT_TEXT, TextLayout::read); map.put(LAYOUT_STATE, StateLayout::read); + map.put(DRAW_CONTENT, DrawContent::read); map.put(COMPONENT_VALUE, ComponentValue::read); map.put(DRAW_ARC, DrawArc::read); @@ -409,6 +417,7 @@ public class Operations { map.put(PARTICLE_LOOP, ParticlesLoop::read); map.put(FUNCTION_CALL, FloatFunctionCall::read); map.put(FUNCTION_DEFINE, FloatFunctionDefine::read); + map.put(CANVAS_OPERATIONS, CanvasOperations::read); map.put(ACCESSIBILITY_SEMANTICS, CoreSemantics::read); map.put(ATTRIBUTE_IMAGE, ImageAttribute::read); diff --git a/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java b/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java index 39f85f600310..e75bd30b381d 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java +++ b/core/java/com/android/internal/widget/remotecompose/core/RemoteComposeBuffer.java @@ -38,6 +38,7 @@ import com.android.internal.widget.remotecompose.core.operations.DrawBitmapFontT import com.android.internal.widget.remotecompose.core.operations.DrawBitmapInt; import com.android.internal.widget.remotecompose.core.operations.DrawBitmapScaled; import com.android.internal.widget.remotecompose.core.operations.DrawCircle; +import com.android.internal.widget.remotecompose.core.operations.DrawContent; import com.android.internal.widget.remotecompose.core.operations.DrawLine; import com.android.internal.widget.remotecompose.core.operations.DrawOval; import com.android.internal.widget.remotecompose.core.operations.DrawPath; @@ -84,6 +85,7 @@ import com.android.internal.widget.remotecompose.core.operations.TimeAttribute; import com.android.internal.widget.remotecompose.core.operations.TouchExpression; import com.android.internal.widget.remotecompose.core.operations.Utils; import com.android.internal.widget.remotecompose.core.operations.layout.CanvasContent; +import com.android.internal.widget.remotecompose.core.operations.layout.CanvasOperations; import com.android.internal.widget.remotecompose.core.operations.layout.ComponentStart; import com.android.internal.widget.remotecompose.core.operations.layout.ContainerEnd; import com.android.internal.widget.remotecompose.core.operations.layout.ImpulseOperation; @@ -1887,7 +1889,7 @@ public class RemoteComposeBuffer { } /** Add a component end tag */ - public void addComponentEnd() { + public void addContainerEnd() { ContainerEnd.apply(mBuffer); } @@ -2231,6 +2233,11 @@ public class RemoteComposeBuffer { LayoutComponentContent.apply(mBuffer, mLastComponentId); } + /** Add a canvas operations start tag */ + public void addCanvasOperationsStart() { + CanvasOperations.apply(mBuffer); + } + /** * Add a component width value * @@ -2427,4 +2434,9 @@ public class RemoteComposeBuffer { TimeAttribute.apply(mBuffer, id, timeId, attribute, args); return Utils.asNan(id); } + + /** In the context of a component draw modifier, draw the content of the component */ + public void drawComponentContent() { + DrawContent.apply(mBuffer); + } } diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/DrawContent.java b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawContent.java new file mode 100644 index 000000000000..e2e22acbeb8f --- /dev/null +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/DrawContent.java @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.internal.widget.remotecompose.core.operations; + +import android.annotation.NonNull; +import android.annotation.Nullable; + +import com.android.internal.widget.remotecompose.core.Operation; +import com.android.internal.widget.remotecompose.core.Operations; +import com.android.internal.widget.remotecompose.core.PaintContext; +import com.android.internal.widget.remotecompose.core.PaintOperation; +import com.android.internal.widget.remotecompose.core.WireBuffer; +import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder; +import com.android.internal.widget.remotecompose.core.operations.layout.LayoutComponent; +import com.android.internal.widget.remotecompose.core.serialize.MapSerializer; +import com.android.internal.widget.remotecompose.core.serialize.Serializable; + +import java.util.List; + +/** The DrawContent command */ +public class DrawContent extends PaintOperation implements Serializable { + private static final int OP_CODE = Operations.DRAW_CONTENT; + private static final String CLASS_NAME = "DrawContent"; + private @Nullable LayoutComponent mComponent; + + @Override + public void write(@NonNull WireBuffer buffer) { + apply(buffer); + } + + /** + * Set the component to be painted + * + * @param component + */ + public void setComponent(LayoutComponent component) { + mComponent = component; + } + + @NonNull + @Override + public String toString() { + return "DrawContent;"; + } + + /** + * Read this operation and add it to the list of operations + * + * @param buffer the buffer to read + * @param operations the list of operations that will be added to + */ + public static void read(@NonNull WireBuffer buffer, @NonNull List<Operation> operations) { + DrawContent op = new DrawContent(); + operations.add(op); + } + + /** + * The name of the class + * + * @return the name + */ + @NonNull + public static String name() { + return CLASS_NAME; + } + + /** + * The OP_CODE for this command + * + * @return the opcode + */ + public static int id() { + return OP_CODE; + } + + /** + * add a draw content operation to the buffer + * + * @param buffer the buffer to add to + */ + public static void apply(@NonNull WireBuffer buffer) { + buffer.start(Operations.DRAW_CONTENT); + } + + /** + * Populate the documentation with a description of this operation + * + * @param doc to append the description to. + */ + public static void documentation(@NonNull DocumentationBuilder doc) { + doc.operation("Layout Operations", OP_CODE, CLASS_NAME) + .description("Draw the component content"); + } + + @Override + public void paint(@NonNull PaintContext context) { + if (mComponent != null) { + mComponent.drawContent(context); + } + } + + @Override + public void serialize(MapSerializer serializer) { + serializer.add("type", CLASS_NAME); + } +} diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/CanvasOperations.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/CanvasOperations.java new file mode 100644 index 000000000000..3e7f1d304315 --- /dev/null +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/CanvasOperations.java @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.internal.widget.remotecompose.core.operations.layout; + +import android.annotation.NonNull; +import android.annotation.Nullable; + +import com.android.internal.widget.remotecompose.core.Operation; +import com.android.internal.widget.remotecompose.core.Operations; +import com.android.internal.widget.remotecompose.core.PaintContext; +import com.android.internal.widget.remotecompose.core.PaintOperation; +import com.android.internal.widget.remotecompose.core.RemoteContext; +import com.android.internal.widget.remotecompose.core.VariableSupport; +import com.android.internal.widget.remotecompose.core.WireBuffer; +import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder; +import com.android.internal.widget.remotecompose.core.operations.ComponentValue; +import com.android.internal.widget.remotecompose.core.operations.DrawContent; +import com.android.internal.widget.remotecompose.core.serialize.MapSerializer; +import com.android.internal.widget.remotecompose.core.serialize.Serializable; + +import java.util.ArrayList; +import java.util.List; + +/** Represents a list of canvas operations. */ +public class CanvasOperations extends PaintOperation + implements VariableSupport, Container, Serializable { + private static final int OP_CODE = Operations.CANVAS_OPERATIONS; + private static final String CLASS_NAME = "CanvasOperations"; + + @NonNull public ArrayList<Operation> mList = new ArrayList<>(); + @Nullable LayoutComponent mComponent; + + /** The constructor */ + public CanvasOperations() {} + + @Override + public void registerListening(RemoteContext context) { + for (Operation operation : mList) { + if (operation instanceof VariableSupport) { + VariableSupport variableSupport = (VariableSupport) operation; + variableSupport.registerListening(context); + } + if (operation instanceof ComponentValue) { + ComponentValue v = (ComponentValue) operation; + mComponent.addComponentValue(v); + } + } + } + + @Override + public void updateVariables(RemoteContext context) { + for (Operation operation : mList) { + if (operation instanceof VariableSupport) { + VariableSupport variableSupport = (VariableSupport) operation; + variableSupport.updateVariables(context); + } + } + } + + /** + * The returns a list to be filled + * + * @return list to be filled + */ + @NonNull + public ArrayList<Operation> getList() { + return mList; + } + + @Override + public void write(@NonNull WireBuffer buffer) { + apply(buffer); + } + + @NonNull + @Override + public String toString() { + StringBuilder builder = new StringBuilder(CLASS_NAME + "\n"); + for (Operation operation : mList) { + builder.append(" "); + builder.append(operation); + builder.append("\n"); + } + return builder.toString(); + } + + @NonNull + @Override + public String deepToString(@NonNull String indent) { + return (indent != null ? indent : "") + toString(); + } + + @Override + public void paint(@NonNull PaintContext context) { + RemoteContext remoteContext = context.getContext(); + for (Operation op : mList) { + if (op instanceof VariableSupport && op.isDirty()) { + ((VariableSupport) op).updateVariables(context.getContext()); + } + remoteContext.incrementOpCount(); + op.apply(context.getContext()); + } + } + + /** + * The name of the class + * + * @return the name + */ + @NonNull + public static String name() { + return "Loop"; + } + + /** + * Apply this operation to the buffer + * + * @param buffer + */ + public static void apply(@NonNull WireBuffer buffer) { + buffer.start(OP_CODE); + } + + /** + * Read this operation and add it to the list of operations + * + * @param buffer the buffer to read + * @param operations the list of operations that will be added to + */ + public static void read(@NonNull WireBuffer buffer, @NonNull List<Operation> operations) { + operations.add(new CanvasOperations()); + } + + /** + * Populate the documentation with a description of this operation + * + * @param doc to append the description to. + */ + public static void documentation(@NonNull DocumentationBuilder doc) { + doc.operation("Operations", OP_CODE, name()) + .description("Impulse Process that runs a list of operations"); + } + + @Override + public void serialize(MapSerializer serializer) { + serializer.add("type", CLASS_NAME).add("list", mList); + } + + /** + * Set layout component + * + * @param layoutComponent + */ + public void setComponent(LayoutComponent layoutComponent) { + mComponent = layoutComponent; + for (Operation op : mList) { + if (op instanceof DrawContent) { + ((DrawContent) op).setComponent(layoutComponent); + } + } + } +} diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/Component.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/Component.java index e332e4be4c8d..c73643682b55 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/Component.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/Component.java @@ -329,6 +329,15 @@ public class Component extends PaintOperation mAnimationSpec = animationSpec; } + /** + * If the component contains variables beside mList, make sure to register them here + * + * @param context + */ + public void registerVariables(RemoteContext context) { + // Nothing here + } + public enum Visibility { GONE, VISIBLE, @@ -976,6 +985,17 @@ public class Component extends PaintOperation } } + /** Extract CanvasOperations if present */ + public @Nullable CanvasOperations getCanvasOperations(LayoutComponent layoutComponent) { + for (Operation op : mList) { + if (op instanceof CanvasOperations) { + ((CanvasOperations) op).setComponent(layoutComponent); + return (CanvasOperations) op; + } + } + return null; + } + /** * Extract child TextData elements * diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LayoutComponent.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LayoutComponent.java index 10cbd4ca2a50..7e2a4ccec222 100644 --- a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LayoutComponent.java +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/LayoutComponent.java @@ -75,6 +75,7 @@ public class LayoutComponent extends Component { protected ArrayList<Component> mChildrenComponents = new ArrayList<>(); // members are not null protected boolean mChildrenHaveZIndex = false; + private CanvasOperations mDrawContentOperations; public LayoutComponent( @Nullable Component parent, @@ -138,6 +139,7 @@ public class LayoutComponent extends Component { mChildrenComponents.clear(); LayoutComponentContent content = (LayoutComponentContent) op; content.getComponents(mChildrenComponents); + mDrawContentOperations = content.getCanvasOperations(this); if (USE_IMAGE_TEMP_FIX) { if (mChildrenComponents.isEmpty() && !mContent.mList.isEmpty()) { CanvasContent canvasContent = @@ -315,6 +317,31 @@ public class LayoutComponent extends Component { } @Override + public void paint(@NonNull PaintContext context) { + if (mDrawContentOperations != null) { + context.save(); + context.translate(mX, mY); + mDrawContentOperations.paint(context); + context.restore(); + return; + } + super.paint(context); + } + + /** + * Paint the component content. Used by the DrawContent operation. (back out mX/mY -- TODO: + * refactor paintingComponent instead, to not include mX/mY etc.) + * + * @param context painting context + */ + public void drawContent(@NonNull PaintContext context) { + context.save(); + context.translate(-mX, -mY); + paintingComponent(context); + context.restore(); + } + + @Override public void paintingComponent(@NonNull PaintContext context) { Component prev = context.getContext().mLastComponent; RemoteContext remoteContext = context.getContext(); @@ -514,4 +541,11 @@ public class LayoutComponent extends Component { return null; } + + @Override + public void registerVariables(RemoteContext context) { + if (mDrawContentOperations != null) { + mDrawContentOperations.registerListening(context); + } + } } diff --git a/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/DrawContentOperation.java b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/DrawContentOperation.java new file mode 100644 index 000000000000..d7abdbae4962 --- /dev/null +++ b/core/java/com/android/internal/widget/remotecompose/core/operations/layout/modifiers/DrawContentOperation.java @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.internal.widget.remotecompose.core.operations.layout.modifiers; + +import android.annotation.NonNull; +import android.annotation.Nullable; + +import com.android.internal.widget.remotecompose.core.Operation; +import com.android.internal.widget.remotecompose.core.Operations; +import com.android.internal.widget.remotecompose.core.RemoteContext; +import com.android.internal.widget.remotecompose.core.VariableSupport; +import com.android.internal.widget.remotecompose.core.WireBuffer; +import com.android.internal.widget.remotecompose.core.documentation.DocumentationBuilder; +import com.android.internal.widget.remotecompose.core.operations.layout.Component; +import com.android.internal.widget.remotecompose.core.operations.layout.DecoratorComponent; +import com.android.internal.widget.remotecompose.core.operations.layout.LayoutComponent; +import com.android.internal.widget.remotecompose.core.operations.utilities.StringSerializer; +import com.android.internal.widget.remotecompose.core.serialize.MapSerializer; +import com.android.internal.widget.remotecompose.core.serialize.SerializeTags; + +import java.util.List; + +/** Represent a drawing of a component */ +public class DrawContentOperation extends Operation + implements ModifierOperation, VariableSupport, DecoratorComponent { + private static final int OP_CODE = Operations.MODIFIER_DRAW_CONTENT; + + private LayoutComponent mParent; + + public DrawContentOperation() {} + + @NonNull + @Override + public String toString() { + return "DrawContentOperation()"; + } + + /** + * Returns the serialized name for this operation + * + * @return the serialized name + */ + @NonNull + public String serializedName() { + return "DRAW_CONTENT"; + } + + @Override + public void serializeToString(int indent, @NonNull StringSerializer serializer) { + serializer.append(indent, serializedName()); + } + + @Override + public void apply(@NonNull RemoteContext context) {} + + @NonNull + @Override + public String deepToString(@NonNull String indent) { + return (indent != null ? indent : "") + toString(); + } + + @Override + public void write(@NonNull WireBuffer buffer) {} + + /** + * Write the operation to the buffer + * + * @param buffer a WireBuffer + */ + public static void apply(@NonNull WireBuffer buffer) { + buffer.start(OP_CODE); + } + + /** + * Read this operation and add it to the list of operations + * + * @param buffer the buffer to read + * @param operations the list of operations that will be added to + */ + public static void read(@NonNull WireBuffer buffer, @NonNull List<Operation> operations) { + operations.add(new DrawContentOperation()); + } + + /** + * Populate the documentation with a description of this operation + * + * @param doc to append the description to. + */ + public static void documentation(@NonNull DocumentationBuilder doc) { + doc.operation("Layout Operations", OP_CODE, "ComponentVisibility") + .description("This operation represents a draw of a component"); + } + + @Override + public void registerListening(@NonNull RemoteContext context) {} + + @Override + public void updateVariables(@NonNull RemoteContext context) {} + + public void setParent(@Nullable LayoutComponent parent) { + mParent = parent; + } + + @Override + public void layout( + @NonNull RemoteContext context, Component component, float width, float height) {} + + @Override + public void serialize(MapSerializer serializer) { + serializer.addTags(SerializeTags.MODIFIER).add("type", "DrawContentOperation"); + } +} diff --git a/core/jni/android_content_res_ApkAssets.cpp b/core/jni/android_content_res_ApkAssets.cpp index 1e7bfe32ba79..66c65d0ac1aa 100644 --- a/core/jni/android_content_res_ApkAssets.cpp +++ b/core/jni/android_content_res_ApkAssets.cpp @@ -111,9 +111,8 @@ static void DeleteGuardedApkAssets(Guarded<AssetManager2::ApkAssetsPtr>& apk_ass class LoaderAssetsProvider : public AssetsProvider { public: static std::unique_ptr<AssetsProvider> Create(JNIEnv* env, jobject assets_provider) { - return (!assets_provider) ? EmptyAssetsProvider::Create() - : std::unique_ptr<AssetsProvider>(new LoaderAssetsProvider( - env, assets_provider)); + return std::unique_ptr<AssetsProvider>{ + assets_provider ? new LoaderAssetsProvider(env, assets_provider) : nullptr}; } bool ForEachFile(const std::string& /* root_path */, @@ -129,8 +128,8 @@ class LoaderAssetsProvider : public AssetsProvider { return debug_name_; } - bool IsUpToDate() const override { - return true; + UpToDate IsUpToDate() const override { + return UpToDate::Always; } ~LoaderAssetsProvider() override { @@ -212,7 +211,7 @@ class LoaderAssetsProvider : public AssetsProvider { auto string_result = static_cast<jstring>(env->CallObjectMethod( assets_provider_, gAssetsProviderOffsets.toString)); ScopedUtfChars str(env, string_result); - debug_name_ = std::string(str.c_str(), str.size()); + debug_name_ = std::string(str.c_str()); } // The global reference to the AssetsProvider @@ -233,9 +232,9 @@ static jlong NativeLoad(JNIEnv* env, jclass /*clazz*/, const format_type_t forma AssetManager2::ApkAssetsPtr apk_assets; switch (format) { case FORMAT_APK: { - auto assets = MultiAssetsProvider::Create(std::move(loader_assets), - ZipAssetsProvider::Create(path.c_str(), - property_flags)); + auto assets = AssetsProvider::CreateWithOverride(ZipAssetsProvider::Create(path.c_str(), + property_flags), + std::move(loader_assets)); apk_assets = ApkAssets::Load(std::move(assets), property_flags); break; } @@ -243,15 +242,17 @@ static jlong NativeLoad(JNIEnv* env, jclass /*clazz*/, const format_type_t forma apk_assets = ApkAssets::LoadOverlay(path.c_str(), property_flags); break; case FORMAT_ARSC: - apk_assets = ApkAssets::LoadTable(AssetsProvider::CreateAssetFromFile(path.c_str()), - std::move(loader_assets), - property_flags); - break; + apk_assets = + ApkAssets::LoadTable(AssetsProvider::CreateAssetFromFile(path.c_str()), + AssetsProvider::CreateFromNullable(std::move(loader_assets)), + property_flags); + break; case FORMAT_DIRECTORY: { - auto assets = MultiAssetsProvider::Create(std::move(loader_assets), - DirectoryAssetsProvider::Create(path.c_str())); - apk_assets = ApkAssets::Load(std::move(assets), property_flags); - break; + auto assets = + AssetsProvider::CreateWithOverride(DirectoryAssetsProvider::Create(path.c_str()), + std::move(loader_assets)); + apk_assets = ApkAssets::Load(std::move(assets), property_flags); + break; } default: const std::string error_msg = base::StringPrintf("Unsupported format type %d", format); @@ -308,18 +309,21 @@ static jlong NativeLoadFromFd(JNIEnv* env, jclass /*clazz*/, const format_type_t switch (format) { case FORMAT_APK: { auto assets = - MultiAssetsProvider::Create(std::move(loader_assets), - ZipAssetsProvider::Create(std::move(dup_fd), - friendly_name_utf8.c_str(), - property_flags)); + AssetsProvider::CreateWithOverride(ZipAssetsProvider::Create(std::move(dup_fd), + friendly_name_utf8 + .c_str(), + property_flags), + std::move(loader_assets)); apk_assets = ApkAssets::Load(std::move(assets), property_flags); break; } case FORMAT_ARSC: - apk_assets = ApkAssets::LoadTable( - AssetsProvider::CreateAssetFromFd(std::move(dup_fd), nullptr /* path */), - std::move(loader_assets), property_flags); - break; + apk_assets = + ApkAssets::LoadTable(AssetsProvider::CreateAssetFromFd(std::move(dup_fd), + nullptr /* path */), + AssetsProvider::CreateFromNullable(std::move(loader_assets)), + property_flags); + break; default: const std::string error_msg = base::StringPrintf("Unsupported format type %d", format); jniThrowException(env, "java/lang/IllegalArgumentException", error_msg.c_str()); @@ -375,23 +379,28 @@ static jlong NativeLoadFromFdOffset(JNIEnv* env, jclass /*clazz*/, const format_ switch (format) { case FORMAT_APK: { auto assets = - MultiAssetsProvider::Create(std::move(loader_assets), - ZipAssetsProvider::Create(std::move(dup_fd), - friendly_name_utf8.c_str(), - property_flags, - static_cast<off64_t>(offset), - static_cast<off64_t>( - length))); + AssetsProvider::CreateWithOverride(ZipAssetsProvider::Create(std::move(dup_fd), + friendly_name_utf8 + .c_str(), + property_flags, + static_cast<off64_t>( + offset), + static_cast<off64_t>( + length)), + std::move(loader_assets)); apk_assets = ApkAssets::Load(std::move(assets), property_flags); break; } case FORMAT_ARSC: - apk_assets = ApkAssets::LoadTable( - AssetsProvider::CreateAssetFromFd(std::move(dup_fd), nullptr /* path */, - static_cast<off64_t>(offset), - static_cast<off64_t>(length)), - std::move(loader_assets), property_flags); - break; + apk_assets = + ApkAssets::LoadTable(AssetsProvider::CreateAssetFromFd(std::move(dup_fd), + nullptr /* path */, + static_cast<off64_t>(offset), + static_cast<off64_t>( + length)), + AssetsProvider::CreateFromNullable(std::move(loader_assets)), + property_flags); + break; default: const std::string error_msg = base::StringPrintf("Unsupported format type %d", format); jniThrowException(env, "java/lang/IllegalArgumentException", error_msg.c_str()); @@ -408,13 +417,16 @@ static jlong NativeLoadFromFdOffset(JNIEnv* env, jclass /*clazz*/, const format_ } static jlong NativeLoadEmpty(JNIEnv* env, jclass /*clazz*/, jint flags, jobject assets_provider) { - auto apk_assets = ApkAssets::Load(LoaderAssetsProvider::Create(env, assets_provider), flags); - if (apk_assets == nullptr) { - const std::string error_msg = - base::StringPrintf("Failed to load empty assets with provider %p", (void*)assets_provider); - jniThrowException(env, "java/io/IOException", error_msg.c_str()); - return 0; - } + auto apk_assets = ApkAssets::Load(AssetsProvider::CreateFromNullable( + LoaderAssetsProvider::Create(env, assets_provider)), + flags); + if (apk_assets == nullptr) { + const std::string error_msg = + base::StringPrintf("Failed to load empty assets with provider %p", + (void*)assets_provider); + jniThrowException(env, "java/io/IOException", error_msg.c_str()); + return 0; + } return CreateGuardedApkAssets(std::move(apk_assets)); } @@ -443,10 +455,10 @@ static jlong NativeGetStringBlock(JNIEnv* /*env*/, jclass /*clazz*/, jlong ptr) return reinterpret_cast<jlong>(apk_assets->GetLoadedArsc()->GetStringPool()); } -static jboolean NativeIsUpToDate(CRITICAL_JNI_PARAMS_COMMA jlong ptr) { +static jint NativeIsUpToDate(CRITICAL_JNI_PARAMS_COMMA jlong ptr) { auto scoped_apk_assets = ScopedLock(ApkAssetsFromLong(ptr)); auto apk_assets = scoped_apk_assets->get(); - return apk_assets->IsUpToDate() ? JNI_TRUE : JNI_FALSE; + return (jint)apk_assets->IsUpToDate(); } static jlong NativeOpenXml(JNIEnv* env, jclass /*clazz*/, jlong ptr, jstring file_name) { @@ -558,7 +570,7 @@ static const JNINativeMethod gApkAssetsMethods[] = { {"nativeGetDebugName", "(J)Ljava/lang/String;", (void*)NativeGetDebugName}, {"nativeGetStringBlock", "(J)J", (void*)NativeGetStringBlock}, // @CriticalNative - {"nativeIsUpToDate", "(J)Z", (void*)NativeIsUpToDate}, + {"nativeIsUpToDate", "(J)I", (void*)NativeIsUpToDate}, {"nativeOpenXml", "(JLjava/lang/String;)J", (void*)NativeOpenXml}, {"nativeGetOverlayableInfo", "(JLjava/lang/String;)Landroid/content/om/OverlayableInfo;", (void*)NativeGetOverlayableInfo}, diff --git a/core/jni/android_media_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/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/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/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java b/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java index 9f398ecf5492..b7d6ab56d6b3 100644 --- a/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java +++ b/core/tests/coretests/src/com/android/internal/app/ChooserActivityTest.java @@ -79,6 +79,7 @@ import android.graphics.drawable.Icon; import android.metrics.LogMaker; import android.net.Uri; import android.os.UserHandle; +import android.os.UserManager; import android.provider.DeviceConfig; import android.service.chooser.ChooserTarget; import android.util.Pair; @@ -3178,7 +3179,11 @@ public class ChooserActivityTest { } private void markWorkProfileUserAvailable() { - ChooserActivityOverrideData.getInstance().workProfileUserHandle = UserHandle.of(10); + if (UserManager.isHeadlessSystemUserMode()) { + ChooserActivityOverrideData.getInstance().workProfileUserHandle = UserHandle.of(11); + } else { + ChooserActivityOverrideData.getInstance().workProfileUserHandle = UserHandle.of(10); + } } private void markCloneProfileUserAvailable() { diff --git a/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java b/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java index dcea9120e2a5..be7f84e3ea8f 100644 --- a/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java +++ b/core/tests/coretests/src/com/android/internal/app/ChooserWrapperActivity.java @@ -155,12 +155,12 @@ public class ChooserWrapperActivity extends ChooserActivity implements IChooserW @Override protected ResolverListController createListController(UserHandle userHandle) { - if (userHandle == UserHandle.SYSTEM) { - when(sOverrides.resolverListController.getUserHandle()).thenReturn(UserHandle.SYSTEM); - return sOverrides.resolverListController; + if (userHandle.equals(sOverrides.workProfileUserHandle)) { + when(sOverrides.workResolverListController.getUserHandle()).thenReturn(userHandle); + return sOverrides.workResolverListController; } - when(sOverrides.workResolverListController.getUserHandle()).thenReturn(userHandle); - return sOverrides.workResolverListController; + when(sOverrides.resolverListController.getUserHandle()).thenReturn(userHandle); + return sOverrides.resolverListController; } @Override diff --git a/graphics/java/android/graphics/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/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 a780fb7a426e..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; } } @@ -1603,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/BubbleTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java index 6be3c1f18b39..a676f41baafe 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java @@ -156,14 +156,18 @@ public class BubbleTransitions { public static class DragData { private final Rect mBounds; private final WindowContainerTransaction mPendingWct; + private final boolean mReleasedOnLeft; /** * @param bounds bounds of the dragged task when the drag was released * @param wct pending operations to be applied when finishing the drag + * @param releasedOnLeft true if the bubble was released in the left drop target */ - public DragData(@Nullable Rect bounds, @Nullable WindowContainerTransaction wct) { + public DragData(@Nullable Rect bounds, @Nullable WindowContainerTransaction wct, + boolean releasedOnLeft) { mBounds = bounds; mPendingWct = wct; + mReleasedOnLeft = releasedOnLeft; } /** @@ -181,6 +185,13 @@ public class BubbleTransitions { public WindowContainerTransaction getPendingWct() { return mPendingWct; } + + /** + * @return true if the bubble was released in the left drop target + */ + public boolean isReleasedOnLeft() { + return mReleasedOnLeft; + } } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index 35475c7ee4ce..2fd8c27d5970 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -759,7 +759,6 @@ public abstract class WMShellModule { FocusTransitionObserver focusTransitionObserver, DesktopModeEventLogger desktopModeEventLogger, DesktopModeUiEventLogger desktopModeUiEventLogger, - DesktopTilingDecorViewModel desktopTilingDecorViewModel, DesktopWallpaperActivityTokenProvider desktopWallpaperActivityTokenProvider, Optional<BubbleController> bubbleController, OverviewToDesktopTransitionObserver overviewToDesktopTransitionObserver, @@ -798,7 +797,6 @@ public abstract class WMShellModule { mainHandler, desktopModeEventLogger, desktopModeUiEventLogger, - desktopTilingDecorViewModel, desktopWallpaperActivityTokenProvider, bubbleController, overviewToDesktopTransitionObserver, @@ -990,7 +988,8 @@ public abstract class WMShellModule { DesktopModeUiEventLogger desktopModeUiEventLogger, WindowDecorTaskResourceLoader taskResourceLoader, RecentsTransitionHandler recentsTransitionHandler, - DesktopModeCompatPolicy desktopModeCompatPolicy + DesktopModeCompatPolicy desktopModeCompatPolicy, + DesktopTilingDecorViewModel desktopTilingDecorViewModel ) { if (!DesktopModeStatus.canEnterDesktopModeOrShowAppHandle(context)) { return Optional.empty(); @@ -1006,7 +1005,8 @@ public abstract class WMShellModule { desktopTasksLimiter, appHandleEducationController, appToWebEducationController, windowDecorCaptionHandleRepository, activityOrientationChangeHandler, focusTransitionObserver, desktopModeEventLogger, desktopModeUiEventLogger, - taskResourceLoader, recentsTransitionHandler, desktopModeCompatPolicy)); + taskResourceLoader, recentsTransitionHandler, desktopModeCompatPolicy, + desktopTilingDecorViewModel)); } @WMSingleton @@ -1278,10 +1278,10 @@ public abstract class WMShellModule { @WMSingleton @Provides static DesktopWindowingEducationTooltipController - provideDesktopWindowingEducationTooltipController( - Context context, - AdditionalSystemViewContainer.Factory additionalSystemViewContainerFactory, - DisplayController displayController) { + provideDesktopWindowingEducationTooltipController( + Context context, + AdditionalSystemViewContainer.Factory additionalSystemViewContainerFactory, + DisplayController displayController) { return new DesktopWindowingEducationTooltipController( context, additionalSystemViewContainerFactory, displayController); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/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/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index f17b680f6fae..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 @@ -136,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 @@ -144,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 @@ -184,7 +183,6 @@ class DesktopTasksController( @ShellMainThread private val handler: Handler, private val desktopModeEventLogger: DesktopModeEventLogger, private val desktopModeUiEventLogger: DesktopModeUiEventLogger, - private val desktopTilingDecorViewModel: DesktopTilingDecorViewModel, private val desktopWallpaperActivityTokenProvider: DesktopWallpaperActivityTokenProvider, private val bubbleController: Optional<BubbleController>, private val overviewToDesktopTransitionObserver: OverviewToDesktopTransitionObserver, @@ -204,7 +202,9 @@ class DesktopTasksController( private var userId: Int private val desktopModeShellCommandHandler: DesktopModeShellCommandHandler = DesktopModeShellCommandHandler(this) + private val mOnAnimationFinishedCallback = { releaseVisualIndicator() } + private lateinit var snapEventHandler: SnapEventHandler private val dragToDesktopStateListener = object : DragToDesktopStateListener { override fun onCommitToDesktopAnimationStart() { @@ -269,7 +269,7 @@ class DesktopTasksController( RecentsTransitionStateListener.stateToString(state), ) recentsTransitionState = state - desktopTilingDecorViewModel.onOverviewAnimationStateChange( + snapEventHandler.onOverviewAnimationStateChange( RecentsTransitionStateListener.isAnimating(state) ) } @@ -300,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) { @@ -784,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( @@ -833,7 +838,7 @@ class DesktopTasksController( val taskId = taskInfo.taskId val displayId = taskInfo.displayId val wct = WindowContainerTransaction() - desktopTilingDecorViewModel.removeTaskIfTiled(displayId, taskId) + snapEventHandler.removeTaskIfTiled(displayId, taskId) performDesktopExitCleanupIfNeeded(taskId, displayId, wct, forceToFullscreen = false) // Notify immersive handler as it might need to exit immersive state. val exitResult = @@ -844,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, @@ -859,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) } } @@ -867,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) } } @@ -986,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() @@ -1228,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)) } @@ -1354,7 +1361,6 @@ class DesktopTasksController( position: SnapPosition, resizeTrigger: ResizeTrigger, inputMethod: InputMethod, - desktopWindowDecoration: DesktopModeWindowDecoration, ) { desktopModeEventLogger.logTaskResizingStarted( resizeTrigger, @@ -1376,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) } @@ -1419,7 +1419,6 @@ class DesktopTasksController( position: SnapPosition, resizeTrigger: ResizeTrigger, inputMethod: InputMethod, - desktopModeWindowDecoration: DesktopModeWindowDecoration, ) { if (!isSnapResizingAllowed(taskInfo)) { Toast.makeText( @@ -1438,7 +1437,6 @@ class DesktopTasksController( position, resizeTrigger, inputMethod, - desktopModeWindowDecoration, ) } @@ -1450,7 +1448,6 @@ class DesktopTasksController( currentDragBounds: Rect, dragStartBounds: Rect, motionEvent: MotionEvent, - desktopModeWindowDecoration: DesktopModeWindowDecoration, ) { releaseVisualIndicator() if (!isSnapResizingAllowed(taskInfo)) { @@ -1498,7 +1495,6 @@ class DesktopTasksController( position, resizeTrigger, DesktopModeEventLogger.getInputMethodFromMotionEvent(motionEvent), - desktopModeWindowDecoration, ) } } @@ -2169,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 @@ -2265,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( @@ -2730,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, @@ -2790,7 +2786,6 @@ class DesktopTasksController( validDragArea: Rect, dragStartBounds: Rect, motionEvent: MotionEvent, - desktopModeWindowDecoration: DesktopModeWindowDecoration, ) { if (taskInfo.configuration.windowConfiguration.windowingMode != WINDOWING_MODE_FREEFORM) { return @@ -2829,7 +2824,6 @@ class DesktopTasksController( currentDragBounds, dragStartBounds, motionEvent, - desktopModeWindowDecoration, ) } IndicatorType.TO_SPLIT_RIGHT_INDICATOR -> { @@ -2844,7 +2838,6 @@ class DesktopTasksController( currentDragBounds, dragStartBounds, motionEvent, - desktopModeWindowDecoration, ) } IndicatorType.NO_INDICATOR, @@ -3130,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 fc29498291da..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 } 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/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/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/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/tiling/SnapEventHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/SnapEventHandler.kt new file mode 100644 index 000000000000..52e24d6fe0d0 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/SnapEventHandler.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.windowdecor.tiling + +import android.app.ActivityManager.RunningTaskInfo +import android.graphics.Rect +import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition + +/** Interface for handling snap to half screen events. */ +interface SnapEventHandler { + /** Snaps an app to half the screen for tiling. */ + fun snapToHalfScreen( + taskInfo: RunningTaskInfo, + currentDragBounds: Rect, + position: SnapPosition, + ): Boolean + + /** Removes a task from tiling if it's tiled, for example on task exiting. */ + fun removeTaskIfTiled(displayId: Int, taskId: Int) + + /** Notifies the tiling handler of user switch. */ + fun onUserChange() + + /** Notifies the tiling handler of overview animation state change. */ + fun onOverviewAnimationStateChange(running: Boolean) + + /** If a task is tiled, delegate moving to front to tiling infrastructure. */ + fun moveTaskToFrontIfTiled(taskInfo: RunningTaskInfo): Boolean +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTransitionsTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTransitionsTest.java index 87ee4f58bfdd..42310caba1c6 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTransitionsTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTransitionsTest.java @@ -215,7 +215,7 @@ public class BubbleTransitionsTest extends ShellTestCase { pendingWct.reorder(pendingDragOpToken, /* onTop= */ false); BubbleTransitions.DragData dragData = new BubbleTransitions.DragData( - draggedTaskBounds, pendingWct + draggedTaskBounds, pendingWct, /* releasedOnLeft= */ false ); final BubbleTransitions.BubbleTransition bt = mBubbleTransitions.startConvertToBubble( diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/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/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index 3ee9501dd8dd..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 @@ -151,8 +151,7 @@ import com.android.wm.shell.transition.Transitions import com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS import com.android.wm.shell.transition.Transitions.TransitionHandler import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModelTestsBase.Companion.HOME_LAUNCHER_PACKAGE_NAME -import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration -import com.android.wm.shell.windowdecor.tiling.DesktopTilingDecorViewModel +import com.android.wm.shell.windowdecor.tiling.SnapEventHandler import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import java.util.Optional @@ -179,6 +178,7 @@ import org.mockito.ArgumentMatchers.isA import org.mockito.ArgumentMatchers.isNull import org.mockito.Mock import org.mockito.Mockito +import org.mockito.Mockito.anyBoolean import org.mockito.Mockito.anyInt import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.mock @@ -233,6 +233,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Mock lateinit var multiInstanceHelper: MultiInstanceHelper @Mock lateinit var desktopModeVisualIndicator: DesktopModeVisualIndicator @Mock lateinit var recentTasksController: RecentTasksController + @Mock lateinit var snapEventHandler: SnapEventHandler @Mock private lateinit var mockInteractionJankMonitor: InteractionJankMonitor @Mock private lateinit var mockSurface: SurfaceControl @Mock private lateinit var taskbarDesktopTaskListener: TaskbarDesktopTaskListener @@ -245,9 +246,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Mock lateinit var repositoryInitializer: DesktopRepositoryInitializer @Mock private lateinit var mockToast: Toast private lateinit var mockitoSession: StaticMockitoSession - @Mock private lateinit var desktopTilingDecorViewModel: DesktopTilingDecorViewModel @Mock private lateinit var bubbleController: BubbleController - @Mock private lateinit var desktopWindowDecoration: DesktopModeWindowDecoration @Mock private lateinit var resources: Resources @Mock lateinit var desktopModeEnterExitTransitionListener: DesktopModeEntryExitTransitionListener @@ -379,6 +378,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() recentsTransitionStateListener = captor.firstValue controller.taskbarDesktopTaskListener = taskbarDesktopTaskListener + controller.setSnapEventHandler(snapEventHandler) assumeTrue(ENABLE_SHELL_TRANSITIONS) @@ -422,7 +422,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() mockHandler, desktopModeEventLogger, desktopModeUiEventLogger, - desktopTilingDecorViewModel, desktopWallpaperActivityTokenProvider, Optional.of(bubbleController), overviewToDesktopTransitionObserver, @@ -2763,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() } @@ -2785,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()) } @@ -2820,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 } } @@ -2835,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) } @@ -2851,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) @@ -2859,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() } @@ -2870,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() } @@ -2888,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) @@ -2896,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) } @@ -2905,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) @@ -2918,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( @@ -4420,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) @@ -4458,7 +4518,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() validDragArea = Rect(0, 50, 2000, 2000), dragStartBounds = Rect(), motionEvent, - desktopWindowDecoration, ) verify(transitions) @@ -4498,7 +4557,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() validDragArea = Rect(0, 50, 2000, 2000), dragStartBounds = Rect(), motionEvent, - desktopWindowDecoration, ) verify(transitions) @@ -4539,7 +4597,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() validDragArea = Rect(0, 50, 2000, 2000), dragStartBounds = Rect(), motionEvent, - desktopWindowDecoration, ) // Assert the task exits desktop mode @@ -4577,7 +4634,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() validDragArea = Rect(0, 50, 2000, 2000), dragStartBounds = Rect(), motionEvent, - desktopWindowDecoration, ) // Assert bounds set to stable bounds @@ -4633,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 @@ -5053,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) @@ -5099,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()) @@ -5143,7 +5196,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() currentDragBounds, preDragBounds, motionEvent, - desktopWindowDecoration, ) val wct = getLatestToggleResizeDesktopTaskWct(currentDragBounds) assertThat(findBoundsChange(wct, task)).isEqualTo(expectedBounds) @@ -5173,7 +5225,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() currentDragBounds, preDragBounds, motionEvent, - desktopWindowDecoration, ) verify(mReturnToDragStartAnimator) .start( @@ -5198,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 @@ -5225,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/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/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/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/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/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 511e54b9abff..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" @@ -1990,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." @@ -2009,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/keyguard/ui/composable/LockscreenContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt index ba85f9570d09..5806458da9b5 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenContent.kt @@ -20,11 +20,8 @@ import android.view.View import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalView -import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.compose.animation.scene.ContentScope import com.android.internal.jank.Cuj import com.android.internal.jank.Cuj.CujType @@ -70,8 +67,7 @@ class LockscreenContent( rememberViewModel("LockscreenContent-scrimViewModel") { notificationScrimViewModelFactory.create() } - val isContentVisible: Boolean by viewModel.isContentVisible.collectAsStateWithLifecycle() - if (!isContentVisible) { + if (!viewModel.isContentVisible) { // If the content isn't supposed to be visible, show a large empty box as it's needed // for scene transition animations (can't just skip rendering everything or shared // elements won't have correct final/initial bounds from animating in and out of the @@ -80,15 +76,13 @@ class LockscreenContent( return } - val coroutineScope = rememberCoroutineScope() - val blueprintId by viewModel.blueprintId(coroutineScope).collectAsStateWithLifecycle() DisposableEffect(view) { clockInteractor.clockEventController.registerListeners(view) onDispose { clockInteractor.clockEventController.unregisterListeners() } } - val blueprint = blueprintByBlueprintId[blueprintId] ?: return + val blueprint = blueprintByBlueprintId[viewModel.blueprintId] ?: return with(blueprint) { Content(viewModel, modifier.sysuiResTag("keyguard_root_view")) NotificationLockscreenScrim(notificationLockscreenScrimViewModel) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt index d2fff06ad746..590a74ee2a0d 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/DefaultBlueprint.kt @@ -23,7 +23,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer @@ -32,7 +31,6 @@ import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntRect import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.compose.animation.scene.ContentScope import com.android.compose.modifiers.padding import com.android.systemui.compose.modifiers.sysuiResTag @@ -45,6 +43,8 @@ import com.android.systemui.keyguard.ui.composable.section.SettingsMenuSection import com.android.systemui.keyguard.ui.composable.section.StatusBarSection import com.android.systemui.keyguard.ui.composable.section.TopAreaSection import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel +import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel.NotificationsPlacement.BelowClock +import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel.NotificationsPlacement.BesideClock import com.android.systemui.res.R import java.util.Optional import javax.inject.Inject @@ -71,11 +71,8 @@ constructor( @Composable override fun ContentScope.Content(viewModel: LockscreenContentViewModel, modifier: Modifier) { val isUdfpsVisible = viewModel.isUdfpsVisible - val isShadeLayoutWide by viewModel.isShadeLayoutWide.collectAsStateWithLifecycle() - val unfoldTranslations by viewModel.unfoldTranslations.collectAsStateWithLifecycle() - val areNotificationsVisible by - viewModel.areNotificationsVisible().collectAsStateWithLifecycle(initialValue = false) - val isBypassEnabled by viewModel.isBypassEnabled.collectAsStateWithLifecycle() + val isBypassEnabled = viewModel.isBypassEnabled + val notificationsPlacement = viewModel.notificationsPlacement if (isBypassEnabled) { with(notificationSection) { HeadsUpNotifications() } @@ -92,7 +89,9 @@ constructor( modifier = Modifier.fillMaxWidth() .padding( - horizontal = { unfoldTranslations.start.roundToInt() } + horizontal = { + viewModel.unfoldTranslations.start.roundToInt() + } ) ) } @@ -101,28 +100,28 @@ constructor( with(topAreaSection) { DefaultClockLayout( smartSpacePaddingTop = viewModel::getSmartSpacePaddingTop, - isShadeLayoutWide = isShadeLayoutWide, modifier = Modifier.fillMaxWidth().graphicsLayer { - translationX = unfoldTranslations.start + translationX = viewModel.unfoldTranslations.start }, ) } - if (isShadeLayoutWide && !isBypassEnabled) { + if (notificationsPlacement is BesideClock && !isBypassEnabled) { with(notificationSection) { Box(modifier = Modifier.fillMaxHeight()) { AodPromotedNotificationArea( modifier = Modifier.fillMaxWidth(0.5f) - .align(alignment = Alignment.TopEnd) + .align(notificationsPlacement.alignment) ) Notifications( - areNotificationsVisible = areNotificationsVisible, + areNotificationsVisible = + viewModel.areNotificationsVisible, burnInParams = null, modifier = Modifier.fillMaxWidth(0.5f) .fillMaxHeight() - .align(alignment = Alignment.TopEnd) + .align(notificationsPlacement.alignment) .padding(top = 12.dp), ) } @@ -138,7 +137,7 @@ constructor( dimensionResource(R.dimen.below_clock_padding_start_icons) with(notificationSection) { - if (!isShadeLayoutWide && !isBypassEnabled) { + if (notificationsPlacement is BelowClock && !isBypassEnabled) { Box(modifier = Modifier.weight(weight = 1f)) { Column(Modifier.align(alignment = Alignment.TopStart)) { AodPromotedNotificationArea( @@ -150,13 +149,13 @@ constructor( ) } Notifications( - areNotificationsVisible = areNotificationsVisible, + areNotificationsVisible = viewModel.areNotificationsVisible, burnInParams = null, ) } } else { Column { - if (!isShadeLayoutWide) { + if (viewModel.notificationsPlacement is BelowClock) { AodPromotedNotificationArea( modifier = Modifier.padding(top = aodPromotedNotifTopPadding) @@ -204,13 +203,17 @@ constructor( isStart = true, applyPadding = true, modifier = - Modifier.graphicsLayer { translationX = unfoldTranslations.start }, + Modifier.graphicsLayer { + translationX = viewModel.unfoldTranslations.start + }, ) Shortcut( isStart = false, applyPadding = true, modifier = - Modifier.graphicsLayer { translationX = unfoldTranslations.end }, + Modifier.graphicsLayer { + translationX = viewModel.unfoldTranslations.end + }, ) } with(settingsMenuSection) { SettingsMenu(onSettingsMenuPlaced) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/MediaCarouselSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/MediaCarouselSection.kt index d8b3f742b447..0876631cf5c1 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/MediaCarouselSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/MediaCarouselSection.kt @@ -41,16 +41,13 @@ constructor( ) { @Composable - fun ContentScope.KeyguardMediaCarousel( - isShadeLayoutWide: Boolean, - modifier: Modifier = Modifier, - ) { + fun ContentScope.KeyguardMediaCarousel(modifier: Modifier = Modifier) { val viewModel = rememberViewModel(traceName = "KeyguardMediaCarousel") { keyguardMediaViewModelFactory.create() } val horizontalPadding = - if (isShadeLayoutWide) { + if (viewModel.isShadeLayoutWide) { dimensionResource(id = R.dimen.notification_side_paddings) } else { dimensionResource(id = R.dimen.notification_side_paddings) + diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt index 6293fc26f96a..013424006668 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/TopAreaSection.kt @@ -60,7 +60,6 @@ constructor( @Composable fun ContentScope.DefaultClockLayout( smartSpacePaddingTop: (Resources) -> Int, - isShadeLayoutWide: Boolean, modifier: Modifier = Modifier, ) { val currentClockLayout by clockViewModel.currentClockLayout.collectAsStateWithLifecycle() @@ -128,7 +127,7 @@ constructor( ) } } - with(mediaCarouselSection) { KeyguardMediaCarousel(isShadeLayoutWide) } + with(mediaCarouselSection) { KeyguardMediaCarousel() } } } diff --git a/packages/SystemUI/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/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractorTest.kt index 9b80ca303cd3..63229dbb47a4 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractorTest.kt @@ -229,6 +229,39 @@ class FromAlternateBouncerTransitionInteractorTest(flags: FlagsParameterization) } @Test + @EnableFlags(FLAG_GLANCEABLE_HUB_V2) + fun transitionToDreaming() = + kosmos.runTest { + fakePowerRepository.updateWakefulness( + WakefulnessState.AWAKE, + WakeSleepReason.POWER_BUTTON, + WakeSleepReason.POWER_BUTTON, + false, + ) + fakeKeyguardRepository.setKeyguardOccluded(false) + fakeKeyguardBouncerRepository.setAlternateVisible(true) + runCurrent() + + transitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.ALTERNATE_BOUNCER, + testScope, + ) + reset(transitionRepository) + + fakeKeyguardRepository.setKeyguardOccluded(true) + fakeKeyguardRepository.setDreaming(true) + fakeKeyguardBouncerRepository.setAlternateVisible(false) + testScope.advanceTimeBy(200) // advance past delay + + assertThat(transitionRepository) + .startedTransition( + from = KeyguardState.ALTERNATE_BOUNCER, + to = KeyguardState.DREAMING, + ) + } + + @Test @DisableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) fun transitionToGone_whenOpeningGlanceableHubEditMode() = testScope.runTest { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorTest.kt index 282bebcd629a..a08c0dea6fa6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorTest.kt @@ -30,6 +30,7 @@ import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepos import com.android.systemui.keyguard.data.repository.keyguardClockRepository import com.android.systemui.keyguard.data.repository.keyguardRepository import com.android.systemui.keyguard.shared.model.ClockSize +import com.android.systemui.keyguard.shared.model.ClockSizeSetting import com.android.systemui.keyguard.shared.model.DozeStateModel import com.android.systemui.keyguard.shared.model.DozeTransitionModel import com.android.systemui.keyguard.shared.model.KeyguardState @@ -68,6 +69,7 @@ class KeyguardClockInteractorTest : SysuiTestCase() { fun clockSize_sceneContainerFlagOff_basedOnRepository() = testScope.runTest { val value by collectLastValue(underTest.clockSize) + kosmos.fakeKeyguardClockRepository.setSelectedClockSize(ClockSizeSetting.DYNAMIC) kosmos.keyguardClockRepository.setClockSize(ClockSize.LARGE) assertThat(value).isEqualTo(ClockSize.LARGE) @@ -76,6 +78,17 @@ class KeyguardClockInteractorTest : SysuiTestCase() { } @Test + @DisableSceneContainer + fun clockSize_sceneContainerFlagOff_smallClockSettingSelected_SMALL() = + testScope.runTest { + val value by collectLastValue(underTest.clockSize) + kosmos.fakeKeyguardClockRepository.setSelectedClockSize(ClockSizeSetting.SMALL) + kosmos.keyguardClockRepository.setClockSize(ClockSize.LARGE) + + assertThat(value).isEqualTo(ClockSize.SMALL) + } + + @Test @EnableSceneContainer fun clockSize_forceSmallClock_SMALL() = testScope.runTest { @@ -91,61 +104,80 @@ class KeyguardClockInteractorTest : SysuiTestCase() { @Test @EnableSceneContainer - fun clockSize_SceneContainerFlagOn_shadeModeSingle_hasNotifs_SMALL() = + fun clockSize_sceneContainerFlagOn_shadeModeSingle_hasNotifs_SMALL() = testScope.runTest { val value by collectLastValue(underTest.clockSize) kosmos.shadeRepository.setShadeLayoutWide(false) kosmos.activeNotificationListRepository.setActiveNotifs(1) + assertThat(value).isEqualTo(ClockSize.SMALL) } @Test @EnableSceneContainer - fun clockSize_SceneContainerFlagOn_shadeModeSingle_hasMedia_SMALL() = + fun clockSize_sceneContainerFlagOn_shadeModeSingle_hasMedia_SMALL() = testScope.runTest { val value by collectLastValue(underTest.clockSize) kosmos.shadeRepository.setShadeLayoutWide(false) val userMedia = MediaData().copy(active = true) kosmos.mediaFilterRepository.addSelectedUserMediaEntry(userMedia) + assertThat(value).isEqualTo(ClockSize.SMALL) } @Test @EnableSceneContainer - fun clockSize_SceneContainerFlagOn_shadeModeSplit_isMediaVisible_SMALL() = + fun clockSize_sceneContainerFlagOn_shadeModeSplit_isMediaVisible_SMALL() = testScope.runTest { val value by collectLastValue(underTest.clockSize) val userMedia = MediaData().copy(active = true) kosmos.shadeRepository.setShadeLayoutWide(true) kosmos.mediaFilterRepository.addSelectedUserMediaEntry(userMedia) kosmos.keyguardRepository.setIsDozing(false) + assertThat(value).isEqualTo(ClockSize.SMALL) } @Test @EnableSceneContainer - fun clockSize_SceneContainerFlagOn_shadeModeSplit_noMedia_LARGE() = + fun clockSize_sceneContainerFlagOn_shadeModeSplit_noMedia_LARGE() = testScope.runTest { val value by collectLastValue(underTest.clockSize) kosmos.shadeRepository.setShadeLayoutWide(true) kosmos.keyguardRepository.setIsDozing(false) + assertThat(value).isEqualTo(ClockSize.LARGE) } @Test @EnableSceneContainer - fun clockSize_SceneContainerFlagOn_shadeModeSplit_isDozing_LARGE() = + fun clockSize_sceneContainerFlagOn_shadeModeSplit_isDozing_LARGE() = testScope.runTest { val value by collectLastValue(underTest.clockSize) val userMedia = MediaData().copy(active = true) kosmos.shadeRepository.setShadeLayoutWide(true) kosmos.mediaFilterRepository.addSelectedUserMediaEntry(userMedia) kosmos.keyguardRepository.setIsDozing(true) + assertThat(value).isEqualTo(ClockSize.LARGE) } @Test @EnableSceneContainer + fun clockSize_sceneContainerFlagOn_shadeModeSplit_smallClockSettingSelectd_SMALL() = + testScope.runTest { + val value by collectLastValue(underTest.clockSize) + val userMedia = MediaData().copy(active = true) + kosmos.fakeKeyguardClockRepository.setSelectedClockSize(ClockSizeSetting.SMALL) + kosmos.shadeRepository.setShadeLayoutWide(true) + kosmos.mediaFilterRepository.addSelectedUserMediaEntry(userMedia) + kosmos.keyguardRepository.setIsDozing(true) + + assertThat(value).isEqualTo(ClockSize.SMALL) + } + + @Test + @EnableSceneContainer fun clockShouldBeCentered_sceneContainerFlagOn_notSplitMode_true() = testScope.runTest { val value by collectLastValue(underTest.clockShouldBeCentered) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/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/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/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/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/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/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/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/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 3b1e609d86e2..2bac87d01bdd 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1310,7 +1310,8 @@ <dimen name="qs_media_seekbar_progress_amplitude">1.5dp</dimen> <dimen name="qs_media_seekbar_progress_phase">8dp</dimen> <dimen name="qs_media_seekbar_progress_stroke_width">2dp</dimen> - <dimen name="qs_media_session_collapsed_guideline">144dp</dimen> + <dimen name="qs_media_session_collapsed_legacy_guideline">144dp</dimen> + <dimen name="qs_media_session_collapsed_guideline">168dp</dimen> <!-- Size of Smartspace media recommendations cards in the QSPanel carousel --> <dimen name="qs_media_rec_default_width">380dp</dimen> diff --git a/packages/SystemUI/res/xml/media_session_collapsed.xml b/packages/SystemUI/res/xml/media_session_collapsed.xml index 66c54a389c8e..b5efd04eeba9 100644 --- a/packages/SystemUI/res/xml/media_session_collapsed.xml +++ b/packages/SystemUI/res/xml/media_session_collapsed.xml @@ -64,6 +64,13 @@ app:layout_constraintBottom_toBottomOf="@+id/album_art" /> <Constraint + android:id="@+id/action_button_guideline" + android:layout_width="0dp" + android:layout_height="0dp" + android:orientation="vertical" + app:layout_constraintGuide_end="@dimen/qs_media_session_collapsed_legacy_guideline" /> + + <Constraint android:id="@+id/header_title" android:layout_width="0dp" android:layout_height="wrap_content" diff --git a/packages/SystemUI/src/com/android/keyguard/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/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/accessibility/WindowMagnificationSettings.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java index c14d28d1c08d..3b6f8f87a1a8 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java @@ -174,13 +174,14 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { // Notify the service to update the magnifier scale only when the progress changed is - // triggered by user interaction on seekbar - if (fromUser) { - final float scale = transformProgressToScale(progress); - // We don't need to update the persisted scale when the seekbar progress is - // changing. The update should be triggered when the changing is ended. - mCallback.onMagnifierScale(scale, /* updatePersistence= */ false); + // triggered by user interaction on seekbar. + if (!fromUser) { + return; } + final float scale = transformProgressToScale(progress); + // We don't need to update the persisted scale when the seekbar progress is + // changing. The update should be triggered when the changing is ended. + mCallback.onMagnifierScale(scale, /* updatePersistence= */ false); } @Override @@ -195,7 +196,7 @@ class WindowMagnificationSettings implements MagnificationGestureDetector.OnGest @Override public void onUserInteractionFinalized(SeekBar seekBar, @ControlUnitType int control) { - // Update the Settings persisted scale only when user interaction with seekbar ends + // Update the Settings persisted scale only when user interaction with seekbar ends. final int progress = seekBar.getProgress(); final float scale = transformProgressToScale(progress); mCallback.onMagnifierScale(scale, /* updatePersistence= */ true); diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogDelegate.kt index eaf541d7b559..76b5d823e0b6 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/accessibility/fontscaling/FontScalingDialogDelegate.kt @@ -88,7 +88,7 @@ constructor( dialog.setPositiveButton( R.string.quick_settings_done, /* onClick = */ null, - /* dismissOnClick = */ true + /* dismissOnClick = */ true, ) } @@ -102,7 +102,7 @@ constructor( labelArray[i] = context.resources.getString( com.android.settingslib.R.string.font_scale_percentage, - (strEntryValues[i].toFloat() * 100).roundToInt() + (strEntryValues[i].toFloat() * 100).roundToInt(), ) } seekBarWithIconButtonsView.setProgressStateLabels(labelArray) @@ -132,7 +132,7 @@ constructor( override fun onUserInteractionFinalized( seekBar: SeekBar, - @ControlUnitType control: Int + @ControlUnitType control: Int, ) { if (control == ControlUnitType.BUTTON) { // The seekbar progress is changed by icon buttons @@ -216,7 +216,7 @@ constructor( !systemSettings.putStringForUser( Settings.System.FONT_SCALE, strEntryValues[lastProgress.get()], - userTracker.userId + userTracker.userId, ) ) { title.post { doneButton.isEnabled = true } @@ -228,13 +228,13 @@ constructor( if ( secureSettings.getStringForUser( Settings.Secure.ACCESSIBILITY_FONT_SCALING_HAS_BEEN_CHANGED, - userTracker.userId + userTracker.userId, ) != ON ) { secureSettings.putStringForUser( Settings.Secure.ACCESSIBILITY_FONT_SCALING_HAS_BEEN_CHANGED, ON, - userTracker.userId + userTracker.userId, ) } } @@ -249,7 +249,7 @@ constructor( title.setTextSize( TypedValue.COMPLEX_UNIT_PX, - previewConfigContext.resources.getDimension(R.dimen.dialog_title_text_size) + previewConfigContext.resources.getDimension(R.dimen.dialog_title_text_size), ) } diff --git a/packages/SystemUI/src/com/android/systemui/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/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/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt index bf0f25ff089e..a3796ab5ee27 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt @@ -118,26 +118,19 @@ constructor( powerInteractor.isAwake, keyguardInteractor.isAodAvailable, communalSceneInteractor.isIdleOnCommunal, - communalInteractor.editModeOpen, + keyguardInteractor.isDreaming, keyguardInteractor.isKeyguardOccluded, ) .filterRelevantKeyguardStateAnd { - (isAlternateBouncerShowing, isPrimaryBouncerShowing, _, _, _) -> + (isAlternateBouncerShowing, isPrimaryBouncerShowing, _, _, _, _) -> !isAlternateBouncerShowing && !isPrimaryBouncerShowing } - .collect { - ( - _, - _, - isAwake, - isAodAvailable, - isIdleOnCommunal, - isCommunalEditMode, - isOccluded) -> + .collect { (_, _, isAwake, isAodAvailable, isIdleOnCommunal, isDreaming, isOccluded) + -> // When unlocking over glanceable hub to enter edit mode, transitioning directly // to GONE prevents the lockscreen flash. Let listenForAlternateBouncerToGone // handle it. - if (isCommunalEditMode) return@collect + if (communalInteractor.editModeOpen.value) return@collect val hubV2 = communalSettingsInteractor.isV2FlagEnabled() val to = if (!isAwake) { @@ -150,8 +143,10 @@ constructor( if (!hubV2 && isIdleOnCommunal) { if (SceneContainerFlag.isEnabled) return@collect KeyguardState.GLANCEABLE_HUB - } else if (isOccluded) { + } else if (isOccluded && !isDreaming) { KeyguardState.OCCLUDED + } else if (hubV2 && isDreaming) { + KeyguardState.DREAMING } else if (hubV2 && isIdleOnCommunal) { if (SceneContainerFlag.isEnabled) return@collect KeyguardState.GLANCEABLE_HUB diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt index 673fa9730c53..63cf4f72e415 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractor.kt @@ -24,7 +24,6 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.data.repository.KeyguardClockRepository import com.android.systemui.keyguard.shared.model.ClockSize import com.android.systemui.keyguard.shared.model.ClockSizeSetting -import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.KeyguardState.AOD import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING import com.android.systemui.keyguard.shared.model.KeyguardState.GONE @@ -33,12 +32,13 @@ import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarou import com.android.systemui.plugins.clocks.ClockController import com.android.systemui.plugins.clocks.ClockId import com.android.systemui.scene.shared.flag.SceneContainerFlag -import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.shade.domain.interactor.ShadeModeInteractor import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor import com.android.systemui.statusbar.notification.promoted.PromotedNotificationUiAod import com.android.systemui.statusbar.notification.promoted.domain.interactor.AODPromotedNotificationInteractor import com.android.systemui.util.kotlin.combine +import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated import com.android.systemui.wallpapers.domain.interactor.WallpaperFocalAreaInteractor import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -61,7 +61,7 @@ constructor( mediaCarouselInteractor: MediaCarouselInteractor, activeNotificationsInteractor: ActiveNotificationsInteractor, aodPromotedNotificationInteractor: AODPromotedNotificationInteractor, - shadeInteractor: ShadeInteractor, + shadeModeInteractor: ShadeModeInteractor, keyguardInteractor: KeyguardInteractor, keyguardTransitionInteractor: KeyguardTransitionInteractor, headsUpNotificationInteractor: HeadsUpNotificationInteractor, @@ -70,8 +70,13 @@ constructor( private val wallpaperFocalAreaInteractor: WallpaperFocalAreaInteractor, ) { private val isOnAod: Flow<Boolean> = - keyguardTransitionInteractor.currentKeyguardState.map { it == KeyguardState.AOD } + keyguardTransitionInteractor.currentKeyguardState.map { it == AOD } + /** + * The clock size setting explicitly selected by the user. When it is `SMALL`, the large clock + * is never shown. When it is `DYNAMIC`, the clock size gets determined based on a combination + * of system signals. + */ val selectedClockSize: StateFlow<ClockSizeSetting> = keyguardClockRepository.selectedClockSize val currentClockId: Flow<ClockId> = keyguardClockRepository.currentClockId @@ -103,36 +108,46 @@ constructor( activeNotificationsInteractor.areAnyNotificationsPresent } - val clockSize: StateFlow<ClockSize> = + private val dynamicClockSize: Flow<ClockSize> = if (SceneContainerFlag.isEnabled) { combine( - shadeInteractor.isShadeLayoutWide, - areAnyNotificationsPresent, - mediaCarouselInteractor.hasActiveMediaOrRecommendation, - keyguardInteractor.isDozing, - isOnAod, - ) { isShadeLayoutWide, hasNotifs, hasMedia, isDozing, isOnAod -> - return@combine when { - keyguardClockRepository.shouldForceSmallClock && !isOnAod -> ClockSize.SMALL - !isShadeLayoutWide && (hasNotifs || hasMedia) -> ClockSize.SMALL - !isShadeLayoutWide -> ClockSize.LARGE - hasMedia && !isDozing -> ClockSize.SMALL - else -> ClockSize.LARGE - } + shadeModeInteractor.isShadeLayoutWide, + areAnyNotificationsPresent, + mediaCarouselInteractor.hasActiveMediaOrRecommendation, + keyguardInteractor.isDozing, + isOnAod, + ) { isShadeLayoutWide, hasNotifs, hasMedia, isDozing, isOnAod -> + when { + keyguardClockRepository.shouldForceSmallClock && !isOnAod -> ClockSize.SMALL + !isShadeLayoutWide && (hasNotifs || hasMedia) -> ClockSize.SMALL + !isShadeLayoutWide -> ClockSize.LARGE + hasMedia && !isDozing -> ClockSize.SMALL + else -> ClockSize.LARGE } - .stateIn( - scope = applicationScope, - started = SharingStarted.WhileSubscribed(), - initialValue = ClockSize.LARGE, - ) + } } else { keyguardClockRepository.clockSize } + val clockSize: StateFlow<ClockSize> = + selectedClockSize + .flatMapLatestConflated { selectedSize -> + if (selectedSize == ClockSizeSetting.SMALL) { + flowOf(ClockSize.SMALL) + } else { + dynamicClockSize + } + } + .stateIn( + scope = applicationScope, + started = SharingStarted.Eagerly, + initialValue = ClockSize.LARGE, + ) + val clockShouldBeCentered: Flow<Boolean> = if (SceneContainerFlag.isEnabled) { combine( - shadeInteractor.isShadeLayoutWide, + shadeModeInteractor.isShadeLayoutWide, areAnyNotificationsPresent, isAodPromotedNotificationPresent, isOnAod, @@ -156,7 +171,7 @@ constructor( } } else { combine( - shadeInteractor.isShadeLayoutWide, + shadeModeInteractor.isShadeLayoutWide, areAnyNotificationsPresent, isAodPromotedNotificationPresent, keyguardInteractor.dozeTransitionModel, @@ -203,7 +218,7 @@ constructor( val renderedClockId: ClockId get() { - return clock?.let { clock -> clock.config.id } + return clock?.config?.id ?: run { Log.e(TAG, "No clock is available") "MISSING_CLOCK_ID" diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt index d60c31719b99..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,8 +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 @@ -237,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() @@ -276,7 +278,7 @@ constructor( step.transitionState == TransitionState.CANCELED) && sceneTransitionPair.value.previousValue.isTransitioning(fromScene, toScene) - return@filter isTransitioningBetweenLockscreenStates || + return@filterTraced isTransitioningBetweenLockscreenStates || isTransitioningBetweenDesiredScenes || terminalStepBelongsToPreviousTransition || belongsToInstantReversedTransition @@ -365,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 } } @@ -393,6 +395,7 @@ constructor( } } } + .traceAs("flatMapLatestWithFinished") /** * Converts old KTF states to UNDEFINED when [SceneContainerFlag] is enabled. @@ -548,6 +551,7 @@ constructor( } } .onStart { emit(false) } + .traceAs("isInTransition-$edge-$edgeWithoutSceneContainer") .distinctUntilChanged() } 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/KeyguardClockViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt index 88fdc83fa7a0..cf5cc264be8d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt @@ -18,7 +18,6 @@ package com.android.systemui.keyguard.ui.viewmodel import android.content.Context import android.content.res.Resources -import androidx.annotation.VisibleForTesting import androidx.constraintlayout.helper.widget.Layer import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor import com.android.systemui.customization.R as customR @@ -27,11 +26,10 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.shared.model.ClockSize -import com.android.systemui.keyguard.shared.model.ClockSizeSetting import com.android.systemui.plugins.clocks.ClockPreviewConfig import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.ShadeDisplayAware -import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.shade.domain.interactor.ShadeModeInteractor import com.android.systemui.statusbar.notification.icon.ui.viewmodel.NotificationIconContainerAlwaysOnDisplayViewModel import com.android.systemui.statusbar.ui.SystemBarUtilsProxy import javax.inject.Inject @@ -47,11 +45,11 @@ import kotlinx.coroutines.flow.stateIn class KeyguardClockViewModel @Inject constructor( - val context: Context, + private val context: Context, keyguardClockInteractor: KeyguardClockInteractor, @Application private val applicationScope: CoroutineScope, aodNotificationIconViewModel: NotificationIconContainerAlwaysOnDisplayViewModel, - @get:VisibleForTesting val shadeInteractor: ShadeInteractor, + private val shadeModeInteractor: ShadeModeInteractor, private val systemBarUtils: SystemBarUtilsProxy, @ShadeDisplayAware configurationInteractor: ConfigurationInteractor, // TODO: b/374267505 - Use ShadeDisplayAware resources here. @@ -59,17 +57,7 @@ constructor( ) { var burnInLayer: Layer? = null - val clockSize: StateFlow<ClockSize> = - combine(keyguardClockInteractor.selectedClockSize, keyguardClockInteractor.clockSize) { - selectedSize, - clockSize -> - if (selectedSize == ClockSizeSetting.SMALL) ClockSize.SMALL else clockSize - } - .stateIn( - scope = applicationScope, - started = SharingStarted.Eagerly, - initialValue = ClockSize.LARGE, - ) + val clockSize: StateFlow<ClockSize> = keyguardClockInteractor.clockSize val isLargeClockVisible: StateFlow<Boolean> = clockSize @@ -118,7 +106,7 @@ constructor( combine( isLargeClockVisible, clockShouldBeCentered, - shadeInteractor.isShadeLayoutWide, + shadeModeInteractor.isShadeLayoutWide, currentClock, ) { isLargeClockVisible, clockShouldBeCentered, isShadeLayoutWide, currentClock -> if (currentClock?.config?.useCustomClockScene == true) { @@ -163,7 +151,7 @@ constructor( fun getSmallClockTopMargin(): Int { return ClockPreviewConfig( context, - shadeInteractor.isShadeLayoutWide.value, + shadeModeInteractor.isShadeLayoutWide.value, SceneContainerFlag.isEnabled, ) .getSmallClockTopPadding(systemBarUtils.getStatusBarHeaderHeightKeyguard()) @@ -172,7 +160,7 @@ constructor( val smallClockTopMargin = combine( configurationInteractor.onAnyConfigurationChange, - shadeInteractor.isShadeLayoutWide, + shadeModeInteractor.isShadeLayoutWide, ) { _, _ -> getSmallClockTopMargin() } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModel.kt index ba03c48c65e9..e70d696a207f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModel.kt @@ -21,6 +21,7 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.Hydrator import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor +import com.android.systemui.shade.domain.interactor.ShadeModeInteractor import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -31,6 +32,7 @@ class KeyguardMediaViewModel constructor( mediaCarouselInteractor: MediaCarouselInteractor, keyguardInteractor: KeyguardInteractor, + shadeModeInteractor: ShadeModeInteractor, ) : ExclusiveActivatable() { private val hydrator = Hydrator("KeyguardMediaViewModel.hydrator") @@ -54,6 +56,12 @@ constructor( mediaCarouselInteractor.hasActiveMediaOrRecommendation.value, ) + val isShadeLayoutWide: Boolean by + hydrator.hydratedStateOf( + traceName = "isShadeLayoutWide", + source = shadeModeInteractor.isShadeLayoutWide, + ) + override suspend fun onActivated(): Nothing { hydrator.activate() } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt index 3e3a89a55f66..ecebaee62862 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModel.kt @@ -17,8 +17,9 @@ package com.android.systemui.keyguard.ui.viewmodel import android.content.res.Resources +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import com.android.app.tracing.coroutines.launchTraced as launch -import com.android.internal.annotations.VisibleForTesting import com.android.systemui.biometrics.AuthController import com.android.systemui.customization.R as customR import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor @@ -30,86 +31,121 @@ import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.transition.KeyguardTransitionAnimationCallback import com.android.systemui.keyguard.shared.transition.KeyguardTransitionAnimationCallbackDelegator import com.android.systemui.lifecycle.ExclusiveActivatable +import com.android.systemui.lifecycle.Hydrator import com.android.systemui.res.R -import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.shade.domain.interactor.ShadeModeInteractor +import com.android.systemui.shade.shared.model.ShadeMode import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.stateIn class LockscreenContentViewModel @AssistedInject constructor( - clockInteractor: KeyguardClockInteractor, - private val interactor: KeyguardBlueprintInteractor, + private val clockInteractor: KeyguardClockInteractor, + interactor: KeyguardBlueprintInteractor, private val authController: AuthController, val touchHandling: KeyguardTouchHandlingViewModel, - private val shadeInteractor: ShadeInteractor, - private val unfoldTransitionInteractor: UnfoldTransitionInteractor, - private val deviceEntryInteractor: DeviceEntryInteractor, - private val transitionInteractor: KeyguardTransitionInteractor, + shadeModeInteractor: ShadeModeInteractor, + unfoldTransitionInteractor: UnfoldTransitionInteractor, + deviceEntryInteractor: DeviceEntryInteractor, + transitionInteractor: KeyguardTransitionInteractor, private val keyguardTransitionAnimationCallbackDelegator: KeyguardTransitionAnimationCallbackDelegator, @Assisted private val keyguardTransitionAnimationCallback: KeyguardTransitionAnimationCallback, ) : ExclusiveActivatable() { - @VisibleForTesting val clockSize = clockInteractor.clockSize + + private val hydrator = Hydrator("LockscreenContentViewModel.hydrator") val isUdfpsVisible: Boolean get() = authController.isUdfpsSupported - val isShadeLayoutWide: StateFlow<Boolean> = shadeInteractor.isShadeLayoutWide + /** Where to place the notifications stack on the lockscreen. */ + val notificationsPlacement: NotificationsPlacement by + hydrator.hydratedStateOf( + traceName = "notificationsPlacement", + initialValue = NotificationsPlacement.BelowClock, + source = + combine(shadeModeInteractor.shadeMode, clockInteractor.clockSize) { + shadeMode, + clockSize -> + if (shadeMode is ShadeMode.Split) { + NotificationsPlacement.BesideClock(alignment = Alignment.TopEnd) + } else if (clockSize == ClockSize.SMALL) { + NotificationsPlacement.BelowClock + } else { + NotificationsPlacement.BesideClock(alignment = Alignment.TopStart) + } + }, + ) - private val _unfoldTranslations = MutableStateFlow(UnfoldTranslations()) /** Amount of horizontal translation that should be applied to elements in the scene. */ - val unfoldTranslations: StateFlow<UnfoldTranslations> = _unfoldTranslations.asStateFlow() + val unfoldTranslations: UnfoldTranslations by + hydrator.hydratedStateOf( + traceName = "unfoldTranslations", + initialValue = UnfoldTranslations(), + source = + combine( + unfoldTransitionInteractor.unfoldTranslationX(isOnStartSide = true), + unfoldTransitionInteractor.unfoldTranslationX(isOnStartSide = false), + ::UnfoldTranslations, + ), + ) - private val _isContentVisible = MutableStateFlow(true) /** Whether the content of the scene UI should be shown. */ - val isContentVisible: StateFlow<Boolean> = _isContentVisible.asStateFlow() + val isContentVisible: Boolean by + hydrator.hydratedStateOf( + traceName = "isContentVisible", + initialValue = true, + // Content is visible unless we're OCCLUDED. Currently, we don't have nice animations + // into and out of OCCLUDED, so the lockscreen/AOD content is hidden immediately upon + // entering/exiting OCCLUDED. + source = transitionInteractor.transitionValue(KeyguardState.OCCLUDED).map { it == 0f }, + ) + + /** Indicates whether lockscreen notifications should be rendered. */ + val areNotificationsVisible: Boolean by + hydrator.hydratedStateOf( + traceName = "areNotificationsVisible", + initialValue = false, + // Content is visible unless we're OCCLUDED. Currently, we don't have nice animations + // into and out of OCCLUDED, so the lockscreen/AOD content is hidden immediately upon + // entering/exiting OCCLUDED. + source = + combine(clockInteractor.clockSize, shadeModeInteractor.isShadeLayoutWide) { + clockSize, + isShadeLayoutWide -> + clockSize == ClockSize.SMALL || isShadeLayoutWide + }, + ) /** @see DeviceEntryInteractor.isBypassEnabled */ - val isBypassEnabled: StateFlow<Boolean> - get() = deviceEntryInteractor.isBypassEnabled + val isBypassEnabled: Boolean by + hydrator.hydratedStateOf( + traceName = "isBypassEnabled", + source = deviceEntryInteractor.isBypassEnabled, + ) + + val blueprintId: String by + hydrator.hydratedStateOf( + traceName = "blueprintId", + initialValue = interactor.getCurrentBlueprint().id, + source = interactor.blueprint.map { it.id }.distinctUntilChanged(), + ) override suspend fun onActivated(): Nothing { coroutineScope { try { + launch { hydrator.activate() } + keyguardTransitionAnimationCallbackDelegator.delegate = keyguardTransitionAnimationCallback - launch { - combine( - unfoldTransitionInteractor.unfoldTranslationX(isOnStartSide = true), - unfoldTransitionInteractor.unfoldTranslationX(isOnStartSide = false), - ) { start, end -> - UnfoldTranslations(start = start, end = end) - } - .collect { _unfoldTranslations.value = it } - } - - launch { - transitionInteractor - .transitionValue(KeyguardState.OCCLUDED) - .map { it > 0f } - .collect { fullyOrPartiallyOccluded -> - // Content is visible unless we're OCCLUDED. Currently, we don't have - // nice - // animations into and out of OCCLUDED, so the lockscreen/AOD content is - // hidden immediately upon entering/exiting OCCLUDED. - _isContentVisible.value = !fullyOrPartiallyOccluded - } - } awaitCancellation() } finally { @@ -118,16 +154,8 @@ constructor( } } - /** Returns a flow that indicates whether lockscreen notifications should be rendered. */ - fun areNotificationsVisible(): Flow<Boolean> { - return combine(clockSize, shadeInteractor.isShadeLayoutWide) { clockSize, isShadeLayoutWide - -> - clockSize == ClockSize.SMALL || isShadeLayoutWide - } - } - fun getSmartSpacePaddingTop(resources: Resources): Int { - return if (clockSize.value == ClockSize.LARGE) { + return if (clockInteractor.clockSize.value == ClockSize.LARGE) { resources.getDimensionPixelSize(customR.dimen.keyguard_smartspace_top_offset) + resources.getDimensionPixelSize(R.dimen.keyguard_clock_top_margin) } else { @@ -135,17 +163,6 @@ constructor( } } - fun blueprintId(scope: CoroutineScope): StateFlow<String> { - return interactor.blueprint - .map { it.id } - .distinctUntilChanged() - .stateIn( - scope = scope, - started = SharingStarted.WhileSubscribed(), - initialValue = interactor.getCurrentBlueprint().id, - ) - } - data class UnfoldTranslations( /** @@ -162,6 +179,15 @@ constructor( val end: Float = 0f, ) + /** Where to place the notifications stack on the lockscreen. */ + sealed interface NotificationsPlacement { + /** Show notifications below the lockscreen clock. */ + data object BelowClock : NotificationsPlacement + + /** Show notifications side-by-side with the clock. */ + data class BesideClock(val alignment: Alignment) : NotificationsPlacement + } + @AssistedFactory interface Factory { fun create( diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/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/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/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/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/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 2501aa59c375..5242feac898b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/compose/ChipContent.kt @@ -37,7 +37,9 @@ import androidx.compose.ui.unit.constrain import androidx.compose.ui.unit.dp import com.android.systemui.res.R import com.android.systemui.statusbar.chips.ui.model.OngoingActivityChipModel +import com.android.systemui.statusbar.chips.ui.viewmodel.formatTimeRemainingData import com.android.systemui.statusbar.chips.ui.viewmodel.rememberChronometerState +import com.android.systemui.statusbar.chips.ui.viewmodel.rememberTimeRemainingState import kotlin.math.min @Composable @@ -119,7 +121,26 @@ fun ChipContent(viewModel: OngoingActivityChipModel.Active, modifier: Modifier = } is OngoingActivityChipModel.Active.ShortTimeDelta -> { - // TODO(b/372657935): Implement ShortTimeDelta content in compose. + val timeRemainingState = rememberTimeRemainingState(futureTimeMillis = viewModel.time) + + timeRemainingState.timeRemainingData?.let { + val text = formatTimeRemainingData(it) + Text( + text = text, + style = textStyle, + color = textColor, + softWrap = false, + modifier = + modifier.hideTextIfDoesNotFit( + text = text, + textStyle = textStyle, + textMeasurer = textMeasurer, + maxTextWidth = maxTextWidth, + startPadding = startPadding, + endPadding = endPadding, + ), + ) + } } is OngoingActivityChipModel.Active.IconOnly -> { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/TimeRemainingState.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/TimeRemainingState.kt new file mode 100644 index 000000000000..eb6ebcaa5796 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/ui/viewmodel/TimeRemainingState.kt @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.chips.ui.viewmodel + +import android.os.SystemClock +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.res.stringResource +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.repeatOnLifecycle +import kotlin.time.Duration +import kotlin.time.Duration.Companion.hours +import kotlin.time.Duration.Companion.minutes +import kotlin.time.DurationUnit +import kotlin.time.toDuration +import kotlinx.coroutines.delay + +/** + * Manages state and updates for the duration remaining between now and a given time in the future. + */ +class TimeRemainingState(private val timeSource: TimeSource, private val futureTimeMillis: Long) { + private var durationRemaining by mutableStateOf(Duration.ZERO) + private var startTimeMillis: Long = 0 + + /** + * [Pair] representing the time unit and its value. + * + * @property first the string resource ID corresponding to the time unit (e.g., minutes, hours). + * @property second the time value of the duration unit. Null if time is less than a minute or + * past. + */ + val timeRemainingData by derivedStateOf { getTimeRemainingData(durationRemaining) } + + suspend fun run() { + startTimeMillis = timeSource.getCurrentTime() + while (true) { + val currentTime = timeSource.getCurrentTime() + durationRemaining = + (futureTimeMillis - currentTime).toDuration(DurationUnit.MILLISECONDS) + // No need to update if duration is more than 1 minute in the past. Because, we will + // stop displaying anything. + if (durationRemaining.inWholeMilliseconds < -1.minutes.inWholeMilliseconds) { + break + } + val delaySkewMillis = (currentTime - startTimeMillis) % 1000L + delay(calculateNextUpdateDelay(durationRemaining) - delaySkewMillis) + } + } + + private fun calculateNextUpdateDelay(duration: Duration): Long { + val durationAbsolute = duration.absoluteValue + return when { + durationAbsolute.inWholeHours < 1 -> { + 1000 + ((durationAbsolute.inWholeMilliseconds % 1.minutes.inWholeMilliseconds)) + } + durationAbsolute.inWholeHours < 24 -> { + 1000 + (durationAbsolute.inWholeMilliseconds % 1.hours.inWholeMilliseconds) + } + else -> 1000 + (durationAbsolute.inWholeMilliseconds % 24.hours.inWholeMilliseconds) + } + } +} + +/** Remember and manage the TimeRemainingState */ +@Composable +fun rememberTimeRemainingState( + futureTimeMillis: Long, + timeSource: TimeSource = remember { TimeSource { SystemClock.elapsedRealtime() } }, +): TimeRemainingState { + + val state = + remember(timeSource, futureTimeMillis) { TimeRemainingState(timeSource, futureTimeMillis) } + val lifecycleOwner = LocalLifecycleOwner.current + LaunchedEffect(lifecycleOwner, timeSource, futureTimeMillis) { + lifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) { state.run() } + } + + return state +} + +private fun getTimeRemainingData(duration: Duration): Pair<Int, Long?>? { + return when { + duration.inWholeMinutes <= -1 -> null + duration.inWholeMinutes < 1 -> Pair(com.android.internal.R.string.now_string_shortest, null) + duration.inWholeHours < 1 -> + Pair(com.android.internal.R.string.duration_minutes_medium, duration.inWholeMinutes) + duration.inWholeDays < 1 -> + Pair(com.android.internal.R.string.duration_hours_medium, duration.inWholeHours) + else -> null + } +} + +/** Formats the time remaining data into a user-readable string. */ +@Composable +fun formatTimeRemainingData(resourcePair: Pair<Int, Long?>): String { + return resourcePair.let { (resourceId, time) -> + when (time) { + null -> stringResource(resourceId) + else -> stringResource(resourceId, time.toInt()) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/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/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/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/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 db25e0889298..6ff711deeb01 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PromotedNotificationInfo.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/PromotedNotificationInfo.java @@ -31,6 +31,8 @@ import com.android.internal.logging.UiEventLogger; import com.android.systemui.res.R; import com.android.systemui.statusbar.notification.AssistantFeedbackController; import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.row.icon.AppIconProvider; +import com.android.systemui.statusbar.notification.row.icon.NotificationIconStyleProvider; /** * The guts of a notification revealed when performing a long press, specifically @@ -50,6 +52,8 @@ public class PromotedNotificationInfo extends NotificationInfo { public void bindNotification( PackageManager pm, INotificationManager iNotificationManager, + AppIconProvider appIconProvider, + NotificationIconStyleProvider iconStyleProvider, OnUserInteractionCallback onUserInteractionCallback, ChannelEditorDialogController channelEditorDialogController, String pkg, @@ -64,11 +68,11 @@ public class PromotedNotificationInfo extends NotificationInfo { boolean wasShownHighPriority, AssistantFeedbackController assistantFeedbackController, MetricsLogger metricsLogger, OnClickListener onCloseClick) throws RemoteException { - super.bindNotification(pm, iNotificationManager, onUserInteractionCallback, - channelEditorDialogController, pkg, notificationChannel, entry, onSettingsClick, - onAppSettingsClick, feedbackClickListener, uiEventLogger, isDeviceProvisioned, - isNonblockable, wasShownHighPriority, assistantFeedbackController, metricsLogger, - onCloseClick); + super.bindNotification(pm, iNotificationManager, appIconProvider, iconStyleProvider, + onUserInteractionCallback, channelEditorDialogController, pkg, notificationChannel, + entry, onSettingsClick, onAppSettingsClick, feedbackClickListener, uiEventLogger, + isDeviceProvisioned, isNonblockable, wasShownHighPriority, + assistantFeedbackController, metricsLogger, onCloseClick); mNotificationManager = iNotificationManager; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/icon/AppIconProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/icon/AppIconProvider.kt index 52a0f6f92355..33d943348cb3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/icon/AppIconProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/icon/AppIconProvider.kt @@ -58,7 +58,7 @@ interface AppIconProvider { packageName: String, context: Context, withWorkProfileBadge: Boolean = false, - themed: Boolean = true, + themed: Boolean = false, ): Drawable /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/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/util/concurrency/SysUIConcurrencyModule.kt b/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.kt index d4cc7d1441a5..5b5f85c12efc 100644 --- a/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.kt +++ b/packages/SystemUI/src/com/android/systemui/util/concurrency/SysUIConcurrencyModule.kt @@ -107,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/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/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/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/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/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorKosmos.kt index 4068a2290559..b781f61723f2 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardClockInteractorKosmos.kt @@ -20,7 +20,7 @@ import com.android.systemui.keyguard.data.repository.keyguardClockRepository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.media.controls.domain.pipeline.interactor.mediaCarouselInteractor -import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.shade.domain.interactor.shadeModeInteractor import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor import com.android.systemui.statusbar.notification.promoted.domain.interactor.aodPromotedNotificationInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor @@ -32,7 +32,7 @@ val Kosmos.keyguardClockInteractor by mediaCarouselInteractor = mediaCarouselInteractor, activeNotificationsInteractor = activeNotificationsInteractor, aodPromotedNotificationInteractor = aodPromotedNotificationInteractor, - shadeInteractor = shadeInteractor, + shadeModeInteractor = shadeModeInteractor, keyguardInteractor = keyguardInteractor, keyguardTransitionInteractor = keyguardTransitionInteractor, headsUpNotificationInteractor = headsUpNotificationInteractor, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelKosmos.kt index c0b39b1df7d5..5dc19a340dd0 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelKosmos.kt @@ -22,7 +22,7 @@ import com.android.systemui.common.ui.domain.interactor.configurationInteractor import com.android.systemui.keyguard.domain.interactor.keyguardClockInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope -import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.shade.domain.interactor.shadeModeInteractor import com.android.systemui.statusbar.notification.icon.ui.viewmodel.notificationIconContainerAlwaysOnDisplayViewModel import com.android.systemui.statusbar.ui.systemBarUtilsProxy @@ -33,7 +33,7 @@ val Kosmos.keyguardClockViewModel by keyguardClockInteractor = keyguardClockInteractor, applicationScope = applicationCoroutineScope, aodNotificationIconViewModel = notificationIconContainerAlwaysOnDisplayViewModel, - shadeInteractor = shadeInteractor, + shadeModeInteractor = shadeModeInteractor, systemBarUtils = systemBarUtilsProxy, configurationInteractor = configurationInteractor, resources = mainResources, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModelFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModelFactoryKosmos.kt index 16d3fdc26613..345d69aa8df0 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModelFactoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardMediaViewModelFactoryKosmos.kt @@ -19,12 +19,17 @@ package com.android.systemui.keyguard.ui.viewmodel import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.media.controls.domain.pipeline.interactor.mediaCarouselInteractor +import com.android.systemui.shade.domain.interactor.shadeModeInteractor val Kosmos.keyguardMediaViewModelFactory by Kosmos.Fixture { object : KeyguardMediaViewModel.Factory { override fun create(): KeyguardMediaViewModel { - return KeyguardMediaViewModel(mediaCarouselInteractor, keyguardInteractor) + return KeyguardMediaViewModel( + mediaCarouselInteractor = mediaCarouselInteractor, + keyguardInteractor = keyguardInteractor, + shadeModeInteractor = shadeModeInteractor, + ) } } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt index dd13b8b143ae..b751e213152e 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelKosmos.kt @@ -25,7 +25,7 @@ import com.android.systemui.keyguard.shared.transition.KeyguardTransitionAnimati import com.android.systemui.keyguard.shared.transition.keyguardTransitionAnimationCallbackDelegator import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture -import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.shade.domain.interactor.shadeModeInteractor import com.android.systemui.unfold.domain.interactor.unfoldTransitionInteractor val Kosmos.lockscreenContentViewModelFactory by Fixture { @@ -38,7 +38,7 @@ val Kosmos.lockscreenContentViewModelFactory by Fixture { interactor = keyguardBlueprintInteractor, authController = authController, touchHandling = keyguardTouchHandlingViewModel, - shadeInteractor = shadeInteractor, + shadeModeInteractor = shadeModeInteractor, unfoldTransitionInteractor = unfoldTransitionInteractor, deviceEntryInteractor = deviceEntryInteractor, transitionInteractor = keyguardTransitionInteractor, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/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/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/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/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/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/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/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 340afb776405..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)) { @@ -5730,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); @@ -5762,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 { @@ -6549,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) @@ -6953,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 @@ -13165,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. @@ -13805,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; } @@ -13833,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; } @@ -13889,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/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/resources/ResourcesManagerShellCommand.java b/services/core/java/com/android/server/resources/ResourcesManagerShellCommand.java index a75d110e3cd1..17739712d65a 100644 --- a/services/core/java/com/android/server/resources/ResourcesManagerShellCommand.java +++ b/services/core/java/com/android/server/resources/ResourcesManagerShellCommand.java @@ -88,6 +88,5 @@ public class ResourcesManagerShellCommand extends ShellCommand { out.println(" Print this help text."); out.println(" dump <PROCESS>"); out.println(" Dump the Resources objects in use as well as the history of Resources"); - } } diff --git a/services/core/java/com/android/server/security/AttestationVerificationPeerDeviceVerifier.java b/services/core/java/com/android/server/security/AttestationVerificationPeerDeviceVerifier.java index f060e4d11e82..82df310db9a4 100644 --- a/services/core/java/com/android/server/security/AttestationVerificationPeerDeviceVerifier.java +++ b/services/core/java/com/android/server/security/AttestationVerificationPeerDeviceVerifier.java @@ -303,7 +303,11 @@ class AttestationVerificationPeerDeviceVerifier { if (mRevocationEnabled) { // Checks Revocation Status List based on // https://developer.android.com/training/articles/security-key-attestation#certificate_status - mCertificateRevocationStatusManager.checkRevocationStatus(certificates); + // The first certificate is the leaf, which is generated at runtime with the attestation + // attributes such as the challenge. It is specific to this attestation instance and + // does not need to be checked for revocation. + mCertificateRevocationStatusManager.checkRevocationStatus( + new ArrayList<>(certificates.subList(1, certificates.size()))); } } diff --git a/services/core/java/com/android/server/security/CertificateRevocationStatusManager.java b/services/core/java/com/android/server/security/CertificateRevocationStatusManager.java index d36d9f5f6636..4cd4b3b84910 100644 --- a/services/core/java/com/android/server/security/CertificateRevocationStatusManager.java +++ b/services/core/java/com/android/server/security/CertificateRevocationStatusManager.java @@ -42,6 +42,7 @@ import java.net.MalformedURLException; import java.net.URL; import java.security.cert.CertPathValidatorException; import java.security.cert.X509Certificate; +import java.time.Duration; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneId; @@ -67,6 +68,8 @@ class CertificateRevocationStatusManager { */ @VisibleForTesting static final int MAX_DAYS_SINCE_LAST_CHECK = 30; + @VisibleForTesting static final int NUM_HOURS_BEFORE_NEXT_CHECK = 24; + /** * The number of days since issue date for an intermediary certificate to be considered fresh * and not require a revocation list check. @@ -127,6 +130,17 @@ class CertificateRevocationStatusManager { serialNumbers.add(serialNumber); } try { + if (isLastCheckedWithin(Duration.ofHours(NUM_HOURS_BEFORE_NEXT_CHECK), serialNumbers)) { + Slog.d( + TAG, + "All certificates have been checked for revocation recently. No need to" + + " check this time."); + return; + } + } catch (IOException ignored) { + // Proceed to check the revocation status + } + try { JSONObject revocationList = fetchRemoteRevocationList(); Map<String, Boolean> areCertificatesRevoked = new HashMap<>(); for (String serialNumber : serialNumbers) { @@ -151,25 +165,32 @@ class CertificateRevocationStatusManager { serialNumbers.remove(serialNumber); } } - Map<String, LocalDateTime> lastRevocationCheckData; try { - lastRevocationCheckData = getLastRevocationCheckData(); + if (!isLastCheckedWithin( + Duration.ofDays(MAX_DAYS_SINCE_LAST_CHECK), serialNumbers)) { + throw new CertPathValidatorException( + "Unable to verify the revocation status of one of the certificates " + + serialNumbers); + } } catch (IOException ex2) { throw new CertPathValidatorException( "Unable to load stored revocation status", ex2); } - for (String serialNumber : serialNumbers) { - if (!lastRevocationCheckData.containsKey(serialNumber) - || lastRevocationCheckData - .get(serialNumber) - .isBefore( - LocalDateTime.now().minusDays(MAX_DAYS_SINCE_LAST_CHECK))) { - throw new CertPathValidatorException( - "Unable to verify the revocation status of certificate " - + serialNumber); - } + } + } + + private boolean isLastCheckedWithin(Duration lastCheckedWithin, List<String> serialNumbers) + throws IOException { + Map<String, LocalDateTime> lastRevocationCheckData = getLastRevocationCheckData(); + for (String serialNumber : serialNumbers) { + if (!lastRevocationCheckData.containsKey(serialNumber) + || lastRevocationCheckData + .get(serialNumber) + .isBefore(LocalDateTime.now().minus(lastCheckedWithin))) { + return false; } } + return true; } private static boolean needToCheckRevocationStatus( diff --git a/services/core/java/com/android/server/security/FileIntegrityService.java b/services/core/java/com/android/server/security/FileIntegrityService.java index bfd86d724583..9f9a9807d973 100644 --- a/services/core/java/com/android/server/security/FileIntegrityService.java +++ b/services/core/java/com/android/server/security/FileIntegrityService.java @@ -54,11 +54,6 @@ public class FileIntegrityService extends SystemService { super(PermissionEnforcer.fromContext(context)); } - @Override - public boolean isApkVeritySupported() { - return VerityUtils.isFsVeritySupported(); - } - private void checkCallerPackageName(String packageName) { final int callingUid = Binder.getCallingUid(); final int callingUserId = UserHandle.getUserId(callingUid); diff --git a/services/core/java/com/android/server/security/intrusiondetection/DataAggregator.java b/services/core/java/com/android/server/security/intrusiondetection/DataAggregator.java index 687442b47fb3..cdeacaa2e43a 100644 --- a/services/core/java/com/android/server/security/intrusiondetection/DataAggregator.java +++ b/services/core/java/com/android/server/security/intrusiondetection/DataAggregator.java @@ -62,7 +62,7 @@ public class DataAggregator { /** Initialize DataSources */ private void initialize() { mDataSources.add(new SecurityLogSource(mContext, this)); - mDataSources.add(new NetworkLogSource(mContext, this)); + mDataSources.add(new NetworkLogSource(this)); } /** diff --git a/services/core/java/com/android/server/security/intrusiondetection/NetworkLogSource.java b/services/core/java/com/android/server/security/intrusiondetection/NetworkLogSource.java index f303a588d30c..fe0cf80a48f2 100644 --- a/services/core/java/com/android/server/security/intrusiondetection/NetworkLogSource.java +++ b/services/core/java/com/android/server/security/intrusiondetection/NetworkLogSource.java @@ -18,7 +18,6 @@ package com.android.server.security.intrusiondetection; import android.app.admin.ConnectEvent; import android.app.admin.DnsEvent; -import android.content.Context; import android.content.pm.PackageManagerInternal; import android.net.IIpConnectivityMetrics; import android.net.INetdEventCallback; @@ -44,8 +43,7 @@ public class NetworkLogSource implements DataSource { private IIpConnectivityMetrics mIpConnectivityMetrics; private long mId; - public NetworkLogSource(Context context, DataAggregator dataAggregator) - throws SecurityException { + public NetworkLogSource(DataAggregator dataAggregator) throws SecurityException { mDataAggregator = dataAggregator; mPm = LocalServices.getService(PackageManagerInternal.class); mId = 0; diff --git a/services/core/java/com/android/server/security/intrusiondetection/SecurityLogSource.java b/services/core/java/com/android/server/security/intrusiondetection/SecurityLogSource.java index 142094c9d9f4..7501799198e8 100644 --- a/services/core/java/com/android/server/security/intrusiondetection/SecurityLogSource.java +++ b/services/core/java/com/android/server/security/intrusiondetection/SecurityLogSource.java @@ -19,14 +19,15 @@ package com.android.server.security.intrusiondetection; import android.Manifest.permission; import android.annotation.RequiresPermission; import android.app.admin.DevicePolicyManager; +import android.app.admin.DevicePolicyManagerInternal; import android.app.admin.SecurityLog.SecurityEvent; import android.content.Context; import android.security.intrusiondetection.IntrusionDetectionEvent; import android.util.Slog; +import com.android.server.LocalServices; + import java.util.List; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; import java.util.function.Consumer; import java.util.stream.Collectors; @@ -36,13 +37,13 @@ public class SecurityLogSource implements DataSource { private SecurityEventCallback mEventCallback; private DevicePolicyManager mDpm; - private Executor mExecutor; + private DevicePolicyManagerInternal mDpmInternal; private DataAggregator mDataAggregator; public SecurityLogSource(Context context, DataAggregator dataAggregator) { mDataAggregator = dataAggregator; mDpm = context.getSystemService(DevicePolicyManager.class); - mExecutor = Executors.newSingleThreadExecutor(); + mDpmInternal = LocalServices.getService(DevicePolicyManagerInternal.class); mEventCallback = new SecurityEventCallback(); } @@ -50,12 +51,13 @@ public class SecurityLogSource implements DataSource { @RequiresPermission(permission.MANAGE_DEVICE_POLICY_AUDIT_LOGGING) public void enable() { enableAuditLog(); - mDpm.setAuditLogEventCallback(mExecutor, mEventCallback); + mDpmInternal.setInternalEventsCallback(mEventCallback); } @Override @RequiresPermission(permission.MANAGE_DEVICE_POLICY_AUDIT_LOGGING) public void disable() { + mDpmInternal.setInternalEventsCallback(null); disableAuditLog(); } @@ -82,10 +84,11 @@ public class SecurityLogSource implements DataSource { @Override public void accept(List<SecurityEvent> events) { - if (events.size() == 0) { + if (events == null || events.size() == 0) { Slog.w(TAG, "No events received; caller may not be authorized"); return; } + List<IntrusionDetectionEvent> intrusionDetectionEvents = events.stream() .filter(event -> event != null) diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 03fe7775edb0..c37b5a055140 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -8051,6 +8051,7 @@ final class ActivityRecord extends WindowToken { mConfigurationSeq = Math.max(++mConfigurationSeq, 1); getResolvedOverrideConfiguration().seq = mConfigurationSeq; + // TODO(b/392069771): Move to AppCompatSandboxingPolicy. // Sandbox max bounds by setting it to the activity bounds, if activity is letterboxed, or // has or will have mAppCompatDisplayInsets for size compat. Also forces an activity to be // sandboxed or not depending upon the configuration settings. @@ -8079,6 +8080,9 @@ final class ActivityRecord extends WindowToken { resolvedConfig.windowConfiguration.setMaxBounds(mTmpBounds); } + mAppCompatController.getSandboxingPolicy().sandboxBoundsIfNeeded(resolvedConfig, + parentWindowingMode); + applySizeOverrideIfNeeded( mDisplayContent, info.applicationInfo, diff --git a/services/core/java/com/android/server/wm/AppCompatController.java b/services/core/java/com/android/server/wm/AppCompatController.java index bed95face1c9..fc504796b0ac 100644 --- a/services/core/java/com/android/server/wm/AppCompatController.java +++ b/services/core/java/com/android/server/wm/AppCompatController.java @@ -44,6 +44,8 @@ class AppCompatController { private final AppCompatLetterboxPolicy mLetterboxPolicy; @NonNull private final AppCompatSizeCompatModePolicy mSizeCompatModePolicy; + @NonNull + private final AppCompatSandboxingPolicy mSandboxingPolicy; AppCompatController(@NonNull WindowManagerService wmService, @NonNull ActivityRecord activityRecord) { @@ -66,6 +68,7 @@ class AppCompatController { mAppCompatOverrides, mTransparentPolicy, wmService.mAppCompatConfiguration); mSizeCompatModePolicy = new AppCompatSizeCompatModePolicy(activityRecord, mAppCompatOverrides); + mSandboxingPolicy = new AppCompatSandboxingPolicy(activityRecord); } @NonNull @@ -143,6 +146,11 @@ class AppCompatController { return mSizeCompatModePolicy; } + @NonNull + AppCompatSandboxingPolicy getSandboxingPolicy() { + return mSandboxingPolicy; + } + void dump(@NonNull PrintWriter pw, @NonNull String prefix) { getTransparentPolicy().dump(pw, prefix); getLetterboxPolicy().dump(pw, prefix); diff --git a/services/core/java/com/android/server/wm/AppCompatSandboxingPolicy.java b/services/core/java/com/android/server/wm/AppCompatSandboxingPolicy.java new file mode 100644 index 000000000000..26cf32b12d4f --- /dev/null +++ b/services/core/java/com/android/server/wm/AppCompatSandboxingPolicy.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.wm; + +import static com.android.server.wm.AppCompatUtils.isInDesktopMode; + +import android.annotation.NonNull; +import android.app.WindowConfiguration.WindowingMode; +import android.content.res.Configuration; +import android.graphics.Rect; + +import com.android.window.flags.Flags; + +/** + * Encapsulate logic related to sandboxing for app compatibility. + */ +class AppCompatSandboxingPolicy { + + @NonNull + private final ActivityRecord mActivityRecord; + + AppCompatSandboxingPolicy(@NonNull ActivityRecord activityRecord) { + mActivityRecord = activityRecord; + } + + /** + * In freeform, the container bounds are scaled with app bounds. Activity bounds can be + * outside of its container bounds if insets are coupled with configuration outside of + * freeform and maintained in freeform for size compat mode. + * + * <p>Sandbox activity bounds in freeform to app bounds to force app to display within the + * container. This prevents UI cropping when activities can draw below insets which are + * normally excluded from appBounds before targetSDK < 35 + * (see ConfigurationContainer#applySizeOverrideIfNeeded). + */ + void sandboxBoundsIfNeeded(@NonNull Configuration resolvedConfig, + @WindowingMode int windowingMode) { + if (!Flags.excludeCaptionFromAppBounds()) { + return; + } + + if (isInDesktopMode(mActivityRecord.mAtmService.mContext, windowingMode)) { + Rect appBounds = resolvedConfig.windowConfiguration.getAppBounds(); + if (appBounds == null || appBounds.isEmpty()) { + // When there is no override bounds, the activity will inherit the bounds from + // parent. + appBounds = mActivityRecord.mResolveConfigHint.mParentAppBoundsOverride; + } + resolvedConfig.windowConfiguration.setBounds(appBounds); + } + } +} diff --git a/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java b/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java index bbc33004ee54..2cfa242bc5fe 100644 --- a/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java +++ b/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java @@ -17,14 +17,13 @@ package com.android.server.wm; import static android.app.WindowConfiguration.ROTATION_UNDEFINED; -import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.content.pm.ActivityInfo.SIZE_CHANGES_SUPPORTED_METADATA; import static android.content.pm.ActivityInfo.SIZE_CHANGES_SUPPORTED_OVERRIDE; import static android.content.pm.ActivityInfo.SIZE_CHANGES_UNSUPPORTED_METADATA; import static android.content.pm.ActivityInfo.SIZE_CHANGES_UNSUPPORTED_OVERRIDE; import static android.content.res.Configuration.ORIENTATION_UNDEFINED; -import static com.android.server.wm.DesktopModeHelper.canEnterDesktopMode; +import static com.android.server.wm.AppCompatUtils.isInDesktopMode; import android.annotation.NonNull; import android.annotation.Nullable; @@ -545,9 +544,8 @@ class AppCompatSizeCompatModePolicy { // Allow an application to be up-scaled if its window is smaller than its // original container or if it's a freeform window in desktop mode. boolean shouldAllowUpscaling = !(contentW <= viewportW && contentH <= viewportH) - || (canEnterDesktopMode(mActivityRecord.mAtmService.mContext) - && newParentConfig.windowConfiguration.getWindowingMode() - == WINDOWING_MODE_FREEFORM); + || isInDesktopMode(mActivityRecord.mAtmService.mContext, + newParentConfig.windowConfiguration.getWindowingMode()); return shouldAllowUpscaling ? Math.min( (float) viewportW / contentW, (float) viewportH / contentH) : 1f; } diff --git a/services/core/java/com/android/server/wm/AppCompatUtils.java b/services/core/java/com/android/server/wm/AppCompatUtils.java index 3e054fc40540..146044008b3f 100644 --- a/services/core/java/com/android/server/wm/AppCompatUtils.java +++ b/services/core/java/com/android/server/wm/AppCompatUtils.java @@ -16,16 +16,20 @@ package com.android.server.wm; +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.content.res.Configuration.UI_MODE_TYPE_MASK; import static android.content.res.Configuration.UI_MODE_TYPE_VR_HEADSET; import static com.android.server.wm.ActivityRecord.State.RESUMED; +import static com.android.server.wm.DesktopModeHelper.canEnterDesktopMode; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.AppCompatTaskInfo; import android.app.CameraCompatTaskInfo; import android.app.TaskInfo; +import android.app.WindowConfiguration.WindowingMode; +import android.content.Context; import android.content.res.Configuration; import android.graphics.Rect; import android.view.InsetsSource; @@ -276,6 +280,14 @@ final class AppCompatUtils { inOutConfig.windowConfiguration.getAppBounds().offset(offsetX, offsetY); } + /** + * Return {@code true} if window is currently in desktop mode. + */ + static boolean isInDesktopMode(@NonNull Context context, + @WindowingMode int parentWindowingMode) { + return parentWindowingMode == WINDOWING_MODE_FREEFORM && canEnterDesktopMode(context); + } + private static void clearAppCompatTaskInfo(@NonNull AppCompatTaskInfo info) { info.topActivityLetterboxVerticalPosition = TaskInfo.PROPERTY_VALUE_UNSET; info.topActivityLetterboxHorizontalPosition = TaskInfo.PROPERTY_VALUE_UNSET; diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java index e76a83453a9d..094ad187686c 100644 --- a/services/core/java/com/android/server/wm/BackNavigationController.java +++ b/services/core/java/com/android/server/wm/BackNavigationController.java @@ -190,7 +190,9 @@ class BackNavigationController { currentActivity = window.mActivityRecord; currentTask = window.getTask(); if ((currentTask != null && !currentTask.isVisibleRequested()) - || (currentActivity != null && !currentActivity.isVisibleRequested())) { + || (currentActivity != null && !currentActivity.isVisibleRequested()) + || (currentActivity != null && currentTask != null + && currentTask.getTopNonFinishingActivity() != currentActivity)) { // Closing transition is happening on focus window and should be update soon, // don't drive back navigation with it. ProtoLog.d(WM_DEBUG_BACK_PREVIEW, "Focus window is closing."); diff --git a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java index 4eaa11bac016..f473b7b7e4fb 100644 --- a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java +++ b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java @@ -60,10 +60,11 @@ class DeferredDisplayUpdater { */ @VisibleForTesting static final DisplayInfoFieldsUpdater DEFERRABLE_FIELDS = (out, override) -> { - // Treat unique id and address change as WM-specific display change as we re-query display - // settings and parameters based on it which could cause window changes + // Treat unique id, address, and canHostTasks change as WM-specific display change as we + // re-query display settings and parameters based on it which could cause window changes. out.uniqueId = override.uniqueId; out.address = override.address; + out.canHostTasks = override.canHostTasks; // Also apply WM-override fields, since they might produce differences in window hierarchy WM_OVERRIDE_FIELDS.setFields(out, override); @@ -433,7 +434,7 @@ class DeferredDisplayUpdater { second.thermalRefreshRateThrottling) || !Objects.equals(first.thermalBrightnessThrottlingDataId, second.thermalBrightnessThrottlingDataId) - || first.canHostTasks != second.canHostTasks) { + ) { diff |= DIFF_NOT_WM_DEFERRABLE; } @@ -454,6 +455,7 @@ class DeferredDisplayUpdater { || !Objects.equals(first.displayShape, second.displayShape) || !Objects.equals(first.uniqueId, second.uniqueId) || !Objects.equals(first.address, second.address) + || first.canHostTasks != second.canHostTasks ) { diff |= DIFF_WM_DEFERRABLE; } diff --git a/services/core/java/com/android/server/wm/DesktopModeHelper.java b/services/core/java/com/android/server/wm/DesktopModeHelper.java index f35930700653..c2255d8d011a 100644 --- a/services/core/java/com/android/server/wm/DesktopModeHelper.java +++ b/services/core/java/com/android/server/wm/DesktopModeHelper.java @@ -51,13 +51,8 @@ public final class DesktopModeHelper { } /** - * Return {@code true} if the current device can hosts desktop sessions on its internal display. + * Return {@code true} if the current device supports desktop mode. */ - @VisibleForTesting - static boolean canInternalDisplayHostDesktops(@NonNull Context context) { - return context.getResources().getBoolean(R.bool.config_canInternalDisplayHostDesktops); - } - // TODO(b/337819319): use a companion object instead. private static boolean isDesktopModeSupported(@NonNull Context context) { return context.getResources().getBoolean(R.bool.config_isDesktopModeSupported); @@ -68,32 +63,45 @@ public final class DesktopModeHelper { } /** + * Return {@code true} if the current device can hosts desktop sessions on its internal display. + */ + @VisibleForTesting + static boolean canInternalDisplayHostDesktops(@NonNull Context context) { + return context.getResources().getBoolean(R.bool.config_canInternalDisplayHostDesktops); + } + + /** * Check if Desktop mode should be enabled because the dev option is shown and enabled. */ private static boolean isDesktopModeEnabledByDevOption(@NonNull Context context) { return DesktopModeFlags.isDesktopModeForcedEnabled() && (isDesktopModeDevOptionsSupported( - context) || isInternalDisplayEligibleToHostDesktops(context)); + context) || isDeviceEligibleForDesktopMode(context)); } @VisibleForTesting - static boolean isInternalDisplayEligibleToHostDesktops(@NonNull Context context) { - return !shouldEnforceDeviceRestrictions() || canInternalDisplayHostDesktops(context) || ( - Flags.enableDesktopModeThroughDevOption() && isDesktopModeDevOptionsSupported( - context)); + static boolean isDeviceEligibleForDesktopMode(@NonNull Context context) { + if (!shouldEnforceDeviceRestrictions()) { + return true; + } + final boolean desktopModeSupported = isDesktopModeSupported(context) + && canInternalDisplayHostDesktops(context); + final boolean desktopModeSupportedByDevOptions = + Flags.enableDesktopModeThroughDevOption() + && isDesktopModeDevOptionsSupported(context); + return desktopModeSupported || desktopModeSupportedByDevOptions; } /** * Return {@code true} if desktop mode can be entered on the current device. */ static boolean canEnterDesktopMode(@NonNull Context context) { - return (isInternalDisplayEligibleToHostDesktops(context) - && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODE.isTrue() - && (isDesktopModeSupported(context) || !shouldEnforceDeviceRestrictions())) + return (isDeviceEligibleForDesktopMode(context) + && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODE.isTrue()) || isDesktopModeEnabledByDevOption(context); } /** Returns {@code true} if desktop experience wallpaper is supported on this device. */ public static boolean isDeviceEligibleForDesktopExperienceWallpaper(@NonNull Context context) { - return enableConnectedDisplaysWallpaper() && canEnterDesktopMode(context); + return enableConnectedDisplaysWallpaper() && isDeviceEligibleForDesktopMode(context); } } diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index 682f3d8cf1e5..703ce7d24468 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -3239,25 +3239,43 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp Slog.e(TAG, "ShouldShowSystemDecors shouldn't be updated when the flag is off."); } - final boolean shouldShow; - if (isDefaultDisplay) { - shouldShow = true; - } else if (isPrivate()) { - shouldShow = false; - } else { - shouldShow = mDisplay.canHostTasks(); + final boolean shouldShowContent; + if (!allowContentModeSwitch()) { + return; } + shouldShowContent = mDisplay.canHostTasks(); - if (shouldShow == mWmService.mDisplayWindowSettings.shouldShowSystemDecorsLocked(this)) { + if (shouldShowContent == mWmService.mDisplayWindowSettings + .shouldShowSystemDecorsLocked(this)) { return; } - mWmService.mDisplayWindowSettings.setShouldShowSystemDecorsLocked(this, shouldShow); + mWmService.mDisplayWindowSettings.setShouldShowSystemDecorsLocked(this, shouldShowContent); - if (!shouldShow) { + if (!shouldShowContent) { clearAllTasksOnDisplay(null /* clearTasksCallback */, false /* isRemovingDisplay */); } } + private boolean allowContentModeSwitch() { + // The default display should always show system decorations. + if (isDefaultDisplay) { + return false; + } + + // Private display should never show system decorations. + if (isPrivate()) { + return false; + } + + // TODO(b/391965805): Remove this after introducing FLAG_ALLOW_SYSTEM_DECORATIONS_CHANGE. + // Virtual displays cannot add or remove system decorations during their lifecycle. + if (mDisplay.getType() == Display.TYPE_VIRTUAL) { + return false; + } + + return true; + } + DisplayCutout loadDisplayCutout(int displayWidth, int displayHeight) { if (mDisplayPolicy == null || mInitialDisplayCutout == null) { return null; @@ -6578,22 +6596,6 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp .getKeyguardController().isKeyguardLocked(mDisplayId); } - boolean isKeyguardLockedOrAodShowing() { - return isKeyguardLocked() || isAodShowing(); - } - - /** - * @return whether aod is showing for this display - */ - boolean isAodShowing() { - final boolean isAodShowing = mRootWindowContainer.mTaskSupervisor - .getKeyguardController().isAodShowing(mDisplayId); - if (mDisplayId == DEFAULT_DISPLAY && isAodShowing) { - return !isKeyguardGoingAway(); - } - return isAodShowing; - } - /** * @return whether keyguard is going away on this display */ diff --git a/services/core/java/com/android/server/wm/DragState.java b/services/core/java/com/android/server/wm/DragState.java index 69f32cb7b8ea..84281b8fbecf 100644 --- a/services/core/java/com/android/server/wm/DragState.java +++ b/services/core/java/com/android/server/wm/DragState.java @@ -122,7 +122,7 @@ class DragState { float mThumbOffsetX, mThumbOffsetY; InputInterceptor mInputInterceptor; ArrayList<WindowState> mNotifiedWindows; - boolean mDragInProgress; + private boolean mDragInProgress; // Set to non -1 value if a valid app requests DRAG_FLAG_HIDE_CALLING_TASK_ON_DRAG_START int mCallingTaskIdToHide; /** @@ -161,7 +161,7 @@ class DragState { private boolean mIsClosing; // Stores the last drop event which was reported to a valid drop target window, or null - // otherwise. This drop event will contain private info and should only be consumed by the + // otherwise. This drop event will contain private info and should only be consumed by the // unhandled drag listener. DragEvent mUnhandledDropEvent; @@ -243,7 +243,7 @@ class DragState { for (WindowState ws : mNotifiedWindows) { float inWindowX = 0; float inWindowY = 0; - SurfaceControl dragSurface = null; + boolean includeDragSurface = false; if (!mDragResult && (ws.mSession.mPid == mPid)) { // Report unconsumed drop location back to the app that started the drag. inWindowX = ws.translateToWindowX(mCurrentDisplayX); @@ -251,13 +251,10 @@ class DragState { if (relinquishDragSurfaceToDragSource()) { // If requested (and allowed), report the drag surface back to the app // starting the drag to handle the return animation - dragSurface = mSurfaceControl; + includeDragSurface = true; } } - DragEvent event = DragEvent.obtain(DragEvent.ACTION_DRAG_ENDED, inWindowX, - inWindowY, mThumbOffsetX, mThumbOffsetY, - mCurrentDisplayContent.getDisplayId(), mFlags, null, null, null, - dragSurface, null, mDragResult); + DragEvent event = obtainDragEndedEvent(inWindowX, inWindowY, includeDragSurface); try { if (DEBUG_DRAG) Slog.d(TAG_WM, "Sending DRAG_ENDED to " + ws); ws.mClient.dispatchDragEvent(event); @@ -310,10 +307,10 @@ class DragState { /** * Creates the drop event for dispatching to the unhandled drag. - * TODO(b/384841906): Update `inWindowX` and `inWindowY` to be display-coordinate. */ - private DragEvent createUnhandledDropEvent(float inWindowX, float inWindowY) { - return obtainDragEvent(DragEvent.ACTION_DROP, inWindowX, inWindowY, mDataDescription, mData, + private DragEvent createUnhandledDropEvent(float inDisplayX, float inDisplayY) { + return obtainDragEvent(DragEvent.ACTION_DROP, inDisplayX, inDisplayY, mDataDescription, + mData, /* includeDragSurface= */ true, /* includeDragFlags= */ true, null /* dragAndDropPermissions */); } @@ -370,11 +367,8 @@ class DragState { } final WindowState touchedWin = mService.mInputToWindowMap.get(token); - // TODO(b/384841906): The x, y here when sent to a window and unhandled, will still be - // relative to the window it was originally sent to. Need to update this to actually be - // display-coordinate. - final DragEvent unhandledDropEvent = createUnhandledDropEvent(inWindowX, inWindowY); if (!isWindowNotified(touchedWin)) { + final DragEvent unhandledDropEvent = createUnhandledDropEvent(inWindowX, inWindowY); // Delegate to the unhandled drag listener as a first pass if (mDragDropController.notifyUnhandledDrop(unhandledDropEvent, "unhandled-drop")) { // The unhandled drag listener will call back to notify whether it has consumed @@ -392,6 +386,8 @@ class DragState { } if (DEBUG_DRAG) Slog.d(TAG_WM, "Sending DROP to " + touchedWin); + final DragEvent unhandledDropEvent = createUnhandledDropEvent( + touchedWin.getBounds().left + inWindowX, touchedWin.getBounds().top + inWindowY); final IBinder clientToken = touchedWin.mClient.asBinder(); final DragEvent event = createDropEvent(inWindowX, inWindowY, touchedWin); @@ -776,28 +772,37 @@ class DragState { displayId, (int) (displayX - mThumbOffsetX), (int) (displayY - mThumbOffsetY)); } - /** - * Returns true if it has sent DRAG_STARTED broadcast out but has not been sent DRAG_END - * broadcast. - */ - boolean isInProgress() { - return mDragInProgress; + private DragEvent obtainDragEndedEvent(float x, float y, boolean includeDragSurface) { + return obtainDragEvent(DragEvent.ACTION_DRAG_ENDED, x, y, /* description= */ + null, /* data= */ null, includeDragSurface, /* includeDragFlags= */ + true, /* dragAndDropPermissions= */ null, mDragResult); + } + + private DragEvent obtainDragEvent(int action, float x, float y, ClipDescription description, + ClipData data, boolean includeDragSurface, boolean includeDragFlags, + IDragAndDropPermissions dragAndDropPermissions) { + return obtainDragEvent(action, x, y, description, data, includeDragSurface, + includeDragFlags, dragAndDropPermissions, /* dragResult= */ false); } /** * `x` and `y` here varies between local window coordinate, relative coordinate to another * window and local display coordinate, all depending on the `action`. Please take a look * at the callers to determine the type. - * TODO(b/384845022): Properly document the events sent based on the event type. + * - ACTION_DRAG_STARTED: (x, y) is relative coordinate to the target window's origin + * (possible to have negative values). + * - ACTION_DROP: + * --- UnhandledDropEvent: (x, y) is in display space coordinate. + * --- DropEvent: (x, y) is in local window coordinate where event is targeted to. + * - ACTION_DRAG_ENDED: (x, y) is in local window coordinate where event is targeted to. */ private DragEvent obtainDragEvent(int action, float x, float y, ClipDescription description, ClipData data, boolean includeDragSurface, boolean includeDragFlags, - IDragAndDropPermissions dragAndDropPermissions) { + IDragAndDropPermissions dragAndDropPermissions, boolean dragResult) { return DragEvent.obtain(action, x, y, mThumbOffsetX, mThumbOffsetY, mCurrentDisplayContent.getDisplayId(), includeDragFlags ? mFlags : 0, null /* localState */, description, data, - includeDragSurface ? mSurfaceControl : null, dragAndDropPermissions, - false /* result */); + includeDragSurface ? mSurfaceControl : null, dragAndDropPermissions, dragResult); } private ValueAnimator createReturnAnimationLocked() { diff --git a/services/core/java/com/android/server/wm/KeyguardController.java b/services/core/java/com/android/server/wm/KeyguardController.java index dd2f49e171a8..6091b8334438 100644 --- a/services/core/java/com/android/server/wm/KeyguardController.java +++ b/services/core/java/com/android/server/wm/KeyguardController.java @@ -18,7 +18,6 @@ package com.android.server.wm; import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; import static android.view.Display.DEFAULT_DISPLAY; -import static android.view.WindowManager.TRANSIT_FLAG_AOD_APPEARING; import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_APPEARING; import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY; import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_GOING_AWAY_NO_ANIMATION; @@ -217,9 +216,6 @@ class KeyguardController { } else if (keyguardShowing && !state.mKeyguardShowing) { transition.addFlag(TRANSIT_FLAG_KEYGUARD_APPEARING); } - if (mWindowManager.mFlags.mAodTransition && aodShowing && !state.mAodShowing) { - transition.addFlag(TRANSIT_FLAG_AOD_APPEARING); - } } } // Update the task snapshot if the screen will not be turned off. To make sure that the @@ -242,27 +238,19 @@ class KeyguardController { state.mAodShowing = aodShowing; state.writeEventLog("setKeyguardShown"); - if (keyguardChanged || aodChanged) { - if (keyguardChanged) { - // Irrelevant to AOD. - state.mKeyguardGoingAway = false; - if (keyguardShowing) { - state.mDismissalRequested = false; - } + if (keyguardChanged) { + // Irrelevant to AOD. + state.mKeyguardGoingAway = false; + if (keyguardShowing) { + state.mDismissalRequested = false; } if (goingAwayRemoved - || (keyguardShowing && !Display.isOffState(dc.getDisplayInfo().state)) - || (mWindowManager.mFlags.mAodTransition && aodShowing)) { + || (keyguardShowing && !Display.isOffState(dc.getDisplayInfo().state))) { // Keyguard decided to show or stopped going away. Send a transition to animate back // to the locked state before holding the sleep token again if (!ENABLE_NEW_KEYGUARD_SHELL_TRANSITIONS) { dc.requestTransitionAndLegacyPrepare( TRANSIT_TO_FRONT, TRANSIT_FLAG_KEYGUARD_APPEARING); - if (mWindowManager.mFlags.mAodTransition && aodShowing - && dc.mTransitionController.isCollecting()) { - dc.mTransitionController.getCollectingTransition().addFlag( - TRANSIT_FLAG_AOD_APPEARING); - } } dc.mWallpaperController.adjustWallpaperWindows(); dc.executeAppTransition(); diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index fe653e454d6c..5217a759c6ae 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -36,7 +36,6 @@ import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING; import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_CLOSE; -import static android.view.WindowManager.TRANSIT_FLAG_AOD_APPEARING; import static android.view.WindowManager.TRANSIT_FLAG_IS_RECENTS; import static android.view.WindowManager.TRANSIT_FLAG_KEYGUARD_LOCKED; import static android.view.WindowManager.TRANSIT_OPEN; @@ -974,10 +973,6 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { return false; } - boolean isInAodAppearTransition() { - return (mFlags & TRANSIT_FLAG_AOD_APPEARING) != 0; - } - /** * Specifies configuration change explicitly for the window container, so it can be chosen as * transition target. This is usually used with transition mode diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java index 25b513d85384..ba7f36419ac5 100644 --- a/services/core/java/com/android/server/wm/TransitionController.java +++ b/services/core/java/com/android/server/wm/TransitionController.java @@ -525,19 +525,6 @@ class TransitionController { return false; } - boolean isInAodAppearTransition() { - if (mCollectingTransition != null && mCollectingTransition.isInAodAppearTransition()) { - return true; - } - for (int i = mWaitingTransitions.size() - 1; i >= 0; --i) { - if (mWaitingTransitions.get(i).isInAodAppearTransition()) return true; - } - for (int i = mPlayingTransitions.size() - 1; i >= 0; --i) { - if (mPlayingTransitions.get(i).isInAodAppearTransition()) return true; - } - return false; - } - /** * @return A pair of the transition and restore-behind target for the given {@param container}. * @param container An ancestor of a transient-launch activity diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java index 70948e1264c4..c1ef208d1d4d 100644 --- a/services/core/java/com/android/server/wm/WallpaperController.java +++ b/services/core/java/com/android/server/wm/WallpaperController.java @@ -166,14 +166,6 @@ class WallpaperController { mFindResults.setWallpaperTarget(w); return false; } - } else if (mService.mFlags.mAodTransition - && mDisplayContent.isKeyguardLockedOrAodShowing()) { - if (mService.mPolicy.isKeyguardHostWindow(w.mAttrs) - && w.mTransitionController.isInAodAppearTransition()) { - if (DEBUG_WALLPAPER) Slog.v(TAG, "Found aod transition wallpaper target: " + w); - mFindResults.setWallpaperTarget(w); - return true; - } } final boolean animationWallpaper = animatingContainer != null @@ -692,8 +684,7 @@ class WallpaperController { private WallpaperWindowToken getTokenForTarget(WindowState target) { if (target == null) return null; WindowState window = mFindResults.getTopWallpaper( - (target.canShowWhenLocked() && mService.isKeyguardLocked()) - || (mService.mFlags.mAodTransition && mDisplayContent.isAodShowing())); + target.canShowWhenLocked() && mService.isKeyguardLocked()); return window == null ? null : window.mToken.asWallpaperToken(); } @@ -736,9 +727,7 @@ class WallpaperController { if (mFindResults.wallpaperTarget == null && mFindResults.useTopWallpaperAsTarget) { mFindResults.setWallpaperTarget( - mFindResults.getTopWallpaper(mService.mFlags.mAodTransition - ? mDisplayContent.isKeyguardLockedOrAodShowing() - : mDisplayContent.isKeyguardLocked())); + mFindResults.getTopWallpaper(mDisplayContent.isKeyguardLocked())); } } @@ -910,17 +899,11 @@ class WallpaperController { if (mDisplayContent.mWmService.mFlags.mEnsureWallpaperInTransitions) { visibleRequested = mWallpaperTarget != null && mWallpaperTarget.isVisibleRequested(); } - updateWallpaperTokens(visibleRequested, - mService.mFlags.mAodTransition - ? mDisplayContent.isKeyguardLockedOrAodShowing() - : mDisplayContent.isKeyguardLocked()); + updateWallpaperTokens(visibleRequested, mDisplayContent.isKeyguardLocked()); ProtoLog.v(WM_DEBUG_WALLPAPER, "Wallpaper at display %d - visibility: %b, keyguardLocked: %b", - mDisplayContent.getDisplayId(), visible, - mService.mFlags.mAodTransition - ? mDisplayContent.isKeyguardLockedOrAodShowing() - : mDisplayContent.isKeyguardLocked()); + mDisplayContent.getDisplayId(), visible, mDisplayContent.isKeyguardLocked()); if (visible && mLastFrozen != mFindResults.isWallpaperTargetForLetterbox) { mLastFrozen = mFindResults.isWallpaperTargetForLetterbox; diff --git a/services/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/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/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 415e3accfa39..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 @@ -16323,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; @@ -16339,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; @@ -16357,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; @@ -16375,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; @@ -16392,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; @@ -16408,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; @@ -16430,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; @@ -16447,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/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/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java index 95bca2b17efb..1dc32b00acba 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java @@ -4486,6 +4486,49 @@ public class SizeCompatTests extends WindowTestsBase { } @Test + @EnableFlags(Flags.FLAG_EXCLUDE_CAPTION_FROM_APP_BOUNDS) + @DisableCompatChanges({ActivityInfo.INSETS_DECOUPLED_CONFIGURATION_ENFORCED}) + public void testInFreeform_boundsSandboxedToAppBounds() { + allowDesktopMode(); + final int dw = 2800; + final int dh = 1400; + final int notchHeight = 100; + final DisplayContent display = new TestDisplayContent.Builder(mAtm, dw, dh) + .setNotch(notchHeight) + .build(); + setUpApp(display); + prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT); + + mTask.mDisplayContent.getDefaultTaskDisplayArea() + .setWindowingMode(WindowConfiguration.WINDOWING_MODE_FREEFORM); + mTask.setWindowingMode(WINDOWING_MODE_FREEFORM); + Rect appBounds = new Rect(0, 0, 1000, 500); + Rect bounds = new Rect(0, 0, 1000, 600); + mTask.getWindowConfiguration().setAppBounds(appBounds); + mTask.getWindowConfiguration().setBounds(bounds); + mActivity.onConfigurationChanged(mTask.getConfiguration()); + + // Bounds are sandboxed to appBounds in freeform. + assertDownScaled(); + assertEquals(mActivity.getWindowConfiguration().getAppBounds(), + mActivity.getWindowConfiguration().getBounds()); + + // Exit freeform. + mTask.mDisplayContent.getDefaultTaskDisplayArea() + .setWindowingMode(WindowConfiguration.WINDOWING_MODE_FULLSCREEN); + mTask.setWindowingMode(WINDOWING_MODE_FULLSCREEN); + mTask.getWindowConfiguration().setBounds(new Rect(0, 0, dw, dh)); + mActivity.onConfigurationChanged(mTask.getConfiguration()); + assertFitted(); + appBounds = mActivity.getWindowConfiguration().getAppBounds(); + bounds = mActivity.getWindowConfiguration().getBounds(); + // Bounds are not sandboxed to appBounds. + assertNotEquals(appBounds, bounds); + assertEquals(notchHeight, appBounds.top - bounds.top); + } + + + @Test @EnableFlags(Flags.FLAG_IGNORE_ASPECT_RATIO_RESTRICTIONS_FOR_RESIZEABLE_FREEFORM_ACTIVITIES) public void testUserAspectRatioOverridesNotAppliedToResizeableFreeformActivity() { final TaskBuilder taskBuilder = diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTracingPerfettoTest.java b/services/tests/wmtests/src/com/android/server/wm/WindowTracingPerfettoTest.java index 12b744546f5e..9367941e32a3 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowTracingPerfettoTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowTracingPerfettoTest.java @@ -18,12 +18,16 @@ package com.android.server.wm; import static android.tools.traces.Utils.busyWaitForDataSourceRegistration; +import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; + import static com.android.dx.mockito.inline.extended.ExtendedMockito.times; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verifyZeroInteractions; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.junit.Assume.assumeTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -34,11 +38,15 @@ import android.platform.test.annotations.Presubmit; import android.tools.ScenarioBuilder; import android.tools.traces.io.ResultWriter; import android.tools.traces.monitors.PerfettoTraceMonitor; +import android.util.Log; import android.view.Choreographer; +import androidx.test.filters.FlakyTest; import androidx.test.filters.SmallTest; +import androidx.test.uiautomator.UiDevice; import org.junit.After; +import org.junit.AfterClass; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; @@ -51,14 +59,15 @@ import java.io.IOException; /** * Test class for {@link WindowTracingPerfetto}. */ +@FlakyTest(bugId = 372558379) @SmallTest @Presubmit public class WindowTracingPerfettoTest { private static final String TEST_DATA_SOURCE_NAME = "android.windowmanager.test"; private static WindowManagerService sWmMock; - private static Choreographer sChoreographer; private static WindowTracing sWindowTracing; + private static Boolean sIsDataSourceRegisteredSuccessfully; private PerfettoTraceMonitor mTraceMonitor; @@ -66,19 +75,39 @@ public class WindowTracingPerfettoTest { public static void setUpOnce() throws Exception { sWmMock = Mockito.mock(WindowManagerService.class); Mockito.doNothing().when(sWmMock).dumpDebugLocked(Mockito.any(), Mockito.anyInt()); - sChoreographer = Mockito.mock(Choreographer.class); - sWindowTracing = new WindowTracingPerfetto(sWmMock, sChoreographer, + sWindowTracing = new WindowTracingPerfetto(sWmMock, Mockito.mock(Choreographer.class), new WindowManagerGlobalLock(), TEST_DATA_SOURCE_NAME); - busyWaitForDataSourceRegistration(TEST_DATA_SOURCE_NAME); + } + + @AfterClass + public static void tearDownOnce() { + sWmMock = null; + sWindowTracing = null; } @Before public void setUp() throws IOException { - Mockito.clearInvocations(sWmMock); + if (sIsDataSourceRegisteredSuccessfully != null) { + assumeTrue("Failed to register data source", sIsDataSourceRegisteredSuccessfully); + return; + } + try { + busyWaitForDataSourceRegistration(TEST_DATA_SOURCE_NAME); + sIsDataSourceRegisteredSuccessfully = true; + } catch (Exception e) { + sIsDataSourceRegisteredSuccessfully = false; + final String perfettoStatus = UiDevice.getInstance(getInstrumentation()) + .executeShellCommand("perfetto --query"); + Log.e(WindowTracingPerfettoTest.class.getSimpleName(), + "Failed to register data source: " + perfettoStatus); + // Only fail once. The rest tests will be skipped by assumeTrue. + fail("Failed to register data source"); + } } @After public void tearDown() throws IOException { + Mockito.clearInvocations(sWmMock); stopTracing(); } diff --git a/tests/AttestationVerificationTest/src/com/android/server/security/CertificateRevocationStatusManagerTest.java b/tests/AttestationVerificationTest/src/com/android/server/security/CertificateRevocationStatusManagerTest.java index c38517ace5e6..586bb76388f6 100644 --- a/tests/AttestationVerificationTest/src/com/android/server/security/CertificateRevocationStatusManagerTest.java +++ b/tests/AttestationVerificationTest/src/com/android/server/security/CertificateRevocationStatusManagerTest.java @@ -277,6 +277,72 @@ public class CertificateRevocationStatusManagerTest { } } + @Test + public void checkRevocationStatus_allCertificatesRecentlyChecked_doesNotFetchRemoteCrl() + throws Exception { + copyFromAssetToFile( + REVOCATION_LIST_WITHOUT_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile); + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mRevocationListUrl, mRevocationStatusFile, false); + mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1); + // indirectly verifies the remote list is not fetched by simulating a remote revocation + copyFromAssetToFile( + REVOCATION_LIST_WITH_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile); + + // no exception + mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1); + } + + @Test + public void checkRevocationStatus_allCertificatesBarelyRecentlyChecked_doesNotFetchRemoteCrl() + throws Exception { + copyFromAssetToFile( + REVOCATION_LIST_WITH_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile); + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mRevocationListUrl, mRevocationStatusFile, false); + Map<String, LocalDateTime> lastCheckedDates = new HashMap<>(); + LocalDateTime barelyRecently = + LocalDateTime.now() + .minusHours( + CertificateRevocationStatusManager.NUM_HOURS_BEFORE_NEXT_CHECK - 1); + for (X509Certificate certificate : mCertificates1) { + lastCheckedDates.put(getSerialNumber(certificate), barelyRecently); + } + mCertificateRevocationStatusManager.storeLastRevocationCheckData(lastCheckedDates); + + // Indirectly verify the remote CRL is not checked by checking there is no exception despite + // a certificate being revoked. This test differs from the next only in the lastCheckedDate, + // one before the NUM_HOURS_BEFORE_NEXT_CHECK cutoff and one after + mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1); + } + + @Test + public void checkRevocationStatus_certificatesRevokedAfterCheck_throwsException() + throws Exception { + copyFromAssetToFile( + REVOCATION_LIST_WITH_CERTIFICATES_USED_IN_THIS_TEST, mRevocationListFile); + mCertificateRevocationStatusManager = + new CertificateRevocationStatusManager( + mContext, mRevocationListUrl, mRevocationStatusFile, false); + Map<String, LocalDateTime> lastCheckedDates = new HashMap<>(); + // To save network use, we do not check the remote CRL if all the certificates are recently + // checked, so we set the lastCheckDate to some time not recent. + LocalDateTime notRecently = + LocalDateTime.now() + .minusHours( + CertificateRevocationStatusManager.NUM_HOURS_BEFORE_NEXT_CHECK + 1); + for (X509Certificate certificate : mCertificates1) { + lastCheckedDates.put(getSerialNumber(certificate), notRecently); + } + mCertificateRevocationStatusManager.storeLastRevocationCheckData(lastCheckedDates); + + assertThrows( + CertPathValidatorException.class, + () -> mCertificateRevocationStatusManager.checkRevocationStatus(mCertificates1)); + } + private List<X509Certificate> getCertificateChain(String fileName) throws Exception { Collection<? extends Certificate> certificates = mFactory.generateCertificates(mContext.getResources().getAssets().open(fileName)); diff --git a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt index 6caf3f973618..a2f6f0051116 100644 --- a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt +++ b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt @@ -333,7 +333,7 @@ class InputManagerServiceTests { fun testKeyActivenessNotifyEventsLifecycle() { service.systemRunning() - fakePermissionEnforcer.grant(android.Manifest.permission.LISTEN_FOR_KEY_ACTIVITY); + fakePermissionEnforcer.grant(android.Manifest.permission.LISTEN_FOR_KEY_ACTIVITY) val inputManager = context.getSystemService(InputManager::class.java) |