diff options
633 files changed, 19163 insertions, 3769 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index f5bf437a738d..c136dd7701cd 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -65,6 +65,7 @@ aconfig_srcjars = [ ":com.android.internal.foldables.flags-aconfig-java{.generated_srcjars}", ":com.android.media.flags.bettertogether-aconfig-java{.generated_srcjars}", ":com.android.net.flags-aconfig-java{.generated_srcjars}", + ":com.android.net.thread.flags-aconfig-java{.generated_srcjars}", ":com.android.server.flags.services-aconfig-java{.generated_srcjars}", ":com.android.text.flags-aconfig-java{.generated_srcjars}", ":com.android.window.flags.window-aconfig-java{.generated_srcjars}", @@ -133,6 +134,7 @@ stubs_defaults { "com.android.input.flags-aconfig", "com.android.media.flags.bettertogether-aconfig", "com.android.net.flags-aconfig", + "com.android.net.thread.flags-aconfig", "com.android.server.flags.services-aconfig", "com.android.text.flags-aconfig", "com.android.window.flags.window-aconfig", @@ -760,12 +762,25 @@ aconfig_declarations { srcs: ["core/java/android/net/flags.aconfig"], } +// Thread network +aconfig_declarations { + name: "com.android.net.thread.flags-aconfig", + package: "com.android.net.thread.flags", + srcs: ["core/java/android/net/thread/flags.aconfig"], +} + java_aconfig_library { name: "com.android.net.flags-aconfig-java", aconfig_declarations: "com.android.net.flags-aconfig", defaults: ["framework-minus-apex-aconfig-java-defaults"], } +java_aconfig_library { + name: "com.android.net.thread.flags-aconfig-java", + aconfig_declarations: "com.android.net.thread.flags-aconfig", + defaults: ["framework-minus-apex-aconfig-java-defaults"], +} + // Media aconfig_declarations { name: "android.media.playback.flags-aconfig", diff --git a/apct-tests/perftests/core/src/android/graphics/perftests/CanvasPerfTest.java b/apct-tests/perftests/core/src/android/graphics/perftests/CanvasPerfTest.java index 3c361d772d3d..95730e836056 100644 --- a/apct-tests/perftests/core/src/android/graphics/perftests/CanvasPerfTest.java +++ b/apct-tests/perftests/core/src/android/graphics/perftests/CanvasPerfTest.java @@ -122,6 +122,8 @@ public class CanvasPerfTest { Bitmap.createScaledBitmap(source, source.getWidth() / 2, source.getHeight() / 2, true) .recycle(); } + source.recycle(); + Runtime.getRuntime().gc(); } @Test @@ -141,6 +143,8 @@ public class CanvasPerfTest { Bitmap.createScaledBitmap(source, source.getWidth() / 2, source.getHeight() / 2, true) .recycle(); } + source.recycle(); + Runtime.getRuntime().gc(); } @Test @@ -158,5 +162,7 @@ public class CanvasPerfTest { Bitmap.createScaledBitmap(source, source.getWidth() / 2, source.getHeight() / 2, true) .recycle(); } + source.recycle(); + Runtime.getRuntime().gc(); } } diff --git a/apex/jobscheduler/service/aconfig/job.aconfig b/apex/jobscheduler/service/aconfig/job.aconfig index de6f0235cd83..5d65d9d0629f 100644 --- a/apex/jobscheduler/service/aconfig/job.aconfig +++ b/apex/jobscheduler/service/aconfig/job.aconfig @@ -1,6 +1,13 @@ package: "com.android.server.job" flag { + name: "do_not_force_rush_execution_at_boot" + namespace: "backstage_power" + description: "Don't force rush job execution right after boot completion" + bug: "321598070" +} + +flag { name: "relax_prefetch_connectivity_constraint_only_on_charger" namespace: "backstage_power" description: "Only relax a prefetch job's connectivity constraint when the device is charging and battery is not low" diff --git a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java index 31214cbb7066..696c3178a4f4 100644 --- a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java +++ b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java @@ -3919,6 +3919,7 @@ public class DeviceIdleController extends SystemService if (locationManager != null && locationManager.getProvider(LocationManager.FUSED_PROVIDER) != null) { + mHasFusedLocation = true; locationManager.requestLocationUpdates(LocationManager.FUSED_PROVIDER, mLocationRequest, AppSchedulingModuleThread.getExecutor(), diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java index b0f378d1752d..7a92cca74795 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java @@ -2720,8 +2720,10 @@ public class JobSchedulerService extends com.android.server.SystemService sc.maybeStartTrackingJobLocked(job, null); } }); - // GO GO GO! - mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget(); + if (!Flags.doNotForceRushExecutionAtBoot()) { + // GO GO GO! + mHandler.obtainMessage(MSG_CHECK_JOB).sendToTarget(); + } } } } @@ -5441,6 +5443,8 @@ public class JobSchedulerService extends com.android.server.SystemService pw.println("Aconfig flags:"); pw.increaseIndent(); + pw.print(Flags.FLAG_DO_NOT_FORCE_RUSH_EXECUTION_AT_BOOT, + Flags.doNotForceRushExecutionAtBoot()); pw.print(Flags.FLAG_THROW_ON_UNSUPPORTED_BIAS_USAGE, Flags.throwOnUnsupportedBiasUsage()); pw.println(); diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java index c14efae3fa62..6f2393adfc7b 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java @@ -344,11 +344,14 @@ public final class JobSchedulerShellCommand extends BasicShellCommandHandler { final String flagName = getNextArgRequired(); switch (flagName) { + case android.app.job.Flags.FLAG_ENFORCE_MINIMUM_TIME_WINDOWS: + pw.println(android.app.job.Flags.enforceMinimumTimeWindows()); + break; case android.app.job.Flags.FLAG_JOB_DEBUG_INFO_APIS: pw.println(android.app.job.Flags.jobDebugInfoApis()); break; - case android.app.job.Flags.FLAG_ENFORCE_MINIMUM_TIME_WINDOWS: - pw.println(android.app.job.Flags.enforceMinimumTimeWindows()); + case com.android.server.job.Flags.FLAG_DO_NOT_FORCE_RUSH_EXECUTION_AT_BOOT: + pw.println(com.android.server.job.Flags.doNotForceRushExecutionAtBoot()); break; case com.android.server.job.Flags.FLAG_THROW_ON_UNSUPPORTED_BIAS_USAGE: pw.println(com.android.server.job.Flags.throwOnUnsupportedBiasUsage()); diff --git a/core/api/current.txt b/core/api/current.txt index 109170615c94..987b5c61219b 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -274,6 +274,7 @@ package android { field public static final String REQUEST_IGNORE_BATTERY_OPTIMIZATIONS = "android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"; field public static final String REQUEST_INSTALL_PACKAGES = "android.permission.REQUEST_INSTALL_PACKAGES"; field public static final String REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE = "android.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE"; + field @FlaggedApi("android.companion.flags.device_presence") public static final String REQUEST_OBSERVE_DEVICE_UUID_PRESENCE = "android.permission.REQUEST_OBSERVE_DEVICE_UUID_PRESENCE"; 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"; @@ -1603,6 +1604,7 @@ package android { field public static final int switchTextOff = 16843628; // 0x101036c field public static final int switchTextOn = 16843627; // 0x101036b field public static final int syncable = 16842777; // 0x1010019 + field @FlaggedApi("android.multiuser.enable_system_user_only_for_services_and_providers") public static final int systemUserOnly; field public static final int tabStripEnabled = 16843453; // 0x10102bd field public static final int tabStripLeft = 16843451; // 0x10102bb field public static final int tabStripRight = 16843452; // 0x10102bc @@ -7699,7 +7701,7 @@ package android.app { method @Nullable public android.app.WallpaperColors getWallpaperColors(int); method @Nullable @RequiresPermission(anyOf={android.Manifest.permission.MANAGE_EXTERNAL_STORAGE, "android.permission.READ_WALLPAPER_INTERNAL"}) public android.os.ParcelFileDescriptor getWallpaperFile(int); method public int getWallpaperId(int); - method public android.app.WallpaperInfo getWallpaperInfo(); + method @RequiresPermission(value="QUERY_ALL_PACKAGES", conditional=true) public android.app.WallpaperInfo getWallpaperInfo(); method @Nullable public android.app.WallpaperInfo getWallpaperInfo(int); method public boolean hasResourceWallpaper(@RawRes int); method public boolean isSetWallpaperAllowed(); @@ -9696,8 +9698,10 @@ package android.companion { method public void requestNotificationAccess(android.content.ComponentName); method @FlaggedApi("android.companion.association_tag") public void setAssociationTag(int, @NonNull String); method @RequiresPermission(android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE) public void startObservingDevicePresence(@NonNull String) throws android.companion.DeviceNotAssociatedException; + method @FlaggedApi("android.companion.device_presence") @RequiresPermission(android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE) public void startObservingDevicePresence(@NonNull android.companion.ObservingDevicePresenceRequest); method public void startSystemDataTransfer(int, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.companion.CompanionException>) throws android.companion.DeviceNotAssociatedException; method @RequiresPermission(android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE) public void stopObservingDevicePresence(@NonNull String) throws android.companion.DeviceNotAssociatedException; + method @FlaggedApi("android.companion.device_presence") @RequiresPermission(android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE) public void stopObservingDevicePresence(@NonNull android.companion.ObservingDevicePresenceRequest); field public static final String EXTRA_ASSOCIATION = "android.companion.extra.ASSOCIATION"; field @Deprecated public static final String EXTRA_DEVICE = "android.companion.extra.DEVICE"; field public static final int FLAG_CALL_METADATA = 1; // 0x1 @@ -9725,13 +9729,7 @@ package android.companion { method @MainThread public void onDeviceAppeared(@NonNull android.companion.AssociationInfo); method @Deprecated @MainThread public void onDeviceDisappeared(@NonNull String); method @MainThread public void onDeviceDisappeared(@NonNull android.companion.AssociationInfo); - method @FlaggedApi("android.companion.device_presence") @MainThread public void onDeviceEvent(@NonNull android.companion.AssociationInfo, int); - field @FlaggedApi("android.companion.device_presence") public static final int DEVICE_EVENT_BLE_APPEARED = 0; // 0x0 - field @FlaggedApi("android.companion.device_presence") public static final int DEVICE_EVENT_BLE_DISAPPEARED = 1; // 0x1 - field @FlaggedApi("android.companion.device_presence") public static final int DEVICE_EVENT_BT_CONNECTED = 2; // 0x2 - field @FlaggedApi("android.companion.device_presence") public static final int DEVICE_EVENT_BT_DISCONNECTED = 3; // 0x3 - field @FlaggedApi("android.companion.device_presence") public static final int DEVICE_EVENT_SELF_MANAGED_APPEARED = 4; // 0x4 - field @FlaggedApi("android.companion.device_presence") public static final int DEVICE_EVENT_SELF_MANAGED_DISAPPEARED = 5; // 0x5 + method @FlaggedApi("android.companion.device_presence") @MainThread public void onDevicePresenceEvent(@NonNull android.companion.DevicePresenceEvent); field public static final String SERVICE_INTERFACE = "android.companion.CompanionDeviceService"; } @@ -9744,6 +9742,38 @@ package android.companion { public class DeviceNotAssociatedException extends java.lang.RuntimeException { } + @FlaggedApi("android.companion.device_presence") public final class DevicePresenceEvent implements android.os.Parcelable { + ctor public DevicePresenceEvent(int, int, @Nullable android.os.ParcelUuid); + method public int describeContents(); + method public int getAssociationId(); + method public int getEvent(); + method @Nullable public android.os.ParcelUuid getUuid(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.companion.DevicePresenceEvent> CREATOR; + field public static final int EVENT_BLE_APPEARED = 0; // 0x0 + field public static final int EVENT_BLE_DISAPPEARED = 1; // 0x1 + field public static final int EVENT_BT_CONNECTED = 2; // 0x2 + field public static final int EVENT_BT_DISCONNECTED = 3; // 0x3 + field public static final int EVENT_SELF_MANAGED_APPEARED = 4; // 0x4 + field public static final int EVENT_SELF_MANAGED_DISAPPEARED = 5; // 0x5 + field public static final int NO_ASSOCIATION = -1; // 0xffffffff + } + + @FlaggedApi("android.companion.device_presence") public final class ObservingDevicePresenceRequest implements android.os.Parcelable { + method public int describeContents(); + method public int getAssociationId(); + method @Nullable public android.os.ParcelUuid getUuid(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.companion.ObservingDevicePresenceRequest> CREATOR; + } + + public static final class ObservingDevicePresenceRequest.Builder { + ctor public ObservingDevicePresenceRequest.Builder(); + method @NonNull public android.companion.ObservingDevicePresenceRequest build(); + method @NonNull public android.companion.ObservingDevicePresenceRequest.Builder setAssociationId(int); + method @NonNull @RequiresPermission(android.Manifest.permission.REQUEST_OBSERVE_DEVICE_UUID_PRESENCE) public android.companion.ObservingDevicePresenceRequest.Builder setUuid(@NonNull android.os.ParcelUuid); + } + public final class WifiDeviceFilter implements android.companion.DeviceFilter<android.net.wifi.ScanResult> { method public int describeContents(); method public void writeToParcel(@NonNull android.os.Parcel, int); @@ -12319,7 +12349,6 @@ package android.content.pm { method public void registerCallback(android.content.pm.LauncherApps.Callback, android.os.Handler); method public void registerPackageInstallerSessionCallback(@NonNull java.util.concurrent.Executor, @NonNull android.content.pm.PackageInstaller.SessionCallback); method public android.content.pm.LauncherActivityInfo resolveActivity(android.content.Intent, android.os.UserHandle); - method @FlaggedApi("android.content.pm.archiving") public void setArchiveCompatibilityOptions(boolean, boolean); method public boolean shouldHideFromSuggestions(@NonNull String, @NonNull android.os.UserHandle); method public void startAppDetailsActivity(android.content.ComponentName, android.os.UserHandle, android.graphics.Rect, android.os.Bundle); method public void startMainActivity(android.content.ComponentName, android.os.UserHandle, android.graphics.Rect, android.os.Bundle); @@ -12448,6 +12477,7 @@ package android.content.pm { method @NonNull public android.content.pm.PackageInstaller.Session openSession(int) throws java.io.IOException; method public void registerSessionCallback(@NonNull android.content.pm.PackageInstaller.SessionCallback); method public void registerSessionCallback(@NonNull android.content.pm.PackageInstaller.SessionCallback, @NonNull android.os.Handler); + method @FlaggedApi("android.content.pm.archiving") @RequiresPermission(anyOf={android.Manifest.permission.INSTALL_PACKAGES, android.Manifest.permission.REQUEST_INSTALL_PACKAGES}) public void reportUnarchivalState(@NonNull android.content.pm.PackageInstaller.UnarchivalState) throws android.content.pm.PackageManager.NameNotFoundException; method @FlaggedApi("android.content.pm.archiving") @RequiresPermission(anyOf={android.Manifest.permission.INSTALL_PACKAGES, android.Manifest.permission.REQUEST_INSTALL_PACKAGES}) public void reportUnarchivalStatus(int, int, long, @Nullable android.app.PendingIntent) throws android.content.pm.PackageManager.NameNotFoundException; method @FlaggedApi("android.content.pm.archiving") @RequiresPermission(anyOf={android.Manifest.permission.DELETE_PACKAGES, android.Manifest.permission.REQUEST_DELETE_PACKAGES}) public void requestArchive(@NonNull String, @NonNull android.content.IntentSender) throws android.content.pm.PackageManager.NameNotFoundException; method @FlaggedApi("android.content.pm.archiving") @RequiresPermission(anyOf={android.Manifest.permission.INSTALL_PACKAGES, android.Manifest.permission.REQUEST_INSTALL_PACKAGES}) public void requestUnarchive(@NonNull String, @NonNull android.content.IntentSender) throws java.io.IOException, android.content.pm.PackageManager.NameNotFoundException; @@ -12677,6 +12707,14 @@ package android.content.pm { field public static final int USER_ACTION_UNSPECIFIED = 0; // 0x0 } + @FlaggedApi("android.content.pm.archiving") public static final class PackageInstaller.UnarchivalState { + method @NonNull public static android.content.pm.PackageInstaller.UnarchivalState createGenericErrorState(int); + method @NonNull public static android.content.pm.PackageInstaller.UnarchivalState createInsufficientStorageState(int, long, @Nullable android.app.PendingIntent); + method @NonNull public static android.content.pm.PackageInstaller.UnarchivalState createNoConnectivityState(int); + method @NonNull public static android.content.pm.PackageInstaller.UnarchivalState createOkState(int); + method @NonNull public static android.content.pm.PackageInstaller.UnarchivalState createUserActionRequiredState(int, @NonNull android.app.PendingIntent); + } + public class PackageItemInfo { ctor public PackageItemInfo(); ctor public PackageItemInfo(android.content.pm.PackageItemInfo); @@ -18331,8 +18369,8 @@ package android.hardware { field public static final int RGBX_8888 = 2; // 0x2 field public static final int RGB_565 = 4; // 0x4 field public static final int RGB_888 = 3; // 0x3 - field @FlaggedApi("com.android.graphics.hwui.flags.requested_formats_v") public static final int RG_1616_UINT = 58; // 0x3a - field @FlaggedApi("com.android.graphics.hwui.flags.requested_formats_v") public static final int R_16_UINT = 57; // 0x39 + field @FlaggedApi("com.android.graphics.hwui.flags.requested_formats_v") public static final int RG_1616 = 58; // 0x3a + field @FlaggedApi("com.android.graphics.hwui.flags.requested_formats_v") public static final int R_16 = 57; // 0x39 field @FlaggedApi("com.android.graphics.hwui.flags.requested_formats_v") public static final int R_8 = 56; // 0x38 field public static final int S_UI8 = 53; // 0x35 field public static final long USAGE_COMPOSER_OVERLAY = 2048L; // 0x800L @@ -18762,14 +18800,14 @@ package android.hardware.biometrics { } @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public final class PromptContentItemBulletedText implements android.os.Parcelable android.hardware.biometrics.PromptContentItem { - ctor public PromptContentItemBulletedText(@NonNull CharSequence); + ctor public PromptContentItemBulletedText(@NonNull String); method public int describeContents(); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.hardware.biometrics.PromptContentItemBulletedText> CREATOR; } @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public final class PromptContentItemPlainText implements android.os.Parcelable android.hardware.biometrics.PromptContentItem { - ctor public PromptContentItemPlainText(@NonNull CharSequence); + ctor public PromptContentItemPlainText(@NonNull String); method public int describeContents(); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.hardware.biometrics.PromptContentItemPlainText> CREATOR; @@ -18780,7 +18818,7 @@ package android.hardware.biometrics { @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public final class PromptVerticalListContentView implements android.os.Parcelable android.hardware.biometrics.PromptContentView { method public int describeContents(); - method @Nullable public CharSequence getDescription(); + method @Nullable public String getDescription(); method @NonNull public java.util.List<android.hardware.biometrics.PromptContentItem> getListItems(); method public static int getMaxEachItemCharacterNumber(); method public static int getMaxItemCount(); @@ -18793,7 +18831,7 @@ package android.hardware.biometrics { method @NonNull public android.hardware.biometrics.PromptVerticalListContentView.Builder addListItem(@NonNull android.hardware.biometrics.PromptContentItem); method @NonNull public android.hardware.biometrics.PromptVerticalListContentView.Builder addListItem(@NonNull android.hardware.biometrics.PromptContentItem, int); method @NonNull public android.hardware.biometrics.PromptVerticalListContentView build(); - method @NonNull public android.hardware.biometrics.PromptVerticalListContentView.Builder setDescription(@NonNull CharSequence); + method @NonNull public android.hardware.biometrics.PromptVerticalListContentView.Builder setDescription(@NonNull String); } } @@ -22158,11 +22196,10 @@ package android.media { @FlaggedApi("android.media.audio.loudness_configurator_api") public class LoudnessCodecController implements java.lang.AutoCloseable { method @FlaggedApi("android.media.audio.loudness_configurator_api") public boolean addMediaCodec(@NonNull android.media.MediaCodec); - method public void close(); + method @FlaggedApi("android.media.audio.loudness_configurator_api") public void close(); method @FlaggedApi("android.media.audio.loudness_configurator_api") @NonNull public static android.media.LoudnessCodecController create(int); method @FlaggedApi("android.media.audio.loudness_configurator_api") @NonNull public static android.media.LoudnessCodecController create(int, @NonNull java.util.concurrent.Executor, @NonNull android.media.LoudnessCodecController.OnLoudnessCodecUpdateListener); method @FlaggedApi("android.media.audio.loudness_configurator_api") @NonNull public android.os.Bundle getLoudnessCodecParams(@NonNull android.media.MediaCodec); - method @FlaggedApi("android.media.audio.loudness_configurator_api") public void release(); method @FlaggedApi("android.media.audio.loudness_configurator_api") public void removeMediaCodec(@NonNull android.media.MediaCodec); } @@ -24262,7 +24299,7 @@ package android.media { method @Nullable public android.media.MediaRouter2.RoutingController getController(@NonNull String); method @NonNull public java.util.List<android.media.MediaRouter2.RoutingController> getControllers(); method @NonNull public static android.media.MediaRouter2 getInstance(@NonNull android.content.Context); - method @FlaggedApi("com.android.media.flags.enable_cross_user_routing_in_media_router2") @NonNull @RequiresPermission(anyOf={android.Manifest.permission.MEDIA_CONTENT_CONTROL, android.Manifest.permission.MEDIA_ROUTING_CONTROL}) public static android.media.MediaRouter2 getInstance(@NonNull android.content.Context, @NonNull android.os.Looper, @NonNull String, @NonNull android.os.UserHandle); + method @FlaggedApi("com.android.media.flags.enable_cross_user_routing_in_media_router2") @NonNull @RequiresPermission(anyOf={android.Manifest.permission.MEDIA_CONTENT_CONTROL, android.Manifest.permission.MEDIA_ROUTING_CONTROL}) public static android.media.MediaRouter2 getInstance(@NonNull android.content.Context, @NonNull String, @NonNull android.os.UserHandle); method @FlaggedApi("com.android.media.flags.enable_rlp_callbacks_in_media_router2") @Nullable public android.media.RouteListingPreference getRouteListingPreference(); method @NonNull public java.util.List<android.media.MediaRoute2Info> getRoutes(); method @NonNull public android.media.MediaRouter2.RoutingController getSystemController(); @@ -36585,6 +36622,7 @@ package android.provider { field @FlaggedApi("com.android.media.flags.enable_privileged_routing_for_media_routing_control") public static final String ACTION_REQUEST_MEDIA_ROUTING_CONTROL = "android.settings.REQUEST_MEDIA_ROUTING_CONTROL"; field public static final String ACTION_REQUEST_SCHEDULE_EXACT_ALARM = "android.settings.REQUEST_SCHEDULE_EXACT_ALARM"; field public static final String ACTION_REQUEST_SET_AUTOFILL_SERVICE = "android.settings.REQUEST_SET_AUTOFILL_SERVICE"; + field @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") public static final String ACTION_SATELLITE_SETTING = "android.settings.SATELLITE_SETTING"; field public static final String ACTION_SEARCH_SETTINGS = "android.search.action.SEARCH_SETTINGS"; field public static final String ACTION_SECURITY_SETTINGS = "android.settings.SECURITY_SETTINGS"; field public static final String ACTION_SETTINGS = "android.settings.SETTINGS"; @@ -41665,10 +41703,10 @@ package android.telecom { method public void disconnect(@NonNull android.telecom.DisconnectCause, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telecom.CallException>); method @NonNull public android.os.ParcelUuid getCallId(); method public void requestCallEndpointChange(@NonNull android.telecom.CallEndpoint, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telecom.CallException>); + method @FlaggedApi("com.android.server.telecom.flags.set_mute_state") public void requestMuteState(boolean, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telecom.CallException>); method public void sendEvent(@NonNull String, @NonNull android.os.Bundle); method public void setActive(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telecom.CallException>); method public void setInactive(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telecom.CallException>); - method @FlaggedApi("com.android.server.telecom.flags.set_mute_state") public void setMuteState(boolean, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telecom.CallException>); method public void startCallStreaming(@NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.telecom.CallException>); } @@ -44954,7 +44992,7 @@ package android.telephony { method public void addOnSubscriptionsChangedListener(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.SubscriptionManager.OnSubscriptionsChangedListener); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void addSubscriptionsIntoGroup(@NonNull java.util.List<java.lang.Integer>, @NonNull android.os.ParcelUuid); method public boolean canManageSubscription(android.telephony.SubscriptionInfo); - method @FlaggedApi("com.android.internal.telephony.flags.enforce_subscription_user_filter") @NonNull public android.telephony.SubscriptionManager createForAllUserProfiles(); + method @FlaggedApi("com.android.internal.telephony.flags.enforce_subscription_user_filter") @NonNull @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_PROFILES) public android.telephony.SubscriptionManager createForAllUserProfiles(); method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public android.os.ParcelUuid createSubscriptionGroup(@NonNull java.util.List<java.lang.Integer>); method @Deprecated public static android.telephony.SubscriptionManager from(android.content.Context); method public java.util.List<android.telephony.SubscriptionInfo> getAccessibleSubscriptionInfoList(); @@ -52426,9 +52464,9 @@ package android.view { field protected static final int[] PRESSED_STATE_SET; field protected static final int[] PRESSED_WINDOW_FOCUSED_STATE_SET; field @FlaggedApi("android.view.flags.toolkit_set_frame_rate_read_only") public static final float REQUESTED_FRAME_RATE_CATEGORY_DEFAULT = 0.0f; - field @FlaggedApi("android.view.flags.toolkit_set_frame_rate_read_only") public static final float REQUESTED_FRAME_RATE_CATEGORY_HIGH = -120.0f; - field @FlaggedApi("android.view.flags.toolkit_set_frame_rate_read_only") public static final float REQUESTED_FRAME_RATE_CATEGORY_LOW = -30.0f; - field @FlaggedApi("android.view.flags.toolkit_set_frame_rate_read_only") public static final float REQUESTED_FRAME_RATE_CATEGORY_NORMAL = -60.0f; + field @FlaggedApi("android.view.flags.toolkit_set_frame_rate_read_only") public static final float REQUESTED_FRAME_RATE_CATEGORY_HIGH = -4.0f; + field @FlaggedApi("android.view.flags.toolkit_set_frame_rate_read_only") public static final float REQUESTED_FRAME_RATE_CATEGORY_LOW = -2.0f; + field @FlaggedApi("android.view.flags.toolkit_set_frame_rate_read_only") public static final float REQUESTED_FRAME_RATE_CATEGORY_NORMAL = -3.0f; field @FlaggedApi("android.view.flags.toolkit_set_frame_rate_read_only") public static final float REQUESTED_FRAME_RATE_CATEGORY_NO_PREFERENCE = -1.0f; field public static final android.util.Property<android.view.View,java.lang.Float> ROTATION; field public static final android.util.Property<android.view.View,java.lang.Float> ROTATION_X; @@ -57082,7 +57120,7 @@ package android.webkit { method public abstract boolean getBuiltInZoomControls(); method public abstract int getCacheMode(); method public abstract String getCursiveFontFamily(); - method @Deprecated public abstract boolean getDatabaseEnabled(); + method public abstract boolean getDatabaseEnabled(); method @Deprecated public abstract String getDatabasePath(); method public abstract int getDefaultFixedFontSize(); method public abstract int getDefaultFontSize(); @@ -57128,7 +57166,7 @@ package android.webkit { method public abstract void setBuiltInZoomControls(boolean); method public abstract void setCacheMode(int); method public abstract void setCursiveFontFamily(String); - method @Deprecated public abstract void setDatabaseEnabled(boolean); + method public abstract void setDatabaseEnabled(boolean); method @Deprecated public abstract void setDatabasePath(String); method public abstract void setDefaultFixedFontSize(int); method public abstract void setDefaultFontSize(int); @@ -59259,6 +59297,7 @@ package android.widget { ctor public RemoteViews(@NonNull java.util.Map<android.util.SizeF,android.widget.RemoteViews>); ctor public RemoteViews(android.widget.RemoteViews); ctor public RemoteViews(android.os.Parcel); + ctor @FlaggedApi("android.appwidget.flags.draw_data_parcel") public RemoteViews(@NonNull android.widget.RemoteViews.DrawInstructions); method public void addStableView(@IdRes int, @NonNull android.widget.RemoteViews, int); method public void addView(@IdRes int, android.widget.RemoteViews); method public android.view.View apply(android.content.Context, android.view.ViewGroup); @@ -59367,6 +59406,15 @@ package android.widget { ctor public RemoteViews.ActionException(String); } + @FlaggedApi("android.appwidget.flags.draw_data_parcel") public static final class RemoteViews.DrawInstructions { + method @FlaggedApi("android.appwidget.flags.draw_data_parcel") public void appendInstructions(@NonNull byte[]); + } + + @FlaggedApi("android.appwidget.flags.draw_data_parcel") public static final class RemoteViews.DrawInstructions.Builder { + ctor @FlaggedApi("android.appwidget.flags.draw_data_parcel") public RemoteViews.DrawInstructions.Builder(@NonNull java.util.List<byte[]>); + method @FlaggedApi("android.appwidget.flags.draw_data_parcel") @NonNull public android.widget.RemoteViews.DrawInstructions build(); + } + public static final class RemoteViews.RemoteCollectionItems implements android.os.Parcelable { method public int describeContents(); method public int getItemCount(); diff --git a/core/api/system-current.txt b/core/api/system-current.txt index fe32bad40e1f..5920c6b97bc5 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -22,7 +22,7 @@ package android { field public static final String ACCESS_RCS_USER_CAPABILITY_EXCHANGE = "android.permission.ACCESS_RCS_USER_CAPABILITY_EXCHANGE"; field public static final String ACCESS_SHARED_LIBRARIES = "android.permission.ACCESS_SHARED_LIBRARIES"; field public static final String ACCESS_SHORTCUTS = "android.permission.ACCESS_SHORTCUTS"; - field public static final String ACCESS_SMARTSPACE = "android.permission.ACCESS_SMARTSPACE"; + field @FlaggedApi("android.app.smartspace.flags.access_smartspace") public static final String ACCESS_SMARTSPACE = "android.permission.ACCESS_SMARTSPACE"; field public static final String ACCESS_SURFACE_FLINGER = "android.permission.ACCESS_SURFACE_FLINGER"; field public static final String ACCESS_TUNED_INFO = "android.permission.ACCESS_TUNED_INFO"; field public static final String ACCESS_TV_DESCRAMBLER = "android.permission.ACCESS_TV_DESCRAMBLER"; @@ -56,7 +56,7 @@ package android { field public static final String BIND_CONTENT_SUGGESTIONS_SERVICE = "android.permission.BIND_CONTENT_SUGGESTIONS_SERVICE"; field public static final String BIND_DIRECTORY_SEARCH = "android.permission.BIND_DIRECTORY_SEARCH"; field public static final String BIND_DISPLAY_HASHING_SERVICE = "android.permission.BIND_DISPLAY_HASHING_SERVICE"; - field @FlaggedApi("com.android.internal.telephony.flags.ap_domain_selection_enabled") public static final String BIND_DOMAIN_SELECTION_SERVICE = "android.permission.BIND_DOMAIN_SELECTION_SERVICE"; + field @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") public static final String BIND_DOMAIN_SELECTION_SERVICE = "android.permission.BIND_DOMAIN_SELECTION_SERVICE"; field public static final String BIND_DOMAIN_VERIFICATION_AGENT = "android.permission.BIND_DOMAIN_VERIFICATION_AGENT"; field public static final String BIND_EUICC_SERVICE = "android.permission.BIND_EUICC_SERVICE"; field public static final String BIND_EXTERNAL_STORAGE_SERVICE = "android.permission.BIND_EXTERNAL_STORAGE_SERVICE"; @@ -190,6 +190,7 @@ package android { field public static final String MANAGE_DEFAULT_APPLICATIONS = "android.permission.MANAGE_DEFAULT_APPLICATIONS"; field public static final String MANAGE_DEVICE_ADMINS = "android.permission.MANAGE_DEVICE_ADMINS"; field public static final String MANAGE_DEVICE_POLICY_APP_EXEMPTIONS = "android.permission.MANAGE_DEVICE_POLICY_APP_EXEMPTIONS"; + field @FlaggedApi("android.permission.flags.enhanced_confirmation_mode_apis_enabled") public static final String MANAGE_ENHANCED_CONFIRMATION_STATES = "android.permission.MANAGE_ENHANCED_CONFIRMATION_STATES"; field public static final String MANAGE_ETHERNET_NETWORKS = "android.permission.MANAGE_ETHERNET_NETWORKS"; field public static final String MANAGE_FACTORY_RESET_PROTECTION = "android.permission.MANAGE_FACTORY_RESET_PROTECTION"; field public static final String MANAGE_GAME_ACTIVITY = "android.permission.MANAGE_GAME_ACTIVITY"; @@ -17158,7 +17159,7 @@ package android.telephony.satellite { method @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") @NonNull @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public java.util.Set<java.lang.Integer> getSatelliteAttachRestrictionReasonsForCarrier(int); method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void pollPendingSatelliteDatagrams(@NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>); method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void provisionSatelliteService(@NonNull String, @NonNull byte[], @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>); - method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void registerForNtnSignalStrengthChanged(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.NtnSignalStrengthCallback) throws android.telephony.satellite.SatelliteManager.SatelliteException; + method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void registerForNtnSignalStrengthChanged(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.NtnSignalStrengthCallback); method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int registerForSatelliteCapabilitiesChanged(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.SatelliteCapabilitiesCallback); method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int registerForSatelliteDatagram(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.SatelliteDatagramCallback); method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public int registerForSatelliteModemStateChanged(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.SatelliteModemStateCallback); @@ -17225,6 +17226,7 @@ package android.telephony.satellite { field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_MODEM_STATE_UNKNOWN = -1; // 0xffffffff field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_ACCESS_BARRED = 16; // 0x10 field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_ERROR = 1; // 0x1 + field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_ILLEGAL_STATE = 23; // 0x17 field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_INVALID_ARGUMENTS = 8; // 0x8 field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_INVALID_MODEM_STATE = 7; // 0x7 field @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public static final int SATELLITE_RESULT_INVALID_TELEPHONY_STATE = 6; // 0x6 diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 949e2ba07a18..1bdbd4c50634 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -6792,6 +6792,7 @@ public final class ActivityThread extends ClientTransactionHandler } } if (killApp) { + // Keep in sync with "perhaps it was removed" case below. mPackages.remove(packages[i]); mResourcePackages.remove(packages[i]); } @@ -6834,23 +6835,24 @@ public final class ActivityThread extends ClientTransactionHandler PackageManager.GET_SHARED_LIBRARY_FILES, UserHandle.myUserId()); - if (mActivities.size() > 0) { - for (ActivityClientRecord ar : mActivities.values()) { - if (ar.activityInfo.applicationInfo.packageName - .equals(packageName)) { - ar.activityInfo.applicationInfo = aInfo; - ar.packageInfo = pkgInfo; + if (aInfo != null) { + if (mActivities.size() > 0) { + for (ActivityClientRecord ar : mActivities.values()) { + if (ar.activityInfo.applicationInfo.packageName + .equals(packageName)) { + ar.activityInfo.applicationInfo = aInfo; + ar.packageInfo = pkgInfo; + } } } - } - final String[] oldResDirs = { pkgInfo.getResDir() }; + final String[] oldResDirs = {pkgInfo.getResDir()}; - final ArrayList<String> oldPaths = new ArrayList<>(); - LoadedApk.makePaths(this, pkgInfo.getApplicationInfo(), oldPaths); - pkgInfo.updateApplicationInfo(aInfo, oldPaths); + final ArrayList<String> oldPaths = new ArrayList<>(); + LoadedApk.makePaths( + this, pkgInfo.getApplicationInfo(), oldPaths); + pkgInfo.updateApplicationInfo(aInfo, oldPaths); - synchronized (mResourcesManager) { // Update affected Resources objects to use new ResourcesImpl mResourcesManager.appendPendingAppInfoUpdate(oldResDirs, aInfo); @@ -6858,6 +6860,12 @@ public final class ActivityThread extends ClientTransactionHandler } } catch (RemoteException e) { } + } else { + // No package, perhaps it was removed? + Slog.e(TAG, "Package [" + packages[i] + "] reported as REPLACED," + + " but missing application info. Assuming REMOVED."); + mPackages.remove(packages[i]); + mResourcePackages.remove(packages[i]); } } } diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index ea37e7fbcce1..ccd8456129eb 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -1548,24 +1548,9 @@ public class AppOpsManager { public static final int OP_READ_SYSTEM_GRAMMATICAL_GENDER = AppProtoEnums.APP_OP_READ_SYSTEM_GRAMMATICAL_GENDER; - /** - * Whether the app has enabled to receive the icon overlay for fetching archived apps. - * - * @hide - */ - public static final int OP_ARCHIVE_ICON_OVERLAY = AppProtoEnums.APP_OP_ARCHIVE_ICON_OVERLAY; - - /** - * Whether the app has enabled compatibility support for unarchival. - * - * @hide - */ - public static final int OP_UNARCHIVAL_CONFIRMATION = - AppProtoEnums.APP_OP_UNARCHIVAL_CONFIRMATION; - /** @hide */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) - public static final int _NUM_OP = 146; + public static final int _NUM_OP = 144; /** * All app ops represented as strings. @@ -1715,8 +1700,6 @@ public class AppOpsManager { OPSTR_ENABLE_MOBILE_DATA_BY_USER, OPSTR_RESERVED_FOR_TESTING, OPSTR_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER, - OPSTR_ARCHIVE_ICON_OVERLAY, - OPSTR_UNARCHIVAL_CONFIRMATION, }) public @interface AppOpString {} @@ -2057,20 +2040,6 @@ public class AppOpsManager { public static final String OPSTR_MEDIA_ROUTING_CONTROL = "android:media_routing_control"; /** - * Whether the app has enabled to receive the icon overlay for fetching archived apps. - * - * @hide - */ - public static final String OPSTR_ARCHIVE_ICON_OVERLAY = "android:archive_icon_overlay"; - - /** - * Whether the app has enabled compatibility support for unarchival. - * - * @hide - */ - public static final String OPSTR_UNARCHIVAL_CONFIRMATION = "android:unarchival_support"; - - /** * AppOp granted to apps that we are started via {@code am instrument -e --no-isolated-storage} * * <p>MediaProvider is the only component (outside of system server) that should care about this @@ -2535,8 +2504,6 @@ public class AppOpsManager { OP_RECEIVE_SANDBOXED_DETECTION_TRAINING_DATA, OP_MEDIA_ROUTING_CONTROL, OP_READ_SYSTEM_GRAMMATICAL_GENDER, - OP_ARCHIVE_ICON_OVERLAY, - OP_UNARCHIVAL_CONFIRMATION, }; static final AppOpInfo[] sAppOpInfos = new AppOpInfo[]{ @@ -2991,14 +2958,9 @@ public class AppOpsManager { .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(), new AppOpInfo.Builder(OP_READ_SYSTEM_GRAMMATICAL_GENDER, OPSTR_READ_SYSTEM_GRAMMATICAL_GENDER, "READ_SYSTEM_GRAMMATICAL_GENDER") - .setPermission(Manifest.permission.READ_SYSTEM_GRAMMATICAL_GENDER) + // will make it an app-op permission in the future. + // .setPermission(Manifest.permission.READ_SYSTEM_GRAMMATICAL_GENDER) .build(), - new AppOpInfo.Builder(OP_ARCHIVE_ICON_OVERLAY, OPSTR_ARCHIVE_ICON_OVERLAY, - "ARCHIVE_ICON_OVERLAY") - .setDefaultMode(MODE_ALLOWED).build(), - new AppOpInfo.Builder(OP_UNARCHIVAL_CONFIRMATION, OPSTR_UNARCHIVAL_CONFIRMATION, - "UNARCHIVAL_CONFIRMATION") - .setDefaultMode(MODE_ALLOWED).build(), }; // The number of longs needed to form a full bitmask of app ops @@ -3133,7 +3095,7 @@ public class AppOpsManager { /** * Retrieve the permission associated with an operation, or null if there is not one. - + * * @param op The operation name. * * @hide diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java index 4f1db7d3784a..34c44f9489d5 100644 --- a/core/java/android/app/ApplicationPackageManager.java +++ b/core/java/android/app/ApplicationPackageManager.java @@ -4032,8 +4032,7 @@ public class ApplicationPackageManager extends PackageManager { private Drawable getArchivedAppIcon(String packageName) { try { return new BitmapDrawable(null, - mPM.getArchivedAppIcon(packageName, new UserHandle(getUserId()), - mContext.getPackageName())); + mPM.getArchivedAppIcon(packageName, new UserHandle(getUserId()))); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/core/java/android/app/HomeVisibilityListener.java b/core/java/android/app/HomeVisibilityListener.java index 1f5f2e4c8237..5dd7ab0f99fa 100644 --- a/core/java/android/app/HomeVisibilityListener.java +++ b/core/java/android/app/HomeVisibilityListener.java @@ -69,6 +69,11 @@ public abstract class HomeVisibilityListener { public HomeVisibilityListener() { mObserver = new android.app.IProcessObserver.Stub() { @Override + public void onProcessStarted(int pid, int processUid, int packageUid, + String packageName, String processName) { + } + + @Override public void onForegroundActivitiesChanged(int pid, int uid, boolean fg) { refreshHomeVisibility(); } diff --git a/core/java/android/app/IActivityClientController.aidl b/core/java/android/app/IActivityClientController.aidl index 7370fc36c23e..5b044f616487 100644 --- a/core/java/android/app/IActivityClientController.aidl +++ b/core/java/android/app/IActivityClientController.aidl @@ -119,7 +119,7 @@ interface IActivityClientController { oneway void setShowWhenLocked(in IBinder token, boolean showWhenLocked); oneway void setInheritShowWhenLocked(in IBinder token, boolean setInheritShownWhenLocked); - oneway void setTurnScreenOn(in IBinder token, boolean turnScreenOn); + void setTurnScreenOn(in IBinder token, boolean turnScreenOn); oneway void setAllowCrossUidActivitySwitchFromBelow(in IBinder token, boolean allowed); oneway void reportActivityFullyDrawn(in IBinder token, boolean restoredFromBundle); oneway void overrideActivityTransition(IBinder token, boolean open, int enterAnim, int exitAnim, diff --git a/core/java/android/app/IProcessObserver.aidl b/core/java/android/app/IProcessObserver.aidl index 7be3620f317b..5c5e72cf9d6f 100644 --- a/core/java/android/app/IProcessObserver.aidl +++ b/core/java/android/app/IProcessObserver.aidl @@ -18,6 +18,17 @@ package android.app; /** {@hide} */ oneway interface IProcessObserver { + /** + * Invoked when an app process starts up. + * + * @param pid The pid of the process. + * @param processUid The UID associated with the process. + * @param packageUid The UID associated with the package. + * @param packageName The name of the package. + * @param processName The name of the process. + */ + void onProcessStarted(int pid, int processUid, int packageUid, + @utf8InCpp String packageName, @utf8InCpp String processName); void onForegroundActivitiesChanged(int pid, int uid, boolean foregroundActivities); void onForegroundServicesChanged(int pid, int uid, int serviceTypes); void onProcessDied(int pid, int uid); diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java index 63f37f150d33..0116ca24ec97 100644 --- a/core/java/android/app/WallpaperManager.java +++ b/core/java/android/app/WallpaperManager.java @@ -261,6 +261,13 @@ public class WallpaperManager { public static final String COMMAND_GOING_TO_SLEEP = "android.wallpaper.goingtosleep"; /** + * Command for {@link #sendWallpaperCommand}: reported when a physical display switch event + * happens, e.g. fold and unfold. + * @hide + */ + public static final String COMMAND_DISPLAY_SWITCH = "android.wallpaper.displayswitch"; + + /** * Command for {@link #sendWallpaperCommand}: reported when the wallpaper that was already * set is re-applied by the user. * @hide @@ -1892,15 +1899,22 @@ public class WallpaperManager { /** * Returns the information about the home screen wallpaper if its current wallpaper is a live - * wallpaper component. Otherwise, if the wallpaper is a static image, this returns null. + * wallpaper component. Otherwise, if the wallpaper is a static image or is not set, or if the + * caller doesn't have the appropriate permissions, this returns {@code null}. * * <p> - * In order to use this, apps should declare a {@code <queries>} tag with the action - * {@code "android.service.wallpaper.WallpaperService"}. Otherwise, + * Before Android U, this method requires the + * {@link android.Manifest.permission#QUERY_ALL_PACKAGES} permission. + * </p> + * + * <p> + * Starting from Android U, in order to use this, apps should declare a {@code <queries>} tag + * with the action {@code "android.service.wallpaper.WallpaperService"}. Otherwise, * this method will return {@code null} if the caller doesn't otherwise have * <a href="{@docRoot}training/package-visibility">visibility</a> of the wallpaper package. * </p> */ + @RequiresPermission(value = "QUERY_ALL_PACKAGES", conditional = true) public WallpaperInfo getWallpaperInfo() { return getWallpaperInfoForUser(mContext.getUserId()); } @@ -1917,19 +1931,14 @@ public class WallpaperManager { } /** - * Returns the information about the home screen wallpaper if its current wallpaper is a live - * wallpaper component. Otherwise, if the wallpaper is a static image or is not set, or if the + * Returns the information about the designated wallpaper if its current wallpaper is a live + * wallpaper component. Otherwise, if the wallpaper is a static image or is not set, or if * the caller doesn't have the appropriate permissions, this returns {@code null}. * * <p> - * Before Android U, this method requires the - * {@link android.Manifest.permission#QUERY_ALL_PACKAGES} permission. - * </p> - * - * <p> - * Starting from Android U, In order to use this, apps should declare a {@code <queries>} tag - * with the action {@code "android.service.wallpaper.WallpaperService"}. Otherwise, - * this method will return {@code null} if the caller doesn't otherwise have + * In order to use this, apps should declare a {@code <queries>} tag with the action + * {@code "android.service.wallpaper.WallpaperService"}. Otherwise, this method will return + * {@code null} if the caller doesn't otherwise have * <a href="{@docRoot}training/package-visibility">visibility</a> of the wallpaper package. * </p> * @@ -1945,7 +1954,7 @@ public class WallpaperManager { /** * Returns the information about the designated wallpaper if its current wallpaper is a live * wallpaper component. Otherwise, if the wallpaper is a static image or is not set, or if the - * the caller doesn't have the appropriate permissions, this returns {@code null}. + * caller doesn't have the appropriate permissions, this returns {@code null}. * * <p> * In order to use this, apps should declare a {@code <queries>} tag diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig index 35ce10223aa6..b3ecd92c56c9 100644 --- a/core/java/android/app/admin/flags/flags.aconfig +++ b/core/java/android/app/admin/flags/flags.aconfig @@ -55,3 +55,10 @@ flag { description: "Guards a bugfix that ends the credential input flow if the managed user has not stopped." bug: "293441361" } + +flag { + name: "default_sms_personal_app_suspension_fix_enabled" + namespace: "enterprise" + description: "Exempt the default sms app of the context user for suspension when calling setPersonalAppsSuspended" + bug: "309183330" +} diff --git a/core/java/android/app/backup/BackupHelperWithLogger.java b/core/java/android/app/backup/BackupHelperWithLogger.java new file mode 100644 index 000000000000..1a59a5302f07 --- /dev/null +++ b/core/java/android/app/backup/BackupHelperWithLogger.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.backup; + +import android.os.ParcelFileDescriptor; + +/** + * Utility class for writing BackupHelpers with added logging capabilities. + * Used for passing a logger object to Helper in key shared backup agents + * + * @hide + */ +public abstract class BackupHelperWithLogger implements BackupHelper { + private BackupRestoreEventLogger mLogger; + private boolean mIsLoggerSet = false; + + public abstract void writeNewStateDescription(ParcelFileDescriptor newState); + + public abstract void restoreEntity(BackupDataInputStream data); + + public abstract void performBackup(ParcelFileDescriptor oldState, BackupDataOutput data, + ParcelFileDescriptor newState); + + /** + * Gets the logger so that the backuphelper can log success/error for each datatype handled + */ + public BackupRestoreEventLogger getLogger() { + return mLogger; + } + + /** + * Allow the shared backup agent to pass a logger to each of its backup helper + */ + public void setLogger(BackupRestoreEventLogger logger) { + mLogger = logger; + mIsLoggerSet = true; + } + + /** + * Allow the helper to check if its shared backup agent has passed a logger + */ + public boolean isLoggerSet() { + return mIsLoggerSet; + } +} diff --git a/core/java/android/app/backup/BlobBackupHelper.java b/core/java/android/app/backup/BlobBackupHelper.java index 82d0a94ce0da..a55ff4899296 100644 --- a/core/java/android/app/backup/BlobBackupHelper.java +++ b/core/java/android/app/backup/BlobBackupHelper.java @@ -39,7 +39,7 @@ import java.util.zip.InflaterInputStream; * * @hide */ -public abstract class BlobBackupHelper implements BackupHelper { +public abstract class BlobBackupHelper extends BackupHelperWithLogger { private static final String TAG = "BlobBackupHelper"; private static final boolean DEBUG = false; diff --git a/core/java/android/companion/CompanionDeviceManager.java b/core/java/android/companion/CompanionDeviceManager.java index 672e343959cb..d74399274a60 100644 --- a/core/java/android/companion/CompanionDeviceManager.java +++ b/core/java/android/companion/CompanionDeviceManager.java @@ -1038,6 +1038,7 @@ public final class CompanionDeviceManager { } } + // TODO(b/315163162) Add @Deprecated keyword after 24Q2 cut. /** * Register to receive callbacks whenever the associated device comes in and out of range. * @@ -1094,7 +1095,7 @@ public final class CompanionDeviceManager { callingUid, callingPid); } } - + // TODO(b/315163162) Add @Deprecated keyword after 24Q2 cut. /** * Unregister for receiving callbacks whenever the associated device comes in and out of range. * @@ -1137,6 +1138,64 @@ public final class CompanionDeviceManager { } /** + * Register to receive callbacks whenever the associated device comes in and out of range. + * + * <p>The app doesn't need to remain running in order to receive its callbacks.</p> + * + * <p>Calling app must check for feature presence of + * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} before calling this API.</p> + * + * <p>For Bluetooth LE devices, this is based on scanning for device with the given address. + * The system will scan for the device when Bluetooth is ON or Bluetooth scanning is ON.</p> + * + * <p>For Bluetooth classic devices this is triggered when the device connects/disconnects.</p> + * + * <p>WiFi devices are not supported.</p> + * + * <p>If a Bluetooth LE device wants to use a rotating mac address, it is recommended to use + * Resolvable Private Address, and ensure the device is bonded to the phone so that android OS + * is able to resolve the address.</p> + * + * @param request A request for setting the types of device for observing device presence. + * + * @see ObservingDevicePresenceRequest.Builder + * @see CompanionDeviceService#onDevicePresenceEvent(DevicePresenceEvent) + */ + @FlaggedApi(Flags.FLAG_DEVICE_PRESENCE) + @RequiresPermission(android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE) + public void startObservingDevicePresence(@NonNull ObservingDevicePresenceRequest request) { + Objects.requireNonNull(request, "request cannot be null"); + + try { + mService.startObservingDevicePresence( + request, mContext.getOpPackageName(), mContext.getUserId()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Unregister for receiving callbacks whenever the associated device comes in and out of range. + * + * Calling app must check for feature presence of + * {@link PackageManager#FEATURE_COMPANION_DEVICE_SETUP} before calling this API. + * + * @param request A request for setting the types of device for observing device presence. + */ + @FlaggedApi(Flags.FLAG_DEVICE_PRESENCE) + @RequiresPermission(android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE) + public void stopObservingDevicePresence(@NonNull ObservingDevicePresenceRequest request) { + Objects.requireNonNull(request, "request cannot be null"); + + try { + mService.stopObservingDevicePresence( + request, mContext.getOpPackageName(), mContext.getUserId()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * Dispatch a message to system for processing. It should only be called by * {@link CompanionDeviceService#dispatchMessageToSystem(int, int, byte[])} * diff --git a/core/java/android/companion/CompanionDeviceService.java b/core/java/android/companion/CompanionDeviceService.java index 4d0267ca0cbb..5ad2348254e2 100644 --- a/core/java/android/companion/CompanionDeviceService.java +++ b/core/java/android/companion/CompanionDeviceService.java @@ -18,7 +18,6 @@ package android.companion; import android.annotation.FlaggedApi; -import android.annotation.IntDef; import android.annotation.MainThread; import android.annotation.NonNull; import android.annotation.Nullable; @@ -33,8 +32,6 @@ import android.util.Log; import java.io.InputStream; import java.io.OutputStream; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; import java.util.Objects; import java.util.concurrent.Executor; @@ -123,62 +120,6 @@ public abstract class CompanionDeviceService extends Service { */ public static final String SERVICE_INTERFACE = "android.companion.CompanionDeviceService"; - /** @hide */ - @IntDef(prefix = {"DEVICE_EVENT"}, value = { - DEVICE_EVENT_BLE_APPEARED, - DEVICE_EVENT_BLE_DISAPPEARED, - DEVICE_EVENT_BT_CONNECTED, - DEVICE_EVENT_BT_DISCONNECTED, - DEVICE_EVENT_SELF_MANAGED_APPEARED, - DEVICE_EVENT_SELF_MANAGED_DISAPPEARED - }) - - @Retention(RetentionPolicy.SOURCE) - public @interface DeviceEvent {} - - /** - * Companion app receives {@link #onDeviceEvent(AssociationInfo, int)} callback - * with this event if the device comes into BLE range. - */ - @FlaggedApi(Flags.FLAG_DEVICE_PRESENCE) - public static final int DEVICE_EVENT_BLE_APPEARED = 0; - - /** - * Companion app receives {@link #onDeviceEvent(AssociationInfo, int)} callback - * with this event if the device is no longer in BLE range. - */ - @FlaggedApi(Flags.FLAG_DEVICE_PRESENCE) - public static final int DEVICE_EVENT_BLE_DISAPPEARED = 1; - - /** - * Companion app receives {@link #onDeviceEvent(AssociationInfo, int)} callback - * with this event when the bluetooth device is connected. - */ - @FlaggedApi(Flags.FLAG_DEVICE_PRESENCE) - public static final int DEVICE_EVENT_BT_CONNECTED = 2; - - /** - * Companion app receives {@link #onDeviceEvent(AssociationInfo, int)} callback - * with this event if the bluetooth device is disconnected. - */ - @FlaggedApi(Flags.FLAG_DEVICE_PRESENCE) - public static final int DEVICE_EVENT_BT_DISCONNECTED = 3; - - /** - * A companion app for a self-managed device will receive the callback - * {@link #onDeviceEvent(AssociationInfo, int)} if it reports that a device has appeared on its - * own. - */ - @FlaggedApi(Flags.FLAG_DEVICE_PRESENCE) - public static final int DEVICE_EVENT_SELF_MANAGED_APPEARED = 4; - - /** - * A companion app for a self-managed device will receive the callback - * {@link #onDeviceEvent(AssociationInfo, int)} if it reports that a device has disappeared on - * its own. - */ - @FlaggedApi(Flags.FLAG_DEVICE_PRESENCE) - public static final int DEVICE_EVENT_SELF_MANAGED_DISAPPEARED = 5; private final Stub mRemote = new Stub(); @@ -306,6 +247,7 @@ public abstract class CompanionDeviceService extends Service { .detachSystemDataTransport(associationId); } + // TODO(b/315163162) Add @Deprecated keyword after 24Q2 cut. /** * Called by system whenever a device associated with this app is connected. * @@ -318,6 +260,7 @@ public abstract class CompanionDeviceService extends Service { } } + // TODO(b/315163162) Add @Deprecated keyword after 24Q2 cut. /** * Called by system whenever a device associated with this app is disconnected. * @@ -331,27 +274,13 @@ public abstract class CompanionDeviceService extends Service { } /** - * Called by the system during device events. - * - * <p>E.g. Event {@link #DEVICE_EVENT_BLE_APPEARED} will be called when the associated - * companion device comes into BLE range. - * <p>Event {@link #DEVICE_EVENT_BLE_DISAPPEARED} will be called when the associated - * companion device is no longer in BLE range. - * <p> Event {@link #DEVICE_EVENT_BT_CONNECTED} will be called when the associated - * companion device is connected. - * <p>Event {@link #DEVICE_EVENT_BT_DISCONNECTED} will be called when the associated - * companion device is disconnected. - * Note that app must receive {@link #DEVICE_EVENT_BLE_APPEARED} first before - * {@link #DEVICE_EVENT_BLE_DISAPPEARED} and {@link #DEVICE_EVENT_BT_CONNECTED} - * before {@link #DEVICE_EVENT_BT_DISCONNECTED}. + * Called by the system during device events. * - * @param associationInfo A record for the companion device. - * @param event Associated companion device's event. + * @see CompanionDeviceManager#startObservingDevicePresence(ObservingDevicePresenceRequest) */ @FlaggedApi(Flags.FLAG_DEVICE_PRESENCE) @MainThread - public void onDeviceEvent(@NonNull AssociationInfo associationInfo, - @DeviceEvent int event) { + public void onDevicePresenceEvent(@NonNull DevicePresenceEvent event) { // Do nothing. Companion apps can override this function. } @@ -390,9 +319,10 @@ public abstract class CompanionDeviceService extends Service { } @Override - public void onDeviceEvent(AssociationInfo associationInfo, int event) { - mMainHandler.postAtFrontOfQueue( - () -> mService.onDeviceEvent(associationInfo, event)); + public void onDevicePresenceEvent(DevicePresenceEvent event) { + if (Flags.devicePresence()) { + mMainHandler.postAtFrontOfQueue(() -> mService.onDevicePresenceEvent(event)); + } } } } diff --git a/core/java/android/companion/DevicePresenceEvent.aidl b/core/java/android/companion/DevicePresenceEvent.aidl new file mode 100644 index 000000000000..15215747f535 --- /dev/null +++ b/core/java/android/companion/DevicePresenceEvent.aidl @@ -0,0 +1,19 @@ + /* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + package android.companion; + + parcelable DevicePresenceEvent; diff --git a/core/java/android/companion/DevicePresenceEvent.java b/core/java/android/companion/DevicePresenceEvent.java new file mode 100644 index 000000000000..30439a5905f9 --- /dev/null +++ b/core/java/android/companion/DevicePresenceEvent.java @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.companion; + +import android.annotation.FlaggedApi; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Parcel; +import android.os.ParcelUuid; +import android.os.Parcelable; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Objects; + +/** + * Event for observing device presence. + * + * @see CompanionDeviceManager#startObservingDevicePresence(ObservingDevicePresenceRequest) + * @see ObservingDevicePresenceRequest.Builder#setUuid(ParcelUuid) + * @see ObservingDevicePresenceRequest.Builder#setAssociationId(int) + */ +@FlaggedApi(Flags.FLAG_DEVICE_PRESENCE) +public final class DevicePresenceEvent implements Parcelable { + + /** @hide */ + @IntDef(prefix = {"EVENT"}, value = { + EVENT_BLE_APPEARED, + EVENT_BLE_DISAPPEARED, + EVENT_BT_CONNECTED, + EVENT_BT_DISCONNECTED, + EVENT_SELF_MANAGED_APPEARED, + EVENT_SELF_MANAGED_DISAPPEARED + }) + + @Retention(RetentionPolicy.SOURCE) + public @interface Event {} + + /** + * Indicate observing device presence base on the ParcelUuid but not association id. + */ + public static final int NO_ASSOCIATION = -1; + + /** + * Companion app receives + * {@link CompanionDeviceService#onDevicePresenceEvent(DevicePresenceEvent)} callback + * with this event if the device comes into BLE range. + */ + public static final int EVENT_BLE_APPEARED = 0; + + /** + * Companion app receives + * {@link CompanionDeviceService#onDevicePresenceEvent(DevicePresenceEvent)} callback + * with this event if the device is no longer in BLE range. + */ + public static final int EVENT_BLE_DISAPPEARED = 1; + + /** + * Companion app receives + * {@link CompanionDeviceService#onDevicePresenceEvent(DevicePresenceEvent)} callback + * with this event when the bluetooth device is connected. + */ + public static final int EVENT_BT_CONNECTED = 2; + + /** + * Companion app receives + * {@link CompanionDeviceService#onDevicePresenceEvent(DevicePresenceEvent)} callback + * with this event if the bluetooth device is disconnected. + */ + public static final int EVENT_BT_DISCONNECTED = 3; + + /** + * A companion app for a self-managed device will receive the callback + * {@link CompanionDeviceService#onDevicePresenceEvent(DevicePresenceEvent)} + * if it reports that a device has appeared on its + * own. + */ + public static final int EVENT_SELF_MANAGED_APPEARED = 4; + + /** + * A companion app for a self-managed device will receive the callback + * {@link CompanionDeviceService#onDevicePresenceEvent(DevicePresenceEvent)} if it reports + * that a device has disappeared on its own. + */ + public static final int EVENT_SELF_MANAGED_DISAPPEARED = 5; + private final int mAssociationId; + private final int mEvent; + @Nullable + private final ParcelUuid mUuid; + + private static final int PARCEL_UUID_NULL = 0; + + private static final int PARCEL_UUID_NOT_NULL = 1; + + /** + * Create a new DevicePresenceEvent. + */ + public DevicePresenceEvent( + int associationId, @Event int event, @Nullable ParcelUuid uuid) { + mAssociationId = associationId; + mEvent = event; + mUuid = uuid; + } + + /** + * @return The association id has been used to observe device presence. + * + * Caller will receive the valid association id if only if using + * {@link ObservingDevicePresenceRequest.Builder#setAssociationId(int)}, otherwise + * return {@link #NO_ASSOCIATION}. + * + * @see ObservingDevicePresenceRequest.Builder#setAssociationId(int) + */ + public int getAssociationId() { + return mAssociationId; + } + + /** + * @return Associated companion device's event. + */ + public int getEvent() { + return mEvent; + } + + /** + * @return The ParcelUuid has been used to observe device presence. + * + * Caller will receive the ParcelUuid if only if using + * {@link ObservingDevicePresenceRequest.Builder#setUuid(ParcelUuid)}, otherwise return null. + * + * @see ObservingDevicePresenceRequest.Builder#setUuid(ParcelUuid) + */ + + @Nullable + public ParcelUuid getUuid() { + return mUuid; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mAssociationId); + dest.writeInt(mEvent); + if (mUuid == null) { + // Write 0 to the parcel to indicate the ParcelUuid is null. + dest.writeInt(PARCEL_UUID_NULL); + } else { + dest.writeInt(PARCEL_UUID_NOT_NULL); + mUuid.writeToParcel(dest, flags); + } + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) return true; + if (!(o instanceof DevicePresenceEvent that)) return false; + + return Objects.equals(mUuid, that.mUuid) + && mAssociationId == that.mAssociationId + && mEvent == that.mEvent; + } + + @Override + public String toString() { + return "ObservingDevicePresenceResult { " + + "Association Id= " + mAssociationId + "," + + "ParcelUuid= " + mUuid + "," + + "Event= " + mEvent + "}"; + } + + @Override + public int hashCode() { + return Objects.hash(mAssociationId, mEvent, mUuid); + } + + @NonNull + public static final Parcelable.Creator<DevicePresenceEvent> CREATOR = + new Parcelable.Creator<DevicePresenceEvent>() { + @Override + public DevicePresenceEvent[] newArray(int size) { + return new DevicePresenceEvent[size]; + } + + @Override + public DevicePresenceEvent createFromParcel(@NonNull Parcel in) { + return new DevicePresenceEvent(in); + } + }; + + private DevicePresenceEvent(@NonNull Parcel in) { + mAssociationId = in.readInt(); + mEvent = in.readInt(); + if (in.readInt() == PARCEL_UUID_NULL) { + mUuid = null; + } else { + mUuid = ParcelUuid.CREATOR.createFromParcel(in); + } + } +} diff --git a/core/java/android/companion/ICompanionDeviceManager.aidl b/core/java/android/companion/ICompanionDeviceManager.aidl index 22689f3b85c7..57d59e5e5bf0 100644 --- a/core/java/android/companion/ICompanionDeviceManager.aidl +++ b/core/java/android/companion/ICompanionDeviceManager.aidl @@ -24,8 +24,11 @@ import android.companion.IOnTransportsChangedListener; import android.companion.ISystemDataTransferCallback; import android.companion.AssociationInfo; import android.companion.AssociationRequest; +import android.companion.ObservingDevicePresenceRequest; import android.companion.datatransfer.PermissionSyncRequest; import android.content.ComponentName; +import android.os.ParcelUuid; + /** * Interface for communication with the core companion device manager service. @@ -132,4 +135,10 @@ interface ICompanionDeviceManager { byte[] getBackupPayload(int userId); void applyRestoredPayload(in byte[] payload, int userId); + + @EnforcePermission("REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE") + void startObservingDevicePresence(in ObservingDevicePresenceRequest request, in String packageName, int userId); + + @EnforcePermission("REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE") + void stopObservingDevicePresence(in ObservingDevicePresenceRequest request, in String packageName, int userId); } diff --git a/core/java/android/companion/ICompanionDeviceService.aidl b/core/java/android/companion/ICompanionDeviceService.aidl index 2a311bf1152f..f5401d2e7dbd 100644 --- a/core/java/android/companion/ICompanionDeviceService.aidl +++ b/core/java/android/companion/ICompanionDeviceService.aidl @@ -17,10 +17,12 @@ package android.companion; import android.companion.AssociationInfo; +import android.companion.DevicePresenceEvent; +import android.os.ParcelUuid; /** @hide */ oneway interface ICompanionDeviceService { void onDeviceAppeared(in AssociationInfo associationInfo); void onDeviceDisappeared(in AssociationInfo associationInfo); - void onDeviceEvent(in AssociationInfo associationInfo, int state); + void onDevicePresenceEvent(in DevicePresenceEvent event); } diff --git a/core/java/android/companion/ObservingDevicePresenceRequest.aidl b/core/java/android/companion/ObservingDevicePresenceRequest.aidl new file mode 100644 index 000000000000..fed060759711 --- /dev/null +++ b/core/java/android/companion/ObservingDevicePresenceRequest.aidl @@ -0,0 +1,19 @@ + /* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + package android.companion; + + parcelable ObservingDevicePresenceRequest;
\ No newline at end of file diff --git a/core/java/android/companion/ObservingDevicePresenceRequest.java b/core/java/android/companion/ObservingDevicePresenceRequest.java new file mode 100644 index 000000000000..f1d594e80bda --- /dev/null +++ b/core/java/android/companion/ObservingDevicePresenceRequest.java @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.companion; + +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.RequiresPermission; +import android.os.Parcel; +import android.os.ParcelUuid; +import android.os.Parcelable; +import android.provider.OneTimeUseBuilder; + +import java.util.Objects; + +/** + * A request for setting the types of device for observing device presence. + * + * <p>Only supports association id or ParcelUuid and calling app must declare uses-permission + * {@link android.Manifest.permission#REQUEST_OBSERVE_DEVICE_UUID_PRESENCE} if using + * {@link Builder#setUuid(ParcelUuid)}.</p> + * + * Calling apps must use either ObservingDevicePresenceRequest.Builder#setUuid(ParcelUuid) or + * ObservingDevicePresenceRequest.Builder#setAssociationId(int), but not both. + * + * @see Builder#setUuid(ParcelUuid) + * @see Builder#setAssociationId(int) + * @see CompanionDeviceManager#startObservingDevicePresence(ObservingDevicePresenceRequest) + */ +@FlaggedApi(Flags.FLAG_DEVICE_PRESENCE) +public final class ObservingDevicePresenceRequest implements Parcelable { + private final int mAssociationId; + @Nullable private final ParcelUuid mUuid; + + private static final int PARCEL_UUID_NULL = 0; + + private static final int PARCEL_UUID_NOT_NULL = 1; + + private ObservingDevicePresenceRequest(int associationId, ParcelUuid uuid) { + mAssociationId = associationId; + mUuid = uuid; + } + + private ObservingDevicePresenceRequest(@NonNull Parcel in) { + mAssociationId = in.readInt(); + if (in.readInt() == PARCEL_UUID_NULL) { + mUuid = null; + } else { + mUuid = ParcelUuid.CREATOR.createFromParcel(in); + } + } + + /** + * @return the association id for observing device presence. It will return + * {@link DevicePresenceEvent#NO_ASSOCIATION} if using + * {@link Builder#setUuid(ParcelUuid)}. + */ + public int getAssociationId() { + return mAssociationId; + } + + /** + * @return the ParcelUuid for observing device presence. + */ + @Nullable + public ParcelUuid getUuid() { + return mUuid; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mAssociationId); + if (mUuid == null) { + // Write 0 to the parcel to indicate the ParcelUuid is null. + dest.writeInt(PARCEL_UUID_NULL); + } else { + dest.writeInt(PARCEL_UUID_NOT_NULL); + mUuid.writeToParcel(dest, flags); + } + + } + + @Override + public int describeContents() { + return 0; + } + + @NonNull + public static final Parcelable.Creator<ObservingDevicePresenceRequest> CREATOR = + new Parcelable.Creator<ObservingDevicePresenceRequest>() { + @Override + public ObservingDevicePresenceRequest[] newArray(int size) { + return new ObservingDevicePresenceRequest[size]; + } + + @Override + public ObservingDevicePresenceRequest createFromParcel(@NonNull Parcel in) { + return new ObservingDevicePresenceRequest(in); + } + }; + + @Override + public String toString() { + return "ObservingDevicePresenceRequest { " + + "Association Id= " + mAssociationId + "," + + "ParcelUuid= " + mUuid + "}"; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) return true; + if (!(o instanceof ObservingDevicePresenceRequest that)) return false; + + return Objects.equals(mUuid, that.mUuid) && mAssociationId == that.mAssociationId; + } + + @Override + public int hashCode() { + return Objects.hash(mAssociationId, mUuid); + } + + /** + * A builder for {@link ObservingDevicePresenceRequest} + */ + public static final class Builder extends OneTimeUseBuilder<ObservingDevicePresenceRequest> { + // Initial the association id to {@link DevicePresenceEvent.NO_ASSOCIATION} + // to indicate the value is not set yet. + private int mAssociationId = DevicePresenceEvent.NO_ASSOCIATION; + private ParcelUuid mUuid; + + public Builder() {} + + /** + * Set the association id to be observed for device presence. + * + * <p>The provided device must be {@link CompanionDeviceManager#associate associated} + * with the calling app before calling this method if using this API. + * + * Caller must implement a single {@link CompanionDeviceService} which will be bound to and + * receive callbacks to + * {@link CompanionDeviceService#onDevicePresenceEvent(DevicePresenceEvent)}.</p> + * + * <p>Calling apps must use either {@link #setUuid(ParcelUuid)} + * or this API, but not both.</p> + * + * @param associationId The association id for observing device presence. + */ + @NonNull + public Builder setAssociationId(int associationId) { + checkNotUsed(); + this.mAssociationId = associationId; + return this; + } + + /** + * Set the ParcelUuid to be observed for device presence. + * + * <p>It does not require to create the association before calling this API. + * This only supports classic Bluetooth scan and caller must implement + * a single {@link CompanionDeviceService} which will be bound to and receive callbacks to + * {@link CompanionDeviceService#onDevicePresenceEvent(DevicePresenceEvent)}.</p> + * + * <p>The Uuid should be matching one of the ParcelUuid form + * {@link android.bluetooth.BluetoothDevice#getUuids()}</p> + * + * <p>Calling apps must use either this API or {@link #setAssociationId(int)}, + * but not both.</p> + * + * @param uuid The ParcelUuid for observing device presence. + */ + @NonNull + @RequiresPermission(android.Manifest.permission.REQUEST_OBSERVE_DEVICE_UUID_PRESENCE) + public Builder setUuid(@NonNull ParcelUuid uuid) { + checkNotUsed(); + this.mUuid = uuid; + return this; + } + + @NonNull + @Override + public ObservingDevicePresenceRequest build() { + markUsed(); + if (mUuid != null && mAssociationId != DevicePresenceEvent.NO_ASSOCIATION) { + throw new IllegalStateException("Cannot observe device presence based on " + + "both ParcelUuid and association ID. Choose one or the other."); + } else if (mUuid == null && mAssociationId <= 0) { + throw new IllegalStateException("Must provide either a ParcelUuid or " + + "a valid association ID to observe device presence."); + } + + return new ObservingDevicePresenceRequest(mAssociationId, mUuid); + } + } +} diff --git a/core/java/android/companion/flags.aconfig b/core/java/android/companion/flags.aconfig index 9e410b86b6bd..d634b64b1a4e 100644 --- a/core/java/android/companion/flags.aconfig +++ b/core/java/android/companion/flags.aconfig @@ -33,4 +33,4 @@ flag { namespace: "companion" description: "Expose perm sync user consent API" bug: "309528663" -}
\ No newline at end of file +} diff --git a/core/java/android/companion/virtual/IVirtualDeviceManager.aidl b/core/java/android/companion/virtual/IVirtualDeviceManager.aidl index 325aa28fde08..83e18ec05599 100644 --- a/core/java/android/companion/virtual/IVirtualDeviceManager.aidl +++ b/core/java/android/companion/virtual/IVirtualDeviceManager.aidl @@ -133,4 +133,10 @@ interface IVirtualDeviceManager { * device. */ boolean isVirtualDeviceOwnedMirrorDisplay(int displayId); + + /** + * Returns all current persistent device IDs, including the ones for which no virtual device + * exists, as long as one may have existed or can be created. + */ + List<String> getAllPersistentDeviceIds(); } diff --git a/core/java/android/content/ContentProvider.java b/core/java/android/content/ContentProvider.java index c7a75ed5ea9c..e9b94c9f5791 100644 --- a/core/java/android/content/ContentProvider.java +++ b/core/java/android/content/ContentProvider.java @@ -41,6 +41,7 @@ import android.content.res.Configuration; import android.database.Cursor; import android.database.MatrixCursor; import android.database.SQLException; +import android.multiuser.Flags; import android.net.Uri; import android.os.AsyncTask; import android.os.Binder; @@ -146,6 +147,7 @@ public abstract class ContentProvider implements ContentInterface, ComponentCall private boolean mExported; private boolean mNoPerms; private boolean mSingleUser; + private boolean mSystemUserOnly; private SparseBooleanArray mUsersRedirectedToOwnerForMedia = new SparseBooleanArray(); private ThreadLocal<AttributionSource> mCallingAttributionSource; @@ -377,7 +379,9 @@ public abstract class ContentProvider implements ContentInterface, ComponentCall != PermissionChecker.PERMISSION_GRANTED && getContext().checkUriPermission(userUri, Binder.getCallingPid(), callingUid, Intent.FLAG_GRANT_READ_URI_PERMISSION) - != PackageManager.PERMISSION_GRANTED) { + != PackageManager.PERMISSION_GRANTED + && !deniedAccessSystemUserOnlyProvider(callingUserId, + mSystemUserOnly)) { FrameworkStatsLog.write(GET_TYPE_ACCESSED_WITHOUT_PERMISSION, enumCheckUriPermission, callingUid, uri.getAuthority(), type); @@ -865,6 +869,10 @@ public abstract class ContentProvider implements ContentInterface, ComponentCall boolean checkUser(int pid, int uid, Context context) { final int callingUserId = UserHandle.getUserId(uid); + if (deniedAccessSystemUserOnlyProvider(callingUserId, mSystemUserOnly)) { + return false; + } + if (callingUserId == context.getUserId() || mSingleUser) { return true; } @@ -987,6 +995,9 @@ public abstract class ContentProvider implements ContentInterface, ComponentCall // last chance, check against any uri grants final int callingUserId = UserHandle.getUserId(uid); + if (deniedAccessSystemUserOnlyProvider(callingUserId, mSystemUserOnly)) { + return PermissionChecker.PERMISSION_HARD_DENIED; + } final Uri userUri = (mSingleUser && !UserHandle.isSameUser(mMyUid, uid)) ? maybeAddUserId(uri, callingUserId) : uri; if (context.checkUriPermission(userUri, pid, uid, Intent.FLAG_GRANT_READ_URI_PERMISSION) @@ -2623,6 +2634,7 @@ public abstract class ContentProvider implements ContentInterface, ComponentCall setPathPermissions(info.pathPermissions); mExported = info.exported; mSingleUser = (info.flags & ProviderInfo.FLAG_SINGLE_USER) != 0; + mSystemUserOnly = (info.flags & ProviderInfo.FLAG_SYSTEM_USER_ONLY) != 0; setAuthorities(info.authority); } if (Build.IS_DEBUGGABLE) { @@ -2756,6 +2768,11 @@ public abstract class ContentProvider implements ContentInterface, ComponentCall String auth = uri.getAuthority(); if (!mSingleUser) { int userId = getUserIdFromAuthority(auth, UserHandle.USER_CURRENT); + if (deniedAccessSystemUserOnlyProvider(mContext.getUserId(), + mSystemUserOnly)) { + throw new SecurityException("Trying to query a SYSTEM user only content" + + " provider from user:" + mContext.getUserId()); + } if (userId != UserHandle.USER_CURRENT && userId != mContext.getUserId() // Since userId specified in content uri, the provider userId would be @@ -2929,4 +2946,16 @@ public abstract class ContentProvider implements ContentInterface, ComponentCall Trace.traceBegin(traceTag, methodName + subInfo); } } + /** + * Return true if access to content provider is denied because it's a SYSTEM user only + * provider and the calling user is not the SYSTEM user. + * + * @param callingUserId UserId of the caller accessing the content provider. + * @param systemUserOnly true when the content provider is only available for the SYSTEM user. + */ + private static boolean deniedAccessSystemUserOnlyProvider(int callingUserId, + boolean systemUserOnly) { + return Flags.enableSystemUserOnlyForServicesAndProviders() + && (callingUserId != UserHandle.USER_SYSTEM && systemUserOnly); + } } diff --git a/core/java/android/content/pm/ILauncherApps.aidl b/core/java/android/content/pm/ILauncherApps.aidl index 62db65f15df3..a97de6368b8c 100644 --- a/core/java/android/content/pm/ILauncherApps.aidl +++ b/core/java/android/content/pm/ILauncherApps.aidl @@ -128,6 +128,4 @@ interface ILauncherApps { /** Unregister a callback, so that it won't be called when LauncherApps dumps. */ void unRegisterDumpCallback(IDumpCallback cb); - - void setArchiveCompatibilityOptions(boolean enableIconOverlay, boolean enableUnarchivalConfirmation); } diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl index 380de965b143..6dc8d4738c87 100644 --- a/core/java/android/content/pm/IPackageManager.aidl +++ b/core/java/android/content/pm/IPackageManager.aidl @@ -840,7 +840,7 @@ interface IPackageManager { ArchivedPackageParcel getArchivedPackage(in String packageName, int userId); - Bitmap getArchivedAppIcon(String packageName, in UserHandle user, String callingPackageName); + Bitmap getArchivedAppIcon(String packageName, in UserHandle user); boolean isAppArchivable(String packageName, in UserHandle user); } diff --git a/core/java/android/content/pm/LauncherApps.java b/core/java/android/content/pm/LauncherApps.java index 50be983ec938..1d2b1aff46bc 100644 --- a/core/java/android/content/pm/LauncherApps.java +++ b/core/java/android/content/pm/LauncherApps.java @@ -1801,31 +1801,6 @@ public class LauncherApps { } } - /** - * Enable or disable different archive compatibility options of the launcher. - * - * @param enableIconOverlay Provides a cloud overlay for archived apps to ensure users are aware - * that a certain app is archived. True by default. - * Launchers might want to disable this operation if they want to provide custom user experience - * to differentiate archived apps. - * @param enableUnarchivalConfirmation If true, the user is shown a confirmation dialog when - * they click an archived app, which explains that the app will be downloaded and restored in - * the background. True by default. - * Launchers might want to disable this operation if they provide sufficient, alternative user - * guidance to highlight that an unarchival is starting and ongoing once an archived app is - * tapped. E.g., this could be achieved by showing the unarchival progress around the icon. - */ - @FlaggedApi(android.content.pm.Flags.FLAG_ARCHIVING) - public void setArchiveCompatibilityOptions(boolean enableIconOverlay, - boolean enableUnarchivalConfirmation) { - try { - mService.setArchiveCompatibilityOptions(enableIconOverlay, - enableUnarchivalConfirmation); - } catch (RemoteException re) { - throw re.rethrowFromSystemServer(); - } - } - /** @return position in mCallbacks for callback or -1 if not present. */ private int findCallbackLocked(Callback callback) { if (callback == null) { diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java index 22926febfb2c..c4bf18d70242 100644 --- a/core/java/android/content/pm/PackageInstaller.java +++ b/core/java/android/content/pm/PackageInstaller.java @@ -738,7 +738,7 @@ public class PackageInstaller { /** * The set of error types that can be set for - * {@link #reportUnarchivalStatus(int, int, PendingIntent)}. + * {@link #reportUnarchivalState}. * * @hide */ @@ -2421,6 +2421,7 @@ public class PackageInstaller { * facilitate the unarchival flow (e.g. user needs to log in). * @throws PackageManager.NameNotFoundException if no unarchival with {@code unarchiveId} exists */ + // TODO(b/314960798) Remove old API once it's unused @RequiresPermission(anyOf = { Manifest.permission.INSTALL_PACKAGES, Manifest.permission.REQUEST_INSTALL_PACKAGES}) @@ -2438,6 +2439,30 @@ public class PackageInstaller { } } + /** + * Reports the state of an unarchival to the system. + * + * @see UnarchivalState for the different state options. + * @throws PackageManager.NameNotFoundException if no unarchival with {@code unarchiveId} exists + */ + @RequiresPermission(anyOf = { + Manifest.permission.INSTALL_PACKAGES, + Manifest.permission.REQUEST_INSTALL_PACKAGES}) + @FlaggedApi(Flags.FLAG_ARCHIVING) + public void reportUnarchivalState(@NonNull UnarchivalState unarchivalState) + throws PackageManager.NameNotFoundException { + Objects.requireNonNull(unarchivalState); + try { + mInstaller.reportUnarchivalStatus(unarchivalState.getUnarchiveId(), + unarchivalState.getStatus(), unarchivalState.getRequiredStorageBytes(), + unarchivalState.getUserActionIntent(), new UserHandle(mUserId)); + } catch (ParcelableException e) { + e.maybeRethrow(PackageManager.NameNotFoundException.class); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + // (b/239722738) This class serves as a bridge between the PackageLite class, which // is a hidden class, and the consumers of this class. (e.g. InstallInstalling.java) // This is a part of an effort to remove dependency on hidden APIs and use SystemAPIs or @@ -4741,10 +4766,10 @@ public class PackageInstaller { codegenVersion = "1.0.23", sourceFile = "frameworks/base/core/java/android/content/pm/PackageInstaller.java", inputSignatures = "private final @android.annotation.Nullable android.graphics.Bitmap mIcon\nprivate final @android.annotation.NonNull java.lang.CharSequence mLabel\nprivate final @android.annotation.NonNull android.icu.util.ULocale mLocale\nprivate final @android.annotation.NonNull java.lang.String mPackageName\npublic static final @android.annotation.NonNull android.os.Parcelable.Creator<android.content.pm.PackageInstaller.PreapprovalDetails> CREATOR\npublic @java.lang.Override void writeToParcel(android.os.Parcel,int)\npublic @java.lang.Override int describeContents()\nclass PreapprovalDetails extends java.lang.Object implements [android.os.Parcelable]\nprivate @android.annotation.Nullable android.graphics.Bitmap mIcon\nprivate @android.annotation.NonNull java.lang.CharSequence mLabel\nprivate @android.annotation.NonNull android.icu.util.ULocale mLocale\nprivate @android.annotation.NonNull java.lang.String mPackageName\nprivate long mBuilderFieldsSet\npublic @android.annotation.NonNull android.content.pm.PackageInstaller.PreapprovalDetails.Builder setIcon(android.graphics.Bitmap)\npublic @android.annotation.NonNull android.content.pm.PackageInstaller.PreapprovalDetails.Builder setLabel(java.lang.CharSequence)\npublic @android.annotation.NonNull android.content.pm.PackageInstaller.PreapprovalDetails.Builder setLocale(android.icu.util.ULocale)\npublic @android.annotation.NonNull android.content.pm.PackageInstaller.PreapprovalDetails.Builder setPackageName(java.lang.String)\npublic @android.annotation.NonNull android.content.pm.PackageInstaller.PreapprovalDetails build()\nprivate void checkNotUsed()\nclass Builder extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genConstructor=false, genToString=true)") + @Deprecated private void __metadata() {} - //@formatter:on // End of generated code @@ -5135,13 +5160,188 @@ public class PackageInstaller { codegenVersion = "1.0.23", sourceFile = "frameworks/base/core/java/android/content/pm/PackageInstaller.java", inputSignatures = "public static final @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints GENTLE_UPDATE\nprivate final boolean mDeviceIdleRequired\nprivate final boolean mAppNotForegroundRequired\nprivate final boolean mAppNotInteractingRequired\nprivate final boolean mAppNotTopVisibleRequired\nprivate final boolean mNotInCallRequired\nclass InstallConstraints extends java.lang.Object implements [android.os.Parcelable]\nprivate boolean mDeviceIdleRequired\nprivate boolean mAppNotForegroundRequired\nprivate boolean mAppNotInteractingRequired\nprivate boolean mAppNotTopVisibleRequired\nprivate boolean mNotInCallRequired\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder setDeviceIdleRequired()\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder setAppNotForegroundRequired()\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder setAppNotInteractingRequired()\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder setAppNotTopVisibleRequired()\npublic @android.annotation.SuppressLint @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints.Builder setNotInCallRequired()\npublic @android.annotation.NonNull android.content.pm.PackageInstaller.InstallConstraints build()\nclass Builder extends java.lang.Object implements []\n@com.android.internal.util.DataClass(genParcelable=true, genHiddenConstructor=true, genEqualsHashCode=true)") + @Deprecated private void __metadata() {} - //@formatter:on // End of generated code } + /** + * Used to communicate the unarchival state in {@link #reportUnarchivalState}. + */ + @FlaggedApi(Flags.FLAG_ARCHIVING) + public static final class UnarchivalState { + + /** + * The caller is able to facilitate the unarchival for the given {@code unarchiveId}. + * + * @param unarchiveId the ID provided by the system as part of the intent.action.UNARCHIVE + * broadcast with EXTRA_UNARCHIVE_ID. + */ + @NonNull + public static UnarchivalState createOkState(int unarchiveId) { + return new UnarchivalState(unarchiveId, UNARCHIVAL_OK, /* requiredStorageBytes= */ -1, + /* userActionIntent= */ null); + } + + /** + * User action is required before commencing with the unarchival for the given + * {@code unarchiveId}. E.g., this could be used if it's necessary for the user to sign-in + * first. + * + * @param unarchiveId the ID provided by the system as part of the + * intent.action.UNARCHIVE + * broadcast with EXTRA_UNARCHIVE_ID. + * @param userActionIntent optional intent to start a follow up action required to + * facilitate the unarchival flow (e.g. user needs to log in). + */ + @NonNull + public static UnarchivalState createUserActionRequiredState(int unarchiveId, + @NonNull PendingIntent userActionIntent) { + Objects.requireNonNull(userActionIntent); + return new UnarchivalState(unarchiveId, UNARCHIVAL_ERROR_USER_ACTION_NEEDED, + /* requiredStorageBytes= */ -1, userActionIntent); + } + + /** + * There is not enough storage to start the unarchival for the given {@code unarchiveId}. + * + * @param unarchiveId the ID provided by the system as part of the + * intent.action.UNARCHIVE + * broadcast with EXTRA_UNARCHIVE_ID. + * @param requiredStorageBytes ff the error is UNARCHIVAL_ERROR_INSUFFICIENT_STORAGE this + * field should be set to specify how many additional bytes of + * storage are required to unarchive the app. + * @param userActionIntent can optionally be set to provide a custom storage-clearing + * action. + */ + @NonNull + public static UnarchivalState createInsufficientStorageState(int unarchiveId, + long requiredStorageBytes, @Nullable PendingIntent userActionIntent) { + return new UnarchivalState(unarchiveId, UNARCHIVAL_ERROR_INSUFFICIENT_STORAGE, + requiredStorageBytes, userActionIntent); + } + + /** + * The device has no data connectivity and unarchival cannot be started for the given + * {@code unarchiveId}. + * + * @param unarchiveId the ID provided by the system as part of the intent.action.UNARCHIVE + * broadcast with EXTRA_UNARCHIVE_ID. + */ + @NonNull + public static UnarchivalState createNoConnectivityState(int unarchiveId) { + return new UnarchivalState(unarchiveId, UNARCHIVAL_ERROR_NO_CONNECTIVITY, + /* requiredStorageBytes= */ -1,/* userActionIntent= */ null); + } + + /** + * Generic error state for all cases that are not covered by other methods in this class. + * + * @param unarchiveId the ID provided by the system as part of the intent.action.UNARCHIVE + * broadcast with EXTRA_UNARCHIVE_ID. + */ + @NonNull + public static UnarchivalState createGenericErrorState(int unarchiveId) { + return new UnarchivalState(unarchiveId, UNARCHIVAL_GENERIC_ERROR, + /* requiredStorageBytes= */ -1,/* userActionIntent= */ null); + } + + + /** + * The ID provided by the system as part of the intent.action.UNARCHIVE broadcast with + * EXTRA_UNARCHIVE_ID. + */ + private final int mUnarchiveId; + + /** Used for the system to provide the user with necessary follow-up steps or errors. */ + @UnarchivalStatus + private final int mStatus; + + /** + * If the error is UNARCHIVAL_ERROR_INSUFFICIENT_STORAGE this field should be set to specify + * how many additional bytes of storage are required to unarchive the app. + */ + private final long mRequiredStorageBytes; + + /** + * Optional intent to start a follow up action required to facilitate the unarchival flow + * (e.g., user needs to log in). + */ + @Nullable + private final PendingIntent mUserActionIntent; + + /** + * Creates a new UnarchivalState. + * + * @param unarchiveId The ID provided by the system as part of the + * intent.action.UNARCHIVE broadcast with + * EXTRA_UNARCHIVE_ID. + * @param status Used for the system to provide the user with necessary + * follow-up steps or errors. + * @param requiredStorageBytes If the error is UNARCHIVAL_ERROR_INSUFFICIENT_STORAGE this + * field should be set to specify + * how many additional bytes of storage are required to + * unarchive the app. + * @param userActionIntent Optional intent to start a follow up action required to + * facilitate the unarchival flow + * (e.g,. user needs to log in). + * @hide + */ + private UnarchivalState( + int unarchiveId, + @UnarchivalStatus int status, + long requiredStorageBytes, + @Nullable PendingIntent userActionIntent) { + this.mUnarchiveId = unarchiveId; + this.mStatus = status; + com.android.internal.util.AnnotationValidations.validate( + UnarchivalStatus.class, null, mStatus); + this.mRequiredStorageBytes = requiredStorageBytes; + this.mUserActionIntent = userActionIntent; + } + + /** + * The ID provided by the system as part of the intent.action.UNARCHIVE broadcast with + * EXTRA_UNARCHIVE_ID. + * + * @hide + */ + int getUnarchiveId() { + return mUnarchiveId; + } + + /** + * Used for the system to provide the user with necessary follow-up steps or errors. + * + * @hide + */ + @UnarchivalStatus int getStatus() { + return mStatus; + } + + /** + * If the error is UNARCHIVAL_ERROR_INSUFFICIENT_STORAGE this field should be set to specify + * how many additional bytes of storage are required to unarchive the app. + * + * @hide + */ + long getRequiredStorageBytes() { + return mRequiredStorageBytes; + } + + /** + * Optional intent to start a follow up action required to facilitate the unarchival flow + * (e.g. user needs to log in). + * + * @hide + */ + @Nullable PendingIntent getUserActionIntent() { + return mUserActionIntent; + } + } + } diff --git a/core/java/android/content/pm/ProviderInfo.java b/core/java/android/content/pm/ProviderInfo.java index 9e553dbfb719..de33fa8b2328 100644 --- a/core/java/android/content/pm/ProviderInfo.java +++ b/core/java/android/content/pm/ProviderInfo.java @@ -89,6 +89,15 @@ public final class ProviderInfo extends ComponentInfo public static final int FLAG_VISIBLE_TO_INSTANT_APP = 0x100000; /** + * Bit in {@link #flags}: If set, this provider will only be available + * for the system user. + * Set from the android.R.attr#systemUserOnly attribute. + * In Sync with {@link ActivityInfo#FLAG_SYSTEM_USER_ONLY} + * @hide + */ + public static final int FLAG_SYSTEM_USER_ONLY = ActivityInfo.FLAG_SYSTEM_USER_ONLY; + + /** * Bit in {@link #flags}: If set, a single instance of the provider will * run for all users on the device. Set from the * {@link android.R.attr#singleUser} attribute. diff --git a/core/java/android/content/pm/ServiceInfo.java b/core/java/android/content/pm/ServiceInfo.java index ae46c027505e..2b378b1f09d0 100644 --- a/core/java/android/content/pm/ServiceInfo.java +++ b/core/java/android/content/pm/ServiceInfo.java @@ -101,6 +101,14 @@ public class ServiceInfo extends ComponentInfo public static final int FLAG_VISIBLE_TO_INSTANT_APP = 0x100000; /** + * @hide Bit in {@link #flags}: If set, this service will only be available + * for the system user. + * Set from the android.R.attr#systemUserOnly attribute. + * In Sync with {@link ActivityInfo#FLAG_SYSTEM_USER_ONLY} + */ + public static final int FLAG_SYSTEM_USER_ONLY = ActivityInfo.FLAG_SYSTEM_USER_ONLY; + + /** * Bit in {@link #flags}: If set, a single instance of the service will * run for all users on the device. Set from the * {@link android.R.attr#singleUser} attribute. diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig index e4e9fbaf2c55..903875bef1fc 100644 --- a/core/java/android/content/pm/flags.aconfig +++ b/core/java/android/content/pm/flags.aconfig @@ -153,3 +153,11 @@ flag { bug: "291135724" is_fixed_read_only: true } + +flag { + name: "fix_system_apps_first_install_time" + namespace: "package_manager_service" + description: "Feature flag to fix the first-install timestamps for system apps." + bug: "321258605" + is_fixed_read_only: true +} diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig index 5bfc012844f8..9644d8095a4d 100644 --- a/core/java/android/content/pm/multiuser.aconfig +++ b/core/java/android/content/pm/multiuser.aconfig @@ -84,4 +84,11 @@ flag { namespace: "profile_experiences" description: "Enable auto-locking private space on device restarts" bug: "296993385" -}
\ No newline at end of file +} +flag { + name: "enable_system_user_only_for_services_and_providers" + namespace: "multiuser" + description: "Enable systemUserOnly manifest attribute for services and providers." + bug: "302354856" + is_fixed_read_only: true +} diff --git a/core/java/android/content/pm/overlay/OverlayPaths.java b/core/java/android/content/pm/overlay/OverlayPaths.java index a4db733af013..bd74b0b9293c 100644 --- a/core/java/android/content/pm/overlay/OverlayPaths.java +++ b/core/java/android/content/pm/overlay/OverlayPaths.java @@ -49,6 +49,13 @@ public class OverlayPaths { public static class Builder { final OverlayPaths mPaths = new OverlayPaths(); + public Builder() {} + + public Builder(@NonNull OverlayPaths base) { + mPaths.mResourceDirs.addAll(base.getResourceDirs()); + mPaths.mOverlayPaths.addAll(base.getOverlayPaths()); + } + /** * Adds a non-APK path to the contents of {@link OverlayPaths#getOverlayPaths()}. */ diff --git a/core/java/android/credentials/CredentialManager.java b/core/java/android/credentials/CredentialManager.java index 796a57bf6880..2e63664df7aa 100644 --- a/core/java/android/credentials/CredentialManager.java +++ b/core/java/android/credentials/CredentialManager.java @@ -32,6 +32,7 @@ import android.content.ComponentName; import android.content.Context; import android.content.IntentSender; import android.os.Binder; +import android.os.Bundle; import android.os.CancellationSignal; import android.os.IBinder; import android.os.ICancellationSignal; @@ -58,6 +59,9 @@ import java.util.concurrent.Executor; @SystemService(Context.CREDENTIAL_SERVICE) public final class CredentialManager { private static final String TAG = "CredentialManager"; + private static final Bundle OPTIONS_SENDER_BAL_OPTIN = ActivityOptions.makeBasic() + .setPendingIntentBackgroundActivityStartMode( + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED).toBundle(); /** @hide */ @IntDef( @@ -757,9 +761,7 @@ public final class CredentialManager { public void onPendingIntent(PendingIntent pendingIntent) { try { mContext.startIntentSender(pendingIntent.getIntentSender(), null, 0, 0, 0, - ActivityOptions.makeBasic() - .setPendingIntentBackgroundActivityStartMode( - ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED).toBundle()); + OPTIONS_SENDER_BAL_OPTIN); } catch (IntentSender.SendIntentException e) { Log.e( TAG, @@ -817,7 +819,8 @@ public final class CredentialManager { @Override public void onPendingIntent(PendingIntent pendingIntent) { try { - mContext.startIntentSender(pendingIntent.getIntentSender(), null, 0, 0, 0); + mContext.startIntentSender(pendingIntent.getIntentSender(), null, 0, 0, 0, + OPTIONS_SENDER_BAL_OPTIN); } catch (IntentSender.SendIntentException e) { Log.e( TAG, diff --git a/core/java/android/credentials/PrepareGetCredentialResponse.java b/core/java/android/credentials/PrepareGetCredentialResponse.java index 212f5716d041..75d671bbb71b 100644 --- a/core/java/android/credentials/PrepareGetCredentialResponse.java +++ b/core/java/android/credentials/PrepareGetCredentialResponse.java @@ -22,9 +22,11 @@ import android.annotation.CallbackExecutor; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; +import android.app.ActivityOptions; import android.app.PendingIntent; import android.content.Context; import android.content.IntentSender; +import android.os.Bundle; import android.os.CancellationSignal; import android.os.OutcomeReceiver; import android.util.Log; @@ -41,6 +43,10 @@ import java.util.concurrent.Executor; */ public final class PrepareGetCredentialResponse { + private static final Bundle OPTIONS_SENDER_BAL_OPTIN = ActivityOptions.makeBasic() + .setPendingIntentBackgroundActivityStartMode( + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED).toBundle(); + /** * A handle that represents a pending get-credential operation. Pass this handle to {@link * CredentialManager#getCredential(Context, PendingGetCredentialHandle, CancellationSignal, @@ -80,7 +86,8 @@ public final class PrepareGetCredentialResponse { @Override public void onPendingIntent(PendingIntent pendingIntent) { try { - context.startIntentSender(pendingIntent.getIntentSender(), null, 0, 0, 0); + context.startIntentSender(pendingIntent.getIntentSender(), null, 0, 0, 0, + OPTIONS_SENDER_BAL_OPTIN); } catch (IntentSender.SendIntentException e) { Log.e(TAG, "startIntentSender() failed for intent for show()", e); executor.execute(() -> callback.onError( @@ -101,7 +108,8 @@ public final class PrepareGetCredentialResponse { }); try { - context.startIntentSender(mPendingIntent.getIntentSender(), null, 0, 0, 0); + context.startIntentSender(mPendingIntent.getIntentSender(), null, 0, 0, 0, + OPTIONS_SENDER_BAL_OPTIN); } catch (IntentSender.SendIntentException e) { Log.e(TAG, "startIntentSender() failed for intent for show()", e); executor.execute(() -> callback.onError( diff --git a/core/java/android/hardware/HardwareBuffer.java b/core/java/android/hardware/HardwareBuffer.java index f5b3a7b56302..0047b7d69282 100644 --- a/core/java/android/hardware/HardwareBuffer.java +++ b/core/java/android/hardware/HardwareBuffer.java @@ -67,8 +67,8 @@ public final class HardwareBuffer implements Parcelable, AutoCloseable { S_UI8, YCBCR_P010, R_8, - R_16_UINT, - RG_1616_UINT, + R_16, + RG_1616, RGBA_10101010, }) public @interface Format { @@ -119,13 +119,13 @@ public final class HardwareBuffer implements Parcelable, AutoCloseable { * implicit unsigned normalized. */ @FlaggedApi(com.android.graphics.hwui.flags.Flags.FLAG_REQUESTED_FORMATS_V) - public static final int R_16_UINT = 0x39; + public static final int R_16 = 0x39; /** * Format: 16 bits each red, green. Bits should be represented in unsigned integer, * instead of the implicit unsigned normalized. */ @FlaggedApi(com.android.graphics.hwui.flags.Flags.FLAG_REQUESTED_FORMATS_V) - public static final int RG_1616_UINT = 0x3a; + public static final int RG_1616 = 0x3a; /** Format: 10 bits each red, green, blue, alpha */ @FlaggedApi(com.android.graphics.hwui.flags.Flags.FLAG_REQUESTED_FORMATS_V) public static final int RGBA_10101010 = 0x3b; diff --git a/core/java/android/hardware/biometrics/AuthenticationStateListener.aidl b/core/java/android/hardware/biometrics/AuthenticationStateListener.aidl index 73ac333cfd89..d51e62e709c2 100644 --- a/core/java/android/hardware/biometrics/AuthenticationStateListener.aidl +++ b/core/java/android/hardware/biometrics/AuthenticationStateListener.aidl @@ -33,4 +33,20 @@ oneway interface AuthenticationStateListener { * Defines behavior in response to authentication stopping */ void onAuthenticationStopped(); + + /** + * Defines behavior in response to a successful authentication + * @param requestReason Reason from [BiometricRequestConstants.RequestReason] for the requested + * authentication + * @param userId The user Id for the requested authentication + */ + void onAuthenticationSucceeded(int requestReason, int userId); + + /** + * Defines behavior in response to a failed authentication + * @param requestReason Reason from [BiometricRequestConstants.RequestReason] for the requested + * authentication + * @param userId The user Id for the requested authentication + */ + void onAuthenticationFailed(int requestReason, int userId); } diff --git a/core/java/android/hardware/biometrics/PromptContentItemBulletedText.java b/core/java/android/hardware/biometrics/PromptContentItemBulletedText.java index c5e5a8076747..25e5cca485d2 100644 --- a/core/java/android/hardware/biometrics/PromptContentItemBulletedText.java +++ b/core/java/android/hardware/biometrics/PromptContentItemBulletedText.java @@ -28,14 +28,14 @@ import android.os.Parcelable; */ @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) public final class PromptContentItemBulletedText implements PromptContentItemParcelable { - private final CharSequence mText; + private final String mText; /** * A list item with bulleted text shown on {@link PromptVerticalListContentView}. * * @param text The text of this list item. */ - public PromptContentItemBulletedText(@NonNull CharSequence text) { + public PromptContentItemBulletedText(@NonNull String text) { mText = text; } @@ -43,7 +43,7 @@ public final class PromptContentItemBulletedText implements PromptContentItemPar * @hide */ @NonNull - public CharSequence getText() { + public String getText() { return mText; } @@ -60,7 +60,7 @@ public final class PromptContentItemBulletedText implements PromptContentItemPar */ @Override public void writeToParcel(@NonNull Parcel dest, int flags) { - dest.writeCharSequence(mText); + dest.writeString(mText); } /** @@ -70,7 +70,7 @@ public final class PromptContentItemBulletedText implements PromptContentItemPar public static final Creator<PromptContentItemBulletedText> CREATOR = new Creator<>() { @Override public PromptContentItemBulletedText createFromParcel(Parcel in) { - return new PromptContentItemBulletedText(in.readCharSequence()); + return new PromptContentItemBulletedText(in.readString()); } @Override diff --git a/core/java/android/hardware/biometrics/PromptContentItemPlainText.java b/core/java/android/hardware/biometrics/PromptContentItemPlainText.java index 6434c5975c12..7919256f9c6d 100644 --- a/core/java/android/hardware/biometrics/PromptContentItemPlainText.java +++ b/core/java/android/hardware/biometrics/PromptContentItemPlainText.java @@ -28,14 +28,14 @@ import android.os.Parcelable; */ @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) public final class PromptContentItemPlainText implements PromptContentItemParcelable { - private final CharSequence mText; + private final String mText; /** * A list item with plain text shown on {@link PromptVerticalListContentView}. * * @param text The text of this list item. */ - public PromptContentItemPlainText(@NonNull CharSequence text) { + public PromptContentItemPlainText(@NonNull String text) { mText = text; } @@ -43,7 +43,7 @@ public final class PromptContentItemPlainText implements PromptContentItemParcel * @hide */ @NonNull - public CharSequence getText() { + public String getText() { return mText; } @@ -60,7 +60,7 @@ public final class PromptContentItemPlainText implements PromptContentItemParcel */ @Override public void writeToParcel(@NonNull Parcel dest, int flags) { - dest.writeCharSequence(mText); + dest.writeString(mText); } /** @@ -70,7 +70,7 @@ public final class PromptContentItemPlainText implements PromptContentItemParcel public static final Creator<PromptContentItemPlainText> CREATOR = new Creator<>() { @Override public PromptContentItemPlainText createFromParcel(Parcel in) { - return new PromptContentItemPlainText(in.readCharSequence()); + return new PromptContentItemPlainText(in.readString()); } @Override diff --git a/core/java/android/hardware/biometrics/PromptVerticalListContentView.java b/core/java/android/hardware/biometrics/PromptVerticalListContentView.java index f3e62907d845..38d32dc73ccb 100644 --- a/core/java/android/hardware/biometrics/PromptVerticalListContentView.java +++ b/core/java/android/hardware/biometrics/PromptVerticalListContentView.java @@ -52,11 +52,11 @@ public final class PromptVerticalListContentView implements PromptContentViewPar private static final int MAX_ITEM_NUMBER = 20; private static final int MAX_EACH_ITEM_CHARACTER_NUMBER = 640; private final List<PromptContentItemParcelable> mContentList; - private final CharSequence mDescription; + private final String mDescription; private PromptVerticalListContentView( @NonNull List<PromptContentItemParcelable> contentList, - @NonNull CharSequence description) { + @NonNull String description) { mContentList = contentList; mDescription = description; } @@ -65,7 +65,7 @@ public final class PromptVerticalListContentView implements PromptContentViewPar mContentList = in.readArrayList( PromptContentItemParcelable.class.getClassLoader(), PromptContentItemParcelable.class); - mDescription = in.readCharSequence(); + mDescription = in.readString(); } /** @@ -84,12 +84,12 @@ public final class PromptVerticalListContentView implements PromptContentViewPar /** * Gets the description for the content view, as set by - * {@link PromptVerticalListContentView.Builder#setDescription(CharSequence)}. + * {@link PromptVerticalListContentView.Builder#setDescription(String)}. * * @return The description for the content view, or null if the content view has no description. */ @Nullable - public CharSequence getDescription() { + public String getDescription() { return mDescription; } @@ -118,7 +118,7 @@ public final class PromptVerticalListContentView implements PromptContentViewPar @Override public void writeToParcel(@androidx.annotation.NonNull Parcel dest, int flags) { dest.writeList(mContentList); - dest.writeCharSequence(mDescription); + dest.writeString(mDescription); } /** @@ -143,7 +143,7 @@ public final class PromptVerticalListContentView implements PromptContentViewPar */ public static final class Builder { private final List<PromptContentItemParcelable> mContentList = new ArrayList<>(); - private CharSequence mDescription; + private String mDescription; /** * Optional: Sets a description that will be shown on the content view. @@ -152,7 +152,7 @@ public final class PromptVerticalListContentView implements PromptContentViewPar * @return This builder. */ @NonNull - public Builder setDescription(@NonNull CharSequence description) { + public Builder setDescription(@NonNull String description) { mDescription = description; return this; } diff --git a/core/java/android/hardware/face/IFaceService.aidl b/core/java/android/hardware/face/IFaceService.aidl index e267e6b22f9d..8e234fa11866 100644 --- a/core/java/android/hardware/face/IFaceService.aidl +++ b/core/java/android/hardware/face/IFaceService.aidl @@ -15,6 +15,7 @@ */ package android.hardware.face; +import android.hardware.biometrics.AuthenticationStateListener; import android.hardware.biometrics.IBiometricSensorReceiver; import android.hardware.biometrics.IBiometricServiceLockoutResetCallback; import android.hardware.biometrics.IBiometricStateListener; @@ -181,6 +182,14 @@ interface IFaceService { // authenticators. The callback is automatically removed after it's invoked. void addAuthenticatorsRegisteredCallback(IFaceAuthenticatorsRegisteredCallback callback); + // Registers AuthenticationStateListener. + @EnforcePermission("USE_BIOMETRIC_INTERNAL") + void registerAuthenticationStateListener(AuthenticationStateListener listener); + + // Unregisters AuthenticationStateListener. + @EnforcePermission("USE_BIOMETRIC_INTERNAL") + void unregisterAuthenticationStateListener(AuthenticationStateListener listener); + // Registers BiometricStateListener. void registerBiometricStateListener(IBiometricStateListener listener); diff --git a/core/java/android/metrics/LogMaker.java b/core/java/android/metrics/LogMaker.java index 8644d9103dcb..f65b713f8967 100644 --- a/core/java/android/metrics/LogMaker.java +++ b/core/java/android/metrics/LogMaker.java @@ -32,6 +32,7 @@ import java.util.Arrays; * @hide */ @SystemApi +@android.ravenwood.annotation.RavenwoodKeepWholeClass public class LogMaker { private static final String TAG = "LogBuilder"; diff --git a/core/java/android/net/NetworkPolicyManager.java b/core/java/android/net/NetworkPolicyManager.java index 365f9130ddd7..594ec18d9996 100644 --- a/core/java/android/net/NetworkPolicyManager.java +++ b/core/java/android/net/NetworkPolicyManager.java @@ -16,6 +16,7 @@ package android.net; +import static android.app.ActivityManager.PROCESS_CAPABILITY_POWER_RESTRICTED_NETWORK; import static android.app.ActivityManager.PROCESS_STATE_UNKNOWN; import static android.app.ActivityManager.procStateToString; import static android.content.pm.PackageManager.GET_SIGNATURES; @@ -170,6 +171,8 @@ public class NetworkPolicyManager { public static final String FIREWALL_CHAIN_NAME_RESTRICTED = "restricted"; /** @hide */ public static final String FIREWALL_CHAIN_NAME_LOW_POWER_STANDBY = "low_power_standby"; + /** @hide */ + public static final String FIREWALL_CHAIN_NAME_BACKGROUND = "background"; private static final boolean ALLOW_PLATFORM_APP_POLICY = true; @@ -180,6 +183,9 @@ public class NetworkPolicyManager { /** @hide */ public static final int TOP_THRESHOLD_STATE = ActivityManager.PROCESS_STATE_BOUND_TOP; + /** @hide */ + public static final int BACKGROUND_THRESHOLD_STATE = ActivityManager.PROCESS_STATE_TOP_SLEEPING; + /** * {@link Intent} extra that indicates which {@link NetworkTemplate} rule it * applies to. @@ -264,6 +270,16 @@ public class NetworkPolicyManager { * @hide */ public static final int ALLOWED_REASON_LOW_POWER_STANDBY_ALLOWLIST = 1 << 6; + + /** + * Flag to indicate that the app is exempt from always-on background network restrictions. + * Note that this is explicitly different to the flag NOT_FOREGROUND which is used to grant + * shared exception to apps from power restrictions like doze, battery saver and app-standby. + * + * @hide + */ + public static final int ALLOWED_REASON_NOT_IN_BACKGROUND = 1 << 7; + /** * Flag to indicate that app is exempt from certain metered network restrictions because user * explicitly exempted it. @@ -822,6 +838,21 @@ public class NetworkPolicyManager { } /** + * This is currently only used as an implementation detail for + * {@link com.android.server.net.NetworkPolicyManagerService}. + * Only put here to be together with other isProcStateAllowed* methods. + * + * @hide + */ + public static boolean isProcStateAllowedNetworkWhileBackground(@Nullable UidState uidState) { + if (uidState == null) { + return false; + } + return uidState.procState < BACKGROUND_THRESHOLD_STATE + || (uidState.capability & PROCESS_CAPABILITY_POWER_RESTRICTED_NETWORK) != 0; + } + + /** * Returns true if {@param procState} is considered foreground and as such will be allowed * to access network when the device is in data saver mode. Otherwise, false. * @hide diff --git a/core/java/android/net/thread/OWNERS b/core/java/android/net/thread/OWNERS new file mode 100644 index 000000000000..55c307b5eb62 --- /dev/null +++ b/core/java/android/net/thread/OWNERS @@ -0,0 +1,3 @@ +# Bug component: 1203089 + +include platform/packages/modules/ThreadNetwork:/OWNERS diff --git a/core/java/android/net/thread/flags.aconfig b/core/java/android/net/thread/flags.aconfig new file mode 100644 index 000000000000..6e72f8ebd8d1 --- /dev/null +++ b/core/java/android/net/thread/flags.aconfig @@ -0,0 +1,8 @@ +package: "com.android.net.thread.flags" + +flag { + name: "thread_user_restriction_enabled" + namespace: "thread_network" + description: "Controls whether user restriction on thread networks is enabled" + bug: "307679182" +} diff --git a/core/java/android/os/Build.java b/core/java/android/os/Build.java index 58717179d64d..3977bdf413d9 100755 --- a/core/java/android/os/Build.java +++ b/core/java/android/os/Build.java @@ -28,6 +28,7 @@ import android.app.ActivityThread; import android.app.Application; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; +import android.ravenwood.annotation.RavenwoodKeepWholeClass; import android.sysprop.DeviceProperties; import android.sysprop.SocProperties; import android.sysprop.TelephonyProperties; @@ -47,6 +48,7 @@ import java.util.stream.Collectors; /** * Information about the current build, extracted from system properties. */ +@RavenwoodKeepWholeClass public class Build { private static final String TAG = "Build"; @@ -307,7 +309,7 @@ public class Build { * compatibility. */ final String[] abiList; - if (VMRuntime.getRuntime().is64Bit()) { + if (android.os.Process.is64Bit()) { abiList = SUPPORTED_64_BIT_ABIS; } else { abiList = SUPPORTED_32_BIT_ABIS; diff --git a/core/java/android/os/SystemProperties.java b/core/java/android/os/SystemProperties.java index aa283a2d019b..a818919d184e 100644 --- a/core/java/android/os/SystemProperties.java +++ b/core/java/android/os/SystemProperties.java @@ -20,6 +20,8 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; import android.compat.annotation.UnsupportedAppUsage; +import android.ravenwood.annotation.RavenwoodKeepWholeClass; +import android.ravenwood.annotation.RavenwoodNativeSubstitutionClass; import android.util.Log; import android.util.MutableInt; @@ -36,6 +38,8 @@ import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.Map; +import java.util.function.Predicate; /** * Gives access to the system properties store. The system properties @@ -51,6 +55,8 @@ import java.util.HashMap; * {@hide} */ @SystemApi +@RavenwoodKeepWholeClass +@RavenwoodNativeSubstitutionClass("com.android.hoststubgen.nativesubstitution.SystemProperties_host") public class SystemProperties { private static final String TAG = "SystemProperties"; private static final boolean TRACK_KEY_ACCESS = false; @@ -94,6 +100,31 @@ public class SystemProperties { } } + /** @hide */ + public static void init$ravenwood(Map<String, String> values, + Predicate<String> keyReadablePredicate, Predicate<String> keyWritablePredicate) { + native_init$ravenwood(values, keyReadablePredicate, keyWritablePredicate, + SystemProperties::callChangeCallbacks); + synchronized (sChangeCallbacks) { + sChangeCallbacks.clear(); + } + } + + /** @hide */ + public static void reset$ravenwood() { + native_reset$ravenwood(); + synchronized (sChangeCallbacks) { + sChangeCallbacks.clear(); + } + } + + // These native methods are currently only implemented by Ravenwood, as it's the only + // mechanism we have to jump to our RavenwoodNativeSubstitutionClass + private static native void native_init$ravenwood(Map<String, String> values, + Predicate<String> keyReadablePredicate, Predicate<String> keyWritablePredicate, + Runnable changeCallback); + private static native void native_reset$ravenwood(); + // The one-argument version of native_get used to be a regular native function. Nowadays, // we use the two-argument form of native_get all the time, but we can't just delete the // one-argument overload: apps use it via reflection, as the UnsupportedAppUsage annotation diff --git a/core/java/android/os/Trace.java b/core/java/android/os/Trace.java index 5d7e04d4ed26..c0b490929c0a 100644 --- a/core/java/android/os/Trace.java +++ b/core/java/android/os/Trace.java @@ -36,6 +36,7 @@ import dalvik.annotation.optimization.FastNative; * href="{@docRoot}tools/debugging/systrace.html">Analyzing Display and Performance * with Systrace</a>. */ +@android.ravenwood.annotation.RavenwoodKeepWholeClass public final class Trace { /* * Writes trace events to the kernel trace buffer. These trace events can be @@ -123,10 +124,26 @@ public final class Trace { @UnsupportedAppUsage @CriticalNative + @android.ravenwood.annotation.RavenwoodReplace private static native long nativeGetEnabledTags(); + @android.ravenwood.annotation.RavenwoodReplace private static native void nativeSetAppTracingAllowed(boolean allowed); + @android.ravenwood.annotation.RavenwoodReplace private static native void nativeSetTracingEnabled(boolean allowed); + private static long nativeGetEnabledTags$ravenwood() { + // Tracing currently completely disabled under Ravenwood + return 0; + } + + private static void nativeSetAppTracingAllowed$ravenwood(boolean allowed) { + // Tracing currently completely disabled under Ravenwood + } + + private static void nativeSetTracingEnabled$ravenwood(boolean allowed) { + // Tracing currently completely disabled under Ravenwood + } + @FastNative private static native void nativeTraceCounter(long tag, String name, long value); @FastNative diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index d946430f18af..ecd6f22607b6 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -138,6 +138,20 @@ public final class Settings { public static final String ACTION_SETTINGS = "android.settings.SETTINGS"; /** + * Activity Action: Show settings to provide guide about carrier satellite messaging. + * <p> + * In some cases, a matching Activity may not exist, so ensure you + * safeguard against this. + * <p> + * Input: Nothing. + * <p> + * Output: Nothing. + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + @FlaggedApi(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG) + public static final String ACTION_SATELLITE_SETTING = "android.settings.SATELLITE_SETTING"; + + /** * Activity Action: Show settings to allow configuration of APNs. * <p> * Input: Nothing. diff --git a/core/java/android/security/flags.aconfig b/core/java/android/security/flags.aconfig index 1994058441d5..43163b3b9051 100644 --- a/core/java/android/security/flags.aconfig +++ b/core/java/android/security/flags.aconfig @@ -58,3 +58,10 @@ flag { bug: "290312729" is_fixed_read_only: true } + +flag { + name: "report_primary_auth_attempts" + namespace: "biometrics" + description: "Report primary auth attempts from LockSettingsService" + bug: "285053096" +} diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java index 1a2be15b2e8d..bbda0684f1d8 100644 --- a/core/java/android/service/wallpaper/WallpaperService.java +++ b/core/java/android/service/wallpaper/WallpaperService.java @@ -16,6 +16,7 @@ package android.service.wallpaper; +import static android.app.WallpaperManager.COMMAND_DISPLAY_SWITCH; import static android.app.WallpaperManager.COMMAND_FREEZE; import static android.app.WallpaperManager.COMMAND_UNFREEZE; import static android.app.WallpaperManager.SetWallpaperFlags; @@ -153,6 +154,7 @@ public abstract class WallpaperService extends Service { static final boolean DEBUG = false; static final float MIN_PAGE_ALLOWED_MARGIN = .05f; private static final int MIN_BITMAP_SCREENSHOT_WIDTH = 64; + private static final long PRESERVE_VISIBLE_TIMEOUT_MS = 1000; private static final long DEFAULT_UPDATE_SCREENSHOT_DURATION = 60 * 1000; //Once per minute private static final @NonNull RectF LOCAL_COLOR_BOUNDS = new RectF(0, 0, 1, 1); @@ -165,6 +167,7 @@ public abstract class WallpaperService extends Service { private static final int MSG_UPDATE_SURFACE = 10000; private static final int MSG_VISIBILITY_CHANGED = 10010; + private static final int MSG_REFRESH_VISIBILITY = 10011; private static final int MSG_WALLPAPER_OFFSETS = 10020; private static final int MSG_WALLPAPER_COMMAND = 10025; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) @@ -248,6 +251,11 @@ public abstract class WallpaperService extends Service { */ private boolean mIsScreenTurningOn; boolean mReportedVisible; + /** + * This is used with {@link #PRESERVE_VISIBLE_TIMEOUT_MS} to avoid intermediate visibility + * changes if the display may be toggled in a short time, e.g. display switch. + */ + boolean mPreserveVisible; boolean mDestroyed; // Set to true after receiving WallpaperManager#COMMAND_FREEZE. It's reset back to false // after receiving WallpaperManager#COMMAND_UNFREEZE. COMMAND_FREEZE is fully applied once @@ -263,7 +271,6 @@ public abstract class WallpaperService extends Service { boolean mDrawingAllowed; boolean mOffsetsChanged; boolean mFixedSizeAllowed; - boolean mShouldDim; // Whether the wallpaper should be dimmed by default (when no additional dimming is applied) // based on its color hints boolean mShouldDimByDefault; @@ -340,9 +347,11 @@ public abstract class WallpaperService extends Service { private Display mDisplay; private Context mDisplayContext; private int mDisplayState; - private float mWallpaperDimAmount = 0.05f; + + private float mCustomDimAmount = 0f; + private float mWallpaperDimAmount = 0f; private float mPreviousWallpaperDimAmount = mWallpaperDimAmount; - private float mDefaultDimAmount = mWallpaperDimAmount; + private float mDefaultDimAmount = 0.05f; SurfaceControl mSurfaceControl = new SurfaceControl(); SurfaceControl mBbqSurfaceControl; @@ -978,11 +987,8 @@ public abstract class WallpaperService extends Service { mShouldDimByDefault = ((colorHints & WallpaperColors.HINT_SUPPORTS_DARK_TEXT) == 0 && (colorHints & WallpaperColors.HINT_SUPPORTS_DARK_THEME) == 0); - // If default dimming value changes and no additional dimming is applied - if (mShouldDimByDefault != mShouldDim && mWallpaperDimAmount == 0f) { - mShouldDim = mShouldDimByDefault; - updateSurfaceDimming(); - } + // Recompute dim in case it changed compared to the previous WallpaperService + updateWallpaperDimming(mCustomDimAmount); } /** @@ -991,28 +997,21 @@ public abstract class WallpaperService extends Service { * @param dimAmount Float amount between [0.0, 1.0] to dim the wallpaper. */ private void updateWallpaperDimming(float dimAmount) { - if (dimAmount == mWallpaperDimAmount) { - return; - } + mCustomDimAmount = Math.min(1f, dimAmount); - // Custom dim amount cannot be less than the default dim amount. - mWallpaperDimAmount = Math.max(mDefaultDimAmount, dimAmount); - // If dim amount is 0f (additional dimming is removed), then the wallpaper should dim - // based on its default wallpaper color hints. - mShouldDim = dimAmount != 0f || mShouldDimByDefault; - updateSurfaceDimming(); - } + // If default dim is enabled, the actual dim amount is at least the default dim amount + mWallpaperDimAmount = (!mShouldDimByDefault) ? mCustomDimAmount + : Math.max(mDefaultDimAmount, mCustomDimAmount); - private void updateSurfaceDimming() { - if (!ENABLE_WALLPAPER_DIMMING || mBbqSurfaceControl == null) { + if (!ENABLE_WALLPAPER_DIMMING || mBbqSurfaceControl == null + || mWallpaperDimAmount == mPreviousWallpaperDimAmount) { return; } SurfaceControl.Transaction surfaceControlTransaction = new SurfaceControl.Transaction(); // TODO: apply the dimming to preview as well once surface transparency works in // preview mode. - if ((!isPreview() && mShouldDim) - || mPreviousWallpaperDimAmount != mWallpaperDimAmount) { + if (!isPreview()) { Log.v(TAG, "Setting wallpaper dimming: " + mWallpaperDimAmount); // Animate dimming to gradually change the wallpaper alpha from the previous @@ -1084,6 +1083,9 @@ public abstract class WallpaperService extends Service { if (pendingCount != 0) { out.print(prefix); out.print("mPendingResizeCount="); out.println(pendingCount); } + if (mPreserveVisible) { + out.print(prefix); out.print("mPreserveVisible=true"); + } synchronized (mLock) { out.print(prefix); out.print("mPendingXOffset="); out.print(mPendingXOffset); out.print(" mPendingXOffset="); out.println(mPendingXOffset); @@ -1534,8 +1536,6 @@ public abstract class WallpaperService extends Service { .createWindowContext(TYPE_WALLPAPER, null /* options */); mDefaultDimAmount = mDisplayContext.getResources().getFloat( com.android.internal.R.dimen.config_wallpaperDimAmount); - mWallpaperDimAmount = mDefaultDimAmount; - mPreviousWallpaperDimAmount = mWallpaperDimAmount; mDisplayState = mDisplay.getCommittedState(); mMergedConfiguration.setOverrideConfiguration( mDisplayContext.getResources().getConfiguration()); @@ -1643,7 +1643,8 @@ public abstract class WallpaperService extends Service { ? false : mIWallpaperEngine.mInfo.supportsAmbientMode(); // Report visibility only if display is fully on or wallpaper supports ambient mode. - boolean visible = mVisible && (displayFullyOn || supportsAmbientMode); + final boolean visible = (mVisible && (displayFullyOn || supportsAmbientMode)) + || mPreserveVisible; if (DEBUG) { Log.v( TAG, @@ -2080,6 +2081,9 @@ public abstract class WallpaperService extends Service { if (!mDestroyed) { if (COMMAND_FREEZE.equals(cmd.action) || COMMAND_UNFREEZE.equals(cmd.action)) { updateFrozenState(/* frozenRequested= */ !COMMAND_UNFREEZE.equals(cmd.action)); + } else if (COMMAND_DISPLAY_SWITCH.equals(cmd.action)) { + handleDisplaySwitch(cmd.z == 1 /* startToSwitch */); + return; } result = onCommand(cmd.action, cmd.x, cmd.y, cmd.z, cmd.extras, cmd.sync); @@ -2095,6 +2099,23 @@ public abstract class WallpaperService extends Service { } } + private void handleDisplaySwitch(boolean startToSwitch) { + if (startToSwitch && mReportedVisible) { + // The display may be off/on in a short time when the display is switching. + // Keep the visible state until onScreenTurnedOn or !startToSwitch is received, so + // the rendering thread can be active to redraw in time when receiving size change. + mPreserveVisible = true; + mCaller.removeMessages(MSG_REFRESH_VISIBILITY); + mCaller.sendMessageDelayed(mCaller.obtainMessage(MSG_REFRESH_VISIBILITY), + PRESERVE_VISIBLE_TIMEOUT_MS); + } else if (!startToSwitch && mPreserveVisible) { + // The switch is finished, so restore to actual visibility. + mPreserveVisible = false; + mCaller.removeMessages(MSG_REFRESH_VISIBILITY); + reportVisibility(false /* forceReport */); + } + } + private void updateFrozenState(boolean frozenRequested) { if (mIWallpaperEngine.mInfo == null // Procees the unfreeze command in case the wallaper became static while @@ -2638,6 +2659,10 @@ public abstract class WallpaperService extends Service { + ": " + message.arg1); mEngine.doVisibilityChanged(message.arg1 != 0); break; + case MSG_REFRESH_VISIBILITY: + mEngine.mPreserveVisible = false; + mEngine.reportVisibility(false /* forceReport */); + break; case MSG_UPDATE_SCREEN_TURNING_ON: if (DEBUG) { Log.v(TAG, diff --git a/core/java/android/text/DynamicLayout.java b/core/java/android/text/DynamicLayout.java index 7b9cb6afd6a0..928604983b70 100644 --- a/core/java/android/text/DynamicLayout.java +++ b/core/java/android/text/DynamicLayout.java @@ -40,6 +40,7 @@ import android.util.Pools.SynchronizedPool; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.ArrayUtils; import com.android.internal.util.GrowingArrayUtils; +import com.android.text.flags.Flags; import java.lang.ref.WeakReference; @@ -1276,8 +1277,21 @@ public class DynamicLayout extends Layout { } public void onSpanRemoved(Spannable s, Object o, int start, int end) { - if (o instanceof UpdateLayout) - transformAndReflow(s, start, end); + if (o instanceof UpdateLayout) { + if (Flags.insertModeCrashWhenDelete()) { + final DynamicLayout dynamicLayout = mLayout.get(); + if (dynamicLayout != null && dynamicLayout.mDisplay instanceof OffsetMapping) { + // It's possible that a Span is removed when the text covering it is + // deleted, in this case, the original start and end of the span might be + // OOB. So it'll reflow the entire string instead. + reflow(s, 0, 0, s.length()); + } else { + reflow(s, start, end - start, end - start); + } + } else { + transformAndReflow(s, start, end); + } + } } public void onSpanChanged(Spannable s, Object o, int start, int end, int nstart, int nend) { @@ -1287,8 +1301,21 @@ public class DynamicLayout extends Layout { // instead of causing an exception start = 0; } - transformAndReflow(s, start, end); - transformAndReflow(s, nstart, nend); + if (Flags.insertModeCrashWhenDelete()) { + final DynamicLayout dynamicLayout = mLayout.get(); + if (dynamicLayout != null && dynamicLayout.mDisplay instanceof OffsetMapping) { + // When text is changed, it'll also trigger onSpanChanged. In this case we + // can't determine the updated range in the transformed text. So it'll + // reflow the entire range instead. + reflow(s, 0, 0, s.length()); + } else { + reflow(s, start, end - start, end - start); + reflow(s, nstart, nend - nstart, nend - nstart); + } + } else { + transformAndReflow(s, start, end); + transformAndReflow(s, nstart, nend); + } } } diff --git a/core/java/android/text/flags/flags.aconfig b/core/java/android/text/flags/flags.aconfig index bf1a59625c93..6e45fea930d2 100644 --- a/core/java/android/text/flags/flags.aconfig +++ b/core/java/android/text/flags/flags.aconfig @@ -89,3 +89,10 @@ flag { description: "Feature flag for clearing focus when the escape key is pressed." bug: "312921137" } + +flag { + name: "insert_mode_crash_when_delete" + namespace: "text" + description: "A feature flag for fixing the crash while delete text in insert mode." + bug: "314254153" +} diff --git a/core/java/android/view/InsetsAnimationControlImpl.java b/core/java/android/view/InsetsAnimationControlImpl.java index 17a3a12d3b79..7f1e037e92d4 100644 --- a/core/java/android/view/InsetsAnimationControlImpl.java +++ b/core/java/android/view/InsetsAnimationControlImpl.java @@ -34,11 +34,11 @@ import static android.view.InsetsController.DEBUG; import static android.view.InsetsController.LAYOUT_INSETS_DURING_ANIMATION_SHOWN; import static android.view.InsetsController.LayoutInsetsDuringAnimation; import static android.view.InsetsSource.ID_IME; -import static android.view.InsetsState.ISIDE_BOTTOM; -import static android.view.InsetsState.ISIDE_FLOATING; -import static android.view.InsetsState.ISIDE_LEFT; -import static android.view.InsetsState.ISIDE_RIGHT; -import static android.view.InsetsState.ISIDE_TOP; +import static android.view.InsetsSource.SIDE_BOTTOM; +import static android.view.InsetsSource.SIDE_NONE; +import static android.view.InsetsSource.SIDE_LEFT; +import static android.view.InsetsSource.SIDE_RIGHT; +import static android.view.InsetsSource.SIDE_TOP; import static android.view.WindowInsets.Type.ime; import static android.view.WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; @@ -60,7 +60,7 @@ import android.util.SparseArray; import android.util.SparseIntArray; import android.util.SparseSetArray; import android.util.proto.ProtoOutputStream; -import android.view.InsetsState.InternalInsetsSide; +import android.view.InsetsSource.InternalInsetsSide; import android.view.SyncRtSurfaceTransactionApplier.SurfaceParams; import android.view.WindowInsets.Type.InsetsType; import android.view.WindowInsetsAnimation.Bounds; @@ -142,7 +142,7 @@ public class InsetsAnimationControlImpl implements InternalInsetsAnimationContro if (mHasZeroInsetsIme) { // IME has shownInsets of ZERO, and can't map to a side by default. // Map zero insets IME to bottom, making it a special case of bottom insets. - idSideMap.put(ID_IME, ISIDE_BOTTOM); + idSideMap.put(ID_IME, SIDE_BOTTOM); } buildSideControlsMap(idSideMap, mSideControlsMap, controls); } else { @@ -286,10 +286,10 @@ public class InsetsAnimationControlImpl implements InternalInsetsAnimationContro } final Insets offset = Insets.subtract(mShownInsets, mPendingInsets); final ArrayList<SurfaceParams> params = new ArrayList<>(); - updateLeashesForSide(ISIDE_LEFT, offset.left, params, outState, mPendingAlpha); - updateLeashesForSide(ISIDE_TOP, offset.top, params, outState, mPendingAlpha); - updateLeashesForSide(ISIDE_RIGHT, offset.right, params, outState, mPendingAlpha); - updateLeashesForSide(ISIDE_BOTTOM, offset.bottom, params, outState, mPendingAlpha); + updateLeashesForSide(SIDE_LEFT, offset.left, params, outState, mPendingAlpha); + updateLeashesForSide(SIDE_TOP, offset.top, params, outState, mPendingAlpha); + updateLeashesForSide(SIDE_RIGHT, offset.right, params, outState, mPendingAlpha); + updateLeashesForSide(SIDE_BOTTOM, offset.bottom, params, outState, mPendingAlpha); mController.applySurfaceParams(params.toArray(new SurfaceParams[params.size()])); mCurrentInsets = mPendingInsets; @@ -499,19 +499,19 @@ public class InsetsAnimationControlImpl implements InternalInsetsAnimationContro final float surfaceOffset = mTranslator != null ? mTranslator.translateLengthInAppWindowToScreen(offset) : offset; switch (side) { - case ISIDE_LEFT: + case SIDE_LEFT: m.postTranslate(-surfaceOffset, 0); frame.offset(-offset, 0); break; - case ISIDE_TOP: + case SIDE_TOP: m.postTranslate(0, -surfaceOffset); frame.offset(0, -offset); break; - case ISIDE_RIGHT: + case SIDE_RIGHT: m.postTranslate(surfaceOffset, 0); frame.offset(offset, 0); break; - case ISIDE_BOTTOM: + case SIDE_BOTTOM: m.postTranslate(0, surfaceOffset); frame.offset(0, offset); break; @@ -543,9 +543,10 @@ public class InsetsAnimationControlImpl implements InternalInsetsAnimationContro // control may be null if it got revoked. continue; } - @InternalInsetsSide int side = InsetsState.getInsetSide(control.getInsetsHint()); - if (side == ISIDE_FLOATING && control.getType() == WindowInsets.Type.ime()) { - side = ISIDE_BOTTOM; + @InternalInsetsSide int side = InsetsSource.getInsetSide(control.getInsetsHint()); + if (side == SIDE_NONE && control.getType() == WindowInsets.Type.ime()) { + // IME might not provide insets when it is fullscreen or floating. + side = SIDE_BOTTOM; } sideControlsMap.add(side, control); } diff --git a/core/java/android/view/InsetsSource.java b/core/java/android/view/InsetsSource.java index 0927d4519b9c..86ab21348e4f 100644 --- a/core/java/android/view/InsetsSource.java +++ b/core/java/android/view/InsetsSource.java @@ -46,6 +46,24 @@ import java.util.StringJoiner; */ public class InsetsSource implements Parcelable { + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = "SIDE_", value = { + SIDE_NONE, + SIDE_LEFT, + SIDE_TOP, + SIDE_RIGHT, + SIDE_BOTTOM, + SIDE_UNKNOWN + }) + public @interface InternalInsetsSide {} + + static final int SIDE_NONE = 0; + static final int SIDE_LEFT = 1; + static final int SIDE_TOP = 2; + static final int SIDE_RIGHT = 3; + static final int SIDE_BOTTOM = 4; + static final int SIDE_UNKNOWN = 5; + /** The insets source ID of IME */ public static final int ID_IME = createId(null, 0, ime()); @@ -101,6 +119,12 @@ public class InsetsSource implements Parcelable { private boolean mVisible; + /** + * Used to decide which side of the relative frame should receive insets when the frame fully + * covers the relative frame. + */ + private @InternalInsetsSide int mSideHint = SIDE_NONE; + private final Rect mTmpFrame = new Rect(); public InsetsSource(int id, @InsetsType int type) { @@ -119,6 +143,7 @@ public class InsetsSource implements Parcelable { ? new Rect(other.mVisibleFrame) : null; mFlags = other.mFlags; + mSideHint = other.mSideHint; } public void set(InsetsSource other) { @@ -128,6 +153,7 @@ public class InsetsSource implements Parcelable { ? new Rect(other.mVisibleFrame) : null; mFlags = other.mFlags; + mSideHint = other.mSideHint; } public InsetsSource setFrame(int left, int top, int right, int bottom) { @@ -160,6 +186,18 @@ public class InsetsSource implements Parcelable { return this; } + /** + * Updates the side hint which is used to decide which side of the relative frame should receive + * insets when the frame fully covers the relative frame. + * + * @param bounds A rectangle which contains the frame. It will be used to calculate the hint. + */ + public InsetsSource updateSideHint(Rect bounds) { + mSideHint = getInsetSide( + calculateInsets(bounds, mFrame, true /* ignoreVisibility */)); + return this; + } + public int getId() { return mId; } @@ -236,8 +274,21 @@ public class InsetsSource implements Parcelable { return Insets.of(0, 0, 0, mTmpFrame.height()); } - // Intersecting at top/bottom - if (mTmpFrame.width() == relativeFrame.width()) { + if (mTmpFrame.equals(relativeFrame)) { + // Covering all sides + switch (mSideHint) { + default: + case SIDE_LEFT: + return Insets.of(mTmpFrame.width(), 0, 0, 0); + case SIDE_TOP: + return Insets.of(0, mTmpFrame.height(), 0, 0); + case SIDE_RIGHT: + return Insets.of(0, 0, mTmpFrame.width(), 0); + case SIDE_BOTTOM: + return Insets.of(0, 0, 0, mTmpFrame.height()); + } + } else if (mTmpFrame.width() == relativeFrame.width()) { + // Intersecting at top/bottom if (mTmpFrame.top == relativeFrame.top) { return Insets.of(0, mTmpFrame.height(), 0, 0); } else if (mTmpFrame.bottom == relativeFrame.bottom) { @@ -249,9 +300,8 @@ public class InsetsSource implements Parcelable { if (mTmpFrame.top == 0) { return Insets.of(0, mTmpFrame.height(), 0, 0); } - } - // Intersecting at left/right - else if (mTmpFrame.height() == relativeFrame.height()) { + } else if (mTmpFrame.height() == relativeFrame.height()) { + // Intersecting at left/right if (mTmpFrame.left == relativeFrame.left) { return Insets.of(mTmpFrame.width(), 0, 0, 0); } else if (mTmpFrame.right == relativeFrame.right) { @@ -283,6 +333,46 @@ public class InsetsSource implements Parcelable { } /** + * Retrieves the side for a certain {@code insets}. It is required that only one field l/t/r/b + * is set in order that this method returns a meaningful result. + */ + static @InternalInsetsSide int getInsetSide(Insets insets) { + if (Insets.NONE.equals(insets)) { + return SIDE_NONE; + } + if (insets.left != 0) { + return SIDE_LEFT; + } + if (insets.top != 0) { + return SIDE_TOP; + } + if (insets.right != 0) { + return SIDE_RIGHT; + } + if (insets.bottom != 0) { + return SIDE_BOTTOM; + } + return SIDE_UNKNOWN; + } + + static String sideToString(@InternalInsetsSide int side) { + switch (side) { + case SIDE_NONE: + return "NONE"; + case SIDE_LEFT: + return "LEFT"; + case SIDE_TOP: + return "TOP"; + case SIDE_RIGHT: + return "RIGHT"; + case SIDE_BOTTOM: + return "BOTTOM"; + default: + return "UNKNOWN:" + side; + } + } + + /** * Creates an identifier of an {@link InsetsSource}. * * @param owner An object owned by the owner. Only the owner can modify its own sources. @@ -331,7 +421,7 @@ public class InsetsSource implements Parcelable { } public static String flagsToString(@Flags int flags) { - final StringJoiner joiner = new StringJoiner(" "); + final StringJoiner joiner = new StringJoiner("|"); if ((flags & FLAG_SUPPRESS_SCRIM) != 0) { joiner.add("SUPPRESS_SCRIM"); } @@ -371,6 +461,7 @@ public class InsetsSource implements Parcelable { } pw.print(" visible="); pw.print(mVisible); pw.print(" flags="); pw.print(flagsToString(mFlags)); + pw.print(" sideHint="); pw.print(sideToString(mSideHint)); pw.println(); } @@ -393,6 +484,7 @@ public class InsetsSource implements Parcelable { if (mType != that.mType) return false; if (mVisible != that.mVisible) return false; if (mFlags != that.mFlags) return false; + if (mSideHint != that.mSideHint) return false; if (excludeInvisibleImeFrames && !mVisible && mType == WindowInsets.Type.ime()) return true; if (!Objects.equals(mVisibleFrame, that.mVisibleFrame)) return false; return mFrame.equals(that.mFrame); @@ -400,7 +492,7 @@ public class InsetsSource implements Parcelable { @Override public int hashCode() { - return Objects.hash(mId, mType, mFrame, mVisibleFrame, mVisible, mFlags); + return Objects.hash(mId, mType, mFrame, mVisibleFrame, mVisible, mFlags, mSideHint); } public InsetsSource(Parcel in) { @@ -414,6 +506,7 @@ public class InsetsSource implements Parcelable { } mVisible = in.readBoolean(); mFlags = in.readInt(); + mSideHint = in.readInt(); } @Override @@ -434,6 +527,7 @@ public class InsetsSource implements Parcelable { } dest.writeBoolean(mVisible); dest.writeInt(mFlags); + dest.writeInt(mSideHint); } @Override @@ -442,7 +536,8 @@ public class InsetsSource implements Parcelable { + " mType=" + WindowInsets.Type.toString(mType) + " mFrame=" + mFrame.toShortString() + " mVisible=" + mVisible - + " mFlags=[" + flagsToString(mFlags) + "]" + + " mFlags=" + flagsToString(mFlags) + + " mSideHint=" + sideToString(mSideHint) + "}"; } diff --git a/core/java/android/view/InsetsState.java b/core/java/android/view/InsetsState.java index 59e0932ecd80..c88da9e2eb4f 100644 --- a/core/java/android/view/InsetsState.java +++ b/core/java/android/view/InsetsState.java @@ -37,7 +37,6 @@ import static android.view.WindowManager.LayoutParams.SOFT_INPUT_MASK_ADJUST; import static android.view.WindowManager.LayoutParams.TYPE_SYSTEM_ERROR; import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER; -import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.WindowConfiguration.ActivityType; @@ -48,6 +47,7 @@ import android.os.Parcelable; import android.util.SparseArray; import android.util.SparseIntArray; import android.util.proto.ProtoOutputStream; +import android.view.InsetsSource.InternalInsetsSide; import android.view.WindowInsets.Type; import android.view.WindowInsets.Type.InsetsType; import android.view.WindowManager.LayoutParams.SoftInputModeFlags; @@ -55,8 +55,6 @@ import android.view.WindowManager.LayoutParams.SoftInputModeFlags; import com.android.internal.annotations.VisibleForTesting; import java.io.PrintWriter; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; import java.util.Objects; import java.util.StringJoiner; @@ -66,23 +64,6 @@ import java.util.StringJoiner; */ public class InsetsState implements Parcelable { - @Retention(RetentionPolicy.SOURCE) - @IntDef(prefix = "ISIDE", value = { - ISIDE_LEFT, - ISIDE_TOP, - ISIDE_RIGHT, - ISIDE_BOTTOM, - ISIDE_FLOATING, - ISIDE_UNKNOWN - }) - public @interface InternalInsetsSide {} - static final int ISIDE_LEFT = 0; - static final int ISIDE_TOP = 1; - static final int ISIDE_RIGHT = 2; - static final int ISIDE_BOTTOM = 3; - static final int ISIDE_FLOATING = 4; - static final int ISIDE_UNKNOWN = 5; - private final SparseArray<InsetsSource> mSources; /** @@ -398,37 +379,14 @@ public class InsetsState implements Parcelable { } if (idSideMap != null) { - @InternalInsetsSide int insetSide = getInsetSide(insets); - if (insetSide != ISIDE_UNKNOWN) { + @InternalInsetsSide int insetSide = InsetsSource.getInsetSide(insets); + if (insetSide != InsetsSource.SIDE_UNKNOWN) { idSideMap.put(source.getId(), insetSide); } } } /** - * Retrieves the side for a certain {@code insets}. It is required that only one field l/t/r/b - * is set in order that this method returns a meaningful result. - */ - static @InternalInsetsSide int getInsetSide(Insets insets) { - if (Insets.NONE.equals(insets)) { - return ISIDE_FLOATING; - } - if (insets.left != 0) { - return ISIDE_LEFT; - } - if (insets.top != 0) { - return ISIDE_TOP; - } - if (insets.right != 0) { - return ISIDE_RIGHT; - } - if (insets.bottom != 0) { - return ISIDE_BOTTOM; - } - return ISIDE_UNKNOWN; - } - - /** * Gets the source mapped from the ID, or creates one if no such mapping has been made. */ public InsetsSource getOrCreateSource(int id, int type) { diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index c98d1d7ecaea..1b22fda9c31a 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -5546,11 +5546,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, @FlaggedApi(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY) public static final float REQUESTED_FRAME_RATE_CATEGORY_NO_PREFERENCE = -1; @FlaggedApi(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY) - public static final float REQUESTED_FRAME_RATE_CATEGORY_LOW = -30; + public static final float REQUESTED_FRAME_RATE_CATEGORY_LOW = -2; @FlaggedApi(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY) - public static final float REQUESTED_FRAME_RATE_CATEGORY_NORMAL = -60; + public static final float REQUESTED_FRAME_RATE_CATEGORY_NORMAL = -3; @FlaggedApi(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY) - public static final float REQUESTED_FRAME_RATE_CATEGORY_HIGH = -120; + public static final float REQUESTED_FRAME_RATE_CATEGORY_HIGH = -4; /** * Simple constructor to use when creating a view from code. @@ -28492,6 +28492,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, surface.destroy(); } session.kill(); + surfaceControl.release(); } } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 3c36227eda0a..7bc832ef9e3f 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -11991,7 +11991,7 @@ public final class ViewRootImpl implements ViewParent, Runnable timeoutRunnable = () -> Log.e(mTag, "Failed to submit the sync transaction after 4s. Likely to ANR " + "soon"); - mHandler.postDelayed(timeoutRunnable, 4L * Build.HW_TIMEOUT_MULTIPLIER); + mHandler.postDelayed(timeoutRunnable, 4000L * Build.HW_TIMEOUT_MULTIPLIER); transaction.addTransactionCommittedListener(mSimpleExecutor, () -> mHandler.removeCallbacks(timeoutRunnable)); surfaceSyncGroup.addTransaction(transaction); diff --git a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig index efae57c9946c..a11ac7cb48ad 100644 --- a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig +++ b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig @@ -94,6 +94,13 @@ flag { } flag { + name: "skip_accessibility_warning_dialog_for_trusted_services" + namespace: "accessibility" + description: "Skips showing the accessibility warning dialog for trusted services." + bug: "303511250" +} + +flag { namespace: "accessibility" name: "update_always_on_a11y_service" description: "Updates the Always-On A11yService state when the user changes the enablement of the shortcut." diff --git a/core/java/android/view/contentcapture/MainContentCaptureSessionV2.java b/core/java/android/view/contentcapture/MainContentCaptureSessionV2.java index bf1d31c8496d..fbb66d1485dd 100644 --- a/core/java/android/view/contentcapture/MainContentCaptureSessionV2.java +++ b/core/java/android/view/contentcapture/MainContentCaptureSessionV2.java @@ -149,9 +149,12 @@ public final class MainContentCaptureSessionV2 extends ContentCaptureSession { * * Because it is not guaranteed that the events will be enqueued from a single thread, the * implementation must be thread-safe to prevent unexpected behaviour. + * + * @hide */ + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) @NonNull - private final ConcurrentLinkedQueue<ContentCaptureEvent> mEventProcessQueue; + public final ConcurrentLinkedQueue<ContentCaptureEvent> mEventProcessQueue; /** * List of events held to be sent to the {@link ContentCaptureService} as a batch. @@ -908,7 +911,7 @@ public final class MainContentCaptureSessionV2 extends ContentCaptureSession { * clear the buffer events then starting sending out current event. */ private void enqueueEvent(@NonNull final ContentCaptureEvent event, boolean forceFlush) { - if (forceFlush) { + if (forceFlush || mEventProcessQueue.size() >= mManager.mOptions.maxBufferSize - 1) { // The buffer events are cleared in the same thread first to prevent new events // being added during the time of context switch. This would disrupt the sequence // of events. diff --git a/core/java/android/view/flags/view_flags.aconfig b/core/java/android/view/flags/view_flags.aconfig index 9f9b7b4b68a9..1dd99baf8d2a 100644 --- a/core/java/android/view/flags/view_flags.aconfig +++ b/core/java/android/view/flags/view_flags.aconfig @@ -8,6 +8,15 @@ flag { } flag { + name: "enable_surface_native_alloc_registration_ro" + namespace: "toolkit" + description: "Feature flag for registering surfaces with the VM for faster" + " cleanup. Fixed readonly version." + bug: "306193257" + is_fixed_read_only: true +} + +flag { name: "enable_use_measure_cache_during_force_layout" namespace: "toolkit" description: "Enables using the measure cache during a view force layout from the second " diff --git a/core/java/android/webkit/WebSettings.java b/core/java/android/webkit/WebSettings.java index d12eda35c745..14c53489ba3a 100644 --- a/core/java/android/webkit/WebSettings.java +++ b/core/java/android/webkit/WebSettings.java @@ -1203,11 +1203,7 @@ public abstract class WebSettings { * changes to this setting after that point. * * @param flag {@code true} if the WebView should use the database storage API - * @deprecated WebSQL is deprecated and this method will become a no-op on all - * Android versions once support is removed in Chromium. See - * https://developer.chrome.com/blog/deprecating-web-sql for more information. */ - @Deprecated public abstract void setDatabaseEnabled(boolean flag); /** @@ -1240,11 +1236,7 @@ public abstract class WebSettings { * * @return {@code true} if the database storage API is enabled * @see #setDatabaseEnabled - * @deprecated WebSQL is deprecated and this method will become a no-op on all - * Android versions once support is removed in Chromium. See - * https://developer.chrome.com/blog/deprecating-web-sql for more information. */ - @Deprecated public abstract boolean getDatabaseEnabled(); /** diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java index ddcfb40e00ce..57d268ced6f4 100644 --- a/core/java/android/widget/Editor.java +++ b/core/java/android/widget/Editor.java @@ -148,6 +148,7 @@ import com.android.internal.util.ArrayUtils; import com.android.internal.util.GrowingArrayUtils; import com.android.internal.util.Preconditions; import com.android.internal.view.FloatingActionMode; +import com.android.text.flags.Flags; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -2343,6 +2344,13 @@ public class Editor { */ void invalidateTextDisplayList(Layout layout, int start, int end) { if (mTextRenderNodes != null && layout instanceof DynamicLayout) { + if (Flags.insertModeCrashWhenDelete() + && mTextView.isOffsetMappingAvailable()) { + // Text is transformed with an OffsetMapping, and we can't know the changed range + // on the transformed text. Invalidate the all display lists instead. + invalidateTextDisplayList(); + return; + } final int startTransformed = mTextView.originalToTransformed(start, OffsetMapping.MAP_STRATEGY_CHARACTER); final int endTransformed = diff --git a/core/java/android/widget/RemoteCanvas.java b/core/java/android/widget/RemoteCanvas.java new file mode 100644 index 000000000000..9a0898c1cf3c --- /dev/null +++ b/core/java/android/widget/RemoteCanvas.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.widget; + +import static android.appwidget.flags.Flags.FLAG_DRAW_DATA_PARCEL; + +import android.annotation.AttrRes; +import android.annotation.FlaggedApi; +import android.annotation.StyleRes; +import android.content.Context; +import android.util.AttributeSet; +import android.util.Log; +import android.util.SparseArray; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; + +import java.util.function.IntConsumer; + +/** + * {@link RemoteCanvas} is designed to support arbitrary protocols between two processes using + * {@link RemoteViews.DrawInstructions}. Upon instantiation in the host process, + * {@link RemoteCanvas#setDrawInstructions(RemoteViews.DrawInstructions)} is called so that the + * host process can render the {@link RemoteViews.DrawInstructions} from the provider process + * accordingly. + * + * @hide + */ +@FlaggedApi(FLAG_DRAW_DATA_PARCEL) +public class RemoteCanvas extends View { + + private static final String TAG = "RemoteCanvas"; + + @Nullable + private SparseArray<Runnable> mCallbacks; + + private final IntConsumer mOnClickHandler = (viewId) -> { + if (mCallbacks == null) { + Log.w(TAG, "Cannot find callback for " + viewId + + ", in fact there were no callbacks from this RemoteViews at all."); + return; + } + final Runnable cb = getCallbacks().get(viewId); + if (cb != null) { + cb.run(); + } else { + Log.w(TAG, "Cannot find callback for " + viewId); + } + }; + + RemoteCanvas(@NonNull Context context) { + super(context); + } + + RemoteCanvas(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + RemoteCanvas(@NonNull Context context, @Nullable AttributeSet attrs, + @AttrRes int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + RemoteCanvas(@NonNull Context context, @Nullable AttributeSet attrs, + @AttrRes int defStyleAttr, @StyleRes int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + /** + * Setter method for the {@link RemoteViews.DrawInstructions} from the provider process for + * the host process to render accordingly. + * + * @param instructions {@link RemoteViews.DrawInstructions} from the provider process. + */ + void setDrawInstructions(@NonNull final RemoteViews.DrawInstructions instructions) { + setTag(instructions); + // TODO: handle draw instructions + // TODO: attach mOnClickHandler + } + + /** + * Adds a callback function to a clickable area in the RemoteCanvas. + * + * @param viewId the viewId of the clickable area + * @param cb the callback function to be triggered when clicked + */ + void addOnClickHandler(final int viewId, @NonNull final Runnable cb) { + getCallbacks().set(viewId, cb); + } + + /** + * Returns all callbacks added to the RemoteCanvas through + * {@link #addOnClickHandler(int, Runnable)}. + */ + @VisibleForTesting + public SparseArray<Runnable> getCallbacks() { + if (mCallbacks == null) { + mCallbacks = new SparseArray<>(); + } + return mCallbacks; + } +} diff --git a/core/java/android/widget/RemoteViews.aidl b/core/java/android/widget/RemoteViews.aidl index 6a5fc03fcc6e..19a5f25d8192 100644 --- a/core/java/android/widget/RemoteViews.aidl +++ b/core/java/android/widget/RemoteViews.aidl @@ -18,3 +18,4 @@ package android.widget; parcelable RemoteViews; parcelable RemoteViews.RemoteCollectionItems; +parcelable RemoteViews.DrawInstructions; diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java index 0d499a1b311e..0654addae2ad 100644 --- a/core/java/android/widget/RemoteViews.java +++ b/core/java/android/widget/RemoteViews.java @@ -16,6 +16,8 @@ package android.widget; +import static android.appwidget.flags.Flags.FLAG_DRAW_DATA_PARCEL; +import static android.appwidget.flags.Flags.drawDataParcel; import static android.appwidget.flags.Flags.remoteAdapterConversion; import static android.view.inputmethod.Flags.FLAG_HOME_SCREEN_HANDWRITING_DELEGATOR; @@ -243,6 +245,7 @@ public class RemoteViews implements Parcelable, Filter { private static final int ATTRIBUTE_REFLECTION_ACTION_TAG = 32; private static final int SET_REMOTE_ADAPTER_TAG = 33; private static final int SET_ON_STYLUS_HANDWRITING_RESPONSE_TAG = 34; + private static final int SET_DRAW_INSTRUCTION_TAG = 35; /** @hide **/ @IntDef(prefix = "MARGIN_", value = { @@ -442,6 +445,19 @@ public class RemoteViews implements Parcelable, Filter { @Nullable private LayoutInflater.Factory2 mLayoutInflaterFactory2; + /** + * Indicates whether this {@link RemoteViews} was instantiated with a {@link DrawInstructions} + * object. {@link DrawInstructions} serves as an alternative protocol for the host process + * to render. + */ + private boolean mHasDrawInstructions; + + @Nullable + private SparseArray<PendingIntent> mPendingIntentTemplate; + + @Nullable + private SparseArray<Intent> mFillInIntent; + private static final InteractionHandler DEFAULT_INTERACTION_HANDLER = (view, pendingIntent, response) -> startPendingIntent(view, pendingIntent, response.getLaunchOptions(view)); @@ -1463,6 +1479,11 @@ public class RemoteViews implements Parcelable, Filter { @Override public void apply(View root, ViewGroup rootParent, ActionApplyParams params) { + if (hasDrawInstructions() && root instanceof RemoteCanvas target) { + target.addOnClickHandler(mViewId, () -> + mResponse.handleViewInteraction(root, params.handler)); + return; + } final View target = root.findViewById(mViewId); if (target == null) return; @@ -3851,6 +3872,45 @@ public class RemoteViews implements Parcelable, Filter { } } + private static class SetDrawInstructionAction extends Action { + + @Nullable + private final DrawInstructions mInstructions; + + SetDrawInstructionAction(@NonNull final DrawInstructions instructions) { + mInstructions = instructions; + } + + SetDrawInstructionAction(@NonNull final Parcel in) { + if (drawDataParcel()) { + mInstructions = DrawInstructions.readFromParcel(in); + } else { + mInstructions = null; + } + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + if (drawDataParcel()) { + DrawInstructions.writeToParcel(mInstructions, dest, flags); + } + } + + @Override + public void apply(View root, ViewGroup rootParent, ActionApplyParams params) + throws ActionException { + if (drawDataParcel() && mInstructions != null + && root instanceof RemoteCanvas remoteCanvas) { + remoteCanvas.setDrawInstructions(mInstructions); + } + } + + @Override + public int getActionTag() { + return SET_DRAW_INSTRUCTION_TAG; + } + } + /** * Create a new RemoteViews object that will display the views contained * in the specified layout file. @@ -4080,6 +4140,7 @@ public class RemoteViews implements Parcelable, Filter { mClassCookies = src.mClassCookies; mIdealSize = src.mIdealSize; mProviderInstanceId = src.mProviderInstanceId; + mHasDrawInstructions = src.mHasDrawInstructions; if (src.hasLandscapeAndPortraitLayouts()) { mLandscape = createInitializedFrom(src.mLandscape, hierarchyRoot); @@ -4114,12 +4175,26 @@ public class RemoteViews implements Parcelable, Filter { /** * Reads a RemoteViews object from a parcel. * - * @param parcel + * @param parcel the parcel object */ public RemoteViews(Parcel parcel) { this(parcel, /* rootData= */ null, /* info= */ null, /* depth= */ 0); } + /** + * Instantiates a RemoteViews object using {@link DrawInstructions}, which serves as an + * alternative to XML layout. {@link DrawInstructions} objects contains the instructions which + * can be interpreted and rendered accordingly in the host process. + * + * @param drawInstructions The {@link DrawInstructions} object + */ + @FlaggedApi(FLAG_DRAW_DATA_PARCEL) + public RemoteViews(@NonNull final DrawInstructions drawInstructions) { + Objects.requireNonNull(drawInstructions); + mHasDrawInstructions = true; + addAction(new SetDrawInstructionAction(drawInstructions)); + } + private RemoteViews(@NonNull Parcel parcel, @Nullable HierarchyRootData rootData, @Nullable ApplicationInfo info, int depth) { if (depth > MAX_NESTED_VIEWS @@ -4178,6 +4253,7 @@ public class RemoteViews implements Parcelable, Filter { } mApplyFlags = parcel.readInt(); mProviderInstanceId = parcel.readLong(); + mHasDrawInstructions = parcel.readBoolean(); // Ensure that all descendants have their caches set up recursively. if (mIsRoot) { @@ -4254,6 +4330,8 @@ public class RemoteViews implements Parcelable, Filter { return new AttributeReflectionAction(parcel); case SET_ON_STYLUS_HANDWRITING_RESPONSE_TAG: return new SetOnStylusHandwritingResponse(parcel); + case SET_DRAW_INSTRUCTION_TAG: + return new SetDrawInstructionAction(parcel); default: throw new ActionException("Tag " + tag + " not found"); } @@ -4747,7 +4825,12 @@ public class RemoteViews implements Parcelable, Filter { * by a child of viewId and executed when that child is clicked */ public void setPendingIntentTemplate(@IdRes int viewId, PendingIntent pendingIntentTemplate) { - addAction(new SetPendingIntentTemplate(viewId, pendingIntentTemplate)); + if (hasDrawInstructions()) { + getPendingIntentTemplate().set(viewId, pendingIntentTemplate); + tryAddRemoteResponse(viewId); + } else { + addAction(new SetPendingIntentTemplate(viewId, pendingIntentTemplate)); + } } /** @@ -4768,7 +4851,12 @@ public class RemoteViews implements Parcelable, Filter { * in order to determine the on-click behavior of the view specified by viewId */ public void setOnClickFillInIntent(@IdRes int viewId, Intent fillInIntent) { - setOnClickResponse(viewId, RemoteResponse.fromFillInIntent(fillInIntent)); + if (hasDrawInstructions()) { + getFillInIntent().set(viewId, fillInIntent); + tryAddRemoteResponse(viewId); + } else { + setOnClickResponse(viewId, RemoteResponse.fromFillInIntent(fillInIntent)); + } } /** @@ -5791,6 +5879,10 @@ public class RemoteViews implements Parcelable, Filter { } } + private boolean hasDrawInstructions() { + return mHasDrawInstructions; + } + private RemoteViews getRemoteViewsToApply(Context context) { if (hasLandscapeAndPortraitLayouts()) { int orientation = context.getResources().getConfiguration().orientation; @@ -5973,6 +6065,10 @@ public class RemoteViews implements Parcelable, Filter { if (applyThemeResId != 0) { inflationContext = new ContextThemeWrapper(inflationContext, applyThemeResId); } + // If the RemoteViews contains draw instructions, just use it instead. + if (rv.hasDrawInstructions()) { + return new RemoteCanvas(inflationContext); + } LayoutInflater inflater = LayoutInflater.from(context); // Clone inflater so we load resources from correct context and @@ -6236,7 +6332,7 @@ public class RemoteViews implements Parcelable, Filter { /** @hide */ public boolean canRecycleView(@Nullable View v) { - if (v == null) { + if (v == null || hasDrawInstructions()) { return false; } Integer previousLayoutId = (Integer) v.getTag(R.id.widget_frame); @@ -6388,6 +6484,32 @@ public class RemoteViews implements Parcelable, Filter { return context; } + @NonNull + private SparseArray<PendingIntent> getPendingIntentTemplate() { + if (mPendingIntentTemplate == null) { + mPendingIntentTemplate = new SparseArray<>(); + } + return mPendingIntentTemplate; + } + + @NonNull + private SparseArray<Intent> getFillInIntent() { + if (mFillInIntent == null) { + mFillInIntent = new SparseArray<>(); + } + return mFillInIntent; + } + + private void tryAddRemoteResponse(final int viewId) { + final PendingIntent pendingIntent = getPendingIntentTemplate().get(viewId); + final Intent intent = getFillInIntent().get(viewId); + if (pendingIntent != null && intent != null) { + addAction(new SetOnClickResponse(viewId, + RemoteResponse.fromPendingIntentTemplateAndFillInIntent( + pendingIntent, intent))); + } + } + /** * Utility class to hold all the options when applying the remote views * @hide @@ -6624,6 +6746,7 @@ public class RemoteViews implements Parcelable, Filter { } dest.writeInt(mApplyFlags); dest.writeLong(mProviderInstanceId); + dest.writeBoolean(mHasDrawInstructions); dest.restoreAllowSquashing(prevSquashingAllowed); } @@ -6926,6 +7049,14 @@ public class RemoteViews implements Parcelable, Filter { return response; } + private static RemoteResponse fromPendingIntentTemplateAndFillInIntent( + @NonNull final PendingIntent pendingIntent, @NonNull final Intent intent) { + RemoteResponse response = new RemoteResponse(); + response.mPendingIntent = pendingIntent; + response.mFillIntent = intent; + return response; + } + /** * Adds a shared element to be transferred as part of the transition between Activities * using cross-Activity scene animations. The position of the first element will be used as @@ -6964,8 +7095,8 @@ public class RemoteViews implements Parcelable, Filter { private void writeToParcel(Parcel dest, int flags) { PendingIntent.writePendingIntentOrNullToParcel(mPendingIntent, dest); - if (mPendingIntent == null) { - // Only write the intent if pending intent is null + dest.writeBoolean((mFillIntent != null)); + if (mFillIntent != null) { dest.writeTypedObject(mFillIntent, flags); } dest.writeInt(mInteractionType); @@ -6975,9 +7106,7 @@ public class RemoteViews implements Parcelable, Filter { private void readFromParcel(Parcel parcel) { mPendingIntent = PendingIntent.readPendingIntentOrNullFromParcel(parcel); - if (mPendingIntent == null) { - mFillIntent = parcel.readTypedObject(Intent.CREATOR); - } + mFillIntent = parcel.readBoolean() ? parcel.readTypedObject(Intent.CREATOR) : null; mInteractionType = parcel.readInt(); int[] viewIds = parcel.createIntArray(); mViewIds = viewIds == null ? null : IntArray.wrap(viewIds); @@ -7054,7 +7183,7 @@ public class RemoteViews implements Parcelable, Filter { /** @hide */ public Pair<Intent, ActivityOptions> getLaunchOptions(View view) { - Intent intent = mPendingIntent != null ? new Intent() : new Intent(mFillIntent); + Intent intent = mFillIntent == null ? new Intent() : new Intent(mFillIntent); intent.setSourceBounds(getSourceBounds(view)); if (view instanceof CompoundButton @@ -7413,6 +7542,98 @@ public class RemoteViews implements Parcelable, Filter { } /** + * A data parcel that carries the instructions to draw the RemoteViews, as an alternative to + * XML layout. + */ + @FlaggedApi(FLAG_DRAW_DATA_PARCEL) + public static final class DrawInstructions { + + @NonNull + private final List<byte[]> mInstructions; + + private DrawInstructions() { + throw new UnsupportedOperationException( + "DrawInstructions cannot be instantiate without instructions"); + } + + private DrawInstructions(@NonNull List<byte[]> instructions) { + // Create and retain an immutable copy of given instructions. + mInstructions = new ArrayList<>(instructions.size()); + for (byte[] instruction : instructions) { + final int len = instruction.length; + final byte[] target = new byte[len]; + System.arraycopy(instruction, 0, target, 0, len); + mInstructions.add(target); + } + } + + @Nullable + private static DrawInstructions readFromParcel(@NonNull final Parcel in) { + int size = in.readInt(); + if (size == -1) { + return null; + } + byte[] instruction; + final List<byte[]> instructions = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + instruction = new byte[in.readInt()]; + in.readByteArray(instruction); + instructions.add(instruction); + } + return new DrawInstructions(instructions); + } + private static void writeToParcel(@Nullable final DrawInstructions drawInstructions, + @NonNull final Parcel dest, final int flags) { + if (drawInstructions == null) { + dest.writeInt(-1); + return; + } + final List<byte[]> instructions = drawInstructions.mInstructions; + dest.writeInt(instructions.size()); + for (byte[] instruction : instructions) { + dest.writeInt(instruction.length); + dest.writeByteArray(instruction); + } + } + + /** + * Append additional instructions to this {@link DrawInstructions} object. + */ + @FlaggedApi(FLAG_DRAW_DATA_PARCEL) + public void appendInstructions(@NonNull final byte[] instructions) { + mInstructions.add(instructions); + } + + /** + * Builder class for {@link DrawInstructions} objects. + */ + @FlaggedApi(FLAG_DRAW_DATA_PARCEL) + public static final class Builder { + + private final List<byte[]> mInstructions; + + /** + * Constructor. + * + * @param instructions Information to draw the RemoteViews. + */ + @FlaggedApi(FLAG_DRAW_DATA_PARCEL) + public Builder(@NonNull final List<byte[]> instructions) { + mInstructions = new ArrayList<>(instructions); + } + + /** + * Creates a {@link DrawInstructions} instance. + */ + @NonNull + @FlaggedApi(FLAG_DRAW_DATA_PARCEL) + public DrawInstructions build() { + return new DrawInstructions(mInstructions); + } + } + } + + /** * Get the ID of the top-level view of the XML layout, if set using * {@link RemoteViews#RemoteViews(String, int, int)}. */ diff --git a/core/java/android/window/TaskFragmentCreationParams.java b/core/java/android/window/TaskFragmentCreationParams.java index 5dbf328b6c0a..93297e64c621 100644 --- a/core/java/android/window/TaskFragmentCreationParams.java +++ b/core/java/android/window/TaskFragmentCreationParams.java @@ -94,11 +94,18 @@ public final class TaskFragmentCreationParams implements Parcelable { @Nullable private final IBinder mPairedActivityToken; + /** + * If {@code true}, transitions are allowed even if the TaskFragment is empty. If + * {@code false}, transitions will wait until the TaskFragment becomes non-empty or other + * conditions are met. Default to {@code false}. + */ + private final boolean mAllowTransitionWhenEmpty; + private TaskFragmentCreationParams( @NonNull TaskFragmentOrganizerToken organizer, @NonNull IBinder fragmentToken, @NonNull IBinder ownerToken, @NonNull Rect initialRelativeBounds, @WindowingMode int windowingMode, @Nullable IBinder pairedPrimaryFragmentToken, - @Nullable IBinder pairedActivityToken) { + @Nullable IBinder pairedActivityToken, boolean allowTransitionWhenEmpty) { if (pairedPrimaryFragmentToken != null && pairedActivityToken != null) { throw new IllegalArgumentException("pairedPrimaryFragmentToken and" + " pairedActivityToken should not be set at the same time."); @@ -110,6 +117,7 @@ public final class TaskFragmentCreationParams implements Parcelable { mWindowingMode = windowingMode; mPairedPrimaryFragmentToken = pairedPrimaryFragmentToken; mPairedActivityToken = pairedActivityToken; + mAllowTransitionWhenEmpty = allowTransitionWhenEmpty; } @NonNull @@ -155,6 +163,11 @@ public final class TaskFragmentCreationParams implements Parcelable { return mPairedActivityToken; } + /** @hide */ + public boolean getAllowTransitionWhenEmpty() { + return mAllowTransitionWhenEmpty; + } + private TaskFragmentCreationParams(Parcel in) { mOrganizer = TaskFragmentOrganizerToken.CREATOR.createFromParcel(in); mFragmentToken = in.readStrongBinder(); @@ -163,6 +176,7 @@ public final class TaskFragmentCreationParams implements Parcelable { mWindowingMode = in.readInt(); mPairedPrimaryFragmentToken = in.readStrongBinder(); mPairedActivityToken = in.readStrongBinder(); + mAllowTransitionWhenEmpty = in.readBoolean(); } /** @hide */ @@ -175,6 +189,7 @@ public final class TaskFragmentCreationParams implements Parcelable { dest.writeInt(mWindowingMode); dest.writeStrongBinder(mPairedPrimaryFragmentToken); dest.writeStrongBinder(mPairedActivityToken); + dest.writeBoolean(mAllowTransitionWhenEmpty); } @NonNull @@ -201,6 +216,7 @@ public final class TaskFragmentCreationParams implements Parcelable { + " windowingMode=" + mWindowingMode + " pairedFragmentToken=" + mPairedPrimaryFragmentToken + " pairedActivityToken=" + mPairedActivityToken + + " allowTransitionWhenEmpty=" + mAllowTransitionWhenEmpty + "}"; } @@ -234,6 +250,8 @@ public final class TaskFragmentCreationParams implements Parcelable { @Nullable private IBinder mPairedActivityToken; + private boolean mAllowTransitionWhenEmpty; + public Builder(@NonNull TaskFragmentOrganizerToken organizer, @NonNull IBinder fragmentToken, @NonNull IBinder ownerToken) { mOrganizer = organizer; @@ -298,12 +316,26 @@ public final class TaskFragmentCreationParams implements Parcelable { return this; } + /** + * Sets whether transitions are allowed when the TaskFragment is empty. If {@code true}, + * transitions are allowed when the TaskFragment is empty. If {@code false}, transitions + * will wait until the TaskFragment becomes non-empty or other conditions are met. Default + * to {@code false}. + * + * @hide + */ + @NonNull + public Builder setAllowTransitionWhenEmpty(boolean allowTransitionWhenEmpty) { + mAllowTransitionWhenEmpty = allowTransitionWhenEmpty; + return this; + } + /** Constructs the options to create TaskFragment with. */ @NonNull public TaskFragmentCreationParams build() { return new TaskFragmentCreationParams(mOrganizer, mFragmentToken, mOwnerToken, mInitialRelativeBounds, mWindowingMode, mPairedPrimaryFragmentToken, - mPairedActivityToken); + mPairedActivityToken, mAllowTransitionWhenEmpty); } } } diff --git a/core/java/android/window/WindowTokenClient.java b/core/java/android/window/WindowTokenClient.java index c20b278f7eaa..7f5331b936e9 100644 --- a/core/java/android/window/WindowTokenClient.java +++ b/core/java/android/window/WindowTokenClient.java @@ -167,6 +167,11 @@ public class WindowTokenClient extends Binder { + ", reported config=" + currentConfig + ", updated config=" + newConfig); } + // Update display first. In case callers want to obtain display information( + // ex: DisplayMetrics) in #onConfigurationChanged callback. + if (displayChanged) { + context.updateDisplay(newDisplayId); + } if (shouldUpdateResources) { // TODO(ag/9789103): update resource manager logic to track non-activity tokens mResourcesManager.updateResourcesForActivity(this, newConfig, newDisplayId); @@ -195,9 +200,6 @@ public class WindowTokenClient extends Binder { } } } - if (displayChanged) { - context.updateDisplay(newDisplayId); - } } /** diff --git a/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig b/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig index cbf6367b3d60..edfbea4f51a4 100644 --- a/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig +++ b/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig @@ -50,3 +50,11 @@ flag { description: "Whether we should allow hiding the size compat restart button" bug: "318840081" } + +flag { + name: "configurable_font_scale_default" + namespace: "large_screen_experiences_app_compat" + description: "Whether the font_scale is read from a device dependent configuration file" + bug: "319808237" + is_fixed_read_only: true +} diff --git a/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java b/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java index eeea17bf39dd..90ca95a7fbab 100644 --- a/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java +++ b/core/java/com/android/internal/config/sysui/SystemUiSystemPropertiesFlags.java @@ -71,7 +71,7 @@ public class SystemUiSystemPropertiesFlags { "persist.debug.sysui.notification.notif_cooldown_t1", 60000); /** Value used by polite notif. feature */ public static final Flag NOTIF_COOLDOWN_T2 = devFlag( - "persist.debug.sysui.notification.notif_cooldown_t2", 5000); + "persist.debug.sysui.notification.notif_cooldown_t2", 10000); /** Value used by polite notif. feature */ public static final Flag NOTIF_VOLUME1 = devFlag( "persist.debug.sysui.notification.notif_volume1", 30); @@ -81,6 +81,10 @@ public class SystemUiSystemPropertiesFlags { public static final Flag NOTIF_COOLDOWN_COUNTER_RESET = devFlag( "persist.debug.sysui.notification.notif_cooldown_counter_reset", 10); + /** Value used by polite notif. feature */ + public static final Flag NOTIF_AVALANCHE_TIMEOUT = devFlag( + "persist.debug.sysui.notification.notif_avalanche_timeout", 120_000); + /** b/303716154: For debugging only: use short bitmap duration. */ public static final Flag DEBUG_SHORT_BITMAP_DURATION = devFlag( "persist.sysui.notification.debug_short_bitmap_duration"); diff --git a/core/java/com/android/internal/logging/MetricsLogger.java b/core/java/com/android/internal/logging/MetricsLogger.java index e58f4f06daea..88aa89aaf48b 100644 --- a/core/java/com/android/internal/logging/MetricsLogger.java +++ b/core/java/com/android/internal/logging/MetricsLogger.java @@ -34,6 +34,7 @@ import com.android.internal.logging.nano.MetricsProto.MetricsEvent; * * @hide */ +@android.ravenwood.annotation.RavenwoodKeepWholeClass public class MetricsLogger { // define metric categories in frameworks/base/proto/src/metrics_constants.proto. // mirror changes in native version at system/core/libmetricslogger/metrics_logger.cpp diff --git a/core/java/com/android/internal/logging/testing/FakeMetricsLogger.java b/core/java/com/android/internal/logging/testing/FakeMetricsLogger.java index 6786427b5d86..df8bf313d06f 100644 --- a/core/java/com/android/internal/logging/testing/FakeMetricsLogger.java +++ b/core/java/com/android/internal/logging/testing/FakeMetricsLogger.java @@ -12,6 +12,7 @@ import java.util.Queue; * * @hide. */ +@android.ravenwood.annotation.RavenwoodKeepWholeClass public class FakeMetricsLogger extends MetricsLogger { private Queue<LogMaker> logs = new LinkedList<>(); diff --git a/core/java/com/android/internal/logging/testing/UiEventLoggerFake.java b/core/java/com/android/internal/logging/testing/UiEventLoggerFake.java index e303890c245a..6787ddc5d64d 100644 --- a/core/java/com/android/internal/logging/testing/UiEventLoggerFake.java +++ b/core/java/com/android/internal/logging/testing/UiEventLoggerFake.java @@ -27,6 +27,7 @@ import java.util.List; * * @hide. */ +@android.ravenwood.annotation.RavenwoodKeepWholeClass public class UiEventLoggerFake implements UiEventLogger { /** * Immutable data class used to record fake log events. diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedProviderUtils.java b/core/java/com/android/internal/pm/pkg/component/ParsedProviderUtils.java index 5d82d0469d56..12aff1c6669f 100644 --- a/core/java/com/android/internal/pm/pkg/component/ParsedProviderUtils.java +++ b/core/java/com/android/internal/pm/pkg/component/ParsedProviderUtils.java @@ -29,6 +29,7 @@ import android.content.pm.parsing.result.ParseResult; import android.content.res.Resources; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; +import android.multiuser.Flags; import android.os.Build; import android.os.PatternMatcher; import android.util.Slog; @@ -126,6 +127,10 @@ public class ParsedProviderUtils { .setFlags(provider.getFlags() | flag(ProviderInfo.FLAG_SINGLE_USER, R.styleable.AndroidManifestProvider_singleUser, sa)); + if (Flags.enableSystemUserOnlyForServicesAndProviders()) { + provider.setFlags(provider.getFlags() | flag(ProviderInfo.FLAG_SYSTEM_USER_ONLY, + R.styleable.AndroidManifestProvider_systemUserOnly, sa)); + } visibleToEphemeral = sa.getBoolean( R.styleable.AndroidManifestProvider_visibleToInstantApps, false); if (visibleToEphemeral) { diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java b/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java index a1dd19a3bc90..4ac542f84226 100644 --- a/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java +++ b/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java @@ -29,6 +29,7 @@ import android.content.pm.parsing.result.ParseResult; import android.content.res.Resources; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; +import android.multiuser.Flags; import android.os.Build; import com.android.internal.R; @@ -105,6 +106,11 @@ public class ParsedServiceUtils { | flag(ServiceInfo.FLAG_SINGLE_USER, R.styleable.AndroidManifestService_singleUser, sa))); + if (Flags.enableSystemUserOnlyForServicesAndProviders()) { + service.setFlags(service.getFlags() | flag(ServiceInfo.FLAG_SYSTEM_USER_ONLY, + R.styleable.AndroidManifestService_systemUserOnly, sa)); + } + visibleToEphemeral = sa.getBoolean( R.styleable.AndroidManifestService_visibleToInstantApps, false); if (visibleToEphemeral) { diff --git a/core/java/com/android/internal/widget/ILockSettingsStateListener.aidl b/core/java/com/android/internal/widget/ILockSettingsStateListener.aidl new file mode 100644 index 000000000000..25e30034fe8f --- /dev/null +++ b/core/java/com/android/internal/widget/ILockSettingsStateListener.aidl @@ -0,0 +1,36 @@ +/* + * 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; + +/** + * Callback interface between LockSettingService and other system services to be notified about the + * state of primary authentication (i.e. PIN/pattern/password). + * @hide + */ +oneway interface ILockSettingsStateListener { + /** + * Defines behavior in response to a successful authentication + * @param userId The user Id for the requested authentication + */ + void onAuthenticationSucceeded(int userId); + + /** + * Defines behavior in response to a failed authentication + * @param userId The user Id for the requested authentication + */ + void onAuthenticationFailed(int userId); +}
\ No newline at end of file diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java index 757978b71a01..b5b3a48dacb7 100644 --- a/core/java/com/android/internal/widget/LockPatternUtils.java +++ b/core/java/com/android/internal/widget/LockPatternUtils.java @@ -333,11 +333,17 @@ public class LockPatternUtils { @UnsupportedAppUsage public LockPatternUtils(Context context) { + this(context, null); + } + + @VisibleForTesting + public LockPatternUtils(Context context, ILockSettings lockSettings) { mContext = context; mContentResolver = context.getContentResolver(); Looper looper = Looper.myLooper(); mHandler = looper != null ? new Handler(looper) : null; + mLockSettingsService = lockSettings; } @UnsupportedAppUsage diff --git a/core/java/com/android/internal/widget/LockSettingsInternal.java b/core/java/com/android/internal/widget/LockSettingsInternal.java index 8114e1fd3bb0..627e8779f9d0 100644 --- a/core/java/com/android/internal/widget/LockSettingsInternal.java +++ b/core/java/com/android/internal/widget/LockSettingsInternal.java @@ -166,4 +166,16 @@ public abstract class LockSettingsInternal { * Refreshes pending strong auth timeout with the latest admin requirement set by device policy. */ public abstract void refreshStrongAuthTimeout(int userId); + + /** + * Register a LockSettingsStateListener + * @param listener The listener to be registered + */ + public abstract void registerLockSettingsStateListener(ILockSettingsStateListener listener); + + /** + * Unregister a LockSettingsStateListener + * @param listener The listener to be unregistered + */ + public abstract void unregisterLockSettingsStateListener(ILockSettingsStateListener listener); } diff --git a/core/java/com/android/internal/widget/MessagingLinearLayout.java b/core/java/com/android/internal/widget/MessagingLinearLayout.java index c06f5f75514f..e07acac52f2c 100644 --- a/core/java/com/android/internal/widget/MessagingLinearLayout.java +++ b/core/java/com/android/internal/widget/MessagingLinearLayout.java @@ -21,6 +21,8 @@ import android.annotation.Px; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; +import android.os.Build; +import android.os.Trace; import android.util.AttributeSet; import android.view.RemotableViewMethod; import android.view.View; @@ -45,6 +47,8 @@ public class MessagingLinearLayout extends ViewGroup { private int mMaxDisplayedLines = Integer.MAX_VALUE; + private static final boolean TRACE_ONMEASURE = Build.isDebuggable(); + public MessagingLinearLayout(Context context, @Nullable AttributeSet attrs) { super(context, attrs); @@ -67,6 +71,10 @@ public class MessagingLinearLayout extends ViewGroup { @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (TRACE_ONMEASURE) { + Trace.beginSection("MessagingLinearLayout#onMeasure"); + trackMeasureSpecs(widthMeasureSpec, heightMeasureSpec); + } // This is essentially a bottom-up linear layout that only adds children that fit entirely // up to a maximum height. int targetHeight = MeasureSpec.getSize(heightMeasureSpec); @@ -177,6 +185,9 @@ public class MessagingLinearLayout extends ViewGroup { resolveSize(Math.max(getSuggestedMinimumWidth(), measuredWidth), widthMeasureSpec), Math.max(getSuggestedMinimumHeight(), totalHeight)); + if (TRACE_ONMEASURE) { + Trace.endSection(); + } } @Override @@ -240,6 +251,25 @@ public class MessagingLinearLayout extends ViewGroup { } } + private void trackMeasureSpecs(int widthMeasureSpec, int heightMeasureSpec) { + if (!TRACE_ONMEASURE) { + return; + } + + final int availableWidth = MeasureSpec.getSize(widthMeasureSpec); + final int widthMode = MeasureSpec.getMode(widthMeasureSpec); + final int availableHeight = MeasureSpec.getSize(heightMeasureSpec); + final int heightMode = MeasureSpec.getMode(heightMeasureSpec); + Trace.setCounter("MessagingLinearLayout#onMeasure_widthMeasureSpecSize", + availableWidth); + Trace.setCounter("MessagingLinearLayout#onMeasure_widthMeasureSpecMode", + widthMode); + Trace.setCounter("MessagingLinearLayout#onMeasure_heightMeasureSpecSize", + availableHeight); + Trace.setCounter("MessagingLinearLayout#onMeasure_heightMeasureSpecMode", + heightMode); + } + @Override protected boolean drawChild(Canvas canvas, View child, long drawingTime) { final LayoutParams lp = (LayoutParams) child.getLayoutParams(); diff --git a/core/java/com/android/server/backup/AccountSyncSettingsBackupHelper.java b/core/java/com/android/server/backup/AccountSyncSettingsBackupHelper.java index ce9ab82614d5..2ff62251d786 100644 --- a/core/java/com/android/server/backup/AccountSyncSettingsBackupHelper.java +++ b/core/java/com/android/server/backup/AccountSyncSettingsBackupHelper.java @@ -21,6 +21,7 @@ import android.accounts.AccountManager; import android.app.backup.BackupDataInputStream; import android.app.backup.BackupDataOutput; import android.app.backup.BackupHelper; +import android.app.backup.BackupHelperWithLogger; import android.content.ContentResolver; import android.content.Context; import android.content.SyncAdapterType; @@ -56,7 +57,7 @@ import java.util.Set; * sync settings are backed up as a JSON object containing all the necessary information for * restoring the sync settings later. */ -public class AccountSyncSettingsBackupHelper implements BackupHelper { +public class AccountSyncSettingsBackupHelper extends BackupHelperWithLogger { private static final String TAG = "AccountSyncSettingsBackupHelper"; private static final boolean DEBUG = false; diff --git a/core/jni/android_util_Process.cpp b/core/jni/android_util_Process.cpp index 7af69f2dff08..6a640a5ab23b 100644 --- a/core/jni/android_util_Process.cpp +++ b/core/jni/android_util_Process.cpp @@ -28,6 +28,7 @@ #include <meminfo/sysmeminfo.h> #include <processgroup/processgroup.h> #include <processgroup/sched_policy.h> +#include <android-base/logging.h> #include <android-base/unique_fd.h> #include <algorithm> @@ -232,6 +233,31 @@ void android_os_Process_setThreadGroupAndCpuset(JNIEnv* env, jobject clazz, int } } +// Look up the user ID of a process in /proc/${pid}/status. The Uid: line is present in +// /proc/${pid}/status since at least kernel v2.5. +static int uid_from_pid(int pid) +{ + int uid = -1; + std::array<char, 64> path; + int res = snprintf(path.data(), path.size(), "/proc/%d/status", pid); + if (res < 0 || res >= static_cast<int>(path.size())) { + DCHECK(false); + return uid; + } + FILE* f = fopen(path.data(), "r"); + if (!f) { + return uid; + } + char line[256]; + while (fgets(line, sizeof(line), f)) { + if (sscanf(line, "Uid: %d", &uid) == 1) { + break; + } + } + fclose(f); + return uid; +} + void android_os_Process_setProcessGroup(JNIEnv* env, jobject clazz, int pid, jint grp) { ALOGV("%s pid=%d grp=%" PRId32, __func__, pid, grp); @@ -275,7 +301,12 @@ void android_os_Process_setProcessGroup(JNIEnv* env, jobject clazz, int pid, jin } } - if (!SetProcessProfilesCached(0, pid, {get_cpuset_policy_profile_name((SchedPolicy)grp)})) + const int uid = uid_from_pid(pid); + if (uid < 0) { + signalExceptionForGroupError(env, ESRCH, pid); + return; + } + if (!SetProcessProfilesCached(uid, pid, {get_cpuset_policy_profile_name((SchedPolicy)grp)})) signalExceptionForGroupError(env, errno ? errno : EPERM, pid); } diff --git a/core/jni/android_view_SurfaceControl.cpp b/core/jni/android_view_SurfaceControl.cpp index 25b2aaf3d737..98f409a334dc 100644 --- a/core/jni/android_view_SurfaceControl.cpp +++ b/core/jni/android_view_SurfaceControl.cpp @@ -343,7 +343,9 @@ public: const std::vector<SurfaceControlStats>& /*stats*/) { JNIEnv* env = getenv(); // Adding a strong reference for java SyncFence - presentFence->incStrong(0); + if (presentFence) { + presentFence->incStrong(0); + } jobject stats = env->NewObject(gTransactionStatsClassInfo.clazz, gTransactionStatsClassInfo.ctor, diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 52cf67981bb3..070e2cb0dc5e 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -2961,7 +2961,7 @@ <p>Protection level: signature @SystemApi @hide - @FlaggedApi("com.android.internal.telephony.flags.ap_domain_selection_enabled") + @FlaggedApi("com.android.internal.telephony.flags.use_oem_domain_selection_service") --> <permission android:name="android.permission.BIND_DOMAIN_SELECTION_SERVICE" android:protectionLevel="signature" /> @@ -3775,6 +3775,13 @@ <permission android:name="android.permission.MANAGE_DEVICE_POLICY_ACROSS_USERS_FULL" android:protectionLevel="internal|role" /> + <!-- Allows an application to access EnhancedConfirmationManager. + @SystemApi + @FlaggedApi("android.permission.flags.enhanced_confirmation_mode_apis_enabled") + @hide This is not a third-party API (intended for OEMs and system apps). --> + <permission android:name="android.permission.MANAGE_ENHANCED_CONFIRMATION_STATES" + android:protectionLevel="signature|installer" /> + <!-- @SystemApi @hide Allows an application to set a device owner on retail demo devices.--> <permission android:name="android.permission.PROVISION_DEMO_DEVICE" android:protectionLevel="signature|setup|knownSigner" @@ -5737,6 +5744,14 @@ android:description="@string/permdesc_observeCompanionDevicePresence" android:protectionLevel="normal" /> + <!-- Allows an application to subscribe to notifications about the nearby devices' presence + status change base on the UUIDs. + <p>Not for use by third-party applications.</p> + @FlaggedApi("android.companion.flags.device_presence") + --> + <permission android:name="android.permission.REQUEST_OBSERVE_DEVICE_UUID_PRESENCE" + android:protectionLevel="signature|privileged" /> + <!-- Allows an application to deliver companion messages to system --> <permission android:name="android.permission.DELIVER_COMPANION_MESSAGES" @@ -7050,6 +7065,7 @@ android:protectionLevel="signature" /> <!-- @SystemApi Allows an application to access the smartspace service as a client. + @FlaggedApi(android.app.smartspace.flags.Flags.FLAG_ACCESS_SMARTSPACE) @hide <p>Not for use by third-party applications.</p> --> <permission android:name="android.permission.ACCESS_SMARTSPACE" android:protectionLevel="signature|privileged|development" /> @@ -7946,11 +7962,11 @@ <!-- @SystemApi Allows an application to read the system grammatical gender. @FlaggedApi("android.app.system_terms_of_address_enabled") - <p>Protection level: signature|privileged|appop + <p>Protection level: signature|privileged @hide --> <permission android:name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER" - android:protectionLevel="signature|privileged|appop"/> + android:protectionLevel="signature|privileged"/> <!-- Attribution for Geofencing service. --> <attribution android:tag="GeofencingService" android:label="@string/geofencing_service"/> diff --git a/core/res/res/drawable/autofill_half_sheet_divider.xml b/core/res/res/drawable/autofill_half_sheet_divider.xml new file mode 100644 index 000000000000..1a96c7dc263e --- /dev/null +++ b/core/res/res/drawable/autofill_half_sheet_divider.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!-- Copied from //frameworks/base/core/res/res/drawable/list_divider_material.xml. --> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:tint="@color/foreground_material_light"> + <solid android:color="#1f000000" /> + <size + android:height="1dp" + android:width="1dp"/> +</shape>
\ No newline at end of file diff --git a/core/res/res/layout/autofill_save.xml b/core/res/res/layout/autofill_save.xml index 27f8138ac5e3..ddedca2865c4 100644 --- a/core/res/res/layout/autofill_save.xml +++ b/core/res/res/layout/autofill_save.xml @@ -31,11 +31,11 @@ android:gravity="center_horizontal" android:orientation="vertical"> <ScrollView + android:id="@+id/autofill_sheet_scroll_view" android:layout_width="fill_parent" android:layout_height="0dp" android:fillViewport="true" - android:layout_weight="1" - android:layout_marginBottom="8dp"> + android:layout_weight="1"> <LinearLayout android:layout_marginStart="@dimen/autofill_save_outer_margin" android:layout_marginEnd="@dimen/autofill_save_outer_margin" @@ -66,16 +66,25 @@ android:layout_height="wrap_content" android:minHeight="0dp" android:visibility="gone"/> - + <View + android:id="@+id/autofill_sheet_scroll_view_space" + android:layout_width="match_parent" + android:layout_height="16dp"/> </LinearLayout> </ScrollView> + <View + android:id="@+id/autofill_sheet_divider" + android:layout_width="match_parent" + android:layout_height="1dp" + style="@style/AutofillHalfSheetDivider" /> + <com.android.internal.widget.ButtonBarLayout android:layout_width="match_parent" android:layout_height="48dp" android:layout_gravity="end" android:clipToPadding="false" - android:layout_marginTop="16dp" + android:layout_marginTop="8dp" android:layout_marginBottom="8dp" android:theme="@style/Theme.DeviceDefault.AutofillHalfScreenDialogButton" android:orientation="horizontal" diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml index 29086a452ee6..35276bf8ead2 100644 --- a/core/res/res/values/attrs_manifest.xml +++ b/core/res/res/values/attrs_manifest.xml @@ -506,6 +506,12 @@ receivers, and providers; it can not be used with activities. --> <attr name="singleUser" format="boolean" /> + <!-- If set to true, only a single instance of this component will + run and be available for the SYSTEM user. Non SYSTEM users will not be + allowed to access the component if this flag is enabled. + This flag can be used with services, receivers, providers and activities. --> + <attr name="systemUserOnly" format="boolean" /> + <!-- Specify a specific process that the associated code is to run in. Use with the application tag (to supply a default process for all application components), or with the activity, receiver, service, @@ -2865,6 +2871,7 @@ Context.createAttributionContext() using the first attribution tag contained here. --> <attr name="attributionTags" /> + <attr name="systemUserOnly" format="boolean" /> </declare-styleable> <!-- Attributes that can be supplied in an AndroidManifest.xml @@ -3023,6 +3030,7 @@ ignored when the process is bound into a shared isolated process by a client. --> <attr name="allowSharedIsolatedProcess" format="boolean" /> + <attr name="systemUserOnly" format="boolean" /> </declare-styleable> <!-- @hide The <code>apex-system-service</code> tag declares an apex system service @@ -3150,7 +3158,7 @@ <attr name="uiOptions" /> <attr name="parentActivityName" /> <attr name="singleUser" /> - <!-- @hide This broadcast receiver or activity will only receive broadcasts for the + <!-- This broadcast receiver or activity will only receive broadcasts for the system user--> <attr name="systemUserOnly" format="boolean" /> <attr name="persistableMode" /> diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 9e1400641084..0d1a98785695 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -4497,6 +4497,16 @@ <!-- URI for default Accessibility notification sound when to enable accessibility shortcut. --> <string name="config_defaultAccessibilityNotificationSound" translatable="false"></string> + <!-- Array of component names, each flattened to a string, for accessibility services that + can be enabled by the user without showing a warning prompt. These services must be + preinstalled. --> + <string-array translatable="false" name="config_trustedAccessibilityServices"> + <!-- + <item>com.example.package.first/com.example.class.FirstService</item> + <item>com.example.package.second/com.example.class.SecondService</item> + --> + </string-array> + <!-- Warning: This API can be dangerous when not implemented properly. In particular, escrow token must NOT be retrievable from device storage. In other words, either escrow token is not stored on device or its ciphertext is stored on device while @@ -6891,4 +6901,11 @@ <!-- Defines suitability of the built-in speaker route. Refer to {@link MediaRoute2Info} to see supported values. --> <integer name="config_mediaRouter_builtInSpeakerSuitability">0</integer> + + <!-- Whether to show a percentage text next to the progressbar while preparing to update the + device --> + <bool name="config_showPercentageTextDuringRebootToUpdate">true</bool> + + <!-- Defines the minimum interval (in ms) between two input-based user-activity poke events. --> + <integer name="config_minMillisBetweenInputUserActivityEvents">100</integer> </resources> diff --git a/core/res/res/values/public-staging.xml b/core/res/res/values/public-staging.xml index 7d2288599841..b8fc052a2fa9 100644 --- a/core/res/res/values/public-staging.xml +++ b/core/res/res/values/public-staging.xml @@ -121,6 +121,8 @@ <public name="adServiceTypes" /> <!-- @hide @SystemApi @FlaggedApi("android.content.res.manifest_flagging") --> <public name="featureFlag"/> + <!-- @FlaggedApi("android.multiuser.enable_system_user_only_for_services_and_providers") --> + <public name="systemUserOnly"/> </staging-public-group> <staging-public-group type="id" first-id="0x01bc0000"> diff --git a/core/res/res/values/styles.xml b/core/res/res/values/styles.xml index 619ec31e37bc..22d028cb079e 100644 --- a/core/res/res/values/styles.xml +++ b/core/res/res/values/styles.xml @@ -1515,6 +1515,11 @@ please see styles_device_defaults.xml. <item name="background">@drawable/btn_outlined</item> </style> + <!-- @hide Divider for Autofill half screen dialog --> + <style name="AutofillHalfSheetDivider"> + <item name="android:background">@drawable/autofill_half_sheet_divider</item> + </style> + <!-- @hide Autofill background for popup window (not for fullscreen) --> <style name="AutofillDatasetPicker"> <item name="elevation">4dp</item> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index ef12d8f4ac0f..3c1a42feef25 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -3630,6 +3630,7 @@ <java-symbol type="string" name="config_defaultAccessibilityService" /> <java-symbol type="string" name="config_defaultAccessibilityNotificationSound" /> <java-symbol type="string" name="accessibility_shortcut_spoken_feedback" /> + <java-symbol type="array" name="config_trustedAccessibilityServices" /> <java-symbol type="string" name="accessibility_select_shortcut_menu_title" /> <java-symbol type="string" name="accessibility_edit_shortcut_menu_button_title" /> @@ -3703,6 +3704,10 @@ <java-symbol type="id" name="autofill_dataset_list"/> <java-symbol type="id" name="autofill_dataset_picker"/> <java-symbol type="id" name="autofill_dataset_title" /> + <java-symbol type="id" name="autofill_sheet_divider"/> + <java-symbol type="id" name="autofill_sheet_scroll_view"/> + <java-symbol type="id" name="autofill_sheet_scroll_view_space"/> + <java-symbol type="id" name="autofill_save_custom_subtitle" /> <java-symbol type="id" name="autofill_save_icon" /> <java-symbol type="id" name="autofill_save_no" /> @@ -5312,4 +5317,9 @@ <!-- Android MediaRouter framework configs. --> <java-symbol type="integer" name="config_mediaRouter_builtInSpeakerSuitability" /> + + <!-- Shutdown thread config flags --> + <java-symbol type="bool" name="config_showPercentageTextDuringRebootToUpdate" /> + + <java-symbol type="integer" name="config_minMillisBetweenInputUserActivityEvents" /> </resources> diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp index d1a90aea835c..440630287b7c 100644 --- a/core/tests/coretests/Android.bp +++ b/core/tests/coretests/Android.bp @@ -212,8 +212,8 @@ android_ravenwood_test { "src/android/database/CursorWindowTest.java", "src/android/os/**/*.java", "src/android/util/**/*.java", + "src/com/android/internal/logging/**/*.java", "src/com/android/internal/os/**/*.java", - "src/com/android/internal/os/LongArrayMultiStateCounterTest.java", "src/com/android/internal/util/**/*.java", "src/com/android/internal/power/EnergyConsumerStatsTest.java", diff --git a/core/tests/coretests/src/android/os/BuildTest.java b/core/tests/coretests/src/android/os/BuildTest.java index 2d3e12331e23..2a718ff2f4aa 100644 --- a/core/tests/coretests/src/android/os/BuildTest.java +++ b/core/tests/coretests/src/android/os/BuildTest.java @@ -20,7 +20,6 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; -import android.platform.test.annotations.IgnoreUnderRavenwood; import android.platform.test.flag.junit.SetFlagsRule; import android.platform.test.ravenwood.RavenwoodRule; @@ -71,7 +70,6 @@ public class BuildTest { */ @Test @SmallTest - @IgnoreUnderRavenwood(blockedBy = Build.class) public void testBuildFields() throws Exception { assertNotEmpty("ID", Build.ID); assertNotEmpty("DISPLAY", Build.DISPLAY); diff --git a/core/tests/coretests/src/android/os/TraceTest.java b/core/tests/coretests/src/android/os/TraceTest.java index 593833ec96a7..b2c005f8a4f4 100644 --- a/core/tests/coretests/src/android/os/TraceTest.java +++ b/core/tests/coretests/src/android/os/TraceTest.java @@ -34,7 +34,6 @@ import org.junit.runner.RunWith; * while tracing on the emulator and then run traceview to view the trace. */ @RunWith(AndroidJUnit4.class) -@IgnoreUnderRavenwood(blockedBy = Trace.class) public class TraceTest { private static final String TAG = "TraceTest"; @@ -46,7 +45,51 @@ public class TraceTest { private int gMethodCalls = 0; @Test + public void testEnableDisable() { + // Currently only verifying that we can invoke without crashing + Trace.setTracingEnabled(true, 0); + Trace.setTracingEnabled(false, 0); + + Trace.setAppTracingAllowed(true); + Trace.setAppTracingAllowed(false); + } + + @Test + public void testBeginEnd() { + // Currently only verifying that we can invoke without crashing + Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, TAG); + Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); + + Trace.asyncTraceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, TAG, 42); + Trace.asyncTraceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER, TAG, 42); + + Trace.asyncTraceForTrackBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, TAG, TAG, 42); + Trace.asyncTraceForTrackEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER, TAG, 42); + + Trace.beginSection(TAG); + Trace.endSection(); + + Trace.beginAsyncSection(TAG, 42); + Trace.endAsyncSection(TAG, 42); + } + + @Test + public void testCounter() { + // Currently only verifying that we can invoke without crashing + Trace.traceCounter(Trace.TRACE_TAG_ACTIVITY_MANAGER, TAG, 42); + Trace.setCounter(TAG, 42); + } + + @Test + public void testInstant() { + // Currently only verifying that we can invoke without crashing + Trace.instant(Trace.TRACE_TAG_ACTIVITY_MANAGER, TAG); + Trace.instantForTrack(Trace.TRACE_TAG_ACTIVITY_MANAGER, TAG, TAG); + } + + @Test public void testNullStrings() { + // Currently only verifying that we can invoke without crashing Trace.traceCounter(Trace.TRACE_TAG_ACTIVITY_MANAGER, null, 42); Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, null); @@ -62,6 +105,7 @@ public class TraceTest { @Test @SmallTest + @IgnoreUnderRavenwood(blockedBy = Debug.class) public void testNativeTracingFromJava() { long start = System.currentTimeMillis(); @@ -82,6 +126,7 @@ public class TraceTest { // This should not run in the automated suite. @Suppress + @IgnoreUnderRavenwood(blockedBy = Debug.class) public void disableTestNativeTracingFromC() { long start = System.currentTimeMillis(); @@ -97,6 +142,7 @@ public class TraceTest { @Test @LargeTest @Suppress // Failing. + @IgnoreUnderRavenwood(blockedBy = Debug.class) public void testMethodTracing() { long start = System.currentTimeMillis(); diff --git a/core/tests/coretests/src/android/view/InsetsStateTest.java b/core/tests/coretests/src/android/view/InsetsStateTest.java index 906d84ec96b6..672875a19f9f 100644 --- a/core/tests/coretests/src/android/view/InsetsStateTest.java +++ b/core/tests/coretests/src/android/view/InsetsStateTest.java @@ -20,8 +20,8 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; import static android.view.InsetsSource.FLAG_FORCE_CONSUMING; import static android.view.InsetsSource.ID_IME; -import static android.view.InsetsState.ISIDE_BOTTOM; -import static android.view.InsetsState.ISIDE_TOP; +import static android.view.InsetsSource.SIDE_BOTTOM; +import static android.view.InsetsSource.SIDE_TOP; import static android.view.RoundedCorner.POSITION_BOTTOM_LEFT; import static android.view.RoundedCorner.POSITION_BOTTOM_RIGHT; import static android.view.RoundedCorner.POSITION_TOP_LEFT; @@ -106,8 +106,8 @@ public class InsetsStateTest { typeSideMap); assertEquals(Insets.of(0, 100, 0, 100), insets.getSystemWindowInsets()); assertEquals(Insets.of(0, 100, 0, 100), insets.getInsets(Type.all())); - assertEquals(ISIDE_TOP, typeSideMap.get(ID_STATUS_BAR)); - assertEquals(ISIDE_BOTTOM, typeSideMap.get(ID_IME)); + assertEquals(SIDE_TOP, typeSideMap.get(ID_STATUS_BAR)); + assertEquals(SIDE_BOTTOM, typeSideMap.get(ID_IME)); assertEquals(Insets.of(0, 100, 0, 0), insets.getInsets(statusBars())); assertEquals(Insets.of(0, 0, 0, 100), insets.getInsets(ime())); } diff --git a/core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionV2Test.java b/core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionV2Test.java index f0f3a9683353..00751288ad62 100644 --- a/core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionV2Test.java +++ b/core/tests/coretests/src/android/view/contentcapture/MainContentCaptureSessionV2Test.java @@ -433,6 +433,72 @@ public class MainContentCaptureSessionV2Test { assertThat(session.mEvents).isEmpty(); } + @Test + public void notifyViewAppearedBelowMaximumBufferSize() throws RemoteException { + ContentCaptureOptions options = + createOptions( + /* enableContentCaptureReceiver= */ true, + /* enableContentProtectionReceiver= */ true); + MainContentCaptureSessionV2 session = createSession(options); + session.mDirectServiceInterface = mMockContentCaptureDirectManager; + + session.onSessionStarted(0x2, null); + for (int i = 0; i < BUFFER_SIZE - 1; i++) { + View view = prepareView(session); + session.notifyViewAppeared(session.newViewStructure(view)); + } + mTestableLooper.processAllMessages(); + + verify(mMockContentCaptureDirectManager, times(0)) + .sendEvents(any(), anyInt(), any()); + assertThat(session.mEvents).isNull(); + assertThat(session.mEventProcessQueue).hasSize(BUFFER_SIZE - 1); + } + + @Test + public void notifyViewAppearedExactAsMaximumBufferSize() throws RemoteException { + ContentCaptureOptions options = + createOptions( + /* enableContentCaptureReceiver= */ true, + /* enableContentProtectionReceiver= */ true); + MainContentCaptureSessionV2 session = createSession(options); + session.mDirectServiceInterface = mMockContentCaptureDirectManager; + + session.onSessionStarted(0x2, null); + for (int i = 0; i < BUFFER_SIZE; i++) { + View view = prepareView(session); + session.notifyViewAppeared(session.newViewStructure(view)); + } + mTestableLooper.processAllMessages(); + + verify(mMockContentCaptureDirectManager, times(1)) + .sendEvents(any(), anyInt(), any()); + assertThat(session.mEvents).isEmpty(); + assertThat(session.mEventProcessQueue).isEmpty(); + } + + @Test + public void notifyViewAppearedAboveMaximumBufferSize() throws RemoteException { + ContentCaptureOptions options = + createOptions( + /* enableContentCaptureReceiver= */ true, + /* enableContentProtectionReceiver= */ true); + MainContentCaptureSessionV2 session = createSession(options); + session.mDirectServiceInterface = mMockContentCaptureDirectManager; + + session.onSessionStarted(0x2, null); + for (int i = 0; i < BUFFER_SIZE * 2 + 1; i++) { + View view = prepareView(session); + session.notifyViewAppeared(session.newViewStructure(view)); + } + mTestableLooper.processAllMessages(); + + verify(mMockContentCaptureDirectManager, times(2)) + .sendEvents(any(), anyInt(), any()); + assertThat(session.mEvents).isEmpty(); + assertThat(session.mEventProcessQueue).hasSize(1); + } + /** Simulates the regular content capture events sequence. */ private void notifyContentCaptureEvents(final MainContentCaptureSessionV2 session) { final ArrayList<Object> events = new ArrayList<>( diff --git a/core/tests/coretests/src/android/widget/RemoteViewsTest.java b/core/tests/coretests/src/android/widget/RemoteViewsTest.java index 15c90474c017..543d73bc34e8 100644 --- a/core/tests/coretests/src/android/widget/RemoteViewsTest.java +++ b/core/tests/coretests/src/android/widget/RemoteViewsTest.java @@ -16,6 +16,8 @@ package android.widget; +import static android.appwidget.flags.Flags.drawDataParcel; + import static com.android.internal.R.id.pending_intent_tag; import static org.junit.Assert.assertArrayEquals; @@ -63,6 +65,7 @@ import org.junit.runner.RunWith; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.concurrent.CountDownLatch; @@ -414,6 +417,48 @@ public class RemoteViewsTest { assertNotNull(view.findViewById(R.id.light_background_text)); } + @Test + public void remoteCanvasCanAccessDrawInstructions() { + if (!drawDataParcel()) { + return; + } + final RemoteViews.DrawInstructions drawInstructions = getDrawInstructions(); + final RemoteViews rv = new RemoteViews(drawInstructions); + final View view = rv.apply(mContext, mContainer); + assertTrue(view instanceof RemoteCanvas); + assertEquals(drawInstructions, view.getTag()); + } + + @Test + public void remoteCanvasWiresClickHandlers() { + if (!drawDataParcel()) { + return; + } + final RemoteViews.DrawInstructions drawInstructions = getDrawInstructions(); + final RemoteViews rv = new RemoteViews(drawInstructions); + final PendingIntent pi = PendingIntent.getActivity(mContext, 0, + new Intent(Intent.ACTION_VIEW), PendingIntent.FLAG_IMMUTABLE); + final Intent i = new Intent().putExtra("TEST", "Success"); + final int viewId = 1; + rv.setPendingIntentTemplate(viewId, pi); + rv.setOnClickFillInIntent(viewId, i); + final View view = rv.apply(mContext, mContainer); + assertTrue(view instanceof RemoteCanvas); + RemoteCanvas target = (RemoteCanvas) view; + assertEquals(1, target.getCallbacks().size()); + assertNotNull(target.getCallbacks().get(viewId)); + } + + private RemoteViews.DrawInstructions getDrawInstructions() { + final byte[] first = new byte[] {'f', 'i', 'r', 's', 't'}; + final byte[] second = new byte[] {'s', 'e', 'c', 'o', 'n', 'd'}; + final RemoteViews.DrawInstructions drawInstructions = + new RemoteViews.DrawInstructions.Builder( + Collections.singletonList(first)).build(); + drawInstructions.appendInstructions(second); + return drawInstructions; + } + private RemoteViews createViewChained(int depth, String... texts) { RemoteViews result = new RemoteViews(mPackage, R.layout.remote_view_host); diff --git a/core/tests/coretests/src/com/android/internal/logging/MetricsLoggerTest.java b/core/tests/coretests/src/com/android/internal/logging/MetricsLoggerTest.java new file mode 100644 index 000000000000..7054cc0f24b4 --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/logging/MetricsLoggerTest.java @@ -0,0 +1,67 @@ +/* + * 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.logging; + +import static com.google.common.truth.Truth.assertThat; + +import android.metrics.LogMaker; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.logging.nano.MetricsProto; +import com.android.internal.logging.testing.FakeMetricsLogger; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class MetricsLoggerTest { + private FakeMetricsLogger mLogger; + + private static final int TEST_ACTION = 42; + + @Before + public void setUp() throws Exception { + mLogger = new FakeMetricsLogger(); + } + + @Test + public void testEmpty() throws Exception { + assertThat(mLogger.getLogs().size()).isEqualTo(0); + } + + @Test + public void testAction() throws Exception { + mLogger.action(TEST_ACTION); + assertThat(mLogger.getLogs().size()).isEqualTo(1); + final LogMaker event = mLogger.getLogs().peek(); + assertThat(event.getType()).isEqualTo(MetricsProto.MetricsEvent.TYPE_ACTION); + assertThat(event.getCategory()).isEqualTo(TEST_ACTION); + } + + @Test + public void testVisible() throws Exception { + // Limited testing to confirm we don't crash + mLogger.visible(TEST_ACTION); + mLogger.hidden(TEST_ACTION); + mLogger.visibility(TEST_ACTION, true); + mLogger.visibility(TEST_ACTION, false); + } +} diff --git a/core/tests/coretests/src/com/android/internal/logging/UiEventLoggerTest.java b/core/tests/coretests/src/com/android/internal/logging/UiEventLoggerTest.java new file mode 100644 index 000000000000..7840f7177278 --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/logging/UiEventLoggerTest.java @@ -0,0 +1,80 @@ +/* + * 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.logging; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.logging.testing.UiEventLoggerFake; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class UiEventLoggerTest { + private UiEventLoggerFake mLogger; + + private static final int TEST_EVENT_ID = 42; + private static final int TEST_INSTANCE_ID = 21; + + private enum MyUiEventEnum implements UiEventLogger.UiEventEnum { + @UiEvent(doc = "Example event") + TEST_EVENT(TEST_EVENT_ID); + + private final int mId; + + MyUiEventEnum(int id) { + mId = id; + } + + @Override + public int getId() { + return mId; + } + } + + private InstanceId TEST_INSTANCE = InstanceId.fakeInstanceId(TEST_INSTANCE_ID); + + @Before + public void setUp() throws Exception { + mLogger = new UiEventLoggerFake(); + } + + @Test + public void testEmpty() throws Exception { + assertThat(mLogger.numLogs()).isEqualTo(0); + } + + @Test + public void testSimple() throws Exception { + mLogger.log(MyUiEventEnum.TEST_EVENT); + assertThat(mLogger.numLogs()).isEqualTo(1); + assertThat(mLogger.eventId(0)).isEqualTo(TEST_EVENT_ID); + } + + @Test + public void testWithInstance() throws Exception { + mLogger.log(MyUiEventEnum.TEST_EVENT, TEST_INSTANCE); + assertThat(mLogger.numLogs()).isEqualTo(1); + assertThat(mLogger.eventId(0)).isEqualTo(TEST_EVENT_ID); + assertThat(mLogger.get(0).instanceId.getId()).isEqualTo(TEST_INSTANCE_ID); + } +} diff --git a/core/tests/coretests/src/com/android/internal/os/MonotonicClockTest.java b/core/tests/coretests/src/com/android/internal/os/MonotonicClockTest.java index ec4c563e469e..06d888b59166 100644 --- a/core/tests/coretests/src/com/android/internal/os/MonotonicClockTest.java +++ b/core/tests/coretests/src/com/android/internal/os/MonotonicClockTest.java @@ -18,10 +18,14 @@ package com.android.internal.os; import static com.google.common.truth.Truth.assertThat; +import android.platform.test.annotations.IgnoreUnderRavenwood; +import android.platform.test.ravenwood.RavenwoodRule; + import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -33,6 +37,9 @@ import java.nio.file.Files; @RunWith(AndroidJUnit4.class) @SmallTest public class MonotonicClockTest { + @Rule + public final RavenwoodRule mRavenwood = new RavenwoodRule(); + private final MockClock mClock = new MockClock(); private File mFile; @@ -70,6 +77,7 @@ public class MonotonicClockTest { } @Test + @IgnoreUnderRavenwood(reason = "b/321832617") public void corruptedFile() throws IOException { // Create an invalid binary XML file to cause IOException: "Unexpected magic number" try (FileWriter w = new FileWriter(mFile)) { diff --git a/core/tests/coretests/src/com/android/internal/widget/LockPatternUtilsTest.java b/core/tests/coretests/src/com/android/internal/widget/LockPatternUtilsTest.java index 1a668f7bdc8f..b90480a1e794 100644 --- a/core/tests/coretests/src/com/android/internal/widget/LockPatternUtilsTest.java +++ b/core/tests/coretests/src/com/android/internal/widget/LockPatternUtilsTest.java @@ -16,20 +16,279 @@ package com.android.internal.widget; +import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_MANAGED; +import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED; + +import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED; +import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT; +import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN; +import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED; + +import static com.google.common.truth.Truth.assertThat; + +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import android.app.trust.TrustManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.ContextWrapper; +import android.content.pm.UserInfo; +import android.os.Looper; +import android.os.RemoteException; import android.os.UserHandle; +import android.os.UserManager; +import android.platform.test.annotations.IgnoreUnderRavenwood; +import android.platform.test.ravenwood.RavenwoodRule; +import android.provider.Settings; +import android.test.mock.MockContentResolver; +import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import com.android.internal.util.test.FakeSettingsProvider; + +import com.google.android.collect.Lists; + +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; + +import java.nio.charset.StandardCharsets; +import java.util.List; @RunWith(AndroidJUnit4.class) @SmallTest +@IgnoreUnderRavenwood(blockedBy = LockPatternUtils.class) public class LockPatternUtilsTest { + @Rule + public final RavenwoodRule mRavenwood = new RavenwoodRule(); + + private ILockSettings mLockSettings; + private static final int USER_ID = 1; + private static final int DEMO_USER_ID = 5; + + private LockPatternUtils mLockPatternUtils; + + private void configureTest(boolean isSecure, boolean isDemoUser, int deviceDemoMode) + throws Exception { + mLockSettings = mock(ILockSettings.class); + final Context context = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext())); + + final MockContentResolver cr = new MockContentResolver(context); + cr.addProvider(Settings.AUTHORITY, new FakeSettingsProvider()); + when(context.getContentResolver()).thenReturn(cr); + Settings.Global.putInt(cr, Settings.Global.DEVICE_DEMO_MODE, deviceDemoMode); + + when(mLockSettings.getCredentialType(DEMO_USER_ID)).thenReturn( + isSecure ? LockPatternUtils.CREDENTIAL_TYPE_PASSWORD + : LockPatternUtils.CREDENTIAL_TYPE_NONE); + when(mLockSettings.getLong("lockscreen.password_type", PASSWORD_QUALITY_UNSPECIFIED, + DEMO_USER_ID)).thenReturn((long) PASSWORD_QUALITY_MANAGED); + when(mLockSettings.hasSecureLockScreen()).thenReturn(true); + mLockPatternUtils = new LockPatternUtils(context, mLockSettings); + + final UserInfo userInfo = mock(UserInfo.class); + when(userInfo.isDemo()).thenReturn(isDemoUser); + final UserManager um = mock(UserManager.class); + when(um.getUserInfo(DEMO_USER_ID)).thenReturn(userInfo); + when(context.getSystemService(Context.USER_SERVICE)).thenReturn(um); + } + + @Test + public void isUserInLockDown() throws Exception { + configureTest(true, false, 2); + + // GIVEN strong auth not required + when(mLockSettings.getStrongAuthForUser(USER_ID)).thenReturn(STRONG_AUTH_NOT_REQUIRED); + + // THEN user isn't in lockdown + assertFalse(mLockPatternUtils.isUserInLockdown(USER_ID)); + + // GIVEN lockdown + when(mLockSettings.getStrongAuthForUser(USER_ID)).thenReturn( + STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN); + + // THEN user is in lockdown + assertTrue(mLockPatternUtils.isUserInLockdown(USER_ID)); + + // GIVEN lockdown and lockout + when(mLockSettings.getStrongAuthForUser(USER_ID)).thenReturn( + STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN | STRONG_AUTH_REQUIRED_AFTER_LOCKOUT); + + // THEN user is in lockdown + assertTrue(mLockPatternUtils.isUserInLockdown(USER_ID)); + } + + @Test + public void isLockScreenDisabled_isDemoUser_true() throws Exception { + configureTest(false, true, 2); + assertTrue(mLockPatternUtils.isLockScreenDisabled(DEMO_USER_ID)); + } + + @Test + public void isLockScreenDisabled_isSecureAndDemoUser_false() throws Exception { + configureTest(true, true, 2); + assertFalse(mLockPatternUtils.isLockScreenDisabled(DEMO_USER_ID)); + } + + @Test + public void isLockScreenDisabled_isNotDemoUser_false() throws Exception { + configureTest(false, false, 2); + assertFalse(mLockPatternUtils.isLockScreenDisabled(DEMO_USER_ID)); + } + + @Test + public void isLockScreenDisabled_isNotInDemoMode_false() throws Exception { + configureTest(false, true, 0); + assertFalse(mLockPatternUtils.isLockScreenDisabled(DEMO_USER_ID)); + } + + @Test + public void testAddWeakEscrowToken() throws RemoteException { + ILockSettings ils = createTestLockSettings(); + byte[] testToken = "test_token".getBytes(StandardCharsets.UTF_8); + int testUserId = 10; + IWeakEscrowTokenActivatedListener listener = createWeakEscrowTokenListener(); + mLockPatternUtils.addWeakEscrowToken(testToken, testUserId, listener); + verify(ils).addWeakEscrowToken(eq(testToken), eq(testUserId), eq(listener)); + } + + @Test + public void testRegisterWeakEscrowTokenRemovedListener() throws RemoteException { + ILockSettings ils = createTestLockSettings(); + IWeakEscrowTokenRemovedListener testListener = createTestAutoEscrowTokenRemovedListener(); + mLockPatternUtils.registerWeakEscrowTokenRemovedListener(testListener); + verify(ils).registerWeakEscrowTokenRemovedListener(eq(testListener)); + } + + @Test + public void testUnregisterWeakEscrowTokenRemovedListener() throws RemoteException { + ILockSettings ils = createTestLockSettings(); + IWeakEscrowTokenRemovedListener testListener = createTestAutoEscrowTokenRemovedListener(); + mLockPatternUtils.unregisterWeakEscrowTokenRemovedListener(testListener); + verify(ils).unregisterWeakEscrowTokenRemovedListener(eq(testListener)); + } + + @Test + public void testRemoveAutoEscrowToken() throws RemoteException { + ILockSettings ils = createTestLockSettings(); + int testUserId = 10; + long testHandle = 100L; + mLockPatternUtils.removeWeakEscrowToken(testHandle, testUserId); + verify(ils).removeWeakEscrowToken(eq(testHandle), eq(testUserId)); + } + + @Test + public void testIsAutoEscrowTokenActive() throws RemoteException { + ILockSettings ils = createTestLockSettings(); + int testUserId = 10; + long testHandle = 100L; + mLockPatternUtils.isWeakEscrowTokenActive(testHandle, testUserId); + verify(ils).isWeakEscrowTokenActive(eq(testHandle), eq(testUserId)); + } + + @Test + public void testIsAutoEscrowTokenValid() throws RemoteException { + ILockSettings ils = createTestLockSettings(); + int testUserId = 10; + byte[] testToken = "test_token".getBytes(StandardCharsets.UTF_8); + long testHandle = 100L; + mLockPatternUtils.isWeakEscrowTokenValid(testHandle, testToken, testUserId); + verify(ils).isWeakEscrowTokenValid(eq(testHandle), eq(testToken), eq(testUserId)); + } + + @Test + public void testSetEnabledTrustAgents() throws RemoteException { + int testUserId = 10; + ILockSettings ils = createTestLockSettings(); + ArgumentCaptor<String> valueCaptor = ArgumentCaptor.forClass(String.class); + doNothing().when(ils).setString(anyString(), valueCaptor.capture(), anyInt()); + List<ComponentName> enabledTrustAgents = Lists.newArrayList( + ComponentName.unflattenFromString("com.android/.TrustAgent"), + ComponentName.unflattenFromString("com.test/.TestAgent")); + + mLockPatternUtils.setEnabledTrustAgents(enabledTrustAgents, testUserId); + + assertThat(valueCaptor.getValue()).isEqualTo("com.android/.TrustAgent,com.test/.TestAgent"); + } + + @Test + public void testGetEnabledTrustAgents() throws RemoteException { + int testUserId = 10; + ILockSettings ils = createTestLockSettings(); + when(ils.getString(anyString(), any(), anyInt())).thenReturn( + "com.android/.TrustAgent,com.test/.TestAgent"); + + List<ComponentName> trustAgents = mLockPatternUtils.getEnabledTrustAgents(testUserId); + + assertThat(trustAgents).containsExactly( + ComponentName.unflattenFromString("com.android/.TrustAgent"), + ComponentName.unflattenFromString("com.test/.TestAgent")); + } + + @Test + public void testSetKnownTrustAgents() throws RemoteException { + int testUserId = 10; + ILockSettings ils = createTestLockSettings(); + ArgumentCaptor<String> valueCaptor = ArgumentCaptor.forClass(String.class); + doNothing().when(ils).setString(anyString(), valueCaptor.capture(), anyInt()); + List<ComponentName> knownTrustAgents = Lists.newArrayList( + ComponentName.unflattenFromString("com.android/.TrustAgent"), + ComponentName.unflattenFromString("com.test/.TestAgent")); + + mLockPatternUtils.setKnownTrustAgents(knownTrustAgents, testUserId); + + assertThat(valueCaptor.getValue()).isEqualTo("com.android/.TrustAgent,com.test/.TestAgent"); + } + + @Test + public void testGetKnownTrustAgents() throws RemoteException { + int testUserId = 10; + ILockSettings ils = createTestLockSettings(); + when(ils.getString(anyString(), any(), anyInt())).thenReturn( + "com.android/.TrustAgent,com.test/.TestAgent"); + + List<ComponentName> trustAgents = mLockPatternUtils.getKnownTrustAgents(testUserId); + + assertThat(trustAgents).containsExactly( + ComponentName.unflattenFromString("com.android/.TrustAgent"), + ComponentName.unflattenFromString("com.test/.TestAgent")); + } + + @Test + public void isBiometricAllowedForUser_afterTrustagentExpired_returnsTrue() + throws RemoteException { + TestStrongAuthTracker tracker = createStrongAuthTracker(); + tracker.changeStrongAuth(SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED); + + assertTrue(tracker.isBiometricAllowedForUser( + /* isStrongBiometric = */ true, + DEMO_USER_ID)); + } + + @Test + public void isBiometricAllowedForUser_afterLockout_returnsFalse() + throws RemoteException { + TestStrongAuthTracker tracker = createStrongAuthTracker(); + tracker.changeStrongAuth(STRONG_AUTH_REQUIRED_AFTER_LOCKOUT); + + assertFalse(tracker.isBiometricAllowedForUser( + /* isStrongBiometric = */ true, + DEMO_USER_ID)); + } @Test public void testUserFrp_isNotRegularUser() throws Exception { @@ -56,4 +315,49 @@ public class LockPatternUtilsTest { assertNotEquals(UserHandle.USER_CURRENT, LockPatternUtils.USER_REPAIR_MODE); assertNotEquals(UserHandle.USER_CURRENT_OR_SELF, LockPatternUtils.USER_REPAIR_MODE); } + + private TestStrongAuthTracker createStrongAuthTracker() { + final Context context = new ContextWrapper(InstrumentationRegistry.getTargetContext()); + return new TestStrongAuthTracker(context, Looper.getMainLooper()); + } + + private static class TestStrongAuthTracker extends LockPatternUtils.StrongAuthTracker { + + TestStrongAuthTracker(Context context, Looper looper) { + super(context, looper); + } + + public void changeStrongAuth(@StrongAuthFlags int strongAuthFlags) { + handleStrongAuthRequiredChanged(strongAuthFlags, DEMO_USER_ID); + } + } + + private ILockSettings createTestLockSettings() { + final Context context = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext())); + + final TrustManager trustManager = mock(TrustManager.class); + when(context.getSystemService(Context.TRUST_SERVICE)).thenReturn(trustManager); + + final ILockSettings ils = mock(ILockSettings.class); + mLockPatternUtils = new LockPatternUtils(context, ils); + return ils; + } + + private IWeakEscrowTokenActivatedListener createWeakEscrowTokenListener() { + return new IWeakEscrowTokenActivatedListener.Stub() { + @Override + public void onWeakEscrowTokenActivated(long handle, int userId) { + // Do nothing. + } + }; + } + + private IWeakEscrowTokenRemovedListener createTestAutoEscrowTokenRemovedListener() { + return new IWeakEscrowTokenRemovedListener.Stub() { + @Override + public void onWeakEscrowTokenRemoved(long handle, int userId) { + // Do nothing. + } + }; + } } diff --git a/core/tests/systemproperties/Android.bp b/core/tests/systemproperties/Android.bp index 765ca3e5110a..21aa3c44044b 100644 --- a/core/tests/systemproperties/Android.bp +++ b/core/tests/systemproperties/Android.bp @@ -15,6 +15,9 @@ android_test { static_libs: [ "android-common", "frameworks-core-util-lib", + "androidx.test.rules", + "androidx.test.ext.junit", + "ravenwood-junit", ], libs: [ "android.test.runner", @@ -23,3 +26,22 @@ android_test { platform_apis: true, certificate: "platform", } + +android_ravenwood_test { + name: "FrameworksCoreSystemPropertiesTestsRavenwood", + static_libs: [ + "android-common", + "frameworks-core-util-lib", + "androidx.test.rules", + "androidx.test.ext.junit", + "ravenwood-junit", + ], + libs: [ + "android.test.runner", + "android.test.base", + ], + srcs: [ + "src/**/*.java", + ], + auto_gen_config: true, +} diff --git a/core/tests/systemproperties/src/android/os/SystemPropertiesTest.java b/core/tests/systemproperties/src/android/os/SystemPropertiesTest.java index 67783bff9299..ea65de088c07 100644 --- a/core/tests/systemproperties/src/android/os/SystemPropertiesTest.java +++ b/core/tests/systemproperties/src/android/os/SystemPropertiesTest.java @@ -16,19 +16,36 @@ package android.os; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import android.platform.test.ravenwood.RavenwoodRule; import android.test.suitebuilder.annotation.SmallTest; -import junit.framework.TestCase; +import org.junit.Rule; +import org.junit.Test; import java.util.Objects; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -public class SystemPropertiesTest extends TestCase { +public class SystemPropertiesTest { + @Rule + public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder() + .setSystemPropertyMutable(KEY, null) + .setSystemPropertyMutable(UNSET_KEY, null) + .setSystemPropertyMutable(PERSIST_KEY, null) + .build(); + private static final String KEY = "sys.testkey"; private static final String UNSET_KEY = "Aiw7woh6ie4toh7W"; private static final String PERSIST_KEY = "persist.sys.testkey"; + @Test @SmallTest public void testStressPersistPropertyConsistency() throws Exception { for (int i = 0; i < 100; ++i) { @@ -38,6 +55,7 @@ public class SystemPropertiesTest extends TestCase { } } + @Test @SmallTest public void testStressMemoryPropertyConsistency() throws Exception { for (int i = 0; i < 100; ++i) { @@ -47,6 +65,7 @@ public class SystemPropertiesTest extends TestCase { } } + @Test @SmallTest public void testProperties() throws Exception { String value; @@ -93,6 +112,7 @@ public class SystemPropertiesTest extends TestCase { assertEquals(expected, value); } + @Test @SmallTest public void testHandle() throws Exception { String value; @@ -114,6 +134,7 @@ public class SystemPropertiesTest extends TestCase { assertEquals(12345, handle.getInt(12345)); } + @Test @SmallTest public void testIntegralProperties() throws Exception { testInt("", 123, 123); @@ -133,6 +154,7 @@ public class SystemPropertiesTest extends TestCase { testLong("-3147483647", 124, -3147483647L); } + @Test @SmallTest public void testUnset() throws Exception { assertEquals("abc", SystemProperties.get(UNSET_KEY, "abc")); @@ -142,6 +164,7 @@ public class SystemPropertiesTest extends TestCase { assertEquals(-10, SystemProperties.getLong(UNSET_KEY, -10)); } + @Test @SmallTest @SuppressWarnings("null") public void testNullKey() throws Exception { @@ -176,6 +199,7 @@ public class SystemPropertiesTest extends TestCase { } } + @Test @SmallTest public void testCallbacks() { // Latches are not really necessary, but are easy to use. @@ -220,6 +244,7 @@ public class SystemPropertiesTest extends TestCase { } } + @Test @SmallTest public void testDigestOf() { final String empty = SystemProperties.digestOf(); diff --git a/core/tests/utiltests/src/com/android/internal/util/LockPatternUtilsTest.java b/core/tests/utiltests/src/com/android/internal/util/LockPatternUtilsTest.java deleted file mode 100644 index dcaf67660ffa..000000000000 --- a/core/tests/utiltests/src/com/android/internal/util/LockPatternUtilsTest.java +++ /dev/null @@ -1,339 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.internal.util; - -import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_MANAGED; -import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED; - -import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED; -import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_LOCKOUT; -import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN; -import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED; - -import static com.google.common.truth.Truth.assertThat; - -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.any; -import static org.mockito.Mockito.anyInt; -import static org.mockito.Mockito.anyString; -import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import android.content.ComponentName; -import android.content.Context; -import android.content.ContextWrapper; -import android.content.pm.UserInfo; -import android.os.Looper; -import android.os.RemoteException; -import android.os.UserManager; -import android.platform.test.annotations.IgnoreUnderRavenwood; -import android.platform.test.ravenwood.RavenwoodRule; -import android.provider.Settings; -import android.test.mock.MockContentResolver; - -import androidx.test.InstrumentationRegistry; -import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; - -import com.android.internal.util.test.FakeSettingsProvider; -import com.android.internal.widget.ILockSettings; -import com.android.internal.widget.IWeakEscrowTokenActivatedListener; -import com.android.internal.widget.IWeakEscrowTokenRemovedListener; -import com.android.internal.widget.LockPatternUtils; - -import com.google.android.collect.Lists; - -import org.junit.Rule; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mockito; - -import java.nio.charset.StandardCharsets; -import java.util.List; - -@RunWith(AndroidJUnit4.class) -@SmallTest -@IgnoreUnderRavenwood(blockedBy = LockPatternUtils.class) -public class LockPatternUtilsTest { - @Rule - public final RavenwoodRule mRavenwood = new RavenwoodRule(); - - private ILockSettings mLockSettings; - private static final int USER_ID = 1; - private static final int DEMO_USER_ID = 5; - - private LockPatternUtils mLockPatternUtils; - - private void configureTest(boolean isSecure, boolean isDemoUser, int deviceDemoMode) - throws Exception { - mLockSettings = Mockito.mock(ILockSettings.class); - final Context context = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext())); - - final MockContentResolver cr = new MockContentResolver(context); - cr.addProvider(Settings.AUTHORITY, new FakeSettingsProvider()); - when(context.getContentResolver()).thenReturn(cr); - Settings.Global.putInt(cr, Settings.Global.DEVICE_DEMO_MODE, deviceDemoMode); - - when(mLockSettings.getCredentialType(DEMO_USER_ID)).thenReturn( - isSecure ? LockPatternUtils.CREDENTIAL_TYPE_PASSWORD - : LockPatternUtils.CREDENTIAL_TYPE_NONE); - when(mLockSettings.getLong("lockscreen.password_type", PASSWORD_QUALITY_UNSPECIFIED, - DEMO_USER_ID)).thenReturn((long) PASSWORD_QUALITY_MANAGED); - // TODO(b/63758238): stop spying the class under test - mLockPatternUtils = spy(new LockPatternUtils(context)); - when(mLockPatternUtils.getLockSettings()).thenReturn(mLockSettings); - doReturn(true).when(mLockPatternUtils).hasSecureLockScreen(); - - final UserInfo userInfo = Mockito.mock(UserInfo.class); - when(userInfo.isDemo()).thenReturn(isDemoUser); - final UserManager um = Mockito.mock(UserManager.class); - when(um.getUserInfo(DEMO_USER_ID)).thenReturn(userInfo); - when(context.getSystemService(Context.USER_SERVICE)).thenReturn(um); - } - - @Test - public void isUserInLockDown() throws Exception { - configureTest(true, false, 2); - - // GIVEN strong auth not required - when(mLockSettings.getStrongAuthForUser(USER_ID)).thenReturn(STRONG_AUTH_NOT_REQUIRED); - - // THEN user isn't in lockdown - assertFalse(mLockPatternUtils.isUserInLockdown(USER_ID)); - - // GIVEN lockdown - when(mLockSettings.getStrongAuthForUser(USER_ID)).thenReturn( - STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN); - - // THEN user is in lockdown - assertTrue(mLockPatternUtils.isUserInLockdown(USER_ID)); - - // GIVEN lockdown and lockout - when(mLockSettings.getStrongAuthForUser(USER_ID)).thenReturn( - STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN | STRONG_AUTH_REQUIRED_AFTER_LOCKOUT); - - // THEN user is in lockdown - assertTrue(mLockPatternUtils.isUserInLockdown(USER_ID)); - } - - @Test - public void isLockScreenDisabled_isDemoUser_true() throws Exception { - configureTest(false, true, 2); - assertTrue(mLockPatternUtils.isLockScreenDisabled(DEMO_USER_ID)); - } - - @Test - public void isLockScreenDisabled_isSecureAndDemoUser_false() throws Exception { - configureTest(true, true, 2); - assertFalse(mLockPatternUtils.isLockScreenDisabled(DEMO_USER_ID)); - } - - @Test - public void isLockScreenDisabled_isNotDemoUser_false() throws Exception { - configureTest(false, false, 2); - assertFalse(mLockPatternUtils.isLockScreenDisabled(DEMO_USER_ID)); - } - - @Test - public void isLockScreenDisabled_isNotInDemoMode_false() throws Exception { - configureTest(false, true, 0); - assertFalse(mLockPatternUtils.isLockScreenDisabled(DEMO_USER_ID)); - } - - @Test - public void testAddWeakEscrowToken() throws RemoteException { - ILockSettings ils = createTestLockSettings(); - byte[] testToken = "test_token".getBytes(StandardCharsets.UTF_8); - int testUserId = 10; - IWeakEscrowTokenActivatedListener listener = createWeakEscrowTokenListener(); - mLockPatternUtils.addWeakEscrowToken(testToken, testUserId, listener); - verify(ils).addWeakEscrowToken(eq(testToken), eq(testUserId), eq(listener)); - } - - @Test - public void testRegisterWeakEscrowTokenRemovedListener() throws RemoteException { - ILockSettings ils = createTestLockSettings(); - IWeakEscrowTokenRemovedListener testListener = createTestAutoEscrowTokenRemovedListener(); - mLockPatternUtils.registerWeakEscrowTokenRemovedListener(testListener); - verify(ils).registerWeakEscrowTokenRemovedListener(eq(testListener)); - } - - @Test - public void testUnregisterWeakEscrowTokenRemovedListener() throws RemoteException { - ILockSettings ils = createTestLockSettings(); - IWeakEscrowTokenRemovedListener testListener = createTestAutoEscrowTokenRemovedListener(); - mLockPatternUtils.unregisterWeakEscrowTokenRemovedListener(testListener); - verify(ils).unregisterWeakEscrowTokenRemovedListener(eq(testListener)); - } - - @Test - public void testRemoveAutoEscrowToken() throws RemoteException { - ILockSettings ils = createTestLockSettings(); - int testUserId = 10; - long testHandle = 100L; - mLockPatternUtils.removeWeakEscrowToken(testHandle, testUserId); - verify(ils).removeWeakEscrowToken(eq(testHandle), eq(testUserId)); - } - - @Test - public void testIsAutoEscrowTokenActive() throws RemoteException { - ILockSettings ils = createTestLockSettings(); - int testUserId = 10; - long testHandle = 100L; - mLockPatternUtils.isWeakEscrowTokenActive(testHandle, testUserId); - verify(ils).isWeakEscrowTokenActive(eq(testHandle), eq(testUserId)); - } - - @Test - public void testIsAutoEscrowTokenValid() throws RemoteException { - ILockSettings ils = createTestLockSettings(); - int testUserId = 10; - byte[] testToken = "test_token".getBytes(StandardCharsets.UTF_8); - long testHandle = 100L; - mLockPatternUtils.isWeakEscrowTokenValid(testHandle, testToken, testUserId); - verify(ils).isWeakEscrowTokenValid(eq(testHandle), eq(testToken), eq(testUserId)); - } - - @Test - public void testSetEnabledTrustAgents() throws RemoteException { - int testUserId = 10; - ILockSettings ils = createTestLockSettings(); - ArgumentCaptor<String> valueCaptor = ArgumentCaptor.forClass(String.class); - doNothing().when(ils).setString(anyString(), valueCaptor.capture(), anyInt()); - List<ComponentName> enabledTrustAgents = Lists.newArrayList( - ComponentName.unflattenFromString("com.android/.TrustAgent"), - ComponentName.unflattenFromString("com.test/.TestAgent")); - - mLockPatternUtils.setEnabledTrustAgents(enabledTrustAgents, testUserId); - - assertThat(valueCaptor.getValue()).isEqualTo("com.android/.TrustAgent,com.test/.TestAgent"); - } - - @Test - public void testGetEnabledTrustAgents() throws RemoteException { - int testUserId = 10; - ILockSettings ils = createTestLockSettings(); - when(ils.getString(anyString(), any(), anyInt())).thenReturn( - "com.android/.TrustAgent,com.test/.TestAgent"); - - List<ComponentName> trustAgents = mLockPatternUtils.getEnabledTrustAgents(testUserId); - - assertThat(trustAgents).containsExactly( - ComponentName.unflattenFromString("com.android/.TrustAgent"), - ComponentName.unflattenFromString("com.test/.TestAgent")); - } - - @Test - public void testSetKnownTrustAgents() throws RemoteException { - int testUserId = 10; - ILockSettings ils = createTestLockSettings(); - ArgumentCaptor<String> valueCaptor = ArgumentCaptor.forClass(String.class); - doNothing().when(ils).setString(anyString(), valueCaptor.capture(), anyInt()); - List<ComponentName> knownTrustAgents = Lists.newArrayList( - ComponentName.unflattenFromString("com.android/.TrustAgent"), - ComponentName.unflattenFromString("com.test/.TestAgent")); - - mLockPatternUtils.setKnownTrustAgents(knownTrustAgents, testUserId); - - assertThat(valueCaptor.getValue()).isEqualTo("com.android/.TrustAgent,com.test/.TestAgent"); - } - - @Test - public void testGetKnownTrustAgents() throws RemoteException { - int testUserId = 10; - ILockSettings ils = createTestLockSettings(); - when(ils.getString(anyString(), any(), anyInt())).thenReturn( - "com.android/.TrustAgent,com.test/.TestAgent"); - - List<ComponentName> trustAgents = mLockPatternUtils.getKnownTrustAgents(testUserId); - - assertThat(trustAgents).containsExactly( - ComponentName.unflattenFromString("com.android/.TrustAgent"), - ComponentName.unflattenFromString("com.test/.TestAgent")); - } - - @Test - public void isBiometricAllowedForUser_afterTrustagentExpired_returnsTrue() - throws RemoteException { - TestStrongAuthTracker tracker = createStrongAuthTracker(); - tracker.changeStrongAuth(SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED); - - assertTrue(tracker.isBiometricAllowedForUser( - /* isStrongBiometric = */ true, - DEMO_USER_ID)); - } - - @Test - public void isBiometricAllowedForUser_afterLockout_returnsFalse() - throws RemoteException { - TestStrongAuthTracker tracker = createStrongAuthTracker(); - tracker.changeStrongAuth(STRONG_AUTH_REQUIRED_AFTER_LOCKOUT); - - assertFalse(tracker.isBiometricAllowedForUser( - /* isStrongBiometric = */ true, - DEMO_USER_ID)); - } - - - private TestStrongAuthTracker createStrongAuthTracker() { - final Context context = new ContextWrapper(InstrumentationRegistry.getTargetContext()); - return new TestStrongAuthTracker(context, Looper.getMainLooper()); - } - - private static class TestStrongAuthTracker extends LockPatternUtils.StrongAuthTracker { - - TestStrongAuthTracker(Context context, Looper looper) { - super(context, looper); - } - - public void changeStrongAuth(@StrongAuthFlags int strongAuthFlags) { - handleStrongAuthRequiredChanged(strongAuthFlags, DEMO_USER_ID); - } - } - - private ILockSettings createTestLockSettings() { - final Context context = spy(new ContextWrapper(InstrumentationRegistry.getTargetContext())); - mLockPatternUtils = spy(new LockPatternUtils(context)); - final ILockSettings ils = Mockito.mock(ILockSettings.class); - when(mLockPatternUtils.getLockSettings()).thenReturn(ils); - return ils; - } - - private IWeakEscrowTokenActivatedListener createWeakEscrowTokenListener() { - return new IWeakEscrowTokenActivatedListener.Stub() { - @Override - public void onWeakEscrowTokenActivated(long handle, int userId) { - // Do nothing. - } - }; - } - - private IWeakEscrowTokenRemovedListener createTestAutoEscrowTokenRemovedListener() { - return new IWeakEscrowTokenRemovedListener.Stub() { - @Override - public void onWeakEscrowTokenRemoved(long handle, int userId) { - // Do nothing. - } - }; - } -} diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index 294b8ae0f6f5..2de305f8e925 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -421,6 +421,8 @@ applications that come with the platform <permission name="android.permission.REQUEST_COMPANION_PROFILE_NEARBY_DEVICE_STREAMING" /> <permission name="android.permission.REQUEST_COMPANION_PROFILE_COMPUTER" /> <permission name="android.permission.REQUEST_COMPANION_SELF_MANAGED" /> + <permission name="android.permission.REQUEST_OBSERVE_DEVICE_UUID_PRESENCE" /> + <!-- Permission required for testing registering pull atom callbacks. --> <permission name="android.permission.REGISTER_STATS_PULL_ATOM"/> <!-- Permission required for testing system audio effect APIs. --> @@ -637,4 +639,8 @@ applications that come with the platform <permission name="com.android.voicemail.permission.READ_VOICEMAIL"/> <permission name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER"/> </privapp-permissions> + + <privapp-permissions package="com.android.devicediagnostics"> + <permission name="android.permission.READ_PRIVILEGED_PHONE_STATE"/> + </privapp-permissions> </permissions> diff --git a/data/etc/services.core.protolog.json b/data/etc/services.core.protolog.json index c77004d4eb17..da91a964565f 100644 --- a/data/etc/services.core.protolog.json +++ b/data/etc/services.core.protolog.json @@ -1867,6 +1867,12 @@ "group": "WM_DEBUG_STATES", "at": "com\/android\/server\/wm\/ActivityTaskSupervisor.java" }, + "-483957611": { + "message": "Resuming configuration dispatch for %s", + "level": "VERBOSE", + "group": "WM_DEBUG_WINDOW_TRANSITIONS_MIN", + "at": "com\/android\/server\/wm\/ActivityRecord.java" + }, "-481924678": { "message": "handleNotObscuredLocked w: %s, w.mHasSurface: %b, w.isOnScreen(): %b, w.isDisplayedLw(): %b, w.mAttrs.userActivityTimeout: %d", "level": "DEBUG", @@ -4021,6 +4027,12 @@ "group": "WM_DEBUG_WINDOW_TRANSITIONS", "at": "com\/android\/server\/wm\/Transition.java" }, + "1473051122": { + "message": "Pausing configuration dispatch for %s", + "level": "VERBOSE", + "group": "WM_DEBUG_WINDOW_TRANSITIONS_MIN", + "at": "com\/android\/server\/wm\/ActivityRecord.java" + }, "1494644409": { "message": " Rejecting as detached: %s", "level": "VERBOSE", diff --git a/data/fonts/Android.bp b/data/fonts/Android.bp index 471acaa0b1b6..f1a6b6974b06 100644 --- a/data/fonts/Android.bp +++ b/data/fonts/Android.bp @@ -80,3 +80,9 @@ prebuilt_fonts_xml { }, }, } + +///////////////////////////////// +// Move `fontchain_lint` to `core/tasks/fontchain_lint.mk`. +// Because `system.img` is a dependency of `fontchain_lint`, it cannot be +// converted to Android.bp for now. +// After system.img can be generated by Soong, then it can be converted to Android.bp. diff --git a/data/fonts/Android.mk b/data/fonts/Android.mk deleted file mode 100644 index a322b829932b..000000000000 --- a/data/fonts/Android.mk +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright (C) 2011 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. - -LOCAL_PATH := $(call my-dir) - -# Run sanity tests on fonts on checkbuild -checkbuild: fontchain_lint - -FONTCHAIN_LINTER := $(HOST_OUT_EXECUTABLES)/fontchain_linter -ifeq ($(MINIMAL_FONT_FOOTPRINT),true) -CHECK_EMOJI := false -else -CHECK_EMOJI := true -endif - -fontchain_lint_timestamp := $(call intermediates-dir-for,PACKAGING,fontchain_lint)/stamp - -.PHONY: fontchain_lint -fontchain_lint: $(fontchain_lint_timestamp) - -fontchain_lint_deps := \ - external/unicode/DerivedAge.txt \ - external/unicode/emoji-data.txt \ - external/unicode/emoji-sequences.txt \ - external/unicode/emoji-variation-sequences.txt \ - external/unicode/emoji-zwj-sequences.txt \ - external/unicode/additions/emoji-data.txt \ - external/unicode/additions/emoji-sequences.txt \ - external/unicode/additions/emoji-zwj-sequences.txt \ - -$(fontchain_lint_timestamp): $(FONTCHAIN_LINTER) $(TARGET_OUT)/etc/fonts.xml $(PRODUCT_OUT)/system.img $(fontchain_lint_deps) - @echo Running fontchain lint - $(FONTCHAIN_LINTER) $(TARGET_OUT) $(CHECK_EMOJI) external/unicode - touch $@ diff --git a/graphics/java/android/graphics/BaseRecordingCanvas.java b/graphics/java/android/graphics/BaseRecordingCanvas.java index d659ddd75f72..4e88b0efd3e5 100644 --- a/graphics/java/android/graphics/BaseRecordingCanvas.java +++ b/graphics/java/android/graphics/BaseRecordingCanvas.java @@ -607,7 +607,8 @@ public class BaseRecordingCanvas extends Canvas { } @Override - public final void drawMesh(@NonNull Mesh mesh, BlendMode blendMode, @NonNull Paint paint) { + public final void drawMesh(@NonNull Mesh mesh, @Nullable BlendMode blendMode, + @NonNull Paint paint) { if (blendMode == null) { blendMode = BlendMode.MODULATE; } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java index 83d555cbdecd..b2e5b75cf0b5 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -17,6 +17,7 @@ package androidx.window.extensions.embedding; import static android.app.ActivityManager.START_SUCCESS; +import static android.app.ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.view.Display.DEFAULT_DISPLAY; @@ -41,10 +42,10 @@ import static androidx.window.extensions.embedding.SplitContainer.isStickyPlaceh import static androidx.window.extensions.embedding.SplitContainer.shouldFinishAssociatedContainerWhenAdjacent; import static androidx.window.extensions.embedding.SplitContainer.shouldFinishAssociatedContainerWhenStacked; import static androidx.window.extensions.embedding.SplitPresenter.RESULT_EXPAND_FAILED_NO_TF_INFO; -import static androidx.window.extensions.embedding.SplitPresenter.boundsSmallerThanMinDimensions; import static androidx.window.extensions.embedding.SplitPresenter.getActivitiesMinDimensionsPair; import static androidx.window.extensions.embedding.SplitPresenter.getActivityIntentMinDimensionsPair; import static androidx.window.extensions.embedding.SplitPresenter.getMinDimensions; +import static androidx.window.extensions.embedding.SplitPresenter.sanitizeBounds; import static androidx.window.extensions.embedding.SplitPresenter.shouldShowSplit; import android.annotation.CallbackExecutor; @@ -110,6 +111,10 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen static final boolean ENABLE_SHELL_TRANSITIONS = SystemProperties.getBoolean("persist.wm.debug.shell_transit", true); + // TODO(b/295993745): remove after prebuilt library is updated. + private static final String KEY_ACTIVITY_STACK_TOKEN = + "androidx.window.extensions.embedding.ActivityStackToken"; + @VisibleForTesting @GuardedBy("mLock") final SplitPresenter mPresenter; @@ -569,7 +574,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen final TransactionRecord transactionRecord = mTransactionManager.startNewTransaction(); final WindowContainerTransaction wct = transactionRecord.getTransaction(); - mPresenter.applyActivityStackAttributes(wct, container, attributes); + mPresenter.applyActivityStackAttributes(wct, container, attributes, + container.getMinDimensions()); transactionRecord.apply(false /* shouldApplyIndependently */); } } @@ -1562,7 +1568,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen private TaskFragmentContainer createEmptyExpandedContainer( @NonNull WindowContainerTransaction wct, @NonNull Intent intent, int taskId, @Nullable Activity launchingActivity) { - return createEmptyContainer(wct, intent, taskId, new Rect(), launchingActivity, + return createEmptyContainer(wct, intent, taskId, + new ActivityStackAttributes.Builder().build(), launchingActivity, null /* overlayTag */, null /* launchOptions */); } @@ -1576,8 +1583,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @Nullable TaskFragmentContainer createEmptyContainer( @NonNull WindowContainerTransaction wct, @NonNull Intent intent, int taskId, - @NonNull Rect bounds, @Nullable Activity launchingActivity, - @Nullable String overlayTag, @Nullable Bundle launchOptions) { + @NonNull ActivityStackAttributes activityStackAttributes, + @Nullable Activity launchingActivity, @Nullable String overlayTag, + @Nullable Bundle launchOptions) { // We need an activity in the organizer process in the same Task to use as the owner // activity, as well as to get the Task window info. final Activity activityInTask; @@ -1600,43 +1608,21 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // Note that taskContainer will not exist before calling #newContainer if the container // is the first embedded TF in the task. final TaskContainer taskContainer = container.getTaskContainer(); - final Rect taskBounds = taskContainer.getTaskProperties().getTaskMetrics().getBounds(); - final Rect sanitizedBounds = sanitizeBounds(bounds, intent, taskBounds); + // TODO(b/265271880): remove redundant logic after all TF operations take fragmentToken. + final Rect taskBounds = taskContainer.getBounds(); + final Rect sanitizedBounds = sanitizeBounds(activityStackAttributes.getRelativeBounds(), + getMinDimensions(intent), taskBounds); final int windowingMode = taskContainer .getWindowingModeForTaskFragment(sanitizedBounds); mPresenter.createTaskFragment(wct, taskFragmentToken, activityInTask.getActivityToken(), sanitizedBounds, windowingMode); - mPresenter.updateAnimationParams(wct, taskFragmentToken, - TaskFragmentAnimationParams.DEFAULT); - mPresenter.setTaskFragmentIsolatedNavigation(wct, taskFragmentToken, - overlayTag != null && !sanitizedBounds.isEmpty()); + mPresenter.applyActivityStackAttributes(wct, container, activityStackAttributes, + getMinDimensions(intent)); return container; } /** - * Returns the expanded bounds if the {@code bounds} violate minimum dimension or are not fully - * covered by the task bounds. Otherwise, returns {@code bounds}. - */ - @NonNull - private static Rect sanitizeBounds(@NonNull Rect bounds, @NonNull Intent intent, - @NonNull Rect taskBounds) { - if (bounds.isEmpty()) { - // Don't need to check if the bounds follows the task bounds. - return bounds; - } - if (boundsSmallerThanMinDimensions(bounds, getMinDimensions(intent))) { - // Expand the bounds if the bounds are smaller than minimum dimensions. - return new Rect(); - } - if (!taskBounds.contains(bounds)) { - // Expand the bounds if the bounds exceed the task bounds. - return new Rect(); - } - return bounds; - } - - /** * Returns a container for the new activity intent to launch into as splitting with the primary * activity. */ @@ -1953,6 +1939,12 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen return; } + if (mActivityStackAttributesCalculator == null) { + Log.e(TAG, "ActivityStackAttributesCalculator is not set. Thus the overlay container" + + " can not be updated."); + return; + } + if (mActivityStackAttributesCalculator != null) { final ActivityStackAttributesCalculatorParams params = new ActivityStackAttributesCalculatorParams( @@ -1962,7 +1954,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen container.getLaunchOptions()); final ActivityStackAttributes attributes = mActivityStackAttributesCalculator .apply(params); - mPresenter.applyActivityStackAttributes(wct, container, attributes); + mPresenter.applyActivityStackAttributes(wct, container, attributes, + container.getMinDimensions()); } } @@ -2598,15 +2591,15 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen mPresenter.createParentContainerInfoFromTaskProperties( mPresenter.getTaskProperties(launchActivity)), overlayTag, options); // Fallback to expand the bounds if there's no activityStackAttributes calculator. - final Rect relativeBounds = mActivityStackAttributesCalculator != null - ? new Rect(mActivityStackAttributesCalculator.apply(params).getRelativeBounds()) - : new Rect(); - final boolean shouldExpandContainer = boundsSmallerThanMinDimensions(relativeBounds, - getMinDimensions(intent)); - // Expand the bounds if the requested bounds are smaller than minimum dimensions. - if (shouldExpandContainer) { - relativeBounds.setEmpty(); + final ActivityStackAttributes attrs; + if (mActivityStackAttributesCalculator != null) { + attrs = mActivityStackAttributesCalculator.apply(params); + } else { + attrs = new ActivityStackAttributes.Builder().build(); + Log.e(TAG, "ActivityStackAttributesCalculator isn't set. Fallback to set overlay " + + "container as expected."); } + final int taskId = getTaskId(launchActivity); if (!overlayContainers.isEmpty()) { for (final TaskFragmentContainer overlayContainer : overlayContainers) { @@ -2626,20 +2619,8 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } if (overlayTag.equals(overlayContainer.getOverlayTag()) && taskId == overlayContainer.getTaskId()) { - // If there's an overlay container with the same tag and task ID, we treat - // the OverlayCreateParams as the update to the container. - final IBinder overlayToken = overlayContainer.getTaskFragmentToken(); - final TaskContainer taskContainer = overlayContainer.getTaskContainer(); - final Rect taskBounds = taskContainer.getTaskProperties().getTaskMetrics() - .getBounds(); - final Rect sanitizedBounds = sanitizeBounds(relativeBounds, intent, taskBounds); - - mPresenter.resizeTaskFragment(wct, overlayToken, sanitizedBounds); - final int windowingMode = taskContainer - .getWindowingModeForTaskFragment(sanitizedBounds); - mPresenter.updateWindowingMode(wct, overlayToken, windowingMode); - mPresenter.setTaskFragmentIsolatedNavigation(wct, overlayContainer, - !sanitizedBounds.isEmpty()); + mPresenter.applyActivityStackAttributes(wct, overlayContainer, attrs, + getMinDimensions(intent)); // We can just return the updated overlay container and don't need to // check other condition since we only have one OverlayCreateParams, and // if the tag and task are matched, it's impossible to match another task @@ -2649,7 +2630,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen } } // Launch the overlay container to the task with taskId. - return createEmptyContainer(wct, intent, taskId, relativeBounds, launchActivity, overlayTag, + return createEmptyContainer(wct, intent, taskId, attrs, launchActivity, overlayTag, options); } @@ -2779,8 +2760,17 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // TODO(b/232042367): Consolidate the activity create handling so that we can handle // cross-process the same as normal. + IBinder activityStackToken = options.getBinder(KEY_ACTIVITY_STACK_TOKEN); + if (activityStackToken != null) { + // Put activityStack token to #KEY_LAUNCH_TASK_FRAGMENT_TOKEN to launch the activity + // into the taskFragment associated with the token. + options.putBinder(KEY_LAUNCH_TASK_FRAGMENT_TOKEN, activityStackToken); + } + // Early return if the launching taskfragment is already been set. - if (options.getBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN) != null) { + // TODO(b/295993745): Use KEY_LAUNCH_TASK_FRAGMENT_TOKEN after WM Jetpack migrates to + // bundle. This is still needed to support #setLaunchingActivityStack. + if (options.getBinder(KEY_LAUNCH_TASK_FRAGMENT_TOKEN) != null) { synchronized (mLock) { mCurrentIntent = intent; } @@ -2837,7 +2827,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen // Amend the request to let the WM know that the activity should be placed in // the dedicated container. // TODO(b/229680885): skip override launching TaskFragment token by split-rule - options.putBinder(ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN, + options.putBinder(KEY_LAUNCH_TASK_FRAGMENT_TOKEN, launchedInTaskFragment.getTaskFragmentToken()); mCurrentIntent = intent; } else { @@ -2855,8 +2845,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen if (mCurrentIntent != null && result != START_SUCCESS) { // Clear the pending appeared intent if the activity was not started // successfully. - final IBinder token = bOptions.getBinder( - ActivityOptions.KEY_LAUNCH_TASK_FRAGMENT_TOKEN); + final IBinder token = bOptions.getBinder(KEY_LAUNCH_TASK_FRAGMENT_TOKEN); if (token != null) { final TaskFragmentContainer container = getContainer(token); if (container != null) { diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java index 8b7fd108f031..2f2da8c53db0 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -16,8 +16,6 @@ package androidx.window.extensions.embedding; -import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; -import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.content.pm.PackageManager.MATCH_ALL; import static androidx.window.extensions.embedding.WindowAttributes.DIM_AREA_ON_TASK; @@ -426,7 +424,8 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { * creation has not been reported from the server yet. */ // TODO(b/190433398): Handle resize if the fragment hasn't appeared yet. - private void resizeTaskFragmentIfRegistered(@NonNull WindowContainerTransaction wct, + @VisibleForTesting + void resizeTaskFragmentIfRegistered(@NonNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer container, @Nullable Rect relBounds) { if (container.getInfo() == null) { @@ -435,7 +434,8 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { resizeTaskFragment(wct, container.getTaskFragmentToken(), relBounds); } - private void updateTaskFragmentWindowingModeIfRegistered( + @VisibleForTesting + void updateTaskFragmentWindowingModeIfRegistered( @NonNull WindowContainerTransaction wct, @NonNull TaskFragmentContainer container, @WindowingMode int windowingMode) { @@ -579,13 +579,53 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { super.setCompanionTaskFragment(wct, primary, secondary); } - void applyActivityStackAttributes(@NonNull WindowContainerTransaction wct, - @NonNull TaskFragmentContainer container, @NonNull ActivityStackAttributes attributes) { - final Rect bounds = attributes.getRelativeBounds(); + void applyActivityStackAttributes( + @NonNull WindowContainerTransaction wct, + @NonNull TaskFragmentContainer container, + @NonNull ActivityStackAttributes attributes, + @Nullable Size minDimensions) { + final Rect taskBounds = container.getTaskContainer().getBounds(); + final Rect relativeBounds = sanitizeBounds(attributes.getRelativeBounds(), minDimensions, + taskBounds); + final boolean isFillParent = relativeBounds.isEmpty(); + final boolean isIsolatedNavigated = !isFillParent && container.isOverlay(); + final boolean dimOnTask = !isFillParent + && attributes.getWindowAttributes().getDimArea() == DIM_AREA_ON_TASK + && Flags.fullscreenDimFlag(); + final IBinder fragmentToken = container.getTaskFragmentToken(); + + // TODO(b/243518738): Update to resizeTaskFragment after we migrate WCT#setRelativeBounds + // and WCT#setWindowingMode to take fragmentToken. + resizeTaskFragmentIfRegistered(wct, container, relativeBounds); + int windowingMode = container.getTaskContainer().getWindowingModeForTaskFragment( + relativeBounds); + updateTaskFragmentWindowingModeIfRegistered(wct, container, windowingMode); + // Always use default animation for standalone ActivityStack. + updateAnimationParams(wct, fragmentToken, TaskFragmentAnimationParams.DEFAULT); + setTaskFragmentIsolatedNavigation(wct, container, isIsolatedNavigated); + setTaskFragmentDimOnTask(wct, fragmentToken, dimOnTask); + } - resizeTaskFragment(wct, container.getTaskFragmentToken(), bounds); - updateWindowingMode(wct, container.getTaskFragmentToken(), - bounds.isEmpty() ? WINDOWING_MODE_FULLSCREEN : WINDOWING_MODE_MULTI_WINDOW); + /** + * Returns the expanded bounds if the {@code bounds} violate minimum dimension or are not fully + * covered by the task bounds. Otherwise, returns {@code bounds}. + */ + @NonNull + static Rect sanitizeBounds(@NonNull Rect bounds, @Nullable Size minDimension, + @NonNull Rect taskBounds) { + if (bounds.isEmpty()) { + // Don't need to check if the bounds follows the task bounds. + return bounds; + } + if (boundsSmallerThanMinDimensions(bounds, minDimension)) { + // Expand the bounds if the bounds are smaller than minimum dimensions. + return new Rect(); + } + if (!taskBounds.contains(bounds)) { + // Expand the bounds if the bounds exceed the task bounds. + return new Rect(); + } + return bounds; } @Override diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java index 71195b6df97e..73109e266905 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/TaskContainer.java @@ -126,6 +126,11 @@ class TaskContainer { } @NonNull + Rect getBounds() { + return mConfiguration.windowConfiguration.getBounds(); + } + + @NonNull TaskProperties getTaskProperties() { return new TaskProperties(mDisplayId, mConfiguration); } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java index 4e7b76057b5d..34d43ad56bb4 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/OverlayPresentationTest.java @@ -16,14 +16,17 @@ package androidx.window.extensions.embedding; +import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.view.Display.DEFAULT_DISPLAY; import static androidx.window.extensions.embedding.ActivityEmbeddingOptionsProperties.KEY_OVERLAY_TAG; import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_BOUNDS; import static androidx.window.extensions.embedding.EmbeddingTestUtils.TASK_ID; +import static androidx.window.extensions.embedding.EmbeddingTestUtils.TEST_TAG; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createMockTaskFragmentInfo; import static androidx.window.extensions.embedding.EmbeddingTestUtils.createSplitPairRuleBuilder; +import static androidx.window.extensions.embedding.WindowAttributes.DIM_AREA_ON_TASK; import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; @@ -36,7 +39,6 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; @@ -56,6 +58,8 @@ import android.os.Handler; import android.os.IBinder; import android.platform.test.annotations.Presubmit; import android.platform.test.flag.junit.SetFlagsRule; +import android.util.Size; +import android.window.TaskFragmentAnimationParams; import android.window.TaskFragmentInfo; import android.window.TaskFragmentParentInfo; import android.window.WindowContainerTransaction; @@ -266,62 +270,21 @@ public class OverlayPresentationTest { } @Test - public void testCreateOrUpdateOverlayTaskFragmentIfNeeded_smallerThanMinDimens_expandOverlay() { + public void testSanitizeBounds_smallerThanMinDimens_expandOverlay() { mIntent.setComponent(new ComponentName(ApplicationProvider.getApplicationContext(), MinimumDimensionActivity.class)); - final Rect bounds = new Rect(0, 0, 100, 100); - mSplitController.setActivityStackAttributesCalculator(params -> - new ActivityStackAttributes.Builder().setRelativeBounds(bounds).build()); - final TaskFragmentContainer overlayContainer = - createOrUpdateOverlayTaskFragmentIfNeeded("test"); - final IBinder overlayToken = overlayContainer.getTaskFragmentToken(); - assertThat(mSplitController.getAllOverlayTaskFragmentContainers()) - .containsExactly(overlayContainer); - assertThat(overlayContainer.areLastRequestedBoundsEqual(new Rect())).isTrue(); - verify(mSplitPresenter).setTaskFragmentIsolatedNavigation(mTransaction, overlayToken, - false); - - // Call createOrUpdateOverlayTaskFragmentIfNeeded again to check the update case. - clearInvocations(mSplitPresenter); - createOrUpdateOverlayTaskFragmentIfNeeded("test"); - - verify(mSplitPresenter).resizeTaskFragment(mTransaction, overlayToken, new Rect()); - verify(mSplitPresenter).updateWindowingMode(mTransaction, overlayToken, - WINDOWING_MODE_UNDEFINED); - verify(mSplitPresenter).setTaskFragmentIsolatedNavigation(mTransaction, overlayContainer, - false); + SplitPresenter.sanitizeBounds(bounds, SplitPresenter.getMinDimensions(mIntent), + TASK_BOUNDS); } @Test - public void testCreateOrUpdateOverlayTaskFragmentIfNeeded_notInTaskBounds_expandOverlay() { + public void testSanitizeBounds_notInTaskBounds_expandOverlay() { final Rect bounds = new Rect(TASK_BOUNDS); bounds.offset(10, 10); - mSplitController.setActivityStackAttributesCalculator(params -> - new ActivityStackAttributes.Builder().setRelativeBounds(bounds).build()); - - final TaskFragmentContainer overlayContainer = - createOrUpdateOverlayTaskFragmentIfNeeded("test"); - final IBinder overlayToken = overlayContainer.getTaskFragmentToken(); - assertThat(mSplitController.getAllOverlayTaskFragmentContainers()) - .containsExactly(overlayContainer); - assertThat(overlayContainer.areLastRequestedBoundsEqual(new Rect())).isTrue(); - verify(mSplitPresenter).setTaskFragmentIsolatedNavigation(mTransaction, overlayToken, - false); - - // Call createOrUpdateOverlayTaskFragmentIfNeeded again to check the update case. - clearInvocations(mSplitPresenter); - createOrUpdateOverlayTaskFragmentIfNeeded("test"); - - verify(mSplitPresenter).resizeTaskFragment(mTransaction, overlayToken, new Rect()); - verify(mSplitPresenter).updateWindowingMode(mTransaction, - overlayToken, WINDOWING_MODE_UNDEFINED); - verify(mSplitPresenter).setTaskFragmentIsolatedNavigation(mTransaction, - overlayContainer, false); - assertThat(mSplitController.getAllOverlayTaskFragmentContainers()) - .containsExactly(overlayContainer); + SplitPresenter.sanitizeBounds(bounds, null, TASK_BOUNDS); } @Test @@ -331,6 +294,7 @@ public class OverlayPresentationTest { new ActivityStackAttributes.Builder().setRelativeBounds(bounds).build()); final TaskFragmentContainer overlayContainer = createOrUpdateOverlayTaskFragmentIfNeeded("test"); + setupTaskFragmentInfo(overlayContainer, mActivity); assertThat(mSplitController.getAllOverlayTaskFragmentContainers()) .containsExactly(overlayContainer); @@ -437,7 +401,7 @@ public class OverlayPresentationTest { assertThrows(NullPointerException.class, () -> mSplitController.updateActivityStackAttributes(new Binder(), null)); - verify(mSplitPresenter, never()).applyActivityStackAttributes(any(), any(), any()); + verify(mSplitPresenter, never()).applyActivityStackAttributes(any(), any(), any(), any()); } @Test @@ -447,7 +411,7 @@ public class OverlayPresentationTest { mSplitController.updateActivityStackAttributes(container.getTaskFragmentToken(), new ActivityStackAttributes.Builder().build()); - verify(mSplitPresenter, never()).applyActivityStackAttributes(any(), any(), any()); + verify(mSplitPresenter, never()).applyActivityStackAttributes(any(), any(), any(), any()); } @Test @@ -457,19 +421,20 @@ public class OverlayPresentationTest { mSplitController.updateActivityStackAttributes(container.getTaskFragmentToken(), new ActivityStackAttributes.Builder().build()); - verify(mSplitPresenter, never()).applyActivityStackAttributes(any(), any(), any()); + verify(mSplitPresenter, never()).applyActivityStackAttributes(any(), any(), any(), any()); } @Test public void testUpdateActivityStackAttributes() { final TaskFragmentContainer container = createTestOverlayContainer(TASK_ID, "test"); - doNothing().when(mSplitPresenter).applyActivityStackAttributes(any(), any(), any()); + doNothing().when(mSplitPresenter).applyActivityStackAttributes(any(), any(), any(), any()); final ActivityStackAttributes attrs = new ActivityStackAttributes.Builder().build(); final IBinder token = container.getTaskFragmentToken(); mSplitController.updateActivityStackAttributes(token, attrs); - verify(mSplitPresenter).applyActivityStackAttributes(any(), eq(container), eq(attrs)); + verify(mSplitPresenter).applyActivityStackAttributes(any(), eq(container), eq(attrs), + any()); } @Test @@ -521,6 +486,89 @@ public class OverlayPresentationTest { .that(taskContainer.getOverlayContainer()).isNull(); } + @Test + public void testApplyActivityStackAttributesForExpandedContainer() { + final TaskFragmentContainer container = createMockTaskFragmentContainer(mActivity); + final IBinder token = container.getTaskFragmentToken(); + final ActivityStackAttributes attributes = new ActivityStackAttributes.Builder().build(); + + mSplitPresenter.applyActivityStackAttributes(mTransaction, container, attributes, + null /* minDimensions */); + + verify(mSplitPresenter).resizeTaskFragmentIfRegistered(mTransaction, container, + attributes.getRelativeBounds()); + verify(mSplitPresenter).updateTaskFragmentWindowingModeIfRegistered(mTransaction, container, + WINDOWING_MODE_UNDEFINED); + verify(mSplitPresenter).updateAnimationParams(mTransaction, token, + TaskFragmentAnimationParams.DEFAULT); + verify(mSplitPresenter).setTaskFragmentIsolatedNavigation(mTransaction, container, false); + verify(mSplitPresenter).setTaskFragmentDimOnTask(mTransaction, token, false); + } + + @Test + public void testApplyActivityStackAttributesForOverlayContainer() { + final TaskFragmentContainer container = createTestOverlayContainer(TASK_ID, TEST_TAG); + final IBinder token = container.getTaskFragmentToken(); + final ActivityStackAttributes attributes = new ActivityStackAttributes.Builder() + .setRelativeBounds(new Rect(0, 0, 200, 200)) + .setWindowAttributes(new WindowAttributes(DIM_AREA_ON_TASK)) + .build(); + + mSplitPresenter.applyActivityStackAttributes(mTransaction, container, attributes, + null /* minDimensions */); + + verify(mSplitPresenter).resizeTaskFragmentIfRegistered(mTransaction, container, + attributes.getRelativeBounds()); + verify(mSplitPresenter).updateTaskFragmentWindowingModeIfRegistered(mTransaction, container, + WINDOWING_MODE_MULTI_WINDOW); + verify(mSplitPresenter).updateAnimationParams(mTransaction, token, + TaskFragmentAnimationParams.DEFAULT); + verify(mSplitPresenter).setTaskFragmentIsolatedNavigation(mTransaction, container, true); + verify(mSplitPresenter).setTaskFragmentDimOnTask(mTransaction, token, true); + } + + @Test + public void testApplyActivityStackAttributesForExpandedOverlayContainer() { + final TaskFragmentContainer container = createTestOverlayContainer(TASK_ID, TEST_TAG); + final IBinder token = container.getTaskFragmentToken(); + final ActivityStackAttributes attributes = new ActivityStackAttributes.Builder().build(); + + mSplitPresenter.applyActivityStackAttributes(mTransaction, container, attributes, + null /* minDimensions */); + + verify(mSplitPresenter).resizeTaskFragmentIfRegistered(mTransaction, container, + attributes.getRelativeBounds()); + verify(mSplitPresenter).updateTaskFragmentWindowingModeIfRegistered(mTransaction, container, + WINDOWING_MODE_UNDEFINED); + verify(mSplitPresenter).updateAnimationParams(mTransaction, token, + TaskFragmentAnimationParams.DEFAULT); + verify(mSplitPresenter).setTaskFragmentIsolatedNavigation(mTransaction, container, false); + verify(mSplitPresenter).setTaskFragmentDimOnTask(mTransaction, token, false); + } + + @Test + public void testApplyActivityStackAttributesForOverlayContainer_exceedsMinDimensions() { + final TaskFragmentContainer container = createTestOverlayContainer(TASK_ID, TEST_TAG); + final IBinder token = container.getTaskFragmentToken(); + final Rect relativeBounds = new Rect(0, 0, 200, 200); + final ActivityStackAttributes attributes = new ActivityStackAttributes.Builder() + .setRelativeBounds(relativeBounds) + .setWindowAttributes(new WindowAttributes(DIM_AREA_ON_TASK)) + .build(); + + mSplitPresenter.applyActivityStackAttributes(mTransaction, container, attributes, + new Size(relativeBounds.width() + 1, relativeBounds.height())); + + verify(mSplitPresenter).resizeTaskFragmentIfRegistered(mTransaction, container, + new Rect()); + verify(mSplitPresenter).updateTaskFragmentWindowingModeIfRegistered(mTransaction, container, + WINDOWING_MODE_UNDEFINED); + verify(mSplitPresenter).updateAnimationParams(mTransaction, token, + TaskFragmentAnimationParams.DEFAULT); + verify(mSplitPresenter).setTaskFragmentIsolatedNavigation(mTransaction, container, false); + verify(mSplitPresenter).setTaskFragmentDimOnTask(mTransaction, token, false); + } + /** * A simplified version of {@link SplitController.ActivityStartMonitor * #createOrUpdateOverlayTaskFragmentIfNeeded} diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewTest.kt new file mode 100644 index 000000000000..4c76168cdeaa --- /dev/null +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewTest.kt @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.bubbles + +import android.content.ComponentName +import android.content.Context +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.wm.shell.taskview.TaskView +import com.android.wm.shell.taskview.TaskViewTaskController + +import com.google.common.truth.Truth.assertThat +import com.google.common.util.concurrent.MoreExecutors.directExecutor +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock + +@SmallTest +@RunWith(AndroidJUnit4::class) +class BubbleTaskViewTest { + + private lateinit var bubbleTaskView: BubbleTaskView + private val context = ApplicationProvider.getApplicationContext<Context>() + + @Before + fun setUp() { + val taskView = TaskView(context, mock<TaskViewTaskController>()) + bubbleTaskView = BubbleTaskView(taskView, directExecutor()) + } + + @Test + fun onTaskCreated_updatesState() { + val componentName = ComponentName(context, "TestClass") + bubbleTaskView.listener.onTaskCreated(123, componentName) + + assertThat(bubbleTaskView.taskId).isEqualTo(123) + assertThat(bubbleTaskView.componentName).isEqualTo(componentName) + assertThat(bubbleTaskView.isCreated).isTrue() + } + + @Test + fun onTaskCreated_callsDelegateListener() { + var actualTaskId = -1 + var actualComponentName: ComponentName? = null + val delegateListener = object : TaskView.Listener { + override fun onTaskCreated(taskId: Int, name: ComponentName) { + actualTaskId = taskId + actualComponentName = name + } + } + bubbleTaskView.delegateListener = delegateListener + + val componentName = ComponentName(context, "TestClass") + bubbleTaskView.listener.onTaskCreated(123, componentName) + + assertThat(actualTaskId).isEqualTo(123) + assertThat(actualComponentName).isEqualTo(componentName) + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java index f3fe895bf9b4..9f7d0ac9bafe 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java @@ -74,7 +74,6 @@ import com.android.wm.shell.R; import com.android.wm.shell.common.AlphaOptimizedButton; import com.android.wm.shell.common.TriangleShape; import com.android.wm.shell.taskview.TaskView; -import com.android.wm.shell.taskview.TaskViewTaskController; import java.io.PrintWriter; @@ -146,7 +145,6 @@ public class BubbleExpandedView extends LinearLayout { private AlphaOptimizedButton mManageButton; private TaskView mTaskView; - private TaskViewTaskController mTaskViewTaskController; private BubbleOverflowContainerView mOverflowView; private int mTaskId = INVALID_TASK_ID; @@ -434,7 +432,8 @@ public class BubbleExpandedView extends LinearLayout { * Initialize {@link BubbleController} and {@link BubbleStackView} here, this method must need * to be called after view inflate. */ - void initialize(BubbleController controller, BubbleStackView stackView, boolean isOverflow) { + void initialize(BubbleController controller, BubbleStackView stackView, boolean isOverflow, + @Nullable BubbleTaskView bubbleTaskView) { mController = controller; mStackView = stackView; mIsOverflow = isOverflow; @@ -451,18 +450,22 @@ public class BubbleExpandedView extends LinearLayout { bringChildToFront(mOverflowView); mManageButton.setVisibility(GONE); } else { - mTaskViewTaskController = new TaskViewTaskController(mContext, - mController.getTaskOrganizer(), - mController.getTaskViewTransitions(), mController.getSyncTransactionQueue()); - mTaskView = new TaskView(mContext, mTaskViewTaskController); - mTaskView.setListener(mController.getMainExecutor(), mTaskViewListener); + mTaskView = bubbleTaskView.getTaskView(); + bubbleTaskView.setDelegateListener(mTaskViewListener); // set a fixed width so it is not recalculated as part of a rotation. the width will be // updated manually after the rotation. FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(getContentWidth(), MATCH_PARENT); + if (mTaskView.getParent() != null) { + ((ViewGroup) mTaskView.getParent()).removeView(mTaskView); + } mExpandedViewContainer.addView(mTaskView, lp); bringChildToFront(mTaskView); + if (bubbleTaskView.isCreated()) { + mTaskViewListener.onTaskCreated( + bubbleTaskView.getTaskId(), bubbleTaskView.getComponentName()); + } } } @@ -876,7 +879,7 @@ public class BubbleExpandedView extends LinearLayout { return; } boolean isNew = mBubble == null || didBackingContentChange(bubble); - if (isNew || bubble != null && bubble.getKey().equals(mBubble.getKey())) { + if (isNew || bubble.getKey().equals(mBubble.getKey())) { mBubble = bubble; mManageButton.setContentDescription(getResources().getString( R.string.bubbles_settings_button_description, bubble.getAppName())); @@ -1107,7 +1110,8 @@ public class BubbleExpandedView extends LinearLayout { * has been removed. * * If this view should be reused after this method is called, then - * {@link #initialize(BubbleController, BubbleStackView, boolean)} must be invoked first. + * {@link #initialize(BubbleController, BubbleStackView, boolean, BubbleTaskView)} + * must be invoked first. */ public void cleanUpExpandedState() { if (DEBUG_BUBBLE_EXPANDED_VIEW) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt index 22e836aacfc5..e5d9acedc903 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt @@ -29,6 +29,7 @@ import android.util.PathParser import android.view.LayoutInflater import android.view.View.VISIBLE import android.widget.FrameLayout +import androidx.core.content.ContextCompat import com.android.launcher3.icons.BubbleIconFactory import com.android.wm.shell.R import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView @@ -57,10 +58,16 @@ class BubbleOverflow(private val context: Context, private val positioner: Bubbl /** Call before use and again if cleanUpExpandedState was called. */ fun initialize(controller: BubbleController, forBubbleBar: Boolean) { if (forBubbleBar) { - createBubbleBarExpandedView().initialize(controller, true /* isOverflow */) + createBubbleBarExpandedView() + .initialize(controller, /* isOverflow= */ true, /* bubbleTaskView= */ null) } else { createExpandedView() - .initialize(controller, controller.stackView, true /* isOverflow */) + .initialize( + controller, + controller.stackView, + /* isOverflow= */ true, + /* bubbleTaskView= */ null + ) } } @@ -113,7 +120,10 @@ class BubbleOverflow(private val context: Context, private val positioner: Bubbl context, res.getDimensionPixelSize(R.dimen.bubble_size), res.getDimensionPixelSize(R.dimen.bubble_badge_size), - res.getColor(com.android.launcher3.icons.R.color.important_conversation), + ContextCompat.getColor( + context, + com.android.launcher3.icons.R.color.important_conversation + ), res.getDimensionPixelSize(com.android.internal.R.dimen.importance_ring_stroke_width) ) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskView.kt new file mode 100644 index 000000000000..2fcd133c7b20 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskView.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.bubbles + +import android.app.ActivityTaskManager.INVALID_TASK_ID +import android.content.ComponentName +import androidx.annotation.VisibleForTesting +import com.android.wm.shell.taskview.TaskView +import java.util.concurrent.Executor + +/** + * A wrapper class around [TaskView] for bubble expanded views. + * + * [delegateListener] allows callers to change listeners after a task has been created. + */ +class BubbleTaskView(val taskView: TaskView, executor: Executor) { + + /** Whether the task is already created. */ + var isCreated = false + private set + + /** The task id. */ + var taskId = INVALID_TASK_ID + private set + + /** The component name of the application running in the task. */ + var componentName: ComponentName? = null + private set + + /** [TaskView.Listener] for users of this class. */ + var delegateListener: TaskView.Listener? = null + + /** A [TaskView.Listener] that delegates to [delegateListener]. */ + @get:VisibleForTesting + val listener = object : TaskView.Listener { + override fun onInitialized() { + delegateListener?.onInitialized() + } + + override fun onReleased() { + delegateListener?.onReleased() + } + + override fun onTaskCreated(taskId: Int, name: ComponentName) { + delegateListener?.onTaskCreated(taskId, name) + this@BubbleTaskView.taskId = taskId + isCreated = true + componentName = name + } + + override fun onTaskVisibilityChanged(taskId: Int, visible: Boolean) { + delegateListener?.onTaskVisibilityChanged(taskId, visible) + } + + override fun onTaskRemovalStarted(taskId: Int) { + delegateListener?.onTaskRemovalStarted(taskId) + } + + override fun onBackPressedOnTaskRoot(taskId: Int) { + delegateListener?.onBackPressedOnTaskRoot(taskId) + } + } + + init { + taskView.setListener(executor, listener) + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java index f6c382fb5b3d..5855a81333d4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java @@ -35,10 +35,7 @@ import android.view.View; import androidx.annotation.Nullable; -import com.android.wm.shell.common.ShellExecutor; -import com.android.wm.shell.common.annotations.ShellMainThread; import com.android.wm.shell.taskview.TaskView; -import com.android.wm.shell.taskview.TaskViewTaskController; /** * Handles creating and updating the {@link TaskView} associated with a {@link Bubble}. @@ -65,7 +62,6 @@ public class BubbleTaskViewHelper { private final Context mContext; private final BubbleController mController; - private final @ShellMainThread ShellExecutor mMainExecutor; private final BubbleTaskViewHelper.Listener mListener; private final View mParentView; @@ -73,7 +69,6 @@ public class BubbleTaskViewHelper { private Bubble mBubble; @Nullable private PendingIntent mPendingIntent; - private TaskViewTaskController mTaskViewTaskController; @Nullable private TaskView mTaskView; private int mTaskId = INVALID_TASK_ID; @@ -204,17 +199,18 @@ public class BubbleTaskViewHelper { public BubbleTaskViewHelper(Context context, BubbleController controller, BubbleTaskViewHelper.Listener listener, + BubbleTaskView bubbleTaskView, View parent) { mContext = context; mController = controller; - mMainExecutor = mController.getMainExecutor(); mListener = listener; mParentView = parent; - mTaskViewTaskController = new TaskViewTaskController(mContext, - mController.getTaskOrganizer(), - mController.getTaskViewTransitions(), mController.getSyncTransactionQueue()); - mTaskView = new TaskView(mContext, mTaskViewTaskController); - mTaskView.setListener(mMainExecutor, mTaskViewListener); + mTaskView = bubbleTaskView.getTaskView(); + bubbleTaskView.setDelegateListener(mTaskViewListener); + if (bubbleTaskView.isCreated()) { + mTaskId = bubbleTaskView.getTaskId(); + mListener.onTaskCreated(); + } } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java index bb30c5eeebcf..c3d899e7dac7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java @@ -46,6 +46,8 @@ import com.android.launcher3.icons.BubbleIconFactory; import com.android.wm.shell.R; import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView; import com.android.wm.shell.bubbles.bar.BubbleBarLayerView; +import com.android.wm.shell.taskview.TaskView; +import com.android.wm.shell.taskview.TaskViewTaskController; import java.lang.ref.WeakReference; import java.util.Objects; @@ -173,10 +175,12 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask BubbleViewInfo info = new BubbleViewInfo(); if (!skipInflation && !b.isInflated()) { + BubbleTaskView bubbleTaskView = createBubbleTaskView(c, controller); LayoutInflater inflater = LayoutInflater.from(c); info.bubbleBarExpandedView = (BubbleBarExpandedView) inflater.inflate( R.layout.bubble_bar_expanded_view, layerView, false /* attachToRoot */); - info.bubbleBarExpandedView.initialize(controller, false /* isOverflow */); + info.bubbleBarExpandedView.initialize( + controller, false /* isOverflow */, bubbleTaskView); } if (!populateCommonInfo(info, c, b, iconFactory)) { @@ -201,9 +205,11 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask R.layout.bubble_view, stackView, false /* attachToRoot */); info.imageView.initialize(controller.getPositioner()); + BubbleTaskView bubbleTaskView = createBubbleTaskView(c, controller); info.expandedView = (BubbleExpandedView) inflater.inflate( R.layout.bubble_expanded_view, stackView, false /* attachToRoot */); - info.expandedView.initialize(controller, stackView, false /* isOverflow */); + info.expandedView.initialize( + controller, stackView, false /* isOverflow */, bubbleTaskView); } if (!populateCommonInfo(info, c, b, iconFactory)) { @@ -219,6 +225,15 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask } return info; } + + private static BubbleTaskView createBubbleTaskView( + Context context, BubbleController controller) { + TaskViewTaskController taskViewTaskController = new TaskViewTaskController(context, + controller.getTaskOrganizer(), + controller.getTaskViewTransitions(), controller.getSyncTransactionQueue()); + TaskView taskView = new TaskView(context, taskViewTaskController); + return new BubbleTaskView(taskView, controller.getMainExecutor()); + } } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java index 66c0c9640477..3cf23ac114ee 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java @@ -16,6 +16,8 @@ package com.android.wm.shell.bubbles.bar; +import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; + import android.annotation.Nullable; import android.app.ActivityManager; import android.content.Context; @@ -27,6 +29,7 @@ import android.graphics.Rect; import android.util.AttributeSet; import android.view.LayoutInflater; import android.view.View; +import android.view.ViewGroup; import android.view.ViewOutlineProvider; import android.widget.FrameLayout; @@ -35,6 +38,7 @@ import com.android.wm.shell.R; import com.android.wm.shell.bubbles.Bubble; import com.android.wm.shell.bubbles.BubbleController; import com.android.wm.shell.bubbles.BubbleOverflowContainerView; +import com.android.wm.shell.bubbles.BubbleTaskView; import com.android.wm.shell.bubbles.BubbleTaskViewHelper; import com.android.wm.shell.bubbles.Bubbles; import com.android.wm.shell.taskview.TaskView; @@ -130,7 +134,8 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView } /** Set the BubbleController on the view, must be called before doing anything else. */ - public void initialize(BubbleController controller, boolean isOverflow) { + public void initialize(BubbleController controller, boolean isOverflow, + @Nullable BubbleTaskView bubbleTaskView) { mController = controller; mIsOverflow = isOverflow; @@ -140,14 +145,19 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView mOverflowView.setBubbleController(mController); addView(mOverflowView); } else { - + mTaskView = bubbleTaskView.getTaskView(); mBubbleTaskViewHelper = new BubbleTaskViewHelper(mContext, mController, - /* listener= */ this, + /* listener= */ this, bubbleTaskView, /* viewParent= */ this); - mTaskView = mBubbleTaskViewHelper.getTaskView(); - addView(mTaskView); + if (mTaskView.getParent() != null) { + ((ViewGroup) mTaskView.getParent()).removeView(mTaskView); + } + FrameLayout.LayoutParams lp = + new FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT); + addView(mTaskView, lp); mTaskView.setEnableSurfaceClipping(true); mTaskView.setCornerRadius(mCornerRadius); + mTaskView.setVisibility(VISIBLE); // Handle view needs to draw on top of task view. bringChildToFront(mHandleView); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java index d023cea6d19d..1232baacdac7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java @@ -1020,7 +1020,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "RecentsController.finishInner: no valid PiP leash;" + "mPipTransaction=%s, mPipTask=%s, mPipTaskId=%d", - mPipTransaction.toString(), mPipTask.toString(), mPipTaskId); + mPipTransaction, mPipTask, mPipTaskId); } else { t.show(pipLeash); PictureInPictureSurfaceTransaction.apply(mPipTransaction, pipLeash, t); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java index 5a74255df49a..e6faa6391cca 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java @@ -93,7 +93,7 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL // On a smaller screen, don't require as much empty space on screen, as offscreen // drags will be restricted too much. - final int requiredEmptySpaceId = mDisplayController.getDisplayContext(mTaskInfo.taskId) + final int requiredEmptySpaceId = mDisplayController.getDisplayContext(mTaskInfo.displayId) .getResources().getConfiguration().smallestScreenWidthDp >= 600 ? R.dimen.freeform_required_visible_empty_space_in_header : R.dimen.small_screen_required_visible_empty_space_in_header; diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/tv/TvPipTestBase.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/tv/TvPipTestBase.kt index 47bff8de377e..0d1853534927 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/tv/TvPipTestBase.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/tv/TvPipTestBase.kt @@ -78,6 +78,14 @@ abstract class TvPipTestBase : PipTestBase(rotationToString(ROTATION_0), ROTATIO uiAutomation.dropShellPermissionIdentity() } + override fun onProcessStarted( + pid: Int, + processUid: Int, + packageUid: Int, + packageName: String, + processName: String + ) {} + override fun onForegroundActivitiesChanged(pid: Int, uid: Int, foreground: Boolean) {} override fun onForegroundServicesChanged(pid: Int, uid: Int, serviceTypes: Int) {} diff --git a/libs/hwui/RenderNode.cpp b/libs/hwui/RenderNode.cpp index 0b42c88aa448..f526a280b113 100644 --- a/libs/hwui/RenderNode.cpp +++ b/libs/hwui/RenderNode.cpp @@ -230,7 +230,7 @@ void RenderNode::pushLayerUpdate(TreeInfo& info) { * stencil buffer may be needed. Views that use a functor to draw will be forced onto a layer. */ void RenderNode::prepareTreeImpl(TreeObserver& observer, TreeInfo& info, bool functorsNeedLayer) { - if (mDamageGenerationId == info.damageGenerationId) { + if (mDamageGenerationId == info.damageGenerationId && mDamageGenerationId != 0) { // We hit the same node a second time in the same tree. We don't know the minimal // damage rect anymore, so just push the biggest we can onto our parent's transform // We push directly onto parent in case we are clipped to bounds but have moved position. diff --git a/libs/hwui/RenderNode.h b/libs/hwui/RenderNode.h index 1f3834be5bef..c9045427bd42 100644 --- a/libs/hwui/RenderNode.h +++ b/libs/hwui/RenderNode.h @@ -262,7 +262,7 @@ private: DisplayList mDisplayList; DisplayList mStagingDisplayList; - int64_t mDamageGenerationId; + int64_t mDamageGenerationId = 0; friend class AnimatorManager; AnimatorManager mAnimatorManager; diff --git a/libs/input/PointerController.cpp b/libs/input/PointerController.cpp index bba9c9764eee..f84107e8792c 100644 --- a/libs/input/PointerController.cpp +++ b/libs/input/PointerController.cpp @@ -111,7 +111,11 @@ PointerController::PointerController(const sp<PointerControllerPolicyInterface>& : PointerController( policy, looper, spriteController, enabled, [](const sp<android::gui::WindowInfosListener>& listener) { - SurfaceComposerClient::getDefault()->addWindowInfosListener(listener); + auto initialInfo = std::make_pair(std::vector<android::gui::WindowInfo>{}, + std::vector<android::gui::DisplayInfo>{}); + SurfaceComposerClient::getDefault()->addWindowInfosListener(listener, + &initialInfo); + return initialInfo.second; }, [](const sp<android::gui::WindowInfosListener>& listener) { SurfaceComposerClient::getDefault()->removeWindowInfosListener(listener); @@ -119,8 +123,9 @@ PointerController::PointerController(const sp<PointerControllerPolicyInterface>& PointerController::PointerController(const sp<PointerControllerPolicyInterface>& policy, const sp<Looper>& looper, SpriteController& spriteController, - bool enabled, WindowListenerConsumer registerListener, - WindowListenerConsumer unregisterListener) + bool enabled, + const WindowListenerRegisterConsumer& registerListener, + WindowListenerUnregisterConsumer unregisterListener) : mEnabled(enabled), mContext(policy, looper, spriteController, *this), mCursorController(mContext), @@ -128,7 +133,8 @@ PointerController::PointerController(const sp<PointerControllerPolicyInterface>& mUnregisterWindowInfosListener(std::move(unregisterListener)) { std::scoped_lock lock(getLock()); mLocked.presentation = Presentation::SPOT; - registerListener(mDisplayInfoListener); + const auto& initialDisplayInfos = registerListener(mDisplayInfoListener); + onDisplayInfosChangedLocked(initialDisplayInfos); } PointerController::~PointerController() { diff --git a/libs/input/PointerController.h b/libs/input/PointerController.h index a8b963367f4c..6ee5707622ca 100644 --- a/libs/input/PointerController.h +++ b/libs/input/PointerController.h @@ -79,14 +79,16 @@ public: std::string dump() override; protected: - using WindowListenerConsumer = + using WindowListenerRegisterConsumer = std::function<std::vector<gui::DisplayInfo>( + const sp<android::gui::WindowInfosListener>&)>; + using WindowListenerUnregisterConsumer = std::function<void(const sp<android::gui::WindowInfosListener>&)>; // Constructor used to test WindowInfosListener registration. PointerController(const sp<PointerControllerPolicyInterface>& policy, const sp<Looper>& looper, SpriteController& spriteController, bool enabled, - WindowListenerConsumer registerListener, - WindowListenerConsumer unregisterListener); + const WindowListenerRegisterConsumer& registerListener, + WindowListenerUnregisterConsumer unregisterListener); PointerController(const sp<PointerControllerPolicyInterface>& policy, const sp<Looper>& looper, SpriteController& spriteController, bool enabled); @@ -129,7 +131,7 @@ private: }; sp<DisplayInfoListener> mDisplayInfoListener; - const WindowListenerConsumer mUnregisterWindowInfosListener; + const WindowListenerUnregisterConsumer mUnregisterWindowInfosListener; const ui::Transform& getTransformForDisplayLocked(int displayId) const REQUIRES(getLock()); diff --git a/libs/input/TouchSpotController.cpp b/libs/input/TouchSpotController.cpp index b8de919fbd8c..99952aa14904 100644 --- a/libs/input/TouchSpotController.cpp +++ b/libs/input/TouchSpotController.cpp @@ -93,7 +93,7 @@ void TouchSpotController::setSpots(const PointerCoords* spotCoords, const uint32 const PointerCoords& c = spotCoords[spotIdToIndex[id]]; ALOGD(" spot %d: position=(%0.3f, %0.3f), pressure=%0.3f, displayId=%" PRId32 ".", id, c.getAxisValue(AMOTION_EVENT_AXIS_X), c.getAxisValue(AMOTION_EVENT_AXIS_Y), - c.getAxisValue(AMOTION_EVENT_AXIS_PRESSURE), displayId); + c.getAxisValue(AMOTION_EVENT_AXIS_PRESSURE), mDisplayId); } #endif diff --git a/libs/input/tests/PointerController_test.cpp b/libs/input/tests/PointerController_test.cpp index adfa91e96ebb..a1bb5b3f1cc4 100644 --- a/libs/input/tests/PointerController_test.cpp +++ b/libs/input/tests/PointerController_test.cpp @@ -160,9 +160,11 @@ public: : PointerController( policy, looper, spriteController, /*enabled=*/true, - [®isteredListener](const sp<android::gui::WindowInfosListener>& listener) { + [®isteredListener](const sp<android::gui::WindowInfosListener>& listener) + -> std::vector<gui::DisplayInfo> { // Register listener registeredListener = listener; + return {}; }, [®isteredListener](const sp<android::gui::WindowInfosListener>& listener) { // Unregister listener diff --git a/media/java/android/media/LoudnessCodecController.java b/media/java/android/media/LoudnessCodecController.java index b3e5c52b27b3..61c913166e1c 100644 --- a/media/java/android/media/LoudnessCodecController.java +++ b/media/java/android/media/LoudnessCodecController.java @@ -32,12 +32,13 @@ import androidx.annotation.Nullable; import java.util.HashMap; import java.util.HashSet; -import java.util.Map; +import java.util.Map.Entry; import java.util.Objects; import java.util.Set; import java.util.concurrent.Executor; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Consumer; /** * Class for getting recommended loudness parameter updates for audio decoders as they are used @@ -320,11 +321,6 @@ public class LoudnessCodecController implements SafeCloseable { * Stops any loudness updates and frees up the resources. */ @FlaggedApi(FLAG_LOUDNESS_CONFIGURATOR_API) - public void release() { - close(); - } - - /** @hide */ @Override public void close() { synchronized (mControllerLock) { @@ -339,9 +335,12 @@ public class LoudnessCodecController implements SafeCloseable { } /** @hide */ - /*package*/ Map<LoudnessCodecInfo, Set<MediaCodec>> getRegisteredMediaCodecs() { + /*package*/ void mediaCodecsConsume( + Consumer<Entry<LoudnessCodecInfo, Set<MediaCodec>>> consumer) { synchronized (mControllerLock) { - return mMediaCodecs; + for (Entry<LoudnessCodecInfo, Set<MediaCodec>> entry : mMediaCodecs.entrySet()) { + consumer.accept(entry); + } } } diff --git a/media/java/android/media/LoudnessCodecDispatcher.java b/media/java/android/media/LoudnessCodecDispatcher.java index 46be54be55ec..fa08658a214f 100644 --- a/media/java/android/media/LoudnessCodecDispatcher.java +++ b/media/java/android/media/LoudnessCodecDispatcher.java @@ -32,7 +32,6 @@ import androidx.annotation.NonNull; import java.util.HashMap; import java.util.Iterator; -import java.util.Map; import java.util.Map.Entry; import java.util.Objects; import java.util.Set; @@ -81,16 +80,15 @@ public class LoudnessCodecDispatcher implements CallbackUtil.DispatcherStub { mConfiguratorListener.computeIfPresent(listener, (l, lcConfig) -> { // send the appropriate bundle for the user to update if (lcConfig.getSessionId() == sessionId) { - final Map<LoudnessCodecInfo, Set<MediaCodec>> mediaCodecsMap = - lcConfig.getRegisteredMediaCodecs(); - for (LoudnessCodecInfo codecInfo : mediaCodecsMap.keySet()) { + lcConfig.mediaCodecsConsume(mcEntry -> { + final LoudnessCodecInfo codecInfo = mcEntry.getKey(); final String infoKey = Integer.toString(codecInfo.hashCode()); Bundle bundle = null; if (params.containsKey(infoKey)) { bundle = new Bundle(params.getPersistableBundle(infoKey)); } - final Set<MediaCodec> mediaCodecs = mediaCodecsMap.get(codecInfo); + final Set<MediaCodec> mediaCodecs = mcEntry.getValue(); for (MediaCodec mediaCodec : mediaCodecs) { final String mediaCodecKey = Integer.toString( mediaCodec.hashCode()); @@ -121,7 +119,7 @@ public class LoudnessCodecDispatcher implements CallbackUtil.DispatcherStub { break; } } - } + }); } return lcConfig; }); diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java index 687feef6c58a..691aa7784d7a 100644 --- a/media/java/android/media/MediaRouter2.java +++ b/media/java/android/media/MediaRouter2.java @@ -196,8 +196,8 @@ public final class MediaRouter2 { * Manifest.permission#MEDIA_CONTENT_CONTROL MEDIA_CONTENT_CONTROL} permission. * @hide */ - // TODO (b/311711420): Deprecate once #getInstance(Context, Looper, String, UserHandle) - // reaches public SDK. + // TODO (b/311711420): Deprecate once #getInstance(Context, String, UserHandle) reaches public + // SDK. @SystemApi @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL) @Nullable @@ -206,7 +206,7 @@ public final class MediaRouter2 { // Capturing the IAE here to not break nullability. try { return findOrCreateProxyInstanceForCallingUser( - context, Looper.getMainLooper(), clientPackageName, context.getUser()); + context, clientPackageName, context.getUser()); } catch (IllegalArgumentException ex) { Log.e(TAG, "Package " + clientPackageName + " not found. Ignoring."); return null; @@ -217,8 +217,6 @@ public final class MediaRouter2 { * Returns a proxy MediaRouter2 instance that allows you to control the routing of an app * specified by {@code clientPackageName} and {@code user}. * - * <p>You can specify any {@link Looper} of choice on which internal state updates will run. - * * <p>Proxy MediaRouter2 instances operate differently than regular MediaRouter2 instances: * * <ul> @@ -237,7 +235,6 @@ public final class MediaRouter2 { * </ul> * * @param context The {@link Context} of the caller. - * @param looper The {@link Looper} on which to process internal state changes. * @param clientPackageName The package name of the app you want to control the routing of. * @param user The {@link UserHandle} of the user running the app for which to get the proxy * router instance. Must match {@link Process#myUserHandle()} if the caller doesn't hold @@ -255,10 +252,9 @@ public final class MediaRouter2 { @NonNull public static MediaRouter2 getInstance( @NonNull Context context, - @NonNull Looper looper, @NonNull String clientPackageName, @NonNull UserHandle user) { - return findOrCreateProxyInstanceForCallingUser(context, looper, clientPackageName, user); + return findOrCreateProxyInstanceForCallingUser(context, clientPackageName, user); } /** @@ -270,9 +266,8 @@ public final class MediaRouter2 { */ @NonNull private static MediaRouter2 findOrCreateProxyInstanceForCallingUser( - Context context, Looper looper, String clientPackageName, UserHandle user) { + Context context, String clientPackageName, UserHandle user) { Objects.requireNonNull(context, "context must not be null"); - Objects.requireNonNull(looper, "looper must not be null"); Objects.requireNonNull(user, "user must not be null"); if (TextUtils.isEmpty(clientPackageName)) { @@ -284,7 +279,8 @@ public final class MediaRouter2 { synchronized (sSystemRouterLock) { MediaRouter2 instance = sAppToProxyRouterMap.get(key); if (instance == null) { - instance = new MediaRouter2(context, looper, clientPackageName, user); + instance = + new MediaRouter2(context, Looper.getMainLooper(), clientPackageName, user); // Register proxy router after instantiation to avoid race condition. ((ProxyMediaRouter2Impl) instance.mImpl).registerProxyRouter(); sAppToProxyRouterMap.put(key, instance); diff --git a/media/java/android/media/tv/BroadcastInfoRequest.java b/media/java/android/media/tv/BroadcastInfoRequest.java index cbd8c1fd53cd..694756c3f1a3 100644 --- a/media/java/android/media/tv/BroadcastInfoRequest.java +++ b/media/java/android/media/tv/BroadcastInfoRequest.java @@ -32,7 +32,8 @@ import java.lang.annotation.RetentionPolicy; public abstract class BroadcastInfoRequest implements Parcelable { /** @hide */ @Retention(RetentionPolicy.SOURCE) - @IntDef({REQUEST_OPTION_REPEAT, REQUEST_OPTION_AUTO_UPDATE}) + @IntDef({REQUEST_OPTION_REPEAT, REQUEST_OPTION_AUTO_UPDATE, + REQUEST_OPTION_ONEWAY, REQUEST_OPTION_ONESHOT}) public @interface RequestOption {} /** @@ -47,6 +48,18 @@ public abstract class BroadcastInfoRequest implements Parcelable { * first time, new values are detected. */ public static final int REQUEST_OPTION_AUTO_UPDATE = 1; + /** + * Request option: one-way + * <p> With this option, no response is expected after sending the request. + * @hide + */ + public static final int REQUEST_OPTION_ONEWAY = 2; + /** + * Request option: one-shot + * <p> With this option, only one response will be given per request. + * @hide + */ + public static final int REQUEST_OPTION_ONESHOT = 3; public static final @NonNull Parcelable.Creator<BroadcastInfoRequest> CREATOR = new Parcelable.Creator<BroadcastInfoRequest>() { diff --git a/media/java/android/media/tv/TvInputManager.java b/media/java/android/media/tv/TvInputManager.java index caddd8ad674f..2b31bfef412e 100644 --- a/media/java/android/media/tv/TvInputManager.java +++ b/media/java/android/media/tv/TvInputManager.java @@ -33,6 +33,7 @@ import android.media.AudioDeviceInfo; import android.media.AudioFormat.Encoding; import android.media.AudioPresentation; import android.media.PlaybackParams; +import android.media.tv.ad.TvAdManager; import android.media.tv.interactive.TvInteractiveAppManager; import android.net.Uri; import android.os.Binder; @@ -2744,6 +2745,7 @@ public final class TvInputManager { private int mVideoHeight; private TvInteractiveAppManager.Session mIAppSession; + private TvAdManager.Session mAdSession; private boolean mIAppNotificationEnabled = false; private Session(IBinder token, InputChannel channel, ITvInputManager service, int userId, @@ -2764,6 +2766,14 @@ public final class TvInputManager { this.mIAppSession = iAppSession; } + public TvAdManager.Session getAdSession() { + return mAdSession; + } + + public void setAdSession(TvAdManager.Session adSession) { + this.mAdSession = adSession; + } + /** * Releases this session. */ diff --git a/media/java/android/media/tv/ad/ITvAdManager.aidl b/media/java/android/media/tv/ad/ITvAdManager.aidl index a747e4995869..9620065ecf33 100644 --- a/media/java/android/media/tv/ad/ITvAdManager.aidl +++ b/media/java/android/media/tv/ad/ITvAdManager.aidl @@ -16,9 +16,13 @@ package android.media.tv.ad; +import android.graphics.Rect; +import android.media.tv.TvTrackInfo; import android.media.tv.ad.ITvAdClient; import android.media.tv.ad.ITvAdManagerCallback; import android.media.tv.ad.TvAdServiceInfo; +import android.net.Uri; +import android.os.Bundle; import android.view.Surface; /** @@ -31,10 +35,27 @@ interface ITvAdManager { in ITvAdClient client, in String serviceId, in String type, int seq, int userId); void releaseSession(in IBinder sessionToken, int userId); void startAdService(in IBinder sessionToken, int userId); + void stopAdService(in IBinder sessionToken, int userId); + void resetAdService(in IBinder sessionToken, int userId); void setSurface(in IBinder sessionToken, in Surface surface, int userId); void dispatchSurfaceChanged(in IBinder sessionToken, int format, int width, int height, int userId); + void sendCurrentVideoBounds(in IBinder sessionToken, in Rect bounds, int userId); + void sendCurrentChannelUri(in IBinder sessionToken, in Uri channelUri, int userId); + void sendTrackInfoList(in IBinder sessionToken, in List<TvTrackInfo> tracks, int userId); + void sendCurrentTvInputId(in IBinder sessionToken, in String inputId, int userId); + void sendSigningResult(in IBinder sessionToken, in String signingId, in byte[] result, + int userId); + + void notifyError(in IBinder sessionToken, in String errMsg, in Bundle params, int userId); + void notifyTvMessage(in IBinder sessionToken, in int type, in Bundle data, int userId); + void registerCallback(in ITvAdManagerCallback callback, int userId); void unregisterCallback(in ITvAdManagerCallback callback, int userId); + + void createMediaView(in IBinder sessionToken, in IBinder windowToken, in Rect frame, + int userId); + void relayoutMediaView(in IBinder sessionToken, in Rect frame, int userId); + void removeMediaView(in IBinder sessionToken, int userId); } diff --git a/media/java/android/media/tv/ad/ITvAdSession.aidl b/media/java/android/media/tv/ad/ITvAdSession.aidl index 751257ce4d4e..69afb175bb83 100644 --- a/media/java/android/media/tv/ad/ITvAdSession.aidl +++ b/media/java/android/media/tv/ad/ITvAdSession.aidl @@ -16,6 +16,10 @@ package android.media.tv.ad; +import android.graphics.Rect; +import android.media.tv.TvTrackInfo; +import android.net.Uri; +import android.os.Bundle; import android.view.Surface; /** @@ -25,6 +29,21 @@ import android.view.Surface; oneway interface ITvAdSession { void release(); void startAdService(); + void stopAdService(); + void resetAdService(); void setSurface(in Surface surface); void dispatchSurfaceChanged(int format, int width, int height); + + void sendCurrentVideoBounds(in Rect bounds); + void sendCurrentChannelUri(in Uri channelUri); + void sendTrackInfoList(in List<TvTrackInfo> tracks); + void sendCurrentTvInputId(in String inputId); + void sendSigningResult(in String signingId, in byte[] result); + + void notifyError(in String errMsg, in Bundle params); + void notifyTvMessage(int type, in Bundle data); + + void createMediaView(in IBinder windowToken, in Rect frame); + void relayoutMediaView(in Rect frame); + void removeMediaView(); } diff --git a/media/java/android/media/tv/ad/ITvAdSessionWrapper.java b/media/java/android/media/tv/ad/ITvAdSessionWrapper.java index 4df2783f6511..251351de609e 100644 --- a/media/java/android/media/tv/ad/ITvAdSessionWrapper.java +++ b/media/java/android/media/tv/ad/ITvAdSessionWrapper.java @@ -16,7 +16,14 @@ package android.media.tv.ad; +import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.Context; +import android.graphics.Rect; +import android.media.tv.TvTrackInfo; +import android.net.Uri; +import android.os.Bundle; +import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.RemoteException; @@ -29,6 +36,8 @@ import android.view.Surface; import com.android.internal.os.HandlerCaller; import com.android.internal.os.SomeArgs; +import java.util.List; + /** * Implements the internal ITvAdSession interface. * @hide @@ -43,6 +52,19 @@ public class ITvAdSessionWrapper private static final int DO_RELEASE = 1; private static final int DO_SET_SURFACE = 2; private static final int DO_DISPATCH_SURFACE_CHANGED = 3; + private static final int DO_CREATE_MEDIA_VIEW = 4; + private static final int DO_RELAYOUT_MEDIA_VIEW = 5; + private static final int DO_REMOVE_MEDIA_VIEW = 6; + private static final int DO_START_AD_SERVICE = 7; + private static final int DO_STOP_AD_SERVICE = 8; + private static final int DO_RESET_AD_SERVICE = 9; + private static final int DO_SEND_CURRENT_VIDEO_BOUNDS = 10; + private static final int DO_SEND_CURRENT_CHANNEL_URI = 11; + private static final int DO_SEND_TRACK_INFO_LIST = 12; + private static final int DO_SEND_CURRENT_TV_INPUT_ID = 13; + private static final int DO_SEND_SIGNING_RESULT = 14; + private static final int DO_NOTIFY_ERROR = 15; + private static final int DO_NOTIFY_TV_MESSAGE = 16; private final HandlerCaller mCaller; private TvAdService.Session mSessionImpl; @@ -61,6 +83,7 @@ public class ITvAdSessionWrapper @Override public void release() { + mSessionImpl.scheduleMediaViewCleanup(); mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_RELEASE)); } @@ -97,6 +120,66 @@ public class ITvAdSessionWrapper args.recycle(); break; } + case DO_CREATE_MEDIA_VIEW: { + SomeArgs args = (SomeArgs) msg.obj; + mSessionImpl.createMediaView((IBinder) args.arg1, (Rect) args.arg2); + args.recycle(); + break; + } + case DO_RELAYOUT_MEDIA_VIEW: { + mSessionImpl.relayoutMediaView((Rect) msg.obj); + break; + } + case DO_REMOVE_MEDIA_VIEW: { + mSessionImpl.removeMediaView(true); + break; + } + case DO_START_AD_SERVICE: { + mSessionImpl.startAdService(); + break; + } + case DO_STOP_AD_SERVICE: { + mSessionImpl.stopAdService(); + break; + } + case DO_RESET_AD_SERVICE: { + mSessionImpl.resetAdService(); + break; + } + case DO_SEND_CURRENT_VIDEO_BOUNDS: { + mSessionImpl.sendCurrentVideoBounds((Rect) msg.obj); + break; + } + case DO_SEND_CURRENT_CHANNEL_URI: { + mSessionImpl.sendCurrentChannelUri((Uri) msg.obj); + break; + } + case DO_SEND_TRACK_INFO_LIST: { + mSessionImpl.sendTrackInfoList((List<TvTrackInfo>) msg.obj); + break; + } + case DO_SEND_CURRENT_TV_INPUT_ID: { + mSessionImpl.sendCurrentTvInputId((String) msg.obj); + break; + } + case DO_SEND_SIGNING_RESULT: { + SomeArgs args = (SomeArgs) msg.obj; + mSessionImpl.sendSigningResult((String) args.arg1, (byte[]) args.arg2); + args.recycle(); + break; + } + case DO_NOTIFY_ERROR: { + SomeArgs args = (SomeArgs) msg.obj; + mSessionImpl.notifyError((String) args.arg1, (Bundle) args.arg2); + args.recycle(); + break; + } + case DO_NOTIFY_TV_MESSAGE: { + SomeArgs args = (SomeArgs) msg.obj; + mSessionImpl.notifyTvMessage((Integer) args.arg1, (Bundle) args.arg2); + args.recycle(); + break; + } default: { Log.w(TAG, "Unhandled message code: " + msg.what); break; @@ -115,7 +198,17 @@ public class ITvAdSessionWrapper @Override public void startAdService() throws RemoteException { + mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_START_AD_SERVICE)); + } + + @Override + public void stopAdService() { + mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_STOP_AD_SERVICE)); + } + @Override + public void resetAdService() { + mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_RESET_AD_SERVICE)); } @Override @@ -129,6 +222,64 @@ public class ITvAdSessionWrapper mCaller.obtainMessageIIII(DO_DISPATCH_SURFACE_CHANGED, format, width, height, 0)); } + @Override + public void sendCurrentVideoBounds(@Nullable Rect bounds) { + mCaller.executeOrSendMessage( + mCaller.obtainMessageO(DO_SEND_CURRENT_VIDEO_BOUNDS, bounds)); + } + + @Override + public void sendCurrentChannelUri(@Nullable Uri channelUri) { + mCaller.executeOrSendMessage( + mCaller.obtainMessageO(DO_SEND_CURRENT_CHANNEL_URI, channelUri)); + } + + @Override + public void sendTrackInfoList(@NonNull List<TvTrackInfo> tracks) { + mCaller.executeOrSendMessage( + mCaller.obtainMessageO(DO_SEND_TRACK_INFO_LIST, tracks)); + } + + @Override + public void sendCurrentTvInputId(@Nullable String inputId) { + mCaller.executeOrSendMessage( + mCaller.obtainMessageO(DO_SEND_CURRENT_TV_INPUT_ID, inputId)); + } + + @Override + public void sendSigningResult(@NonNull String signingId, @NonNull byte[] result) { + mCaller.executeOrSendMessage( + mCaller.obtainMessageOO(DO_SEND_SIGNING_RESULT, signingId, result)); + } + + @Override + public void notifyError(@NonNull String errMsg, @NonNull Bundle params) { + mCaller.executeOrSendMessage( + mCaller.obtainMessageOO(DO_NOTIFY_ERROR, errMsg, params)); + } + + @Override + public void notifyTvMessage(int type, Bundle data) { + mCaller.executeOrSendMessage( + mCaller.obtainMessageOO(DO_NOTIFY_TV_MESSAGE, type, data)); + } + + @Override + public void createMediaView(IBinder windowToken, Rect frame) { + mCaller.executeOrSendMessage( + mCaller.obtainMessageOO(DO_CREATE_MEDIA_VIEW, windowToken, frame)); + } + + @Override + public void relayoutMediaView(Rect frame) { + mCaller.executeOrSendMessage(mCaller.obtainMessageO(DO_RELAYOUT_MEDIA_VIEW, frame)); + } + + @Override + public void removeMediaView() { + mCaller.executeOrSendMessage(mCaller.obtainMessage(DO_REMOVE_MEDIA_VIEW)); + } + private final class TvAdEventReceiver extends InputEventReceiver { TvAdEventReceiver(InputChannel inputChannel, Looper looper) { super(inputChannel, looper); diff --git a/media/java/android/media/tv/ad/TvAdManager.java b/media/java/android/media/tv/ad/TvAdManager.java index 9c7505197dec..4dce72f62785 100644 --- a/media/java/android/media/tv/ad/TvAdManager.java +++ b/media/java/android/media/tv/ad/TvAdManager.java @@ -21,8 +21,12 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemService; import android.content.Context; +import android.graphics.Rect; import android.media.tv.TvInputManager; +import android.media.tv.TvTrackInfo; import android.media.tv.flags.Flags; +import android.net.Uri; +import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Looper; @@ -35,6 +39,7 @@ import android.view.InputChannel; import android.view.InputEvent; import android.view.InputEventSender; import android.view.Surface; +import android.view.View; import com.android.internal.util.Preconditions; @@ -250,6 +255,14 @@ public class TvAdManager { mSessionCallbackRecordMap = sessionCallbackRecordMap; } + public TvInputManager.Session getInputSession() { + return mInputSession; + } + + public void setInputSession(TvInputManager.Session inputSession) { + mInputSession = inputSession; + } + /** * Releases this session. */ @@ -286,6 +299,67 @@ public class TvAdManager { } /** + * Creates a media view. Once the media view is created, {@link #relayoutMediaView} + * should be called whenever the layout of its containing view is changed. + * {@link #removeMediaView()} should be called to remove the media view. + * Since a session can have only one media view, this method should be called only once + * or it can be called again after calling {@link #removeMediaView()}. + * + * @param view A view for AD service. + * @param frame A position of the media view. + * @throws IllegalStateException if {@code view} is not attached to a window. + */ + void createMediaView(@NonNull View view, @NonNull Rect frame) { + Preconditions.checkNotNull(view); + Preconditions.checkNotNull(frame); + if (view.getWindowToken() == null) { + throw new IllegalStateException("view must be attached to a window"); + } + if (mToken == null) { + Log.w(TAG, "The session has been already released"); + return; + } + try { + mService.createMediaView(mToken, view.getWindowToken(), frame, mUserId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Relayouts the current media view. + * + * @param frame A new position of the media view. + */ + void relayoutMediaView(@NonNull Rect frame) { + Preconditions.checkNotNull(frame); + if (mToken == null) { + Log.w(TAG, "The session has been already released"); + return; + } + try { + mService.relayoutMediaView(mToken, frame, mUserId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Removes the current media view. + */ + void removeMediaView() { + if (mToken == null) { + Log.w(TAG, "The session has been already released"); + return; + } + try { + mService.removeMediaView(mToken, mUserId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * Notifies of any structural changes (format or size) of the surface passed in * {@link #setSurface}. * @@ -348,6 +422,117 @@ public class TvAdManager { } } + void stopAdService() { + if (mToken == null) { + Log.w(TAG, "The session has been already released"); + return; + } + try { + mService.stopAdService(mToken, mUserId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + void resetAdService() { + if (mToken == null) { + Log.w(TAG, "The session has been already released"); + return; + } + try { + mService.resetAdService(mToken, mUserId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + void sendCurrentVideoBounds(@NonNull Rect bounds) { + if (mToken == null) { + Log.w(TAG, "The session has been already released"); + return; + } + try { + mService.sendCurrentVideoBounds(mToken, bounds, mUserId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + void sendCurrentChannelUri(@Nullable Uri channelUri) { + if (mToken == null) { + Log.w(TAG, "The session has been already released"); + return; + } + try { + mService.sendCurrentChannelUri(mToken, channelUri, mUserId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + void sendTrackInfoList(@NonNull List<TvTrackInfo> tracks) { + if (mToken == null) { + Log.w(TAG, "The session has been already released"); + return; + } + try { + mService.sendTrackInfoList(mToken, tracks, mUserId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + void sendCurrentTvInputId(@Nullable String inputId) { + if (mToken == null) { + Log.w(TAG, "The session has been already released"); + return; + } + try { + mService.sendCurrentTvInputId(mToken, inputId, mUserId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + void sendSigningResult(@NonNull String signingId, @NonNull byte[] result) { + if (mToken == null) { + Log.w(TAG, "The session has been already released"); + return; + } + try { + mService.sendSigningResult(mToken, signingId, result, mUserId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + void notifyError(@NonNull String errMsg, @NonNull Bundle params) { + if (mToken == null) { + Log.w(TAG, "The session has been already released"); + return; + } + try { + mService.notifyError(mToken, errMsg, params, mUserId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Notifies AD service session when a new TV message is received. + */ + public void notifyTvMessage(int type, Bundle data) { + if (mToken == null) { + Log.w(TAG, "The session has been already released"); + return; + } + try { + mService.notifyTvMessage(mToken, type, data, mUserId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + private final class InputEventHandler extends Handler { public static final int MSG_SEND_INPUT_EVENT = 1; public static final int MSG_TIMEOUT_INPUT_EVENT = 2; diff --git a/media/java/android/media/tv/ad/TvAdService.java b/media/java/android/media/tv/ad/TvAdService.java index 699570397e34..5d818375ad53 100644 --- a/media/java/android/media/tv/ad/TvAdService.java +++ b/media/java/android/media/tv/ad/TvAdService.java @@ -20,19 +20,28 @@ import android.annotation.CallSuper; import android.annotation.MainThread; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.Px; import android.annotation.SdkConstant; import android.annotation.SuppressLint; +import android.app.ActivityManager; import android.app.Service; import android.content.Context; import android.content.Intent; import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.media.tv.TvInputManager; +import android.media.tv.TvTrackInfo; +import android.net.Uri; +import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Message; +import android.os.Process; import android.os.RemoteCallbackList; import android.os.RemoteException; import android.util.Log; +import android.view.Gravity; import android.view.InputChannel; import android.view.InputDevice; import android.view.InputEvent; @@ -42,6 +51,7 @@ import android.view.MotionEvent; import android.view.Surface; import android.view.View; import android.view.WindowManager; +import android.widget.FrameLayout; import com.android.internal.os.SomeArgs; @@ -56,6 +66,8 @@ public abstract class TvAdService extends Service { private static final boolean DEBUG = false; private static final String TAG = "TvAdService"; + private static final int DETACH_MEDIA_VIEW_TIMEOUT_MS = 5000; + /** * Name under which a TvAdService component publishes information about itself. This meta-data * must reference an XML resource containing an @@ -151,7 +163,14 @@ public abstract class TvAdService extends Service { private final Context mContext; final Handler mHandler; private final WindowManager mWindowManager; + private WindowManager.LayoutParams mWindowParams; private Surface mSurface; + private FrameLayout mMediaViewContainer; + private View mMediaView; + private MediaViewCleanUpTask mMediaViewCleanUpTask; + private boolean mMediaViewEnabled; + private IBinder mWindowToken; + private Rect mMediaFrame; /** @@ -166,6 +185,48 @@ public abstract class TvAdService extends Service { } /** + * Enables or disables the media view. + * + * <p>By default, the media view is disabled. Must be called explicitly after the + * session is created to enable the media view. + * + * <p>The TV AD service can disable its media view when needed. + * + * @param enable {@code true} if you want to enable the media view. {@code false} + * otherwise. + * @hide + */ + @CallSuper + public void setMediaViewEnabled(final boolean enable) { + mHandler.post(new Runnable() { + @Override + public void run() { + if (enable == mMediaViewEnabled) { + return; + } + mMediaViewEnabled = enable; + if (enable) { + if (mWindowToken != null) { + createMediaView(mWindowToken, mMediaFrame); + } + } else { + removeMediaView(false); + } + } + }); + } + + /** + * Returns {@code true} if media view is enabled, {@code false} otherwise. + * + * @see #setMediaViewEnabled(boolean) + * @hide + */ + public boolean isMediaViewEnabled() { + return mMediaViewEnabled; + } + + /** * Releases TvAdService session. */ public abstract void onRelease(); @@ -180,18 +241,44 @@ public abstract class TvAdService extends Service { mSessionCallback = null; mPendingActions.clear(); } + // Removes the media view lastly so that any hanging on the main thread can be handled + // in {@link #scheduleMediaViewCleanup}. + removeMediaView(true); } /** * Starts TvAdService session. + * @hide */ public void onStartAdService() { } + /** + * Stops TvAdService session. + * @hide + */ + public void onStopAdService() { + } + + /** + * Resets TvAdService session. + * @hide + */ + public void onResetAdService() { + } + void startAdService() { onStartAdService(); } + void stopAdService() { + onStopAdService(); + } + + void resetAdService() { + onResetAdService(); + } + @Override public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) { return false; @@ -307,6 +394,109 @@ public abstract class TvAdService extends Service { } /** + * Receives current video bounds. + * + * @param bounds the rectangle area for rendering the current video. + * @hide + */ + public void onCurrentVideoBounds(@NonNull Rect bounds) { + } + + /** + * Receives current channel URI. + * @hide + */ + public void onCurrentChannelUri(@Nullable Uri channelUri) { + } + + /** + * Receives track list. + * @hide + */ + public void onTrackInfoList(@NonNull List<TvTrackInfo> tracks) { + } + + /** + * Receives current TV input ID. + * @hide + */ + public void onCurrentTvInputId(@Nullable String inputId) { + } + + /** + * Receives signing result. + * + * @param signingId the ID to identify the request. It's the same as the corresponding ID in + * {@link Session#requestSigning(String, String, String, byte[])} + * @param result the signed result. + * + * @see #requestSigning(String, String, String, byte[]) + * @hide + */ + public void onSigningResult(@NonNull String signingId, @NonNull byte[] result) { + } + + /** + * Called when the application sends information of an error. + * + * @param errMsg the message of the error. + * @param params additional parameters of the error. For example, the signingId of {@link + * TvAdView.TvAdCallback#onRequestSigning(String, String, String, String, byte[])} + * can be included to identify the related signing request, and the method name + * "onRequestSigning" can also be added to the params. + * + * @see TvAdView#ERROR_KEY_METHOD_NAME + * @hide + */ + public void onError(@NonNull String errMsg, @NonNull Bundle params) { + } + + /** + * Called when a TV message is received + * + * @param type The type of message received, such as + * {@link TvInputManager#TV_MESSAGE_TYPE_WATERMARK} + * @param data The raw data of the message. The bundle keys are: + * {@link TvInputManager#TV_MESSAGE_KEY_STREAM_ID}, + * {@link TvInputManager#TV_MESSAGE_KEY_GROUP_ID}, + * {@link TvInputManager#TV_MESSAGE_KEY_SUBTYPE}, + * {@link TvInputManager#TV_MESSAGE_KEY_RAW_DATA}. + * See {@link TvInputManager#TV_MESSAGE_KEY_SUBTYPE} for more information on + * how to parse this data. + * @hide + */ + public void onTvMessage(@TvInputManager.TvMessageType int type, + @NonNull Bundle data) { + } + + /** + * Called when the size of the media view is changed by the application. + * + * <p>This is always called at least once when the session is created regardless of whether + * the media view is enabled or not. The media view container size is the same as the + * containing {@link TvAdView}. Note that the size of the underlying surface can + * be different if the surface was changed by calling {@link #layoutSurface}. + * + * @param width The width of the media view, in pixels. + * @param height The height of the media view, in pixels. + * @hide + */ + public void onMediaViewSizeChanged(@Px int width, @Px int height) { + } + + /** + * Called when the application requests to create a media view. Each session + * implementation can override this method and return its own view. + * + * @return a view attached to the media window. {@code null} if no media view is created. + * @hide + */ + @Nullable + public View onCreateMediaView() { + return null; + } + + /** * Takes care of dispatching incoming input events and tells whether the event was handled. */ int dispatchInputEvent(InputEvent event, InputEventReceiver receiver) { @@ -373,6 +563,37 @@ public abstract class TvAdService extends Service { onSurfaceChanged(format, width, height); } + void sendCurrentVideoBounds(@NonNull Rect bounds) { + onCurrentVideoBounds(bounds); + } + + void sendCurrentChannelUri(@Nullable Uri channelUri) { + onCurrentChannelUri(channelUri); + } + + void sendTrackInfoList(@NonNull List<TvTrackInfo> tracks) { + onTrackInfoList(tracks); + } + + void sendCurrentTvInputId(@Nullable String inputId) { + onCurrentTvInputId(inputId); + } + + void sendSigningResult(String signingId, byte[] result) { + onSigningResult(signingId, result); + } + + void notifyError(String errMsg, Bundle params) { + onError(errMsg, params); + } + + void notifyTvMessage(int type, Bundle data) { + if (DEBUG) { + Log.d(TAG, "notifyTvMessage (type=" + type + ", data= " + data + ")"); + } + onTvMessage(type, data); + } + private void executeOrPostRunnableOnMainThread(Runnable action) { synchronized (mLock) { if (mSessionCallback == null) { @@ -388,6 +609,137 @@ public abstract class TvAdService extends Service { } } } + + /** + * Creates a media view. This calls {@link #onCreateMediaView} to get a view to attach + * to the media window. + * + * @param windowToken A window token of the application. + * @param frame A position of the media view. + */ + void createMediaView(IBinder windowToken, Rect frame) { + if (mMediaViewContainer != null) { + removeMediaView(false); + } + if (DEBUG) Log.d(TAG, "create media view(" + frame + ")"); + mWindowToken = windowToken; + mMediaFrame = frame; + onMediaViewSizeChanged(frame.right - frame.left, frame.bottom - frame.top); + if (!mMediaViewEnabled) { + return; + } + mMediaView = onCreateMediaView(); + if (mMediaView == null) { + return; + } + if (mMediaViewCleanUpTask != null) { + mMediaViewCleanUpTask.cancel(true); + mMediaViewCleanUpTask = null; + } + // Creates a container view to check hanging on the media view detaching. + // Adding/removing the media view to/from the container make the view attach/detach + // logic run on the main thread. + mMediaViewContainer = new FrameLayout(mContext.getApplicationContext()); + mMediaViewContainer.addView(mMediaView); + + int type = WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA; + // We make the overlay view non-focusable and non-touchable so that + // the application that owns the window token can decide whether to consume or + // dispatch the input events. + int flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE + | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS; + if (ActivityManager.isHighEndGfx()) { + flags |= WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED; + } + mWindowParams = new WindowManager.LayoutParams( + frame.right - frame.left, frame.bottom - frame.top, + frame.left, frame.top, type, flags, PixelFormat.TRANSPARENT); + mWindowParams.privateFlags |= + WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION; + mWindowParams.gravity = Gravity.START | Gravity.TOP; + mWindowParams.token = windowToken; + mWindowManager.addView(mMediaViewContainer, mWindowParams); + } + + /** + * Relayouts the current media view. + * + * @param frame A new position of the media view. + */ + void relayoutMediaView(Rect frame) { + if (DEBUG) Log.d(TAG, "relayoutMediaView(" + frame + ")"); + if (mMediaFrame == null || mMediaFrame.width() != frame.width() + || mMediaFrame.height() != frame.height()) { + // Note: relayoutMediaView is called whenever TvAdView's layout is + // changed regardless of setMediaViewEnabled. + onMediaViewSizeChanged(frame.right - frame.left, frame.bottom - frame.top); + } + mMediaFrame = frame; + if (!mMediaViewEnabled || mMediaViewContainer == null) { + return; + } + mWindowParams.x = frame.left; + mWindowParams.y = frame.top; + mWindowParams.width = frame.right - frame.left; + mWindowParams.height = frame.bottom - frame.top; + mWindowManager.updateViewLayout(mMediaViewContainer, mWindowParams); + } + + /** + * Removes the current media view. + */ + void removeMediaView(boolean clearWindowToken) { + if (DEBUG) Log.d(TAG, "removeMediaView(" + mMediaViewContainer + ")"); + if (clearWindowToken) { + mWindowToken = null; + mMediaFrame = null; + } + if (mMediaViewContainer != null) { + // Removes the media view from the view hierarchy in advance so that it can be + // cleaned up in the {@link MediaViewCleanUpTask} if the remove process is + // hanging. + mMediaViewContainer.removeView(mMediaView); + mMediaView = null; + mWindowManager.removeView(mMediaViewContainer); + mMediaViewContainer = null; + mWindowParams = null; + } + } + + /** + * Schedules a task which checks whether the media view is detached and kills the process + * if it is not. Note that this method is expected to be called in a non-main thread. + */ + void scheduleMediaViewCleanup() { + View mediaViewParent = mMediaViewContainer; + if (mediaViewParent != null) { + mMediaViewCleanUpTask = new MediaViewCleanUpTask(); + mMediaViewCleanUpTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, + mediaViewParent); + } + } + } + + private static final class MediaViewCleanUpTask extends AsyncTask<View, Void, Void> { + @Override + protected Void doInBackground(View... views) { + View mediaViewParent = views[0]; + try { + Thread.sleep(DETACH_MEDIA_VIEW_TIMEOUT_MS); + } catch (InterruptedException e) { + return null; + } + if (isCancelled()) { + return null; + } + if (mediaViewParent.isAttachedToWindow()) { + Log.e(TAG, "Time out on releasing media view. Killing " + + mediaViewParent.getContext().getPackageName()); + android.os.Process.killProcess(Process.myPid()); + } + return null; + } } diff --git a/media/java/android/media/tv/ad/TvAdView.java b/media/java/android/media/tv/ad/TvAdView.java index 5e67fe9f697b..ec23b7c4532d 100644 --- a/media/java/android/media/tv/ad/TvAdView.java +++ b/media/java/android/media/tv/ad/TvAdView.java @@ -16,12 +16,20 @@ package android.media.tv.ad; +import android.annotation.CallbackExecutor; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.content.res.Resources; import android.content.res.XmlResourceParser; import android.graphics.PixelFormat; +import android.graphics.Rect; +import android.graphics.RectF; +import android.media.tv.TvInputManager; +import android.media.tv.TvTrackInfo; +import android.media.tv.TvView; +import android.net.Uri; +import android.os.Bundle; import android.os.Handler; import android.util.AttributeSet; import android.util.Log; @@ -32,6 +40,9 @@ import android.view.SurfaceView; import android.view.View; import android.view.ViewGroup; +import java.util.List; +import java.util.concurrent.Executor; + /** * Displays contents of TV AD services. * @hide @@ -40,11 +51,22 @@ public class TvAdView extends ViewGroup { private static final String TAG = "TvAdView"; private static final boolean DEBUG = false; + /** + * The name of the method where the error happened, if applicable. For example, if there is an + * error during signing, the request name is "onRequestSigning". + * @see #notifyError(String, Bundle) + * @hide + */ + public static final String ERROR_KEY_METHOD_NAME = "method_name"; + private final TvAdManager mTvAdManager; private final Handler mHandler = new Handler(); + private final Object mCallbackLock = new Object(); private TvAdManager.Session mSession; private MySessionCallback mSessionCallback; + private TvAdCallback mCallback; + private Executor mCallbackExecutor; private final AttributeSet mAttrs; private final int mDefStyleAttr; @@ -64,6 +86,9 @@ public class TvAdView extends ViewGroup { private int mSurfaceViewTop; private int mSurfaceViewBottom; + private boolean mMediaViewCreated; + private Rect mMediaViewFrame; + private final SurfaceHolder.Callback mSurfaceHolderCallback = new SurfaceHolder.Callback() { @@ -121,6 +146,51 @@ public class TvAdView extends ViewGroup { mTvAdManager = (TvAdManager) getContext().getSystemService(Context.TV_AD_SERVICE); } + /** + * Sets the TvAdView to receive events from TvInputService. This method links the session of + * TvAdManager to TvInputManager session, so the TvAdService can get the TvInputService events. + * + * @param tvView the TvView to be linked to this TvAdView via linking of Sessions. {@code null} + * to unlink the TvView. + * @return {@code true} if it's linked successfully; {@code false} otherwise. + * @hide + */ + public boolean setTvView(@Nullable TvView tvView) { + if (tvView == null) { + return unsetTvView(); + } + TvInputManager.Session inputSession = tvView.getInputSession(); + if (inputSession == null || mSession == null) { + return false; + } + mSession.setInputSession(inputSession); + inputSession.setAdSession(mSession); + return true; + } + + private boolean unsetTvView() { + if (mSession == null || mSession.getInputSession() == null) { + return false; + } + mSession.getInputSession().setAdSession(null); + mSession.setInputSession(null); + return true; + } + + /** @hide */ + @Override + public void onAttachedToWindow() { + super.onAttachedToWindow(); + createSessionMediaView(); + } + + /** @hide */ + @Override + public void onDetachedFromWindow() { + removeSessionMediaView(); + super.onDetachedFromWindow(); + } + @Override public void onLayout(boolean changed, int left, int top, int right, int bottom) { if (DEBUG) { @@ -150,6 +220,11 @@ public class TvAdView extends ViewGroup { public void onVisibilityChanged(@NonNull View changedView, int visibility) { super.onVisibilityChanged(changedView, visibility); mSurfaceView.setVisibility(visibility); + if (visibility == View.VISIBLE) { + createSessionMediaView(); + } else { + removeSessionMediaView(); + } } private void resetSurfaceView() { @@ -162,6 +237,7 @@ public class TvAdView extends ViewGroup { @Override protected void updateSurface() { super.updateSurface(); + relayoutSessionMediaView(); }}; // The surface view's content should be treated as secure all the time. mSurfaceView.setSecure(true); @@ -174,6 +250,69 @@ public class TvAdView extends ViewGroup { addView(mSurfaceView); } + /** + * Resets this TvAdView to release its resources. + * + * <p>It can be reused by call {@link #prepareAdService(String, String)}. + * @hide + */ + public void reset() { + if (DEBUG) Log.d(TAG, "reset()"); + resetInternal(); + } + + private void resetInternal() { + mSessionCallback = null; + if (mSession != null) { + setSessionSurface(null); + removeSessionMediaView(); + mUseRequestedSurfaceLayout = false; + mSession.release(); + mSession = null; + resetSurfaceView(); + } + } + + private void createSessionMediaView() { + // TODO: handle z-order + if (mSession == null || !isAttachedToWindow() || mMediaViewCreated) { + return; + } + mMediaViewFrame = getViewFrameOnScreen(); + mSession.createMediaView(this, mMediaViewFrame); + mMediaViewCreated = true; + } + + private void removeSessionMediaView() { + if (mSession == null || !mMediaViewCreated) { + return; + } + mSession.removeMediaView(); + mMediaViewCreated = false; + mMediaViewFrame = null; + } + + private void relayoutSessionMediaView() { + if (mSession == null || !isAttachedToWindow() || !mMediaViewCreated) { + return; + } + Rect viewFrame = getViewFrameOnScreen(); + if (viewFrame.equals(mMediaViewFrame)) { + return; + } + mSession.relayoutMediaView(viewFrame); + mMediaViewFrame = viewFrame; + } + + private Rect getViewFrameOnScreen() { + Rect frame = new Rect(); + getGlobalVisibleRect(frame); + RectF frameF = new RectF(frame); + getMatrix().mapRect(frameF); + frameF.round(frame); + return frame; + } + private void setSessionSurface(Surface surface) { if (mSession == null) { return; @@ -185,7 +324,7 @@ public class TvAdView extends ViewGroup { if (mSession == null) { return; } - //mSession.dispatchSurfaceChanged(format, width, height); + mSession.dispatchSurfaceChanged(format, width, height); } /** @@ -205,16 +344,196 @@ public class TvAdView extends ViewGroup { /** * Starts the AD service. + * @hide */ public void startAdService() { if (DEBUG) { - Log.d(TAG, "start"); + Log.d(TAG, "startAdService"); } if (mSession != null) { mSession.startAdService(); } } + /** + * Stops the AD service. + */ + public void stopAdService() { + if (DEBUG) { + Log.d(TAG, "stopAdService"); + } + if (mSession != null) { + mSession.stopAdService(); + } + } + + /** + * Resets the AD service. + * + * <p>This releases the resources of the corresponding {@link TvAdService.Session}. + */ + public void resetAdService() { + if (DEBUG) { + Log.d(TAG, "resetAdService"); + } + if (mSession != null) { + mSession.resetAdService(); + } + } + + /** + * Sends current video bounds to related TV AD service. + * + * @param bounds the rectangle area for rendering the current video. + */ + public void sendCurrentVideoBounds(@NonNull Rect bounds) { + if (DEBUG) { + Log.d(TAG, "sendCurrentVideoBounds"); + } + if (mSession != null) { + mSession.sendCurrentVideoBounds(bounds); + } + } + + /** + * Sends current channel URI to related TV AD service. + * + * @param channelUri The current channel URI; {@code null} if there is no currently tuned + * channel. + */ + public void sendCurrentChannelUri(@Nullable Uri channelUri) { + if (DEBUG) { + Log.d(TAG, "sendCurrentChannelUri"); + } + if (mSession != null) { + mSession.sendCurrentChannelUri(channelUri); + } + } + + /** + * Sends track info list to related TV AD service. + */ + public void sendTrackInfoList(@Nullable List<TvTrackInfo> tracks) { + if (DEBUG) { + Log.d(TAG, "sendTrackInfoList"); + } + if (mSession != null) { + mSession.sendTrackInfoList(tracks); + } + } + + /** + * Sends current TV input ID to related TV AD service. + * + * @param inputId The current TV input ID whose channel is tuned. {@code null} if no channel is + * tuned. + * @see android.media.tv.TvInputInfo + */ + public void sendCurrentTvInputId(@Nullable String inputId) { + if (DEBUG) { + Log.d(TAG, "sendCurrentTvInputId"); + } + if (mSession != null) { + mSession.sendCurrentTvInputId(inputId); + } + } + + /** + * Sends signing result to related TV AD service. + * + * <p>This is used when the corresponding server of the ADs requires signing during handshaking, + * and the AD service doesn't have the built-in private key. The private key is provided by the + * content providers and pre-built in the related app, such as TV app. + * + * @param signingId the ID to identify the request. It's the same as the corresponding ID in + * {@link TvAdService.Session#requestSigning(String, String, String, byte[])} + * @param result the signed result. + * @hide + */ + public void sendSigningResult(@NonNull String signingId, @NonNull byte[] result) { + if (DEBUG) { + Log.d(TAG, "sendSigningResult"); + } + if (mSession != null) { + mSession.sendSigningResult(signingId, result); + } + } + + /** + * Notifies the corresponding {@link TvAdService} when there is an error. + * + * @param errMsg the message of the error. + * @param params additional parameters of the error. For example, the signingId of {@link + * TvAdView.TvAdCallback#onRequestSigning(String, String, String, String, byte[])} can be + * included to identify the related signing request, and the method name "onRequestSigning" + * can also be added to the params. + * + * @see #ERROR_KEY_METHOD_NAME + */ + public void notifyError(@NonNull String errMsg, @NonNull Bundle params) { + if (DEBUG) { + Log.d(TAG, "notifyError msg=" + errMsg + "; params=" + params); + } + if (mSession != null) { + mSession.notifyError(errMsg, params); + } + } + + /** + * This is called to notify the corresponding TV AD service when a new TV message is received. + * + * @param type The type of message received, such as + * {@link TvInputManager#TV_MESSAGE_TYPE_WATERMARK} + * @param data The raw data of the message. The bundle keys are: + * {@link TvInputManager#TV_MESSAGE_KEY_STREAM_ID}, + * {@link TvInputManager#TV_MESSAGE_KEY_GROUP_ID}, + * {@link TvInputManager#TV_MESSAGE_KEY_SUBTYPE}, + * {@link TvInputManager#TV_MESSAGE_KEY_RAW_DATA}. + * See {@link TvInputManager#TV_MESSAGE_KEY_SUBTYPE} for more information on + * how to parse this data. + */ + public void notifyTvMessage(@NonNull @TvInputManager.TvMessageType int type, + @NonNull Bundle data) { + if (DEBUG) { + Log.d(TAG, "notifyTvMessage type=" + type + + "; data=" + data); + } + if (mSession != null) { + mSession.notifyTvMessage(type, data); + } + } + + /** + * Sets the callback to be invoked when an event is dispatched to this TvAdView. + * + * @param callback the callback to receive events. MUST NOT be {@code null}. + * + * @see #clearCallback() + * @hide + */ + public void setCallback( + @NonNull @CallbackExecutor Executor executor, + @NonNull TvAdCallback callback) { + com.android.internal.util.AnnotationValidations.validate(NonNull.class, null, callback); + synchronized (mCallbackLock) { + mCallbackExecutor = executor; + mCallback = callback; + } + } + + /** + * Clears the callback. + * + * @see #setCallback(Executor, TvAdCallback) + * @hide + */ + public void clearCallback() { + synchronized (mCallbackLock) { + mCallback = null; + mCallbackExecutor = null; + } + } + private class MySessionCallback extends TvAdManager.SessionCallback { final String mServiceId; @@ -246,6 +565,7 @@ public class TvAdView extends ViewGroup { dispatchSurfaceChanged(mSurfaceFormat, mSurfaceWidth, mSurfaceHeight); } } + createSessionMediaView(); } else { // Failed to create // Todo: forward error to Tv App @@ -262,6 +582,8 @@ public class TvAdView extends ViewGroup { Log.w(TAG, "onSessionReleased - session not created"); return; } + mMediaViewCreated = false; + mMediaViewFrame = null; mSessionCallback = null; mSession = null; } @@ -285,4 +607,11 @@ public class TvAdView extends ViewGroup { requestLayout(); } } + + /** + * Callback used to receive various status updates on the {@link TvAdView}. + * @hide + */ + public abstract static class TvAdCallback { + } } diff --git a/media/jni/soundpool/StreamManager.cpp b/media/jni/soundpool/StreamManager.cpp index 52060f1e6209..66fec1c528e7 100644 --- a/media/jni/soundpool/StreamManager.cpp +++ b/media/jni/soundpool/StreamManager.cpp @@ -35,10 +35,9 @@ static constexpr int32_t kMaxStreams = 32; // In R, we change this to true, as it is the correct way per SoundPool documentation. static constexpr bool kStealActiveStream_OldestFirst = true; -// kPlayOnCallingThread = true prior to R. // Changing to false means calls to play() are almost instantaneous instead of taking around // ~10ms to launch the AudioTrack. It is perhaps 100x faster. -static constexpr bool kPlayOnCallingThread = true; +static constexpr bool kPlayOnCallingThread = false; // Amount of time for a StreamManager thread to wait before closing. static constexpr int64_t kWaitTimeBeforeCloseNs = 9 * NANOS_PER_SECOND; diff --git a/media/jni/soundpool/StreamManager.h b/media/jni/soundpool/StreamManager.h index adbab4b0f9d9..340b49bc6d6c 100644 --- a/media/jni/soundpool/StreamManager.h +++ b/media/jni/soundpool/StreamManager.h @@ -48,7 +48,7 @@ class JavaThread { public: JavaThread(std::function<void()> f, const char *name) : mF{std::move(f)} { - createThreadEtc(staticFunction, this, name); + createThreadEtc(staticFunction, this, name, ANDROID_PRIORITY_AUDIO); } JavaThread(JavaThread &&) = delete; // uses "this" ptr, not moveable. diff --git a/media/jni/soundpool/android_media_SoundPool.cpp b/media/jni/soundpool/android_media_SoundPool.cpp index 25040a942061..e872a58c96cf 100644 --- a/media/jni/soundpool/android_media_SoundPool.cpp +++ b/media/jni/soundpool/android_media_SoundPool.cpp @@ -86,7 +86,7 @@ public: } // Retrieves the associated object, returns nullValue T if not available. - T get(JNIEnv *env, jobject thiz) { + T get(JNIEnv *env, jobject thiz) const { std::lock_guard lg(mLock); // NOLINTNEXTLINE(performance-no-int-to-ptr) auto ptr = reinterpret_cast<T*>(env->GetLongField(thiz, mFieldId)); @@ -167,8 +167,10 @@ private: // is possible by checking if the WeakGlobalRef is null equivalent. auto& getSoundPoolManager() { - static ObjectManager<std::shared_ptr<SoundPool>> soundPoolManager(fields.mNativeContext); - return soundPoolManager; + // never-delete singleton + static auto soundPoolManager = + new ObjectManager<std::shared_ptr<SoundPool>>(fields.mNativeContext); + return *soundPoolManager; } inline auto getSoundPool(JNIEnv *env, jobject thiz) { @@ -274,8 +276,9 @@ static_assert(std::is_same_v<JWeakValue*, jweak>); auto& getSoundPoolJavaRefManager() { // Note this can store shared_ptrs to either jweak and jobject, // as the underlying type is identical. - static ConcurrentHashMap<SoundPool *, std::shared_ptr<JWeakValue>> concurrentHashMap; - return concurrentHashMap; + static auto concurrentHashMap = + new ConcurrentHashMap<SoundPool *, std::shared_ptr<JWeakValue>>(); + return *concurrentHashMap; } // make_shared_globalref_from_localref() creates a sharable Java global diff --git a/media/tests/LoudnessCodecApiTest/src/com/android/loudnesscodecapitest/LoudnessCodecControllerTest.java b/media/tests/LoudnessCodecApiTest/src/com/android/loudnesscodecapitest/LoudnessCodecControllerTest.java index 4f6ede508f7a..46256ba7cccb 100644 --- a/media/tests/LoudnessCodecApiTest/src/com/android/loudnesscodecapitest/LoudnessCodecControllerTest.java +++ b/media/tests/LoudnessCodecApiTest/src/com/android/loudnesscodecapitest/LoudnessCodecControllerTest.java @@ -126,7 +126,7 @@ public class LoudnessCodecControllerTest { try { mLcc.addMediaCodec(mediaCodec); - mLcc.release(); // stops updats + mLcc.close(); // stops updates verify(mAudioService).stopLoudnessCodecUpdates(eq(mSessionId)); } finally { diff --git a/native/android/performance_hint.cpp b/native/android/performance_hint.cpp index abe4a3d18b38..c5729444507e 100644 --- a/native/android/performance_hint.cpp +++ b/native/android/performance_hint.cpp @@ -228,11 +228,6 @@ int APerformanceHintSession::updateTargetWorkDuration(int64_t targetDurationNano } int APerformanceHintSession::reportActualWorkDuration(int64_t actualDurationNanos) { - if (actualDurationNanos <= 0) { - ALOGE("%s: actualDurationNanos must be positive", __FUNCTION__); - return EINVAL; - } - WorkDuration workDuration(0, actualDurationNanos, actualDurationNanos, 0); return reportActualWorkDurationInternal(&workDuration); @@ -320,23 +315,6 @@ int APerformanceHintSession::setPreferPowerEfficiency(bool enabled) { int APerformanceHintSession::reportActualWorkDuration(AWorkDuration* aWorkDuration) { WorkDuration* workDuration = static_cast<WorkDuration*>(aWorkDuration); - if (workDuration->workPeriodStartTimestampNanos <= 0) { - ALOGE("%s: workPeriodStartTimestampNanos must be positive", __FUNCTION__); - return EINVAL; - } - if (workDuration->actualTotalDurationNanos <= 0) { - ALOGE("%s: actualDurationNanos must be positive", __FUNCTION__); - return EINVAL; - } - if (workDuration->actualCpuDurationNanos <= 0) { - ALOGE("%s: cpuDurationNanos must be positive", __FUNCTION__); - return EINVAL; - } - if (workDuration->actualGpuDurationNanos < 0) { - ALOGE("%s: gpuDurationNanos must be non negative", __FUNCTION__); - return EINVAL; - } - return reportActualWorkDurationInternal(workDuration); } @@ -428,62 +406,87 @@ APerformanceHintManager* APerformanceHint_getManager() { return APerformanceHintManager::getInstance(); } +#define VALIDATE_PTR(ptr) \ + LOG_ALWAYS_FATAL_IF(ptr == nullptr, "%s: " #ptr " is nullptr", __FUNCTION__); + +#define VALIDATE_INT(value, cmp) \ + if (!(value cmp)) { \ + ALOGE("%s: Invalid value. Check failed: (" #value " " #cmp ") with value: %" PRIi64, \ + __FUNCTION__, value); \ + return EINVAL; \ + } + +#define WARN_INT(value, cmp) \ + if (!(value cmp)) { \ + ALOGE("%s: Invalid value. Check failed: (" #value " " #cmp ") with value: %" PRIi64, \ + __FUNCTION__, value); \ + } + APerformanceHintSession* APerformanceHint_createSession(APerformanceHintManager* manager, const int32_t* threadIds, size_t size, int64_t initialTargetWorkDurationNanos) { + VALIDATE_PTR(manager) + VALIDATE_PTR(threadIds) return manager->createSession(threadIds, size, initialTargetWorkDurationNanos); } int64_t APerformanceHint_getPreferredUpdateRateNanos(APerformanceHintManager* manager) { + VALIDATE_PTR(manager) return manager->getPreferredRateNanos(); } int APerformanceHint_updateTargetWorkDuration(APerformanceHintSession* session, int64_t targetDurationNanos) { + VALIDATE_PTR(session) return session->updateTargetWorkDuration(targetDurationNanos); } int APerformanceHint_reportActualWorkDuration(APerformanceHintSession* session, int64_t actualDurationNanos) { + VALIDATE_PTR(session) + VALIDATE_INT(actualDurationNanos, > 0) return session->reportActualWorkDuration(actualDurationNanos); } void APerformanceHint_closeSession(APerformanceHintSession* session) { + VALIDATE_PTR(session) delete session; } int APerformanceHint_sendHint(void* session, SessionHint hint) { + VALIDATE_PTR(session) return reinterpret_cast<APerformanceHintSession*>(session)->sendHint(hint); } int APerformanceHint_setThreads(APerformanceHintSession* session, const pid_t* threadIds, size_t size) { - if (session == nullptr) { - return EINVAL; - } + VALIDATE_PTR(session) + VALIDATE_PTR(threadIds) return session->setThreads(threadIds, size); } int APerformanceHint_getThreadIds(void* aPerformanceHintSession, int32_t* const threadIds, size_t* const size) { - if (aPerformanceHintSession == nullptr) { - return EINVAL; - } + VALIDATE_PTR(aPerformanceHintSession) return static_cast<APerformanceHintSession*>(aPerformanceHintSession) ->getThreadIds(threadIds, size); } int APerformanceHint_setPreferPowerEfficiency(APerformanceHintSession* session, bool enabled) { + VALIDATE_PTR(session) return session->setPreferPowerEfficiency(enabled); } int APerformanceHint_reportActualWorkDuration2(APerformanceHintSession* session, - AWorkDuration* workDuration) { - if (session == nullptr || workDuration == nullptr) { - ALOGE("Invalid value: (session %p, workDuration %p)", session, workDuration); - return EINVAL; - } - return session->reportActualWorkDuration(workDuration); + AWorkDuration* workDurationPtr) { + VALIDATE_PTR(session) + VALIDATE_PTR(workDurationPtr) + WorkDuration& workDuration = *static_cast<WorkDuration*>(workDurationPtr); + VALIDATE_INT(workDuration.workPeriodStartTimestampNanos, > 0) + VALIDATE_INT(workDuration.actualTotalDurationNanos, > 0) + VALIDATE_INT(workDuration.actualCpuDurationNanos, > 0) + VALIDATE_INT(workDuration.actualGpuDurationNanos, >= 0) + return session->reportActualWorkDuration(workDurationPtr); } AWorkDuration* AWorkDuration_create() { @@ -492,46 +495,36 @@ AWorkDuration* AWorkDuration_create() { } void AWorkDuration_release(AWorkDuration* aWorkDuration) { - if (aWorkDuration == nullptr) { - ALOGE("%s: aWorkDuration is nullptr", __FUNCTION__); - } + VALIDATE_PTR(aWorkDuration) delete aWorkDuration; } void AWorkDuration_setWorkPeriodStartTimestampNanos(AWorkDuration* aWorkDuration, int64_t workPeriodStartTimestampNanos) { - if (aWorkDuration == nullptr || workPeriodStartTimestampNanos <= 0) { - ALOGE("%s: Invalid value. (AWorkDuration: %p, workPeriodStartTimestampNanos: %" PRIi64 ")", - __FUNCTION__, aWorkDuration, workPeriodStartTimestampNanos); - } + VALIDATE_PTR(aWorkDuration) + WARN_INT(workPeriodStartTimestampNanos, > 0) static_cast<WorkDuration*>(aWorkDuration)->workPeriodStartTimestampNanos = workPeriodStartTimestampNanos; } void AWorkDuration_setActualTotalDurationNanos(AWorkDuration* aWorkDuration, int64_t actualTotalDurationNanos) { - if (aWorkDuration == nullptr || actualTotalDurationNanos <= 0) { - ALOGE("%s: Invalid value. (AWorkDuration: %p, actualTotalDurationNanos: %" PRIi64 ")", - __FUNCTION__, aWorkDuration, actualTotalDurationNanos); - } + VALIDATE_PTR(aWorkDuration) + WARN_INT(actualTotalDurationNanos, > 0) static_cast<WorkDuration*>(aWorkDuration)->actualTotalDurationNanos = actualTotalDurationNanos; } void AWorkDuration_setActualCpuDurationNanos(AWorkDuration* aWorkDuration, int64_t actualCpuDurationNanos) { - if (aWorkDuration == nullptr || actualCpuDurationNanos <= 0) { - ALOGE("%s: Invalid value. (AWorkDuration: %p, actualCpuDurationNanos: %" PRIi64 ")", - __FUNCTION__, aWorkDuration, actualCpuDurationNanos); - } + VALIDATE_PTR(aWorkDuration) + WARN_INT(actualCpuDurationNanos, > 0) static_cast<WorkDuration*>(aWorkDuration)->actualCpuDurationNanos = actualCpuDurationNanos; } void AWorkDuration_setActualGpuDurationNanos(AWorkDuration* aWorkDuration, int64_t actualGpuDurationNanos) { - if (aWorkDuration == nullptr || actualGpuDurationNanos < 0) { - ALOGE("%s: Invalid value. (AWorkDuration: %p, actualGpuDurationNanos: %" PRIi64 ")", - __FUNCTION__, aWorkDuration, actualGpuDurationNanos); - } + VALIDATE_PTR(aWorkDuration) + WARN_INT(actualGpuDurationNanos, >= 0) static_cast<WorkDuration*>(aWorkDuration)->actualGpuDurationNanos = actualGpuDurationNanos; } diff --git a/native/graphics/jni/Android.bp b/native/graphics/jni/Android.bp index 10c570b30d7a..8ea46329af58 100644 --- a/native/graphics/jni/Android.bp +++ b/native/graphics/jni/Android.bp @@ -72,6 +72,9 @@ cc_library_shared { ], }, }, + stubs: { + symbol_file: "libjnigraphics.map.txt", + }, } // The headers module is in frameworks/native/Android.bp. @@ -93,15 +96,18 @@ cc_defaults { ], static_libs: ["libarect"], fuzz_config: { - cc: ["dichenzhang@google.com","scroggo@google.com"], + cc: [ + "dichenzhang@google.com", + "scroggo@google.com", + ], asan_options: [ "detect_odr_violation=1", ], hwasan_options: [ - // Image decoders may attempt to allocate a large amount of memory - // (especially if the encoded image is large). This doesn't - // necessarily mean there is a bug. Set allocator_may_return_null=1 - // for hwasan so the fuzzer can continue running. + // Image decoders may attempt to allocate a large amount of memory + // (especially if the encoded image is large). This doesn't + // necessarily mean there is a bug. Set allocator_may_return_null=1 + // for hwasan so the fuzzer can continue running. "allocator_may_return_null = 1", ], }, diff --git a/nfc/api/system-current.txt b/nfc/api/system-current.txt index dc2a625aacfd..3524f8cce04c 100644 --- a/nfc/api/system-current.txt +++ b/nfc/api/system-current.txt @@ -45,7 +45,7 @@ package android.nfc { package android.nfc.cardemulation { public final class CardEmulation { - method @FlaggedApi("android.permission.flags.wallet_role_enabled") @Nullable @RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO) public android.nfc.cardemulation.ApduServiceInfo getPreferredPaymentService(); + method @FlaggedApi("android.permission.flags.wallet_role_enabled") @Nullable @RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO) public static android.content.ComponentName getPreferredPaymentService(@NonNull android.content.Context); method @FlaggedApi("android.nfc.enable_nfc_mainline") @NonNull public java.util.List<android.nfc.cardemulation.ApduServiceInfo> getServices(@NonNull String, int); } diff --git a/nfc/java/android/nfc/cardemulation/CardEmulation.java b/nfc/java/android/nfc/cardemulation/CardEmulation.java index 0943392a68ad..9d38e4c5b297 100644 --- a/nfc/java/android/nfc/cardemulation/CardEmulation.java +++ b/nfc/java/android/nfc/cardemulation/CardEmulation.java @@ -16,6 +16,7 @@ package android.nfc.cardemulation; +import android.Manifest; import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; @@ -23,6 +24,7 @@ import android.annotation.RequiresPermission; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.annotation.SystemApi; +import android.annotation.UserHandleAware; import android.annotation.UserIdInt; import android.app.Activity; import android.content.ComponentName; @@ -1138,31 +1140,28 @@ public final class CardEmulation { } /** - * Returns the {@link Settings.Secure#NFC_PAYMENT_DEFAULT_COMPONENT} for the given user. + * Returns the value of {@link Settings.Secure#NFC_PAYMENT_DEFAULT_COMPONENT}. + * + * @param context A context + * @return A ComponentName for the setting value, or null. * * @hide */ @SystemApi + @UserHandleAware @RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO) + @SuppressWarnings("AndroidFrameworkClientSidePermissionCheck") @FlaggedApi(android.permission.flags.Flags.FLAG_WALLET_ROLE_ENABLED) @Nullable - public ApduServiceInfo getPreferredPaymentService() { - try { - return sService.getPreferredPaymentService(mContext.getUser().getIdentifier()); - } catch (RemoteException e) { - // Try one more time - recoverService(); - if (sService == null) { - Log.e(TAG, "Failed to recover CardEmulationService."); - return null; - } - try { - return sService.getPreferredPaymentService(mContext.getUser().getIdentifier()); - } catch (RemoteException ee) { - Log.e(TAG, "Failed to reach CardEmulationService."); - return null; - } + public static ComponentName getPreferredPaymentService(@NonNull Context context) { + context.checkCallingOrSelfPermission(Manifest.permission.NFC_PREFERRED_PAYMENT_INFO); + String defaultPaymentComponent = Settings.Secure.getString(context.getContentResolver(), + Constants.SETTINGS_SECURE_NFC_PAYMENT_DEFAULT_COMPONENT); + + if (defaultPaymentComponent == null) { + return null; } - } + return ComponentName.unflattenFromString(defaultPaymentComponent); + } } diff --git a/packages/CredentialManager/res/drawable/fill_dialog_dynamic_list_item_one.xml b/packages/CredentialManager/res/drawable/fill_dialog_dynamic_list_item_one.xml index 5becc86927d2..f13402c7206d 100644 --- a/packages/CredentialManager/res/drawable/fill_dialog_dynamic_list_item_one.xml +++ b/packages/CredentialManager/res/drawable/fill_dialog_dynamic_list_item_one.xml @@ -23,7 +23,7 @@ android:shape="rectangle" android:top="1dp"> <shape> - <corners android:radius="16dp" /> + <corners android:radius="4dp" /> <solid android:color="@color/dropdown_container" /> </shape> </item> diff --git a/packages/CredentialManager/res/drawable/more_options_list_item.xml b/packages/CredentialManager/res/drawable/more_options_list_item.xml new file mode 100644 index 000000000000..d7b509ee48fd --- /dev/null +++ b/packages/CredentialManager/res/drawable/more_options_list_item.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ 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. + --> + +<ripple xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" tools:ignore="NewApi" + android:color="@android:color/transparent"> + <item + android:bottom="1dp" + android:shape="rectangle" + android:top="1dp"> + <shape> + <corners android:bottomLeftRadius="4dp" + android:bottomRightRadius="4dp"/> + <solid android:color="@color/sign_in_options_container" /> + </shape> + </item> +</ripple>
\ No newline at end of file diff --git a/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml b/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml new file mode 100644 index 000000000000..929756cdf9cc --- /dev/null +++ b/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml @@ -0,0 +1,42 @@ +<!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@android:id/content" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:layout_marginEnd="@dimen/dropdown_layout_horizontal_margin" + android:elevation="3dp"> + + <ImageView + android:id="@android:id/icon1" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_centerVertical="true" + android:layout_alignParentStart="true" + android:contentDescription="@string/provider_icon_content_description" + android:background="@null"/> + <TextView + android:id="@android:id/text1" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentTop="true" + android:layout_toEndOf="@android:id/icon1" + android:minWidth="@dimen/autofill_dropdown_textview_min_width" + android:maxWidth="@dimen/autofill_dropdown_textview_max_width" + style="@style/autofill.TextTitle"/> + +</RelativeLayout> diff --git a/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml b/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml index cb6c6b473244..1fe5e0ed41f9 100644 --- a/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml +++ b/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml @@ -17,22 +17,25 @@ android:id="@android:id/content" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:maxWidth="@dimen/autofill_dropdown_layout_width" + android:layout_marginEnd="@dimen/dropdown_layout_horizontal_margin" android:elevation="3dp"> <ImageView android:id="@android:id/icon1" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:contentDescription="@string/provider_icon_content_description" android:layout_centerVertical="true" android:layout_alignParentStart="true" android:background="@null"/> <TextView android:id="@android:id/text1" - android:layout_width="@dimen/autofill_dropdown_text_width" + android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentTop="true" android:layout_toEndOf="@android:id/icon1" + android:minWidth="@dimen/autofill_dropdown_textview_min_width" + android:maxWidth="@dimen/autofill_dropdown_textview_max_width" style="@style/autofill.TextTitle"/> <TextView android:id="@android:id/text2" @@ -40,6 +43,8 @@ android:layout_height="wrap_content" android:layout_below="@android:id/text1" android:layout_toEndOf="@android:id/icon1" + android:minWidth="@dimen/autofill_dropdown_textview_min_width" + android:maxWidth="@dimen/autofill_dropdown_textview_max_width" style="@style/autofill.TextSubtitle"/> </RelativeLayout> diff --git a/packages/CredentialManager/res/values/colors.xml b/packages/CredentialManager/res/values/colors.xml index dcb7ef9c3ed8..7cb1d01972b7 100644 --- a/packages/CredentialManager/res/values/colors.xml +++ b/packages/CredentialManager/res/values/colors.xml @@ -20,4 +20,6 @@ <color name="text_primary">#1A1B20</color> <color name="text_secondary">#44474F</color> <color name="dropdown_container">#F3F3FA</color> + <color name="sign_in_options_container">#DADADA</color> + <color name="sign_in_options_icon_color">#1B1B1B</color> </resources>
\ No newline at end of file diff --git a/packages/CredentialManager/res/values/dimens.xml b/packages/CredentialManager/res/values/dimens.xml index 2a4719d027e2..3a8c78f6d854 100644 --- a/packages/CredentialManager/res/values/dimens.xml +++ b/packages/CredentialManager/res/values/dimens.xml @@ -18,11 +18,13 @@ <resources> <dimen name="autofill_view_top_padding">12dp</dimen> - <dimen name="autofill_view_right_padding">24dp</dimen> + <dimen name="autofill_view_right_padding">12dp</dimen> <dimen name="autofill_view_bottom_padding">12dp</dimen> <dimen name="autofill_view_left_padding">16dp</dimen> <dimen name="autofill_view_icon_to_text_padding">10dp</dimen> <dimen name="autofill_icon_size">24dp</dimen> - <dimen name="autofill_dropdown_layout_width">296dp</dimen> - <dimen name="autofill_dropdown_text_width">240dp</dimen> + <dimen name="autofill_dropdown_textview_min_width">112dp</dimen> + <dimen name="autofill_dropdown_textview_max_width">230dp</dimen> + <dimen name="dropdown_layout_horizontal_margin">24dp</dimen> + <integer name="autofill_max_visible_datasets">3</integer> </resources>
\ No newline at end of file diff --git a/packages/CredentialManager/res/values/strings.xml b/packages/CredentialManager/res/values/strings.xml index 605e77bef34e..f98164b8788c 100644 --- a/packages/CredentialManager/res/values/strings.xml +++ b/packages/CredentialManager/res/values/strings.xml @@ -168,4 +168,9 @@ <string name="get_dialog_option_headline_use_a_different_device">Use a different device</string> <!-- Text shown on a snackbar when the app cancelled the UI. [CHAR LIMIT=120] --> <string name="request_cancelled_by">Request cancelled by <xliff:g id="app_name" example="YouTube">%1$s</xliff:g></string> + + <!-- Strings for dropdown presentation. --> + <!-- Text shown in the dropdown presentation to select more sign in options. [CHAR LIMIT=120] --> + <string name="dropdown_presentation_more_sign_in_options_text">Sign-in options</string> + <string name="provider_icon_content_description">Credential provider icon</string> </resources>
\ No newline at end of file diff --git a/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt index 03ac605222ba..985f3228f402 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt @@ -30,6 +30,7 @@ import android.credentials.ui.GetCredentialProviderData import android.os.Bundle import android.os.CancellationSignal import android.os.OutcomeReceiver +import android.provider.Settings import android.credentials.Credential import android.service.autofill.AutofillService import android.service.autofill.Dataset @@ -48,7 +49,9 @@ import android.view.autofill.IAutoFillManagerClient import android.view.autofill.AutofillId import android.widget.inline.InlinePresentationSpec import android.credentials.CredentialManager +import android.widget.RemoteViews import androidx.autofill.inline.v1.InlineSuggestionUi +import androidx.core.content.ContextCompat import androidx.credentials.provider.CustomCredentialEntry import androidx.credentials.provider.PasswordCredentialEntry import androidx.credentials.provider.PublicKeyCredentialEntry @@ -115,7 +118,7 @@ class CredentialAutofillService : AutofillService() { } val getCredRequest: GetCredentialRequest? = getCredManRequest(structure, sessionId, - requestId) + requestId) if (getCredRequest == null) { Log.i(TAG, "No credential manager request found") callback.onFailure("No credential manager request found") @@ -307,10 +310,14 @@ class CredentialAutofillService : AutofillService() { val inlineMaxSuggestedCount = inlineSuggestionsRequest?.maxSuggestionCount ?: 0 val inlinePresentationSpecs = inlineSuggestionsRequest?.inlinePresentationSpecs val inlinePresentationSpecsCount = inlinePresentationSpecs?.size ?: 0 - var maxItemCount = totalEntryCount - if (inlineMaxSuggestedCount > 0) { - maxItemCount = maxItemCount.coerceAtMost(inlineMaxSuggestedCount) - } + val maxDropdownDisplayLimit = this.resources.getInteger( + com.android.credentialmanager.R.integer.autofill_max_visible_datasets) + var maxInlineItemCount = totalEntryCount + maxInlineItemCount = maxInlineItemCount.coerceAtMost(inlineMaxSuggestedCount) + val lastDropdownDatasetIndex = Settings.Global.getInt(this.contentResolver, + Settings.Global.AUTOFILL_MAX_VISIBLE_DATASETS, + (maxDropdownDisplayLimit - 1).coerceAtMost(totalEntryCount - 1)) + var i = 0 var datasetAdded = false @@ -333,13 +340,8 @@ class CredentialAutofillService : AutofillService() { Log.e(TAG, "PendingIntent was missing from the entry.") return@usernameLoop } - if (inlinePresentationSpecs == null) { - Log.i(TAG, "Inline presentation spec is null, " + - "building dropdown presentation only") - } - if (i >= maxItemCount) { - Log.e(TAG, "Skipping because reached the max item count.") - return@usernameLoop + if (i >= maxInlineItemCount && i >= lastDropdownDatasetIndex) { + return@usernameLoop; } val icon: Icon = if (primaryEntry.icon == null) { // The empty entry icon has non-null icon reference but null drawable reference. @@ -351,38 +353,26 @@ class CredentialAutofillService : AutofillService() { } // Create inline presentation var inlinePresentation: InlinePresentation? = null - var spec: InlinePresentationSpec? - if (inlinePresentationSpecs != null) { - if (i < inlinePresentationSpecsCount) { - spec = inlinePresentationSpecs[i] + if (inlinePresentationSpecs != null && i < maxInlineItemCount) { + val spec: InlinePresentationSpec? = if (i < inlinePresentationSpecsCount) { + inlinePresentationSpecs[i] } else { - spec = inlinePresentationSpecs[inlinePresentationSpecsCount - 1] + inlinePresentationSpecs[inlinePresentationSpecsCount - 1] } - val displayName: String = if (primaryEntry.credentialType == - CredentialType.PASSKEY && primaryEntry.displayName != null) { - primaryEntry.displayName!! - } else { - primaryEntry.userName - } - val sliceBuilder = InlineSuggestionUi - .newContentBuilder(pendingIntent) - .setTitle(displayName) - sliceBuilder.setStartIcon(icon) - if (primaryEntry.credentialType == - CredentialType.PASSKEY && duplicateDisplayNamesForPasskeys[displayName] - == true) { - sliceBuilder.setSubtitle(primaryEntry.userName) - } - inlinePresentation = InlinePresentation( - sliceBuilder.build().slice, spec, /* pinned= */ false) + inlinePresentation = createInlinePresentation(primaryEntry, pendingIntent, icon, + spec!!, duplicateDisplayNamesForPasskeys) + } + var dropdownPresentation: RemoteViews? = null + if (i < lastDropdownDatasetIndex) { + dropdownPresentation = RemoteViewsFactory + .createDropdownPresentation(this, icon, primaryEntry) } - val dropdownPresentation = RemoteViewsFactory.createDropdownPresentation( - this, icon, primaryEntry) - i++ val dataSetBuilder = Dataset.Builder() val presentationBuilder = Presentations.Builder() - .setMenuPresentation(dropdownPresentation) + if (dropdownPresentation != null) { + presentationBuilder.setMenuPresentation(dropdownPresentation) + } if (inlinePresentation != null) { presentationBuilder.setInlinePresentation(inlinePresentation) } @@ -398,6 +388,12 @@ class CredentialAutofillService : AutofillService() { .setAuthenticationExtras(fillInIntent.extras) .build()) datasetAdded = true + i++ + + if (i == lastDropdownDatasetIndex && bottomSheetPendingIntent != null) { + addDropdownMoreOptionsPresentation(bottomSheetPendingIntent, autofillId, + fillResponseBuilder) + } } val pinnedSpec = getLastInlinePresentationSpec(inlinePresentationSpecs, inlinePresentationSpecsCount) @@ -408,6 +404,49 @@ class CredentialAutofillService : AutofillService() { return datasetAdded } + private fun createInlinePresentation(primaryEntry: CredentialEntryInfo, + pendingIntent: PendingIntent, + icon: Icon, + spec: InlinePresentationSpec, + duplicateDisplayNameForPasskeys: MutableMap<String, Boolean>): + InlinePresentation { + val displayName: String = if (primaryEntry.credentialType == CredentialType.PASSKEY + && primaryEntry.displayName != null) { + primaryEntry.displayName!! + } else { + primaryEntry.userName + } + val sliceBuilder = InlineSuggestionUi + .newContentBuilder(pendingIntent) + .setTitle(displayName) + sliceBuilder.setStartIcon(icon) + if (primaryEntry.credentialType == + CredentialType.PASSKEY && duplicateDisplayNameForPasskeys[displayName] == true) { + sliceBuilder.setSubtitle(primaryEntry.userName) + } + return InlinePresentation( + sliceBuilder.build().slice, spec, /* pinned= */ false) + } + + private fun addDropdownMoreOptionsPresentation( + bottomSheetPendingIntent: PendingIntent, + autofillId: AutofillId, + fillResponseBuilder: FillResponse.Builder) { + val presentationBuilder = Presentations.Builder() + .setMenuPresentation(RemoteViewsFactory.createMoreSignInOptionsPresentation(this)) + + fillResponseBuilder.addDataset( + Dataset.Builder() + .setField( + autofillId, + Field.Builder().setPresentations( + presentationBuilder.build()) + .build()) + .setAuthentication(bottomSheetPendingIntent.intentSender) + .build() + ) + } + private fun getLastInlinePresentationSpec( inlinePresentationSpecs: List<InlinePresentationSpec>?, inlinePresentationSpecsCount: Int @@ -534,9 +573,9 @@ class CredentialAutofillService : AutofillService() { } private fun getCredManRequest( - structure: AssistStructure, - sessionId: Int, - requestId: Int + structure: AssistStructure, + sessionId: Int, + requestId: Int ): GetCredentialRequest? { val credentialOptions: MutableList<CredentialOption> = mutableListOf() traverseStructure(structure, credentialOptions) diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt index e039dead043e..68f1c861d51b 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/RemoteViewsFactory.kt @@ -44,7 +44,7 @@ class RemoteViewsFactory { if (credentialEntryInfo.credentialType == CredentialType.UNKNOWN) { return remoteViews } - setRemoteViewsPaddings(remoteViews, context) + setRemoteViewsPaddings(remoteViews, context, /* primaryTextBottomPadding=*/0) if (credentialEntryInfo.credentialType == CredentialType.PASSKEY) { val displayName = credentialEntryInfo.displayName ?: credentialEntryInfo.userName remoteViews.setTextViewText(android.R.id.text1, displayName) @@ -81,8 +81,46 @@ class RemoteViewsFactory { return remoteViews } + fun createMoreSignInOptionsPresentation(context: Context): RemoteViews { + var layoutId: Int = com.android.credentialmanager.R.layout + .credman_dropdown_bottom_sheet + val remoteViews = RemoteViews(context.packageName, layoutId) + setRemoteViewsPaddings(remoteViews, context) + remoteViews.setTextViewText(android.R.id.text1, ContextCompat.getString(context, + com.android.credentialmanager + .R.string.dropdown_presentation_more_sign_in_options_text)) + + val textColorPrimary = ContextCompat.getColor(context, + com.android.credentialmanager.R.color.text_primary) + remoteViews.setTextColor(android.R.id.text1, textColorPrimary) + val icon = Icon.createWithResource(context, com + .android.credentialmanager.R.drawable.more_horiz_24px) + icon.setTint(ContextCompat.getColor(context, + com.android.credentialmanager.R.color.sign_in_options_icon_color)) + remoteViews.setImageViewIcon(android.R.id.icon1, icon) + remoteViews.setBoolean( + android.R.id.icon1, setAdjustViewBoundsMethodName, true); + remoteViews.setInt( + android.R.id.icon1, + setMaxHeightMethodName, + context.resources.getDimensionPixelSize( + com.android.credentialmanager.R.dimen.autofill_icon_size)); + val drawableId = + com.android.credentialmanager.R.drawable.more_options_list_item + remoteViews.setInt( + android.R.id.content, setBackgroundResourceMethodName, drawableId); + return remoteViews + } + private fun setRemoteViewsPaddings( remoteViews: RemoteViews, context: Context) { + val bottomPadding = context.resources.getDimensionPixelSize( + com.android.credentialmanager.R.dimen.autofill_view_bottom_padding) + setRemoteViewsPaddings(remoteViews, context, bottomPadding) + } + + private fun setRemoteViewsPaddings( + remoteViews: RemoteViews, context: Context, primaryTextBottomPadding: Int) { val leftPadding = context.resources.getDimensionPixelSize( com.android.credentialmanager.R.dimen.autofill_view_left_padding) val iconToTextPadding = context.resources.getDimensionPixelSize( @@ -104,7 +142,7 @@ class RemoteViewsFactory { iconToTextPadding, /* top=*/topPadding, /* right=*/rightPadding, - /* bottom=*/0) + primaryTextBottomPadding) remoteViews.setViewPadding( android.R.id.text2, iconToTextPadding, diff --git a/packages/SettingsLib/Android.bp b/packages/SettingsLib/Android.bp index c2cb75709b45..bd56aae1cb17 100644 --- a/packages/SettingsLib/Android.bp +++ b/packages/SettingsLib/Android.bp @@ -16,6 +16,7 @@ android_library { static_libs: [ "androidx.localbroadcastmanager_localbroadcastmanager", "androidx.room_room-runtime", + "androidx.sqlite_sqlite", "zxing-core", "guava", diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/MoreOptions.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/MoreOptions.kt index 84fea158175f..d92a863e53bd 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/MoreOptions.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/MoreOptions.kt @@ -35,8 +35,8 @@ import androidx.compose.ui.res.stringResource /** * Scope for the children of [MoreOptionsAction]. */ -interface MoreOptionsScope { - fun dismiss() +abstract class MoreOptionsScope { + abstract fun dismiss() @Composable fun MenuItem(text: String, enabled: Boolean = true, onClick: () -> Unit) { @@ -60,7 +60,7 @@ fun MoreOptionsAction( val onDismiss = { expanded = false } DropdownMenu(expanded = expanded, onDismissRequest = onDismiss) { val moreOptionsScope = remember(this) { - object : MoreOptionsScope { + object : MoreOptionsScope() { override fun dismiss() { onDismiss() } diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/scaffold/RestrictedMenuItemTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/scaffold/RestrictedMenuItemTest.kt index 983284cbabb7..2ccf323de2a3 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/scaffold/RestrictedMenuItemTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/scaffold/RestrictedMenuItemTest.kt @@ -130,7 +130,7 @@ class RestrictedMenuItemTest { } private fun setContent(restrictions: Restrictions) { - val fakeMoreOptionsScope = object : MoreOptionsScope { + val fakeMoreOptionsScope = object : MoreOptionsScope() { override fun dismiss() {} } composeTestRule.setContent { diff --git a/packages/SettingsLib/src/com/android/settingslib/applications/InterestingConfigChanges.java b/packages/SettingsLib/src/com/android/settingslib/applications/InterestingConfigChanges.java index 5d520ce5d81f..7e2d0af5c075 100644 --- a/packages/SettingsLib/src/com/android/settingslib/applications/InterestingConfigChanges.java +++ b/packages/SettingsLib/src/com/android/settingslib/applications/InterestingConfigChanges.java @@ -21,6 +21,8 @@ import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.content.res.Resources; +import androidx.annotation.NonNull; + /** * A class for applying config changes and determing if doing so resulting in any "interesting" * changes. @@ -48,8 +50,15 @@ public class InterestingConfigChanges { */ @SuppressLint("NewApi") public boolean applyNewConfig(Resources res) { + return applyNewConfig(res.getConfiguration()); + } + + /** + * Applies the given config change and returns whether an "interesting" change happened. + */ + public boolean applyNewConfig(@NonNull Configuration configuration) { int configChanges = mLastConfiguration.updateFrom( - Configuration.generateDelta(mLastConfiguration, res.getConfiguration())); + Configuration.generateDelta(mLastConfiguration, configuration)); return (configChanges & (mFlags)) != 0; } } diff --git a/packages/SettingsLib/src/com/android/settingslib/users/EditUserInfoController.java b/packages/SettingsLib/src/com/android/settingslib/users/EditUserInfoController.java index cd5f59731e7f..b015b2bce60a 100644 --- a/packages/SettingsLib/src/com/android/settingslib/users/EditUserInfoController.java +++ b/packages/SettingsLib/src/com/android/settingslib/users/EditUserInfoController.java @@ -234,6 +234,6 @@ public class EditUserInfoController { EditUserPhotoController createEditUserPhotoController(Activity activity, ActivityStarter activityStarter, ImageView userPhotoView) { return new EditUserPhotoController(activity, activityStarter, userPhotoView, - mSavedPhoto, mSavedDrawable, mFileAuthority); + mSavedPhoto, mSavedDrawable, mFileAuthority, false); } } diff --git a/packages/SettingsLib/src/com/android/settingslib/users/EditUserPhotoController.java b/packages/SettingsLib/src/com/android/settingslib/users/EditUserPhotoController.java index e83b9bc25799..b2de5a948836 100644 --- a/packages/SettingsLib/src/com/android/settingslib/users/EditUserPhotoController.java +++ b/packages/SettingsLib/src/com/android/settingslib/users/EditUserPhotoController.java @@ -62,6 +62,7 @@ public class EditUserPhotoController { private static final String AVATAR_PICKER_ACTION = "com.android.avatarpicker" + ".FULL_SCREEN_ACTIVITY"; + static final String EXTRA_IS_USER_NEW = "is_user_new"; private final Activity mActivity; private final ActivityStarter mActivityStarter; @@ -72,9 +73,13 @@ public class EditUserPhotoController { private Bitmap mNewUserPhotoBitmap; private Drawable mNewUserPhotoDrawable; private String mCachedDrawablePath; - public EditUserPhotoController(Activity activity, ActivityStarter activityStarter, ImageView view, Bitmap savedBitmap, Drawable savedDrawable, String fileAuthority) { + this(activity, activityStarter, view, savedBitmap, savedDrawable, fileAuthority, true); + } + public EditUserPhotoController(Activity activity, ActivityStarter activityStarter, + ImageView view, Bitmap savedBitmap, Drawable savedDrawable, String fileAuthority, + boolean isUserNew) { mActivity = activity; mActivityStarter = activityStarter; mFileAuthority = fileAuthority; @@ -82,7 +87,7 @@ public class EditUserPhotoController { mImagesDir = new File(activity.getCacheDir(), IMAGES_DIR); mImagesDir.mkdir(); mImageView = view; - mImageView.setOnClickListener(v -> showAvatarPicker()); + mImageView.setOnClickListener(v -> showAvatarPicker(isUserNew)); mNewUserPhotoBitmap = savedBitmap; mNewUserPhotoDrawable = savedDrawable; @@ -117,11 +122,12 @@ public class EditUserPhotoController { return mNewUserPhotoDrawable; } - private void showAvatarPicker() { + private void showAvatarPicker(boolean isUserNew) { Intent intent; if (Flags.avatarSync()) { intent = new Intent(AVATAR_PICKER_ACTION); intent.addCategory(Intent.CATEGORY_DEFAULT); + intent.putExtra(EXTRA_IS_USER_NEW, isUserNew); } else { intent = new Intent(mImageView.getContext(), AvatarPickerActivity.class); } diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index cc63996494a0..d38454221f76 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -343,6 +343,7 @@ <uses-permission android:name="android.permission.REQUEST_COMPANION_PROFILE_GLASSES" /> <uses-permission android:name="android.permission.REQUEST_COMPANION_SELF_MANAGED" /> <uses-permission android:name="android.permission.USE_COMPANION_TRANSPORTS" /> + <uses-permission android:name="android.permission.REQUEST_OBSERVE_DEVICE_UUID_PRESENCE" /> <uses-permission android:name="android.permission.MANAGE_APPOPS" /> <uses-permission android:name="android.permission.WATCH_APPOPS" /> diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index d61ae7eccc42..80656e9253db 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -361,6 +361,7 @@ android_library { "androidx.test.ext.junit", "androidx.test.ext.truth", "kotlin-test", + "SystemUICustomizationTestUtils", ], libs: [ "android.test.runner", @@ -439,6 +440,7 @@ android_robolectric_test { "androidx.test.ext.junit", "inline-mockito-robolectric-prebuilt", "platform-parametric-runner-lib", + "SystemUICustomizationTestUtils", ], libs: [ "android.test.runner", diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 168e6e003dc6..54ab5d1e726f 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -899,6 +899,14 @@ android:exported="true" /> + <activity + android:name=".volume.panel.ui.activity.VolumePanelActivity" + android:label="@string/sound_settings" + android:excludeFromRecents="true" + android:exported="false" + android:launchMode="singleInstance" + android:theme="@style/Theme.VolumePanelActivity" /> + <activity android:name=".wallet.ui.WalletActivity" android:label="@string/wallet_title" android:theme="@style/Wallet.Theme" diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 2c35c777ab12..7eca04a5f85b 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -364,3 +364,10 @@ flag { description: "Enables styled focus states on pin input field if keyboard is connected" bug: "316106516" } + +flag { + name: "enable_notif_linearlayout_optimized" + namespace: "systemui" + description: "Enables notification specific LinearLayout optimization" + bug: "316110233" +} diff --git a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt index 2052e2c01410..3c325940c325 100644 --- a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt +++ b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt @@ -17,6 +17,7 @@ package com.android.systemui.compose +import android.app.Dialog import android.content.Context import android.view.View import android.view.WindowInsets @@ -26,11 +27,14 @@ import com.android.systemui.bouncer.ui.BouncerDialogFactory import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel import com.android.systemui.communal.widgets.WidgetConfigurator +import com.android.systemui.keyboard.stickykeys.ui.viewmodel.StickyKeysIndicatorViewModel import com.android.systemui.people.ui.viewmodel.PeopleViewModel import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel import com.android.systemui.scene.shared.model.Scene import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel +import com.android.systemui.statusbar.phone.SystemUIDialogFactory +import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelViewModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.StateFlow @@ -60,6 +64,14 @@ object ComposeFacade : BaseComposeFacade { throwComposeUnavailableError() } + override fun setVolumePanelActivityContent( + activity: ComponentActivity, + viewModel: VolumePanelViewModel, + onDismissAnimationFinished: () -> Unit, + ) { + throwComposeUnavailableError() + } + override fun createFooterActionsView( context: Context, viewModel: FooterActionsViewModel, @@ -78,6 +90,13 @@ object ComposeFacade : BaseComposeFacade { throwComposeUnavailableError() } + override fun createStickyKeysDialog( + dialogFactory: SystemUIDialogFactory, + viewModel: StickyKeysIndicatorViewModel + ): Dialog { + throwComposeUnavailableError() + } + override fun createCommunalView( context: Context, viewModel: BaseCommunalViewModel, diff --git a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/volume/panel/component/bottombar/BottomBarModule.kt b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/volume/panel/component/bottombar/BottomBarModule.kt new file mode 100644 index 000000000000..c8dae76f9f1a --- /dev/null +++ b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/volume/panel/component/bottombar/BottomBarModule.kt @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.panel.component.bottombar + +import dagger.Module + +@Module interface BottomBarModule diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt index b607d596390d..afb860e62261 100644 --- a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt +++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt @@ -16,6 +16,7 @@ package com.android.systemui.compose +import android.app.Dialog import android.content.Context import android.graphics.Point import android.view.View @@ -38,6 +39,8 @@ import com.android.systemui.communal.ui.compose.CommunalContainer import com.android.systemui.communal.ui.compose.CommunalHub import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel import com.android.systemui.communal.widgets.WidgetConfigurator +import com.android.systemui.keyboard.stickykeys.ui.view.StickyKeysIndicator +import com.android.systemui.keyboard.stickykeys.ui.viewmodel.StickyKeysIndicatorViewModel import com.android.systemui.people.ui.compose.PeopleScreen import com.android.systemui.people.ui.viewmodel.PeopleViewModel import com.android.systemui.qs.footer.ui.compose.FooterActions @@ -47,6 +50,10 @@ import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.ui.composable.ComposableScene import com.android.systemui.scene.ui.composable.SceneContainer import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel +import com.android.systemui.statusbar.phone.SystemUIDialogFactory +import com.android.systemui.statusbar.phone.create +import com.android.systemui.volume.panel.ui.composable.VolumePanelRoot +import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelViewModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -86,6 +93,19 @@ object ComposeFacade : BaseComposeFacade { } } + override fun setVolumePanelActivityContent( + activity: ComponentActivity, + viewModel: VolumePanelViewModel, + onDismissAnimationFinished: () -> Unit, + ) { + activity.setContent { + VolumePanelRoot( + viewModel = viewModel, + onDismissAnimationFinished = onDismissAnimationFinished, + ) + } + } + override fun createFooterActionsView( context: Context, viewModel: FooterActionsViewModel, @@ -120,6 +140,13 @@ object ComposeFacade : BaseComposeFacade { } } + override fun createStickyKeysDialog( + dialogFactory: SystemUIDialogFactory, + viewModel: StickyKeysIndicatorViewModel + ): Dialog { + return dialogFactory.create { StickyKeysIndicator(viewModel) } + } + override fun createCommunalView( context: Context, viewModel: BaseCommunalViewModel, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt index c073b79ba5a3..a22fecf3688d 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt @@ -64,12 +64,11 @@ fun CommunalContainer( transitions = sceneTransitions, ) - // Don't show hub mode UI if keyguard is not present. This is important since we're in the - // shade, which can be opened from many locations. - val isKeyguardShowing by viewModel.isKeyguardVisible.collectAsState(initial = false) + // Don't show hub mode UI if communal is not available. Communal is only available if it has + // been enabled via settings and either keyguard is showing, or, the device is currently + // dreaming. val isCommunalAvailable by viewModel.isCommunalAvailable.collectAsState() - - if (!isKeyguardShowing || !isCommunalAvailable) { + if (!isCommunalAvailable) { return } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt index 409f15bb4bb8..761e74e52237 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt @@ -97,11 +97,13 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.window.Popup import androidx.core.view.setPadding +import com.android.compose.modifiers.thenIf import com.android.compose.theme.LocalAndroidColorScheme import com.android.systemui.communal.domain.model.CommunalContentModel import com.android.systemui.communal.shared.model.CommunalContentSize import com.android.systemui.communal.ui.compose.Dimensions.CardOutlineWidth import com.android.systemui.communal.ui.compose.extensions.allowGestures +import com.android.systemui.communal.ui.compose.extensions.detectLongPressGesture import com.android.systemui.communal.ui.compose.extensions.firstItemAtOffset import com.android.systemui.communal.ui.compose.extensions.observeTapsWithoutConsuming import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel @@ -132,6 +134,8 @@ fun CommunalHub( val removeButtonEnabled by remember { derivedStateOf { selectedIndex.value != null || reorderingWidgets } } + val (isButtonToEditWidgetsShowing, setIsButtonToEditWidgetsShowing) = + remember { mutableStateOf(false) } val contentPadding = gridContentPadding(viewModel.isEditMode, toolbarSize) val contentOffset = beforeContentPadding(contentPadding).toOffset() @@ -158,6 +162,11 @@ fun CommunalHub( } viewModel.setSelectedIndex(newIndex) } + } + .thenIf(!viewModel.isEditMode) { + Modifier.pointerInput(Unit) { + detectLongPressGesture { offset -> setIsButtonToEditWidgetsShowing(true) } + } }, ) { CommunalHubLazyGrid( @@ -207,6 +216,16 @@ fun CommunalHub( PopupOnDismissCtaTile(viewModel::onHidePopupAfterDismissCta) } + if (isButtonToEditWidgetsShowing) { + ButtonToEditWidgets( + onClick = { + setIsButtonToEditWidgetsShowing(false) + viewModel.onOpenWidgetEditor() + }, + onHide = { setIsButtonToEditWidgetsShowing(false) }, + ) + } + // This spacer covers the edge of the LazyHorizontalGrid and prevents it from receiving // touches, so that the SceneTransitionLayout can intercept the touches and allow an edge // swipe back to the blank scene. @@ -414,6 +433,34 @@ private fun Toolbar( } @Composable +private fun ButtonToEditWidgets( + onClick: () -> Unit, + onHide: () -> Unit, +) { + Popup(alignment = Alignment.TopCenter, offset = IntOffset(0, 40), onDismissRequest = onHide) { + val colors = LocalAndroidColorScheme.current + Button( + modifier = + Modifier.height(56.dp).background(colors.secondary, RoundedCornerShape(50.dp)), + onClick = onClick, + ) { + Icon( + imageVector = Icons.Outlined.Widgets, + contentDescription = stringResource(R.string.button_to_configure_widgets_text), + tint = colors.onSecondary, + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.size(8.dp)) + Text( + text = stringResource(R.string.button_to_configure_widgets_text), + style = MaterialTheme.typography.titleSmall, + color = colors.onSecondary, + ) + } + } +} + +@Composable private fun PopupOnDismissCtaTile(onHidePopupAfterDismissCta: () -> Unit) { Popup( alignment = Alignment.TopCenter, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/PointerInputScopeExt.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/PointerInputScopeExt.kt index 14074944259b..bc1e429e57cf 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/PointerInputScopeExt.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/extensions/PointerInputScopeExt.kt @@ -20,9 +20,13 @@ import androidx.compose.foundation.gestures.awaitEachGesture import androidx.compose.foundation.gestures.awaitFirstDown import androidx.compose.foundation.gestures.waitForUpOrCancellation import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.pointer.AwaitPointerEventScope import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.input.pointer.PointerEventTimeoutCancellationException import androidx.compose.ui.input.pointer.PointerInputChange import androidx.compose.ui.input.pointer.PointerInputScope +import androidx.compose.ui.util.fastAny +import androidx.compose.ui.util.fastForEach import kotlinx.coroutines.coroutineScope /** @@ -44,6 +48,41 @@ suspend fun PointerInputScope.observeTapsWithoutConsuming( } } +/** + * Detect long press gesture and calls onLongPress when detected. The callback parameter receives an + * Offset representing the position relative to the containing element. + */ +suspend fun PointerInputScope.detectLongPressGesture( + pass: PointerEventPass = PointerEventPass.Initial, + onLongPress: ((Offset) -> Unit), +) = coroutineScope { + awaitEachGesture { + val down = awaitFirstDown(pass = pass) + val longPressTimeout = viewConfiguration.longPressTimeoutMillis + // wait for first tap up or long press + try { + withTimeout(longPressTimeout) { waitForUpOrCancellation(pass = pass) } + } catch (_: PointerEventTimeoutCancellationException) { + // withTimeout throws exception if timeout has passed before block completes + onLongPress.invoke(down.position) + consumeUntilUp(pass) + } + } +} + +/** + * Consumes all pointer events until nothing is pressed and then returns. This method assumes that + * something is currently pressed. + */ +private suspend fun AwaitPointerEventScope.consumeUntilUp( + pass: PointerEventPass = PointerEventPass.Initial +) { + do { + val event = awaitPointerEvent(pass = pass) + event.changes.fastForEach { it.consume() } + } while (event.changes.fastAny { it.pressed }) +} + /** Consume all gestures on the initial pass so that child elements do not receive them. */ suspend fun PointerInputScope.consumeAllGestures() = coroutineScope { awaitEachGesture { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyboard/stickykeys/ui/view/StickyKeysIndicator.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyboard/stickykeys/ui/view/StickyKeysIndicator.kt new file mode 100644 index 000000000000..68e57b5d51b8 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyboard/stickykeys/ui/view/StickyKeysIndicator.kt @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyboard.stickykeys.ui.view + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.key +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.android.systemui.keyboard.stickykeys.shared.model.Locked +import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey +import com.android.systemui.keyboard.stickykeys.ui.viewmodel.StickyKeysIndicatorViewModel + +@Composable +fun StickyKeysIndicator(viewModel: StickyKeysIndicatorViewModel) { + val stickyKeys by viewModel.indicatorContent.collectAsState(emptyMap()) + StickyKeysIndicator(stickyKeys) +} + +@Composable +fun StickyKeysIndicator(stickyKeys: Map<ModifierKey, Locked>, modifier: Modifier = Modifier) { + Surface( + color = MaterialTheme.colorScheme.surface, + shape = MaterialTheme.shapes.medium, + modifier = modifier + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.padding(16.dp) + ) { + stickyKeys.forEach { (key, isLocked) -> + key(key) { + Text( + text = key.text, + fontWeight = if (isLocked.locked) FontWeight.Bold else FontWeight.Normal + ) + } + } + } + } +} 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 56d6879e614e..bf02d8abf73c 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 @@ -173,7 +173,8 @@ constructor( val belowLockIconPlaceable = belowLockIconMeasurable.measure( noMinConstraints.copy( - maxHeight = constraints.maxHeight - lockIconBounds.bottom + maxHeight = + (constraints.maxHeight - lockIconBounds.bottom).coerceAtLeast(0) ) ) val startShortcutPleaceable = startShortcutMeasurable.measure(noMinConstraints) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/SplitShadeBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/SplitShadeBlueprint.kt index fdf11668ae76..616a7b4752a0 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/SplitShadeBlueprint.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/SplitShadeBlueprint.kt @@ -16,19 +16,42 @@ package com.android.systemui.keyguard.ui.composable.blueprint -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.material3.Text +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +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.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.platform.LocalContext +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 com.android.compose.animation.scene.SceneScope +import com.android.compose.modifiers.padding +import com.android.systemui.Flags +import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.ui.composable.LockscreenLongPress +import com.android.systemui.keyguard.ui.composable.section.AmbientIndicationSection +import com.android.systemui.keyguard.ui.composable.section.BottomAreaSection +import com.android.systemui.keyguard.ui.composable.section.ClockSection +import com.android.systemui.keyguard.ui.composable.section.LockSection +import com.android.systemui.keyguard.ui.composable.section.NotificationSection +import com.android.systemui.keyguard.ui.composable.section.SettingsMenuSection +import com.android.systemui.keyguard.ui.composable.section.SmartSpaceSection +import com.android.systemui.keyguard.ui.composable.section.StatusBarSection import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel +import com.android.systemui.res.R +import com.android.systemui.shade.LargeScreenHeaderHelper import dagger.Binds import dagger.Module import dagger.multibindings.IntoSet +import java.util.Optional import javax.inject.Inject /** @@ -39,22 +62,174 @@ class SplitShadeBlueprint @Inject constructor( private val viewModel: LockscreenContentViewModel, + private val statusBarSection: StatusBarSection, + private val clockSection: ClockSection, + private val smartSpaceSection: SmartSpaceSection, + private val notificationSection: NotificationSection, + private val lockSection: LockSection, + private val ambientIndicationSectionOptional: Optional<AmbientIndicationSection>, + private val bottomAreaSection: BottomAreaSection, + private val settingsMenuSection: SettingsMenuSection, + private val clockInteractor: KeyguardClockInteractor, + private val largeScreenHeaderHelper: LargeScreenHeaderHelper, ) : LockscreenSceneBlueprint { override val id: String = "split-shade" @Composable override fun SceneScope.Content(modifier: Modifier) { + val isUdfpsVisible = viewModel.isUdfpsVisible + val burnIn = rememberBurnIn(clockInteractor) + val resources = LocalContext.current.resources + LockscreenLongPress( viewModel = viewModel.longPress, modifier = modifier, - ) { _ -> - Box(modifier.background(Color.Black)) { - Text( - text = "TODO(b/316211368): split shade blueprint", - color = Color.White, - modifier = Modifier.align(Alignment.Center), - ) + ) { onSettingsMenuPlaced -> + Layout( + content = { + // Constrained to above the lock icon. + Column( + modifier = Modifier.fillMaxSize(), + ) { + with(statusBarSection) { StatusBar(modifier = Modifier.fillMaxWidth()) } + Row( + modifier = Modifier.fillMaxSize(), + ) { + Column( + modifier = Modifier.fillMaxHeight().weight(weight = 1f), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + with(smartSpaceSection) { + SmartSpace( + burnInParams = burnIn.parameters, + onTopChanged = burnIn.onSmartspaceTopChanged, + modifier = + Modifier.fillMaxWidth() + .padding( + top = { + viewModel.getSmartSpacePaddingTop(resources) + } + ), + ) + } + + Spacer(modifier = Modifier.weight(weight = 1f)) + with(clockSection) { LargeClock() } + Spacer(modifier = Modifier.weight(weight = 1f)) + } + with(notificationSection) { + val splitShadeTopMargin: Dp = + if (Flags.centralizedStatusBarDimensRefactor()) { + largeScreenHeaderHelper.getLargeScreenHeaderHeight().dp + } else { + dimensionResource( + id = R.dimen.large_screen_shade_header_height + ) + } + Notifications( + modifier = + Modifier.fillMaxHeight() + .weight(weight = 1f) + .padding(top = splitShadeTopMargin) + ) + } + } + + if (!isUdfpsVisible && ambientIndicationSectionOptional.isPresent) { + with(ambientIndicationSectionOptional.get()) { + AmbientIndication(modifier = Modifier.fillMaxWidth()) + } + } + } + + with(lockSection) { LockIcon() } + + // Aligned to bottom and constrained to below the lock icon. + Column(modifier = Modifier.fillMaxWidth()) { + if (isUdfpsVisible && ambientIndicationSectionOptional.isPresent) { + with(ambientIndicationSectionOptional.get()) { + AmbientIndication(modifier = Modifier.fillMaxWidth()) + } + } + + with(bottomAreaSection) { + IndicationArea(modifier = Modifier.fillMaxWidth()) + } + } + + // Aligned to bottom and NOT constrained by the lock icon. + with(bottomAreaSection) { + Shortcut(isStart = true, applyPadding = true) + Shortcut(isStart = false, applyPadding = true) + } + with(settingsMenuSection) { SettingsMenu(onSettingsMenuPlaced) } + }, + modifier = Modifier.fillMaxSize(), + ) { measurables, constraints -> + check(measurables.size == 6) + val aboveLockIconMeasurable = measurables[0] + val lockIconMeasurable = measurables[1] + val belowLockIconMeasurable = measurables[2] + val startShortcutMeasurable = measurables[3] + val endShortcutMeasurable = measurables[4] + val settingsMenuMeasurable = measurables[5] + + val noMinConstraints = + constraints.copy( + minWidth = 0, + minHeight = 0, + ) + val lockIconPlaceable = lockIconMeasurable.measure(noMinConstraints) + val lockIconBounds = + IntRect( + left = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Left], + top = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Top], + right = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Right], + bottom = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Bottom], + ) + + val aboveLockIconPlaceable = + aboveLockIconMeasurable.measure( + noMinConstraints.copy(maxHeight = lockIconBounds.top) + ) + val belowLockIconPlaceable = + belowLockIconMeasurable.measure( + noMinConstraints.copy( + maxHeight = + (constraints.maxHeight - lockIconBounds.bottom).coerceAtLeast(0) + ) + ) + val startShortcutPleaceable = startShortcutMeasurable.measure(noMinConstraints) + val endShortcutPleaceable = endShortcutMeasurable.measure(noMinConstraints) + val settingsMenuPlaceable = settingsMenuMeasurable.measure(noMinConstraints) + + layout(constraints.maxWidth, constraints.maxHeight) { + aboveLockIconPlaceable.place( + x = 0, + y = 0, + ) + lockIconPlaceable.place( + x = lockIconBounds.left, + y = lockIconBounds.top, + ) + belowLockIconPlaceable.place( + x = 0, + y = constraints.maxHeight - belowLockIconPlaceable.height, + ) + startShortcutPleaceable.place( + x = 0, + y = constraints.maxHeight - startShortcutPleaceable.height, + ) + endShortcutPleaceable.place( + x = constraints.maxWidth - endShortcutPleaceable.width, + y = constraints.maxHeight - endShortcutPleaceable.height, + ) + settingsMenuPlaceable.place( + x = (constraints.maxWidth - settingsMenuPlaceable.width) / 2, + y = constraints.maxHeight - settingsMenuPlaceable.height, + ) + } } } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/ClockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/ClockSection.kt index f40b871e923c..8f218792ee32 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/ClockSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/ClockSection.kt @@ -16,7 +16,8 @@ package com.android.systemui.keyguard.ui.composable.section -import androidx.compose.foundation.layout.fillMaxWidth +import android.view.ViewGroup +import android.widget.FrameLayout import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect @@ -75,7 +76,13 @@ constructor( ) { content { AndroidView( - factory = { checkNotNull(currentClock).smallClock.view }, + factory = { context -> + FrameLayout(context).apply { + val newClockView = checkNotNull(currentClock).smallClock.view + (newClockView.parent as? ViewGroup)?.removeView(newClockView) + addView(newClockView) + } + }, modifier = Modifier.padding( horizontal = @@ -83,6 +90,12 @@ constructor( ) .padding(top = { viewModel.getSmallClockTopMargin(view.context) }) .onTopPlacementChanged(onTopChanged), + update = { + val newClockView = checkNotNull(currentClock).smallClock.view + it.removeAllViews() + (newClockView.parent as? ViewGroup)?.removeView(newClockView) + it.addView(newClockView) + }, ) } } @@ -116,8 +129,19 @@ constructor( ) { content { AndroidView( - factory = { checkNotNull(currentClock).largeClock.view }, - modifier = Modifier.fillMaxWidth() + factory = { context -> + FrameLayout(context).apply { + val newClockView = checkNotNull(currentClock).largeClock.view + (newClockView.parent as? ViewGroup)?.removeView(newClockView) + addView(newClockView) + } + }, + update = { + val newClockView = checkNotNull(currentClock).largeClock.view + it.removeAllViews() + (newClockView.parent as? ViewGroup)?.removeView(newClockView) + it.addView(newClockView) + }, ) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt index 9778e53d8f69..c027c499c0b7 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt @@ -16,17 +16,16 @@ package com.android.systemui.qs.ui.composable -import android.view.ContextThemeWrapper import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.defaultMinSize -import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp @@ -53,14 +52,6 @@ object QuickSettings { } } -@Composable -private fun QuickSettingsTheme(content: @Composable () -> Unit) { - val context = LocalContext.current - val themedContext = - remember(context) { ContextThemeWrapper(context, R.style.Theme_SystemUI_QuickSettings) } - CompositionLocalProvider(LocalContext provides themedContext) { content() } -} - private fun SceneScope.stateForQuickSettingsContent(): QSSceneAdapter.State { return when (val transitionState = layoutState.transitionState) { is TransitionState.Idle -> { @@ -115,6 +106,7 @@ private fun QuickSettingsContent( modifier: Modifier = Modifier, ) { val qsView by qsSceneAdapter.qsView.collectAsState(null) + val isCustomizing by qsSceneAdapter.isCustomizing.collectAsState() QuickSettingsTheme { val context = LocalContext.current @@ -124,14 +116,27 @@ private fun QuickSettingsContent( } } qsView?.let { view -> - AndroidView( - modifier = modifier.fillMaxSize().background(colorAttr(R.attr.underSurface)), - factory = { _ -> - qsSceneAdapter.setState(state) - view - }, - update = { qsSceneAdapter.setState(state) } - ) + Box( + modifier = + modifier + .fillMaxWidth() + .then( + if (isCustomizing) { + Modifier.fillMaxHeight() + } else { + Modifier.wrapContentHeight() + } + ) + ) { + AndroidView( + modifier = Modifier.fillMaxWidth().background(colorAttr(R.attr.underSurface)), + factory = { _ -> + qsSceneAdapter.setState(state) + view + }, + update = { qsSceneAdapter.setState(state) } + ) + } } } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt index d8c7290b76b8..bbfe0fda049a 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt @@ -24,31 +24,44 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.background +import androidx.compose.foundation.clipScrollableContainer +import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.MaterialTheme import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.unit.dp import com.android.compose.animation.scene.SceneScope +import com.android.compose.animation.scene.TransitionState import com.android.compose.windowsizeclass.LocalWindowSizeClass import com.android.systemui.battery.BatteryMeterViewController +import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.notifications.ui.composable.HeadsUpNotificationSpace +import com.android.systemui.qs.footer.ui.compose.FooterActions import com.android.systemui.qs.ui.viewmodel.QuickSettingsSceneViewModel import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.ui.composable.ComposableScene +import com.android.systemui.scene.ui.composable.toTransitionSceneKey import com.android.systemui.shade.ui.composable.CollapsedShadeHeader import com.android.systemui.shade.ui.composable.ExpandedShadeHeader import com.android.systemui.shade.ui.composable.Shade @@ -105,57 +118,120 @@ private fun SceneScope.QuickSettingsScene( ) { // TODO(b/280887232): implement the real UI. Box(modifier = modifier.fillMaxSize()) { - Box(modifier = Modifier.fillMaxSize()) { - val isCustomizing by viewModel.qsSceneAdapter.isCustomizing.collectAsState() - val collapsedHeaderHeight = - with(LocalDensity.current) { ShadeHeader.Dimensions.CollapsedHeight.roundToPx() } - Spacer( - modifier = - Modifier.element(Shade.Elements.ScrimBackground) - .fillMaxSize() - .background(MaterialTheme.colorScheme.scrim, shape = Shade.Shapes.Scrim) - ) - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = - Modifier.fillMaxSize().padding(start = 16.dp, end = 16.dp, bottom = 48.dp) - ) { - when (LocalWindowSizeClass.current.widthSizeClass) { - WindowWidthSizeClass.Compact -> - AnimatedVisibility( - visible = !isCustomizing, - enter = - expandVertically( - animationSpec = tween(1000), - initialHeight = { collapsedHeaderHeight }, - ) + fadeIn(tween(1000)), - exit = - shrinkVertically( - animationSpec = tween(1000), - targetHeight = { collapsedHeaderHeight }, - shrinkTowards = Alignment.Top, - ) + fadeOut(tween(1000)), - ) { - ExpandedShadeHeader( + val isCustomizing by viewModel.qsSceneAdapter.isCustomizing.collectAsState() + val collapsedHeaderHeight = + with(LocalDensity.current) { ShadeHeader.Dimensions.CollapsedHeight.roundToPx() } + val lifecycleOwner = LocalLifecycleOwner.current + val footerActionsViewModel = + remember(lifecycleOwner, viewModel) { + viewModel.getFooterActionsViewModel(lifecycleOwner) + } + val scrollState = rememberScrollState() + // When animating into the scene, we don't want it to be able to scroll, as it could mess + // up with the expansion animation. + val isScrollable = + when (val state = layoutState.transitionState) { + is TransitionState.Idle -> true + is TransitionState.Transition -> { + state.fromScene == SceneKey.QuickSettings.toTransitionSceneKey() + } + } + + LaunchedEffect(isCustomizing, scrollState) { + if (isCustomizing) { + scrollState.scrollTo(0) + } + } + + // This is the background for the whole scene, as the elements don't necessarily provide + // a background that extends to the edges. + Spacer( + modifier = + Modifier.element(Shade.Elements.ScrimBackground) + .fillMaxSize() + .background(MaterialTheme.colorScheme.scrim, shape = Shade.Shapes.Scrim) + ) + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = + Modifier.fillMaxSize() + // bottom should be tied to insets + .padding(bottom = 16.dp) + ) { + Box(modifier = Modifier.fillMaxSize().weight(1f)) { + val shadeHeaderAndQuickSettingsModifier = + if (isCustomizing) { + Modifier.fillMaxHeight().align(Alignment.TopCenter) + } else { + Modifier.verticalNestedScrollToScene() + .verticalScroll( + scrollState, + enabled = isScrollable, + ) + .clipScrollableContainer(Orientation.Horizontal) + .fillMaxWidth() + .wrapContentHeight(unbounded = true) + .align(Alignment.TopCenter) + } + + Column( + modifier = shadeHeaderAndQuickSettingsModifier, + ) { + when (LocalWindowSizeClass.current.widthSizeClass) { + WindowWidthSizeClass.Compact -> + AnimatedVisibility( + visible = !isCustomizing, + enter = + expandVertically( + animationSpec = tween(100), + initialHeight = { collapsedHeaderHeight }, + ) + fadeIn(tween(100)), + exit = + shrinkVertically( + animationSpec = tween(100), + targetHeight = { collapsedHeaderHeight }, + shrinkTowards = Alignment.Top, + ) + fadeOut(tween(100)), + ) { + ExpandedShadeHeader( + viewModel = viewModel.shadeHeaderViewModel, + createTintedIconManager = createTintedIconManager, + createBatteryMeterViewController = + createBatteryMeterViewController, + statusBarIconController = statusBarIconController, + modifier = Modifier.padding(horizontal = 16.dp), + ) + } + else -> + CollapsedShadeHeader( viewModel = viewModel.shadeHeaderViewModel, createTintedIconManager = createTintedIconManager, createBatteryMeterViewController = createBatteryMeterViewController, statusBarIconController = statusBarIconController, + modifier = Modifier.padding(horizontal = 16.dp), ) - } - else -> - CollapsedShadeHeader( - viewModel = viewModel.shadeHeaderViewModel, - createTintedIconManager = createTintedIconManager, - createBatteryMeterViewController = createBatteryMeterViewController, - statusBarIconController = statusBarIconController, - ) + } + Spacer(modifier = Modifier.height(16.dp)) + // This view has its own horizontal padding + QuickSettings( + modifier = Modifier.sysuiResTag("expanded_qs_scroll_view"), + viewModel.qsSceneAdapter, + ) + } + } + AnimatedVisibility( + visible = !isCustomizing, + modifier = Modifier.align(Alignment.CenterHorizontally).fillMaxWidth() + ) { + QuickSettingsTheme { + // This view has its own horizontal padding + // TODO(b/321716470) This should use a lifecycle tied to the scene. + FooterActions( + viewModel = footerActionsViewModel, + qsVisibilityLifecycleOwner = lifecycleOwner, + modifier = Modifier.element(QuickSettings.Elements.FooterActions) + ) } - Spacer(modifier = Modifier.height(16.dp)) - QuickSettings( - modifier = Modifier.fillMaxHeight(), - viewModel.qsSceneAdapter, - ) } } HeadsUpNotificationSpace( diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsTheme.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsTheme.kt new file mode 100644 index 000000000000..87b6f95b0ae6 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsTheme.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.ui.composable + +import android.view.ContextThemeWrapper +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import com.android.systemui.res.R + +@Composable +fun QuickSettingsTheme(content: @Composable () -> Unit) { + val context = LocalContext.current + val themedContext = + remember(context) { ContextThemeWrapper(context, R.style.Theme_SystemUI_QuickSettings) } + CompositionLocalProvider(LocalContext provides themedContext) { content() } +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/bottombar/BottomBarModule.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/bottombar/BottomBarModule.kt new file mode 100644 index 000000000000..43d545368536 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/bottombar/BottomBarModule.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.panel.component.bottombar + +import com.android.systemui.volume.panel.component.bottombar.ui.BottomBarComponent +import com.android.systemui.volume.panel.component.shared.model.VolumePanelComponents +import com.android.systemui.volume.panel.domain.AlwaysAvailableCriteria +import com.android.systemui.volume.panel.domain.ComponentAvailabilityCriteria +import com.android.systemui.volume.panel.shared.model.VolumePanelUiComponent +import dagger.Binds +import dagger.Module +import dagger.multibindings.IntoMap +import dagger.multibindings.StringKey + +@Module +interface BottomBarModule { + + @Binds + @IntoMap + @StringKey(VolumePanelComponents.BOTTOM_BAR) + fun bindMediaVolumeSliderComponent(component: BottomBarComponent): VolumePanelUiComponent + + @Binds + @IntoMap + @StringKey(VolumePanelComponents.BOTTOM_BAR) + fun bindComponentAvailabilityCriteria( + defaultCriteria: AlwaysAvailableCriteria + ): ComponentAvailabilityCriteria +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/bottombar/ui/BottomBarComponent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/bottombar/ui/BottomBarComponent.kt new file mode 100644 index 000000000000..03c07f714541 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/bottombar/ui/BottomBarComponent.kt @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.panel.component.bottombar.ui + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import com.android.compose.PlatformButton +import com.android.compose.PlatformOutlinedButton +import com.android.systemui.res.R +import com.android.systemui.volume.panel.component.bottombar.ui.viewmodel.BottomBarViewModel +import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope +import com.android.systemui.volume.panel.ui.composable.ComposeVolumePanelUiComponent +import com.android.systemui.volume.panel.ui.composable.VolumePanelComposeScope +import javax.inject.Inject + +@VolumePanelScope +class BottomBarComponent +@Inject +constructor( + private val viewModel: BottomBarViewModel, +) : ComposeVolumePanelUiComponent { + + @Composable + override fun VolumePanelComposeScope.Content(modifier: Modifier) { + Row( + modifier = modifier.height(48.dp).fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + PlatformOutlinedButton(onClick = viewModel::onSettingsClicked) { + Text(text = stringResource(R.string.volume_panel_dialog_settings_button)) + } + PlatformButton(onClick = viewModel::onDoneClicked) { + Text(text = stringResource(R.string.inline_done_button)) + } + } + } +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/ComposeVolumePanelUiComponent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/ComposeVolumePanelUiComponent.kt new file mode 100644 index 000000000000..e1834eea1434 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/ComposeVolumePanelUiComponent.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.panel.ui.composable + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.android.systemui.volume.panel.shared.model.VolumePanelUiComponent + +/** + * Compose implementation of [VolumePanelUiComponent]. Each new UI component should implement this + * interface. + */ +interface ComposeVolumePanelUiComponent : VolumePanelUiComponent { + + @Composable fun VolumePanelComposeScope.Content(modifier: Modifier) +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VerticalVolumePanelContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VerticalVolumePanelContent.kt new file mode 100644 index 000000000000..dcd22febbc86 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VerticalVolumePanelContent.kt @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.panel.ui.composable + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.android.systemui.volume.panel.ui.viewmodel.ComponentState + +@Composable +fun VolumePanelComposeScope.VerticalVolumePanelContent( + components: List<ComponentState>, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(20.dp), + ) { + for (component in components) { + AnimatedVisibility(component.isVisible) { + with(component.component as ComposeVolumePanelUiComponent) { Content(Modifier) } + } + } + } +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelComposeScope.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelComposeScope.kt new file mode 100644 index 000000000000..c70c6b1ad861 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelComposeScope.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.panel.ui.composable + +import android.content.res.Configuration.Orientation +import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelState + +class VolumePanelComposeScope(private val state: VolumePanelState) { + + /** + * Layout orientation of the panel. It doesn't necessarily aligns with the device orientation, + * because in some cases we want to show bigger version of a portrait orientation when the + * device is in landscape. + */ + @Orientation + val orientation: Int + get() = state.orientation +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelRoot.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelRoot.kt new file mode 100644 index 000000000000..3487184d36b4 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelRoot.kt @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.panel.ui.composable + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.MutableTransitionState +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.clickable +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.dimensionResource +import com.android.compose.theme.PlatformTheme +import com.android.systemui.res.R +import com.android.systemui.volume.panel.ui.layout.ComponentsLayout +import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelState +import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelViewModel + +@Composable +fun VolumePanelRoot( + viewModel: VolumePanelViewModel, + onDismissAnimationFinished: () -> Unit, + modifier: Modifier = Modifier, +) { + PlatformTheme(isSystemInDarkTheme()) { + val state: VolumePanelState by viewModel.volumePanelState.collectAsState() + val components by viewModel.componentsLayout.collectAsState(null) + + val transitionState = + remember { MutableTransitionState(false) }.apply { targetState = state.isVisible } + + LaunchedEffect(transitionState.targetState, transitionState.isIdle) { + if (!transitionState.targetState && transitionState.isIdle) { + onDismissAnimationFinished() + } + } + + Column( + modifier = + modifier + .fillMaxSize() + .statusBarsPadding() + .clickable(onClick = { viewModel.dismissPanel() }), + verticalArrangement = Arrangement.Bottom, + ) { + AnimatedVisibility( + visibleState = transitionState, + enter = slideInVertically { it }, + exit = slideOutVertically { it }, + ) { + val radius = dimensionResource(R.dimen.volume_panel_corner_radius) + Surface( + shape = RoundedCornerShape(topStart = radius, topEnd = radius), + color = MaterialTheme.colorScheme.surfaceBright, + ) { + Column { + components?.let { componentsState -> + with(VolumePanelComposeScope(state)) { Components(componentsState) } + } + } + } + } + } + } +} + +@Composable +private fun VolumePanelComposeScope.Components(state: ComponentsLayout) { + if (orientation == Configuration.ORIENTATION_PORTRAIT) { + VerticalVolumePanelContent( + components = state.contentComponents, + modifier = Modifier.padding(dimensionResource(R.dimen.volume_panel_content_padding)), + ) + } else { + TODO("Add landscape layout") + } + + val horizontalPadding = dimensionResource(R.dimen.volume_panel_bottom_bar_horizontal_padding) + if (state.bottomBarComponent.isVisible) { + with(state.bottomBarComponent.component as ComposeVolumePanelUiComponent) { + Content( + Modifier.navigationBarsPadding() + .padding( + start = horizontalPadding, + end = horizontalPadding, + bottom = dimensionResource(R.dimen.volume_panel_bottom_bar_bottom_padding), + ) + ) + } + } +} diff --git a/packages/SystemUI/customization/Android.bp b/packages/SystemUI/customization/Android.bp index 1d1849680040..81b5bd43bdbd 100644 --- a/packages/SystemUI/customization/Android.bp +++ b/packages/SystemUI/customization/Android.bp @@ -34,15 +34,19 @@ android_library { "PluginCoreLib", "SystemUIPluginLib", "SystemUIUnfoldLib", - "androidx.dynamicanimation_dynamicanimation", + "kotlinx_coroutines", + "dagger2", + "jsr330", + ], + libs: [ + // Keep android-specific libraries as libs instead of static_libs, so that they don't break + // things when included as transitive dependencies in robolectric targets. "androidx.concurrent_concurrent-futures", + "androidx.dynamicanimation_dynamicanimation", "androidx.lifecycle_lifecycle-runtime-ktx", "androidx.lifecycle_lifecycle-viewmodel-ktx", "androidx.recyclerview_recyclerview", "kotlinx_coroutines_android", - "kotlinx_coroutines", - "dagger2", - "jsr330", ], resource_dirs: [ "res", diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/AccessibilityQsShortcutsRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/AccessibilityQsShortcutsRepositoryImplTest.kt new file mode 100644 index 000000000000..9287edf4ee51 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/AccessibilityQsShortcutsRepositoryImplTest.kt @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.accessibility.data.repository + +import android.provider.Settings +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.util.settings.FakeSettings +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class AccessibilityQsShortcutsRepositoryImplTest : SysuiTestCase() { + private val testDispatcher = StandardTestDispatcher() + private val testScope = TestScope(testDispatcher) + private val secureSettings = FakeSettings() + + private val userA11yQsShortcutsRepositoryFactory = + object : UserA11yQsShortcutsRepository.Factory { + override fun create(userId: Int): UserA11yQsShortcutsRepository { + return UserA11yQsShortcutsRepository( + userId, + secureSettings, + testScope.backgroundScope, + testDispatcher, + ) + } + } + + private val underTest = + AccessibilityQsShortcutsRepositoryImpl(userA11yQsShortcutsRepositoryFactory) + + @Test + fun a11yQsShortcutTargetsForCorrectUsers() = + testScope.runTest { + val user0 = 0 + val targetsForUser0 = setOf("a", "b", "c") + val user1 = 1 + val targetsForUser1 = setOf("A") + val targetsFromUser0 by collectLastValue(underTest.a11yQsShortcutTargets(user0)) + val targetsFromUser1 by collectLastValue(underTest.a11yQsShortcutTargets(user1)) + + storeA11yQsShortcutTargetsForUser(targetsForUser0, user0) + storeA11yQsShortcutTargetsForUser(targetsForUser1, user1) + + assertThat(targetsFromUser0).isEqualTo(targetsForUser0) + assertThat(targetsFromUser1).isEqualTo(targetsForUser1) + } + + private fun storeA11yQsShortcutTargetsForUser(a11yQsTargets: Set<String>, forUser: Int) { + secureSettings.putStringForUser( + SETTING_NAME, + a11yQsTargets.joinToString(separator = ":"), + forUser + ) + } + + companion object { + private const val SETTING_NAME = Settings.Secure.ACCESSIBILITY_QS_TARGETS + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/UserA11yQsShortcutsRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/UserA11yQsShortcutsRepositoryTest.kt new file mode 100644 index 000000000000..ce22e288e292 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/UserA11yQsShortcutsRepositoryTest.kt @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.accessibility.data.repository + +import android.provider.Settings +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.util.settings.FakeSettings +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class UserA11yQsShortcutsRepositoryTest : SysuiTestCase() { + private val secureSettings = FakeSettings() + private val testDispatcher = StandardTestDispatcher() + private val testScope = TestScope(testDispatcher) + + private val underTest = + UserA11yQsShortcutsRepository( + USER_ID, + secureSettings, + testScope.backgroundScope, + testDispatcher + ) + + @Test + fun targetsMatchesSetting() = + testScope.runTest { + val observedTargets by collectLastValue(underTest.targets) + val a11yQsTargets = setOf("a", "b", "c") + secureSettings.putStringForUser( + SETTING_NAME, + a11yQsTargets.joinToString(SEPARATOR), + USER_ID + ) + + assertThat(observedTargets).isEqualTo(a11yQsTargets) + } + + companion object { + private const val USER_ID = 0 + private const val SEPARATOR = ":" + private const val SETTING_NAME = Settings.Secure.ACCESSIBILITY_QS_TARGETS + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractorTest.kt index 8d6d052b8769..a862112b56e8 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractorTest.kt @@ -37,6 +37,8 @@ import com.android.systemui.biometrics.shared.model.DisplayRotation.ROTATION_90 import com.android.systemui.biometrics.shared.model.FingerprintSensorType import com.android.systemui.biometrics.shared.model.SensorStrength import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.keyguard.data.repository.biometricSettingsRepository +import com.android.systemui.keyguard.data.repository.fakeBiometricSettingsRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.KeyguardState @@ -112,6 +114,7 @@ class SideFpsSensorInteractorTest : SysuiTestCase() { windowManager, displayStateInteractor, Optional.of(fingerprintInteractiveToAuthProvider), + kosmos.biometricSettingsRepository, kosmos.keyguardTransitionInteractor, SideFpsLogger(logcatLogBuffer("SfpsLogger")) ) @@ -420,6 +423,7 @@ class SideFpsSensorInteractorTest : SysuiTestCase() { @Test fun isProlongedTouchRequiredForAuthentication_dependsOnSettingsToggle() = testScope.runTest { + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true) val isEnabled by collectLastValue(underTest.isProlongedTouchRequiredForAuthentication) setupFingerprint(FingerprintSensorType.POWER_BUTTON) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalRepositoryImplTest.kt index 81d5344ed264..bd9ca3035a07 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalRepositoryImplTest.kt @@ -16,24 +16,28 @@ package com.android.systemui.communal.data.repository +import android.content.pm.UserInfo import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.systemui.Flags.FLAG_COMMUNAL_HUB import com.android.systemui.SysuiTestCase import com.android.systemui.communal.shared.model.CommunalSceneKey import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.flags.FakeFeatureFlagsClassic import com.android.systemui.flags.Flags -import com.android.systemui.scene.data.repository.SceneContainerRepository +import com.android.systemui.flags.fakeFeatureFlagsClassic +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope import com.android.systemui.scene.data.repository.sceneContainerRepository -import com.android.systemui.scene.shared.flag.FakeSceneContainerFlags +import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel import com.android.systemui.testKosmos +import com.android.systemui.user.data.repository.FakeUserRepository +import com.android.systemui.user.data.repository.fakeUserRepository +import com.android.systemui.util.settings.FakeSettings import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test @@ -44,19 +48,23 @@ import org.junit.runner.RunWith class CommunalRepositoryImplTest : SysuiTestCase() { private lateinit var underTest: CommunalRepositoryImpl - private val testDispatcher = StandardTestDispatcher() - private val testScope = TestScope(testDispatcher) + private lateinit var secureSettings: FakeSettings + private lateinit var userRepository: FakeUserRepository - private lateinit var featureFlagsClassic: FakeFeatureFlagsClassic - private lateinit var sceneContainerRepository: SceneContainerRepository + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val sceneContainerRepository = kosmos.sceneContainerRepository @Before fun setUp() { - val kosmos = testKosmos() - sceneContainerRepository = kosmos.sceneContainerRepository - featureFlagsClassic = FakeFeatureFlagsClassic() + secureSettings = FakeSettings() + userRepository = kosmos.fakeUserRepository - featureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true) + val listOfUserInfo = listOf(MAIN_USER_INFO) + userRepository.setUserInfos(listOfUserInfo) + + kosmos.fakeFeatureFlagsClassic.apply { set(Flags.COMMUNAL_SERVICE_ENABLED, true) } + mSetFlagsRule.enableFlags(FLAG_COMMUNAL_HUB) underTest = createRepositoryImpl(false) } @@ -64,9 +72,13 @@ class CommunalRepositoryImplTest : SysuiTestCase() { private fun createRepositoryImpl(sceneContainerEnabled: Boolean): CommunalRepositoryImpl { return CommunalRepositoryImpl( testScope.backgroundScope, - featureFlagsClassic, - FakeSceneContainerFlags(enabled = sceneContainerEnabled), + testScope.backgroundScope, + kosmos.testDispatcher, + kosmos.fakeFeatureFlagsClassic, + kosmos.fakeSceneContainerFlags.apply { enabled = sceneContainerEnabled }, sceneContainerRepository, + kosmos.fakeUserRepository, + secureSettings, ) } @@ -147,4 +159,29 @@ class CommunalRepositoryImplTest : SysuiTestCase() { assertThat(transitionState) .isEqualTo(ObservableCommunalTransitionState.Idle(CommunalSceneKey.DEFAULT)) } + + @Test + fun communalEnabledState_false_whenGlanceableHubSettingFalse() = + testScope.runTest { + userRepository.setSelectedUserInfo(MAIN_USER_INFO) + secureSettings.putIntForUser(GLANCEABLE_HUB_ENABLED, 0, MAIN_USER_INFO.id) + + val communalEnabled by collectLastValue(underTest.communalEnabledState) + assertThat(communalEnabled).isFalse() + } + + @Test + fun communalEnabledState_true_whenGlanceableHubSettingTrue() = + testScope.runTest { + userRepository.setSelectedUserInfo(MAIN_USER_INFO) + secureSettings.putIntForUser(GLANCEABLE_HUB_ENABLED, 1, MAIN_USER_INFO.id) + + val communalEnabled by collectLastValue(underTest.communalEnabledState) + assertThat(communalEnabled).isTrue() + } + + companion object { + private const val GLANCEABLE_HUB_ENABLED = "glanceable_hub_enabled" + private val MAIN_USER_INFO = UserInfo(0, "primary", UserInfo.FLAG_MAIN) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt index 86279ef24ca7..1b7117f41bbb 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt @@ -124,6 +124,7 @@ class CommunalInteractorTest : SysuiTestCase() { keyguardRepository.setIsEncryptedOrLockdown(false) userRepository.setSelectedUserInfo(mainUser) + keyguardRepository.setKeyguardShowing(true) runCurrent() assertThat(isAvailable).isTrue() @@ -150,12 +151,27 @@ class CommunalInteractorTest : SysuiTestCase() { keyguardRepository.setIsEncryptedOrLockdown(false) userRepository.setSelectedUserInfo(secondaryUser) + keyguardRepository.setKeyguardShowing(true) runCurrent() assertThat(isAvailable).isFalse() } @Test + fun isCommunalAvailable_whenDreaming_true() = + testScope.runTest { + val isAvailable by collectLastValue(underTest.isCommunalAvailable) + assertThat(isAvailable).isFalse() + + keyguardRepository.setIsEncryptedOrLockdown(false) + userRepository.setSelectedUserInfo(mainUser) + keyguardRepository.setDreaming(true) + runCurrent() + + assertThat(isAvailable).isTrue() + } + + @Test fun updateAppWidgetHostActive_uponStorageUnlockAsMainUser_true() = testScope.runTest { collectLastValue(underTest.isCommunalAvailable) @@ -163,6 +179,7 @@ class CommunalInteractorTest : SysuiTestCase() { keyguardRepository.setIsEncryptedOrLockdown(false) userRepository.setSelectedUserInfo(mainUser) + keyguardRepository.setKeyguardShowing(true) runCurrent() assertThat(widgetRepository.isHostActive()).isTrue() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/CommunalTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/CommunalTouchHandlerTest.java new file mode 100644 index 000000000000..74c197075461 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/CommunalTouchHandlerTest.java @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.dreams.touch; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.verify; + +import android.view.GestureDetector; +import android.view.MotionEvent; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; +import com.android.systemui.shared.system.InputChannelCompat; +import com.android.systemui.statusbar.NotificationShadeWindowController; +import com.android.systemui.statusbar.phone.CentralSurfaces; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import java.util.Optional; + +@SmallTest +@RunWith(AndroidJUnit4.class) +public class CommunalTouchHandlerTest extends SysuiTestCase { + @Mock + CentralSurfaces mCentralSurfaces; + @Mock + NotificationShadeWindowController mNotificationShadeWindowController; + @Mock + DreamTouchHandler.TouchSession mTouchSession; + CommunalTouchHandler mTouchHandler; + + private static final int INITIATION_WIDTH = 20; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + mTouchHandler = new CommunalTouchHandler( + Optional.of(mCentralSurfaces), + mNotificationShadeWindowController, + INITIATION_WIDTH); + } + + @Test + public void testSessionStartForcesShadeOpen() { + mTouchHandler.onSessionStart(mTouchSession); + verify(mNotificationShadeWindowController).setForcePluginOpen(true, mTouchHandler); + } + + @Test + public void testEventPropagation() { + final MotionEvent motionEvent = Mockito.mock(MotionEvent.class); + + final ArgumentCaptor<InputChannelCompat.InputEventListener> + inputEventListenerArgumentCaptor = + ArgumentCaptor.forClass(InputChannelCompat.InputEventListener.class); + + mTouchHandler.onSessionStart(mTouchSession); + verify(mTouchSession).registerInputListener(inputEventListenerArgumentCaptor.capture()); + inputEventListenerArgumentCaptor.getValue().onInputEvent(motionEvent); + verify(mCentralSurfaces).handleDreamTouch(motionEvent); + } + + @Test + public void testTouchPilferingOnScroll() { + final MotionEvent motionEvent1 = Mockito.mock(MotionEvent.class); + final MotionEvent motionEvent2 = Mockito.mock(MotionEvent.class); + + final ArgumentCaptor<GestureDetector.OnGestureListener> gestureListenerArgumentCaptor = + ArgumentCaptor.forClass(GestureDetector.OnGestureListener.class); + + mTouchHandler.onSessionStart(mTouchSession); + verify(mTouchSession).registerGestureListener(gestureListenerArgumentCaptor.capture()); + + assertThat(gestureListenerArgumentCaptor.getValue() + .onScroll(motionEvent1, motionEvent2, 1, 1)) + .isTrue(); + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SeekableSliderHapticPluginTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SeekableSliderHapticPluginTest.kt new file mode 100644 index 000000000000..ea766f8ea9bb --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/haptics/slider/SeekableSliderHapticPluginTest.kt @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.haptics.slider + +import android.widget.SeekBar +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testScope +import com.android.systemui.statusbar.VibratorHelper +import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.time.fakeSystemClock +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mock +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule + +@SmallTest +@RunWith(AndroidJUnit4::class) +@OptIn(ExperimentalCoroutinesApi::class) +class SeekableSliderHapticPluginTest : SysuiTestCase() { + + private val kosmos = Kosmos() + + @Rule @JvmField val mMockitoRule: MockitoRule = MockitoJUnit.rule() + @Mock private lateinit var vibratorHelper: VibratorHelper + private val seekBar = SeekBar(mContext) + private lateinit var plugin: SeekableSliderHapticPlugin + + @Before + fun setup() { + whenever(vibratorHelper.getPrimitiveDurations(anyInt())).thenReturn(intArrayOf(0)) + } + + @Test + fun start_beginsTrackingSlider() = runOnStartedPlugin { assertThat(plugin.isTracking).isTrue() } + + @Test + fun stop_stopsTrackingSlider() = runOnStartedPlugin { + // WHEN called to stop + plugin.stop() + + // THEN stops tracking + assertThat(plugin.isTracking).isFalse() + } + + @Test + fun start_afterStop_startsTheTrackingAgain() = runOnStartedPlugin { + // WHEN the plugin is restarted + plugin.stop() + plugin.start() + + // THEN the tracking begins again + assertThat(plugin.isTracking).isTrue() + } + + @Test + fun onKeyDown_startsWaiting() = runOnStartedPlugin { + // WHEN a keyDown event is recorded + plugin.onKeyDown() + + // THEN the timer starts waiting + assertThat(plugin.isKeyUpTimerWaiting).isTrue() + } + + @Test + fun keyUpWaitComplete_triggersOnArrowUp() = runOnStartedPlugin { + // GIVEN an onKeyDown that starts the wait and a program progress change that advances the + // slider state to ARROW_HANDLE_MOVED_ONCE + plugin.onKeyDown() + plugin.onProgressChanged(seekBar, 50, false) + testScheduler.runCurrent() + assertThat(plugin.trackerState).isEqualTo(SliderState.ARROW_HANDLE_MOVED_ONCE) + + // WHEN the key-up wait completes after the timeout plus a small buffer + advanceTimeBy(KEY_UP_TIMEOUT + 10L) + + // THEN the onArrowUp event is delivered causing the slider tracker to move to IDLE + assertThat(plugin.trackerState).isEqualTo(SliderState.IDLE) + assertThat(plugin.isKeyUpTimerWaiting).isFalse() + } + + @Test + fun onKeyDown_whileWaiting_restartsWait() = runOnStartedPlugin { + // GIVEN an onKeyDown that starts the wait and a program progress change that advances the + // slider state to ARROW_HANDLE_MOVED_ONCE + plugin.onKeyDown() + plugin.onProgressChanged(seekBar, 50, false) + testScheduler.runCurrent() + assertThat(plugin.trackerState).isEqualTo(SliderState.ARROW_HANDLE_MOVED_ONCE) + + // WHEN half the timeout period has elapsed and a new keyDown event occurs + advanceTimeBy(KEY_UP_TIMEOUT / 2) + plugin.onKeyDown() + + // AFTER advancing by a period of time that should have complete the original wait + advanceTimeBy(KEY_UP_TIMEOUT / 2 + 10L) + + // THEN the timer is still waiting and the slider tracker remains on ARROW_HANDLE_MOVED_ONCE + assertThat(plugin.isKeyUpTimerWaiting).isTrue() + assertThat(plugin.trackerState).isEqualTo(SliderState.ARROW_HANDLE_MOVED_ONCE) + } + + private fun runOnStartedPlugin(test: suspend TestScope.() -> Unit) = + with(kosmos) { + testScope.runTest { + createPlugin(this, UnconfinedTestDispatcher(testScheduler)) + // GIVEN that the plugin is started + plugin.start() + + // THEN run the test + test() + } + } + + private fun createPlugin(scope: CoroutineScope, dispatcher: CoroutineDispatcher) { + plugin = + SeekableSliderHapticPlugin( + vibratorHelper, + kosmos.fakeSystemClock, + dispatcher, + scope, + ) + } + + companion object { + private const val KEY_UP_TIMEOUT = 100L + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt index cceb76725615..6cc680baf938 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt @@ -25,10 +25,14 @@ import androidx.test.filters.SmallTest import com.android.systemui.Flags as AConfigFlags import com.android.systemui.Flags.FLAG_NEW_AOD_TRANSITION import com.android.systemui.SysuiTestCase +import com.android.systemui.communal.data.repository.communalRepository +import com.android.systemui.communal.shared.model.CommunalSceneKey +import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState import com.android.systemui.coroutines.collectLastValue import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep @@ -43,6 +47,7 @@ import com.android.systemui.util.ui.stopAnimating import com.android.systemui.util.ui.value import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -55,7 +60,9 @@ class KeyguardRootViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository + private val keyguardInteractor = kosmos.keyguardInteractor private val keyguardRepository = kosmos.fakeKeyguardRepository + private val communalRepository = kosmos.communalRepository private val screenOffAnimationController = kosmos.screenOffAnimationController private val deviceEntryRepository = kosmos.fakeDeviceEntryRepository private val notificationsKeyguardInteractor = kosmos.notificationsKeyguardInteractor @@ -220,7 +227,25 @@ class KeyguardRootViewModelTest : SysuiTestCase() { } @Test - fun alpha_glanceableHubOpen_isZero() = + fun alpha_idleOnHub_isZero() = + testScope.runTest { + val alpha by collectLastValue(underTest.alpha) + + // Hub transition state is idle with hub open. + communalRepository.setTransitionState( + flowOf(ObservableCommunalTransitionState.Idle(CommunalSceneKey.Communal)) + ) + runCurrent() + + // Set keyguard alpha to 1.0f. + keyguardInteractor.setAlpha(1.0f) + + // Alpha property remains 0 regardless. + assertThat(alpha).isEqualTo(0f) + } + + @Test + fun alpha_transitionToHub_isZero() = testScope.runTest { val alpha by collectLastValue(underTest.alpha) @@ -234,7 +259,7 @@ class KeyguardRootViewModelTest : SysuiTestCase() { } @Test - fun alpha_glanceableHubClosed_isOne() = + fun alpha_transitionFromHubToLockscreen_isOne() = testScope.runTest { val alpha by collectLastValue(underTest.alpha) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModelTest.kt new file mode 100644 index 000000000000..2fe4ef78bfdc --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModelTest.kt @@ -0,0 +1,189 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.ui.viewmodel + +import android.content.applicationContext +import android.hardware.biometrics.BiometricFingerprintConstants +import android.os.PowerManager +import android.testing.TestableLooper +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.Flags +import com.android.systemui.SysuiTestCase +import com.android.systemui.biometrics.data.repository.fakeFingerprintPropertyRepository +import com.android.systemui.biometrics.domain.interactor.displayStateInteractor +import com.android.systemui.biometrics.domain.interactor.sideFpsSensorInteractor +import com.android.systemui.biometrics.fakeFingerprintInteractiveToAuthProvider +import com.android.systemui.biometrics.shared.model.FingerprintSensorType +import com.android.systemui.biometrics.shared.model.SensorStrength +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.deviceentry.domain.interactor.deviceEntryFingerprintAuthInteractor +import com.android.systemui.keyguard.data.repository.fakeBiometricSettingsRepository +import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor +import com.android.systemui.keyguard.shared.model.AcquiredFingerprintAuthenticationStatus +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionState +import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.power.data.repository.fakePowerRepository +import com.android.systemui.power.domain.interactor.powerInteractor +import com.android.systemui.res.R +import com.android.systemui.statusbar.phone.dozeServiceHost +import com.android.systemui.testKosmos +import com.android.systemui.util.mockito.mock +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.verify + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@TestableLooper.RunWithLooper +@RunWith(AndroidJUnit4::class) +class SideFpsProgressBarViewModelTest : SysuiTestCase() { + private val kosmos = testKosmos() + private lateinit var underTest: SideFpsProgressBarViewModel + private val testScope = kosmos.testScope + private lateinit var mTestableLooper: TestableLooper + + @Before + fun setup() { + mTestableLooper = TestableLooper.get(this) + allowTestableLooperAsMainThread() + } + + private suspend fun setupRestToUnlockEnabled() { + mSetFlagsRule.enableFlags(Flags.FLAG_REST_TO_UNLOCK) + overrideResource(R.bool.config_restToUnlockSupported, true) + kosmos.fakeFingerprintPropertyRepository.setProperties( + 1, + SensorStrength.STRONG, + FingerprintSensorType.POWER_BUTTON, + mutableMapOf(Pair("sensor", mock())) + ) + kosmos.fakeFingerprintInteractiveToAuthProvider.enabledForCurrentUser.value = true + kosmos.fakeKeyguardTransitionRepository.sendTransitionStep( + TransitionStep( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.AOD, + value = 0.0f, + transitionState = TransitionState.STARTED + ) + ) + kosmos.fakeKeyguardTransitionRepository.sendTransitionStep( + TransitionStep( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.AOD, + value = 1.0f, + transitionState = TransitionState.FINISHED + ) + ) + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true) + } + + @Test + fun whenConfigDisabled_featureIsDisabled() = + testScope.runTest { + overrideResource(R.bool.config_restToUnlockSupported, false) + underTest = createViewModel() + val enabled by collectLastValue(underTest.isProlongedTouchRequiredForAuthentication) + + assertThat(enabled).isFalse() + } + + @Test + fun whenConfigEnabledSensorIsPowerButtonAndSettingsToggleIsEnabled_featureIsEnabled() = + testScope.runTest { + overrideResource(R.bool.config_restToUnlockSupported, true) + underTest = createViewModel() + val enabled by collectLastValue(underTest.isProlongedTouchRequiredForAuthentication) + + assertThat(enabled).isFalse() + kosmos.fakeFingerprintPropertyRepository.setProperties( + 1, + SensorStrength.STRONG, + FingerprintSensorType.POWER_BUTTON, + mutableMapOf(Pair("sensor", mock())) + ) + assertThat(enabled).isFalse() + + kosmos.fakeFingerprintInteractiveToAuthProvider.enabledForCurrentUser.value = true + kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true) + + runCurrent() + assertThat(enabled).isTrue() + } + + @Test + fun whenFingerprintAcquiredStartsWhenNotDozing_wakesUpDevice() = + testScope.runTest { + setupRestToUnlockEnabled() + underTest = createViewModel() + + kosmos.fakeKeyguardRepository.setIsDozing(false) + kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( + AcquiredFingerprintAuthenticationStatus( + BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START + ) + ) + + runCurrent() + + assertThat(kosmos.fakePowerRepository.lastWakeReason) + .isEqualTo(PowerManager.WAKE_REASON_BIOMETRIC) + } + + @Test + fun whenFingerprintAcquiredStartsWhenDozing_pulsesAod() = + testScope.runTest { + setupRestToUnlockEnabled() + underTest = createViewModel() + + kosmos.fakeKeyguardRepository.setIsDozing(true) + kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( + AcquiredFingerprintAuthenticationStatus( + BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_START + ) + ) + + runCurrent() + + verify(kosmos.dozeServiceHost).fireSideFpsAcquisitionStarted() + } + + private fun createViewModel() = + SideFpsProgressBarViewModel( + kosmos.applicationContext, + kosmos.deviceEntryFingerprintAuthInteractor, + kosmos.sideFpsSensorInteractor, + kosmos.dozeServiceHost, + kosmos.keyguardInteractor, + kosmos.displayStateInteractor, + kosmos.testDispatcher, + kosmos.applicationCoroutineScope, + kosmos.powerInteractor, + ) +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/A11yShortcutAutoAddableListTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/A11yShortcutAutoAddableListTest.kt new file mode 100644 index 000000000000..311122d7f8d5 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/A11yShortcutAutoAddableListTest.kt @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.pipeline.domain.autoaddable + +import android.content.ComponentName +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.view.accessibility.Flags +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.internal.accessibility.AccessibilityShortcutController +import com.android.systemui.SysuiTestCase +import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.tiles.ColorCorrectionTile +import com.android.systemui.qs.tiles.ColorInversionTile +import com.android.systemui.qs.tiles.OneHandedModeTile +import com.android.systemui.qs.tiles.ReduceBrightColorsTile +import com.android.systemui.util.mockito.mock +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class A11yShortcutAutoAddableListTest : SysuiTestCase() { + + private val factory = + object : A11yShortcutAutoAddable.Factory { + override fun create( + spec: TileSpec, + componentName: ComponentName + ): A11yShortcutAutoAddable { + return A11yShortcutAutoAddable(mock(), mock(), spec, componentName) + } + } + + @Test + @DisableFlags(Flags.FLAG_A11Y_QS_SHORTCUT) + fun getA11yShortcutAutoAddables_withA11yQsShortcutFlagOff_emptyResult() { + val autoAddables = A11yShortcutAutoAddableList.getA11yShortcutAutoAddables(factory) + + assertThat(autoAddables).isEmpty() + } + + @Test + @EnableFlags(Flags.FLAG_A11Y_QS_SHORTCUT) + fun getA11yShortcutAutoAddables_withA11yQsShortcutFlagOn_correctAutoAddables() { + val expected = + setOf( + factory.create( + TileSpec.create(ColorCorrectionTile.TILE_SPEC), + AccessibilityShortcutController.DALTONIZER_COMPONENT_NAME + ), + factory.create( + TileSpec.create(ColorInversionTile.TILE_SPEC), + AccessibilityShortcutController.COLOR_INVERSION_COMPONENT_NAME + ), + factory.create( + TileSpec.create(OneHandedModeTile.TILE_SPEC), + AccessibilityShortcutController.ONE_HANDED_COMPONENT_NAME + ), + factory.create( + TileSpec.create(ReduceBrightColorsTile.TILE_SPEC), + AccessibilityShortcutController.REDUCE_BRIGHT_COLORS_COMPONENT_NAME + ), + ) + + val autoAddables = A11yShortcutAutoAddableList.getA11yShortcutAutoAddables(factory) + + assertThat(autoAddables).isNotEmpty() + assertThat(autoAddables).containsExactlyElementsIn(expected) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/A11yShortcutAutoAddableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/A11yShortcutAutoAddableTest.kt new file mode 100644 index 000000000000..3b33a43d9341 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/domain/autoaddable/A11yShortcutAutoAddableTest.kt @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.pipeline.domain.autoaddable + +import android.content.ComponentName +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.accessibility.data.repository.FakeAccessibilityQsShortcutsRepository +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.coroutines.collectValues +import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal +import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking +import com.android.systemui.qs.pipeline.shared.TileSpec +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class A11yShortcutAutoAddableTest : SysuiTestCase() { + private val testDispatcher = StandardTestDispatcher() + private val testScope = TestScope(testDispatcher) + + private val a11yQsShortcutsRepository = FakeAccessibilityQsShortcutsRepository() + private val underTest = + A11yShortcutAutoAddable(a11yQsShortcutsRepository, testDispatcher, SPEC, TARGET_COMPONENT) + + @Test + fun settingNotSet_noSignal() = + testScope.runTest { + val signal by collectLastValue(underTest.autoAddSignal(USER_ID)) + + assertThat(signal).isNull() // null means no emitted value + } + + @Test + fun settingSetWithTarget_addSignal() = + testScope.runTest { + val signal by collectLastValue(underTest.autoAddSignal(USER_ID)) + assertThat(signal).isNull() + + a11yQsShortcutsRepository.setA11yQsShortcutTargets( + USER_ID, + setOf(TARGET_COMPONENT_FLATTEN) + ) + + assertThat(signal).isEqualTo(AutoAddSignal.Add(SPEC)) + } + + @Test + fun settingSetWithoutTarget_removeSignal() = + testScope.runTest { + val signal by collectLastValue(flow = underTest.autoAddSignal(USER_ID)) + assertThat(signal).isNull() + + a11yQsShortcutsRepository.setA11yQsShortcutTargets( + USER_ID, + setOf(OTHER_COMPONENT_FLATTEN) + ) + + assertThat(signal).isEqualTo(AutoAddSignal.Remove(SPEC)) + } + + @Test + fun settingSetWithMultipleComponents_containsTarget_addSignal() = + testScope.runTest { + val signal by collectLastValue(underTest.autoAddSignal(USER_ID)) + assertThat(signal).isNull() + + a11yQsShortcutsRepository.setA11yQsShortcutTargets( + USER_ID, + setOf(OTHER_COMPONENT_FLATTEN, TARGET_COMPONENT_FLATTEN) + ) + + assertThat(signal).isEqualTo(AutoAddSignal.Add(SPEC)) + } + + @Test + fun settingSetWithMultipleComponents_doesNotContainTarget_removeSignal() = + testScope.runTest { + val signal by collectLastValue(underTest.autoAddSignal(USER_ID)) + assertThat(signal).isNull() + + a11yQsShortcutsRepository.setA11yQsShortcutTargets( + USER_ID, + setOf(OTHER_COMPONENT_FLATTEN, OTHER_COMPONENT_FLATTEN) + ) + + assertThat(signal).isEqualTo(AutoAddSignal.Remove(SPEC)) + } + + @Test + fun multipleChangesWithTarget_onlyOneAddSignal() = + testScope.runTest { + val signals by collectValues(underTest.autoAddSignal(USER_ID)) + assertThat(signals).isEmpty() + + repeat(3) { + a11yQsShortcutsRepository.setA11yQsShortcutTargets( + USER_ID, + setOf(TARGET_COMPONENT_FLATTEN) + ) + } + + assertThat(signals.size).isEqualTo(1) + assertThat(signals[0]).isEqualTo(AutoAddSignal.Add(SPEC)) + } + + @Test + fun multipleChangesWithoutTarget_onlyOneRemoveSignal() = + testScope.runTest { + val signals by collectValues(underTest.autoAddSignal(USER_ID)) + assertThat(signals).isEmpty() + + repeat(3) { + a11yQsShortcutsRepository.setA11yQsShortcutTargets( + USER_ID, + setOf("$OTHER_COMPONENT_FLATTEN$it") + ) + } + + assertThat(signals.size).isEqualTo(1) + assertThat(signals[0]).isEqualTo(AutoAddSignal.Remove(SPEC)) + } + + @Test + fun settingSetWithTargetForUsers_onlySignalInThatUser() = + testScope.runTest { + val otherUserId = USER_ID + 1 + val signalTargetUser by collectLastValue(underTest.autoAddSignal(USER_ID)) + val signalOtherUser by collectLastValue(underTest.autoAddSignal(otherUserId)) + assertThat(signalTargetUser).isNull() + assertThat(signalOtherUser).isNull() + + a11yQsShortcutsRepository.setA11yQsShortcutTargets( + USER_ID, + setOf(TARGET_COMPONENT_FLATTEN) + ) + + assertThat(signalTargetUser).isEqualTo(AutoAddSignal.Add(SPEC)) + assertThat(signalOtherUser).isNull() + } + + @Test + fun strategyAlways() { + assertThat(underTest.autoAddTracking).isEqualTo(AutoAddTracking.Always) + } + + companion object { + private val SPEC = TileSpec.create("spec") + private val TARGET_COMPONENT = ComponentName("FakePkgName", "FakeClassName") + private val TARGET_COMPONENT_FLATTEN = TARGET_COMPONENT.flattenToString() + private val OTHER_COMPONENT_FLATTEN = + ComponentName("FakePkgName", "OtherClassName").flattenToString() + private const val USER_ID = 0 + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt index d9b1ea1aedcc..cae20d006dec 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt @@ -16,12 +16,16 @@ package com.android.systemui.qs.ui.adapter +import android.content.res.Configuration import android.os.Bundle +import android.view.Surface import android.view.View import androidx.asynclayoutinflater.view.AsyncLayoutInflater import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository +import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor import com.android.systemui.coroutines.collectLastValue import com.android.systemui.qs.QSImpl import com.android.systemui.qs.dagger.QSComponent @@ -34,6 +38,7 @@ import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.nullable import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat +import java.util.Locale import javax.inject.Provider import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher @@ -81,11 +86,17 @@ class QSSceneAdapterImplTest : SysuiTestCase() { .also { components.add(it) } } } + private val configuration = Configuration(context.resources.configuration) + + private val fakeConfigurationRepository = + FakeConfigurationRepository().apply { onConfigurationChange(configuration) } + private val configurationInteractor = ConfigurationInteractor(fakeConfigurationRepository) private val mockAsyncLayoutInflater = mock<AsyncLayoutInflater>() { whenever(inflate(anyInt(), nullable(), any())).then { invocation -> val mockView = mock<View>() + whenever(mockView.context).thenReturn(context) invocation .getArgument<AsyncLayoutInflater.OnInflateFinishedListener>(2) .onInflateFinished( @@ -102,6 +113,7 @@ class QSSceneAdapterImplTest : SysuiTestCase() { qsImplProvider, testDispatcher, testScope.backgroundScope, + configurationInteractor, { mockAsyncLayoutInflater }, ) @@ -297,6 +309,9 @@ class QSSceneAdapterImplTest : SysuiTestCase() { @Test fun reinflation_previousStateDestroyed() = testScope.runTest { + // Run all flows... In particular, initial configuration propagation that could cause + // QSImpl to re-inflate. + runCurrent() val qsImpl by collectLastValue(underTest.qsImpl) underTest.inflate(context) @@ -322,4 +337,81 @@ class QSSceneAdapterImplTest : SysuiTestCase() { bundleArgCaptor.value, ) } + + @Test + fun changeInLocale_reinflation() = + testScope.runTest { + val qsImpl by collectLastValue(underTest.qsImpl) + + underTest.inflate(context) + runCurrent() + + val oldQsImpl = qsImpl!! + + val newLocale = + if (configuration.locales[0] == Locale("en-US")) { + Locale("es-UY") + } else { + Locale("en-US") + } + configuration.setLocale(newLocale) + fakeConfigurationRepository.onConfigurationChange(configuration) + runCurrent() + + assertThat(oldQsImpl).isNotSameInstanceAs(qsImpl!!) + } + + @Test + fun changeInFontSize_reinflation() = + testScope.runTest { + val qsImpl by collectLastValue(underTest.qsImpl) + + underTest.inflate(context) + runCurrent() + + val oldQsImpl = qsImpl!! + + configuration.fontScale *= 2 + fakeConfigurationRepository.onConfigurationChange(configuration) + runCurrent() + + assertThat(oldQsImpl).isNotSameInstanceAs(qsImpl!!) + } + + @Test + fun changeInAssetPath_reinflation() = + testScope.runTest { + val qsImpl by collectLastValue(underTest.qsImpl) + + underTest.inflate(context) + runCurrent() + + val oldQsImpl = qsImpl!! + + configuration.assetsSeq += 1 + fakeConfigurationRepository.onConfigurationChange(configuration) + runCurrent() + + assertThat(oldQsImpl).isNotSameInstanceAs(qsImpl!!) + } + + @Test + fun otherChangesInConfiguration_noReinflation_configurationChangeDispatched() = + testScope.runTest { + val qsImpl by collectLastValue(underTest.qsImpl) + + underTest.inflate(context) + runCurrent() + + val oldQsImpl = qsImpl!! + configuration.densityDpi *= 2 + configuration.windowConfiguration.maxBounds.scale(2f) + configuration.windowConfiguration.rotation = Surface.ROTATION_270 + fakeConfigurationRepository.onConfigurationChange(configuration) + runCurrent() + + assertThat(oldQsImpl).isSameInstanceAs(qsImpl!!) + verify(qsImpl!!).onConfigurationChanged(configuration) + verify(qsImpl!!.view).dispatchConfigurationChanged(configuration) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt index d7a794149869..42200a3d33ec 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt @@ -23,6 +23,8 @@ import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.FakeFeatureFlagsClassic import com.android.systemui.flags.Flags import com.android.systemui.kosmos.testScope +import com.android.systemui.qs.FooterActionsController +import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel import com.android.systemui.qs.ui.adapter.FakeQSSceneAdapter import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Direction @@ -39,12 +41,16 @@ import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsVi import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnectivityRepository import com.android.systemui.testKosmos +import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Mockito.times +import org.mockito.Mockito.verify @SmallTest @RunWith(AndroidJUnit4::class) @@ -56,6 +62,12 @@ class QuickSettingsSceneViewModelTest : SysuiTestCase() { private val mobileIconsInteractor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock()) private val flags = FakeFeatureFlagsClassic().also { it.set(Flags.NEW_NETWORK_SLICE_UI, false) } private val qsFlexiglassAdapter = FakeQSSceneAdapter { mock() } + private val footerActionsViewModel = mock<FooterActionsViewModel>() + private val footerActionsViewModelFactory = + mock<FooterActionsViewModel.Factory> { + whenever(create(any())).thenReturn(footerActionsViewModel) + } + private val footerActionsController = mock<FooterActionsController>() private var mobileIconsViewModel: MobileIconsViewModel = MobileIconsViewModel( @@ -94,6 +106,8 @@ class QuickSettingsSceneViewModelTest : SysuiTestCase() { shadeHeaderViewModel = shadeHeaderViewModel, qsSceneAdapter = qsFlexiglassAdapter, notifications = kosmos.notificationsPlaceholderViewModel, + footerActionsViewModelFactory, + footerActionsController, ) } @@ -125,4 +139,12 @@ class QuickSettingsSceneViewModelTest : SysuiTestCase() { ) ) } + + @Test + fun gettingViewModelInitializesControllerOnlyOnce() { + underTest.getFooterActionsViewModel(mock()) + underTest.getFooterActionsViewModel(mock()) + + verify(footerActionsController, times(1)).init() + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/WindowRootViewVisibilityInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/WindowRootViewVisibilityInteractorTest.kt index f23716ccca54..d5e43f44426b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/WindowRootViewVisibilityInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/WindowRootViewVisibilityInteractorTest.kt @@ -25,10 +25,13 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.shared.model.StatusBarState +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testScope import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest import com.android.systemui.power.domain.interactor.PowerInteractorFactory import com.android.systemui.scene.data.repository.WindowRootViewVisibilityRepository +import com.android.systemui.scene.shared.flag.sceneContainerFlags import com.android.systemui.statusbar.NotificationPresenter import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository import com.android.systemui.statusbar.notification.data.repository.setActiveNotifs @@ -44,7 +47,6 @@ import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Test @@ -56,7 +58,8 @@ import org.mockito.Mockito.verify @RunWith(AndroidJUnit4::class) class WindowRootViewVisibilityInteractorTest : SysuiTestCase() { - private val testScope = TestScope() + private val kosmos = Kosmos() + private val testScope = kosmos.testScope private val testDispatcher = StandardTestDispatcher() private val iStatusBarService = mock<IStatusBarService>() private val executor = FakeExecutor(FakeSystemClock()) @@ -79,6 +82,8 @@ class WindowRootViewVisibilityInteractorTest : SysuiTestCase() { headsUpManager, powerInteractor, activeNotificationsInteractor, + kosmos.sceneContainerFlags, + kosmos::sceneInteractor, ) .apply { setUp(notificationPresenter, notificationsController) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ShadeControllerSceneImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ShadeControllerSceneImplTest.kt new file mode 100644 index 000000000000..51b834207cfd --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ShadeControllerSceneImplTest.kt @@ -0,0 +1,202 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shade + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository +import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor +import com.android.systemui.flags.Flags +import com.android.systemui.flags.fakeFeatureFlagsClassic +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testCase +import com.android.systemui.kosmos.testScope +import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags +import com.android.systemui.scene.shared.model.ObservableTransitionState +import com.android.systemui.scene.shared.model.SceneKey +import com.android.systemui.scene.shared.model.SceneModel +import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.statusbar.CommandQueue +import com.android.systemui.util.mockito.mock +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.never +import org.mockito.Mockito.times +import org.mockito.Mockito.verify + +@ExperimentalCoroutinesApi +@SmallTest +@RunWith(AndroidJUnit4::class) +class ShadeControllerSceneImplTest : SysuiTestCase() { + private val kosmos = Kosmos() + private val testScope = kosmos.testScope + private val sceneInteractor = kosmos.sceneInteractor + private val deviceEntryInteractor = kosmos.deviceEntryInteractor + + private lateinit var shadeInteractor: ShadeInteractor + private lateinit var underTest: ShadeControllerSceneImpl + + @Before + fun setup() { + kosmos.testCase = this + kosmos.fakeSceneContainerFlags.enabled = true + kosmos.fakeFeatureFlagsClassic.apply { + set(Flags.FULL_SCREEN_USER_SWITCHER, false) + set(Flags.NSSL_DEBUG_LINES, false) + set(Flags.FULL_SCREEN_USER_SWITCHER, false) + } + kosmos.fakeDeviceEntryRepository.setUnlocked(true) + testScope.runCurrent() + shadeInteractor = kosmos.shadeInteractor + underTest = kosmos.shadeControllerSceneImpl + } + + @Test + fun animateCollapseShade_noForceNoExpansion() = + testScope.runTest { + // GIVEN shade is collapsed and a post-collapse action is enqueued + val testRunnable = mock<Runnable>() + setDeviceEntered(true) + setCollapsed() + underTest.addPostCollapseAction(testRunnable) + + // WHEN a collapse is requested + underTest.animateCollapseShade(0, force = false, delayed = false, 1f) + runCurrent() + + // THEN the shade remains collapsed and the post-collapse action ran + assertThat(sceneInteractor.desiredScene.value.key).isEqualTo(SceneKey.Gone) + verify(testRunnable, times(1)).run() + } + + @Test + fun animateCollapseShade_expandedExcludeFlagOn() = + testScope.runTest { + // GIVEN shade is fully expanded and a post-collapse action is enqueued + val testRunnable = mock<Runnable>() + underTest.addPostCollapseAction(testRunnable) + setDeviceEntered(true) + setShadeFullyExpanded() + + // WHEN a collapse is requested with FLAG_EXCLUDE_NOTIFICATION_PANEL + underTest.animateCollapseShade(CommandQueue.FLAG_EXCLUDE_NOTIFICATION_PANEL) + runCurrent() + + // THEN the shade remains expanded and the post-collapse action did not run + assertThat(sceneInteractor.desiredScene.value.key).isEqualTo(SceneKey.Shade) + assertThat(shadeInteractor.isAnyFullyExpanded.value).isTrue() + verify(testRunnable, never()).run() + } + + @Test + fun animateCollapseShade_locked() = + testScope.runTest { + // GIVEN shade is fully expanded on lockscreen + setDeviceEntered(false) + setShadeFullyExpanded() + + // WHEN a collapse is requested + underTest.animateCollapseShade() + runCurrent() + + // THEN the shade collapses back to lockscreen and the post-collapse action ran + assertThat(sceneInteractor.desiredScene.value.key).isEqualTo(SceneKey.Lockscreen) + } + + @Test + fun animateCollapseShade_unlocked() = + testScope.runTest { + // GIVEN shade is fully expanded on an unlocked device + setDeviceEntered(true) + setShadeFullyExpanded() + + // WHEN a collapse is requested + underTest.animateCollapseShade() + runCurrent() + + // THEN the shade collapses back to lockscreen and the post-collapse action ran + assertThat(sceneInteractor.desiredScene.value.key).isEqualTo(SceneKey.Gone) + } + + @Test + fun onCollapseShade_runPostCollapseActionsCalled() = + testScope.runTest { + // GIVEN shade is expanded and a post-collapse action is enqueued + val testRunnable = mock<Runnable>() + setShadeFullyExpanded() + underTest.addPostCollapseAction(testRunnable) + + // WHEN shade collapses + setCollapsed() + + // THEN post-collapse action ran + verify(testRunnable, times(1)).run() + } + + @Test + fun postOnShadeExpanded() = + testScope.runTest { + // GIVEN shade is collapsed and a post-collapse action is enqueued + val testRunnable = mock<Runnable>() + setCollapsed() + underTest.postOnShadeExpanded(testRunnable) + + // WHEN shade expands + setShadeFullyExpanded() + + // THEN post-collapse action ran + verify(testRunnable, times(1)).run() + } + + private fun setScene(key: SceneKey) { + sceneInteractor.changeScene(SceneModel(key), "test") + sceneInteractor.setTransitionState( + MutableStateFlow<ObservableTransitionState>(ObservableTransitionState.Idle(key)) + ) + testScope.runCurrent() + } + + private fun setDeviceEntered(isEntered: Boolean) { + setScene( + if (isEntered) { + SceneKey.Gone + } else { + SceneKey.Lockscreen + } + ) + assertThat(deviceEntryInteractor.isDeviceEntered.value).isEqualTo(isEntered) + } + + private fun setCollapsed() { + setScene(SceneKey.Gone) + assertThat(shadeInteractor.isAnyExpanded.value).isFalse() + } + + private fun setShadeFullyExpanded() { + setScene(SceneKey.Shade) + assertThat(shadeInteractor.isAnyFullyExpanded.value).isTrue() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt index 0a10b2c85ebe..0c7ce970cf3a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerTest.kt @@ -16,11 +16,10 @@ package com.android.systemui.statusbar.notification.collection.render +import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager -import com.android.systemui.flags.FakeFeatureFlagsClassic -import com.android.systemui.flags.Flags import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder import com.android.systemui.statusbar.notification.collection.ListEntry import com.android.systemui.statusbar.notification.collection.NotifPipeline @@ -34,18 +33,19 @@ import com.google.common.truth.Truth.assertThat import org.junit.Assert.assertThrows import org.junit.Before import org.junit.Test +import org.junit.runner.RunWith import org.mockito.Mockito.never import org.mockito.Mockito.verify import org.mockito.Mockito.verifyNoMoreInteractions import org.mockito.Mockito.`when` as whenever @SmallTest +@RunWith(AndroidJUnit4::class) class GroupExpansionManagerTest : SysuiTestCase() { - private lateinit var gem: GroupExpansionManagerImpl + private lateinit var underTest: GroupExpansionManagerImpl private val dumpManager: DumpManager = mock() private val groupMembershipManager: GroupMembershipManager = mock() - private val featureFlags = FakeFeatureFlagsClassic() private val pipeline: NotifPipeline = mock() private lateinit var beforeRenderListListener: OnBeforeRenderListListener @@ -85,79 +85,57 @@ class GroupExpansionManagerTest : SysuiTestCase() { whenever(groupMembershipManager.getGroupSummary(summary1)).thenReturn(summary1) whenever(groupMembershipManager.getGroupSummary(summary2)).thenReturn(summary2) - gem = GroupExpansionManagerImpl(dumpManager, groupMembershipManager, featureFlags) + underTest = GroupExpansionManagerImpl(dumpManager, groupMembershipManager) } @Test - fun testNotifyOnlyOnChange_enabled() { - featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, true) - + fun notifyOnlyOnChange() { var listenerCalledCount = 0 - gem.registerGroupExpansionChangeListener { _, _ -> listenerCalledCount++ } + underTest.registerGroupExpansionChangeListener { _, _ -> listenerCalledCount++ } - gem.setGroupExpanded(summary1, false) + underTest.setGroupExpanded(summary1, false) assertThat(listenerCalledCount).isEqualTo(0) - gem.setGroupExpanded(summary1, true) + underTest.setGroupExpanded(summary1, true) assertThat(listenerCalledCount).isEqualTo(1) - gem.setGroupExpanded(summary2, true) - assertThat(listenerCalledCount).isEqualTo(2) - gem.setGroupExpanded(summary1, true) + underTest.setGroupExpanded(summary2, true) assertThat(listenerCalledCount).isEqualTo(2) - gem.setGroupExpanded(summary2, false) - assertThat(listenerCalledCount).isEqualTo(3) - } - - @Test - fun testNotifyOnlyOnChange_disabled() { - featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, false) - - var listenerCalledCount = 0 - gem.registerGroupExpansionChangeListener { _, _ -> listenerCalledCount++ } - - gem.setGroupExpanded(summary1, false) - assertThat(listenerCalledCount).isEqualTo(1) - gem.setGroupExpanded(summary1, true) + underTest.setGroupExpanded(summary1, true) assertThat(listenerCalledCount).isEqualTo(2) - gem.setGroupExpanded(summary2, true) + underTest.setGroupExpanded(summary2, false) assertThat(listenerCalledCount).isEqualTo(3) - gem.setGroupExpanded(summary1, true) - assertThat(listenerCalledCount).isEqualTo(4) - gem.setGroupExpanded(summary2, false) - assertThat(listenerCalledCount).isEqualTo(5) } @Test - fun testExpandUnattachedEntry() { - featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, true) - + fun expandUnattachedEntry() { // First, expand the entry when it is attached. - gem.setGroupExpanded(summary1, true) - assertThat(gem.isGroupExpanded(summary1)).isTrue() + underTest.setGroupExpanded(summary1, true) + assertThat(underTest.isGroupExpanded(summary1)).isTrue() // Un-attach it, and un-expand it. NotificationEntryBuilder.setNewParent(summary1, null) - gem.setGroupExpanded(summary1, false) + underTest.setGroupExpanded(summary1, false) // Expanding again should throw. - assertThrows(IllegalArgumentException::class.java) { gem.setGroupExpanded(summary1, true) } + assertThrows(IllegalArgumentException::class.java) { + underTest.setGroupExpanded(summary1, true) + } } @Test - fun testSyncWithPipeline() { - featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, true) - gem.attach(pipeline) + fun syncWithPipeline() { + underTest.attach(pipeline) beforeRenderListListener = withArgCaptor { verify(pipeline).addOnBeforeRenderListListener(capture()) } val listener: OnGroupExpansionChangeListener = mock() - gem.registerGroupExpansionChangeListener(listener) + underTest.registerGroupExpansionChangeListener(listener) beforeRenderListListener.onBeforeRenderList(entries) verify(listener, never()).onGroupExpansionChange(any(), any()) // Expand one of the groups. - gem.setGroupExpanded(summary1, true) + underTest.setGroupExpanded(summary1, true) verify(listener).onGroupExpansionChange(summary1.row, true) // Empty the pipeline list and verify that the group is no longer expanded. diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerTest.kt index c1ffa641c6a4..2cbcc5a8d925 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerTest.kt @@ -16,67 +16,35 @@ package com.android.systemui.statusbar.notification.collection.render +import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.flags.FakeFeatureFlagsClassic -import com.android.systemui.flags.Flags import com.android.systemui.statusbar.notification.collection.GroupEntry import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder import com.google.common.truth.Truth.assertThat -import org.junit.Before import org.junit.Test +import org.junit.runner.RunWith @SmallTest +@RunWith(AndroidJUnit4::class) class GroupMembershipManagerTest : SysuiTestCase() { - private lateinit var gmm: GroupMembershipManagerImpl - - private val featureFlags = FakeFeatureFlagsClassic() - - @Before - fun setUp() { - gmm = GroupMembershipManagerImpl(featureFlags) - } + private var underTest = GroupMembershipManagerImpl() @Test - fun testIsChildInGroup_topLevel() { - featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, false) + fun isChildInGroup_topLevel() { val topLevelEntry = NotificationEntryBuilder().setParent(GroupEntry.ROOT_ENTRY).build() - assertThat(gmm.isChildInGroup(topLevelEntry)).isFalse() - } - - @Test - fun testIsChildInGroup_noParent_old() { - featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, false) - val noParentEntry = NotificationEntryBuilder().setParent(null).build() - assertThat(gmm.isChildInGroup(noParentEntry)).isTrue() + assertThat(underTest.isChildInGroup(topLevelEntry)).isFalse() } @Test - fun testIsChildInGroup_noParent_new() { - featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, true) + fun isChildInGroup_noParent() { val noParentEntry = NotificationEntryBuilder().setParent(null).build() - assertThat(gmm.isChildInGroup(noParentEntry)).isFalse() - } - @Test - fun testIsChildInGroup_summary_old() { - featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, false) - - val groupKey = "group" - val summary = - NotificationEntryBuilder() - .setGroup(mContext, groupKey) - .setGroupSummary(mContext, true) - .build() - GroupEntryBuilder().setKey(groupKey).setSummary(summary).build() - - assertThat(gmm.isChildInGroup(summary)).isTrue() + assertThat(underTest.isChildInGroup(noParentEntry)).isFalse() } @Test - fun testIsChildInGroup_summary_new() { - featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, true) - + fun isChildInGroup_summary() { val groupKey = "group" val summary = NotificationEntryBuilder() @@ -85,27 +53,17 @@ class GroupMembershipManagerTest : SysuiTestCase() { .build() GroupEntryBuilder().setKey(groupKey).setSummary(summary).build() - assertThat(gmm.isChildInGroup(summary)).isFalse() + assertThat(underTest.isChildInGroup(summary)).isFalse() } @Test - fun testIsChildInGroup_child() { - featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, false) - val childEntry = NotificationEntryBuilder().build() - assertThat(gmm.isChildInGroup(childEntry)).isTrue() - } - - @Test - fun testIsGroupSummary_topLevelEntry() { - featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, true) + fun isGroupSummary_topLevelEntry() { val entry = NotificationEntryBuilder().setParent(GroupEntry.ROOT_ENTRY).build() - assertThat(gmm.isGroupSummary(entry)).isFalse() + assertThat(underTest.isGroupSummary(entry)).isFalse() } @Test - fun testIsGroupSummary_summary() { - featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, true) - + fun isGroupSummary_summary() { val groupKey = "group" val summary = NotificationEntryBuilder() @@ -114,13 +72,11 @@ class GroupMembershipManagerTest : SysuiTestCase() { .build() GroupEntryBuilder().setKey(groupKey).setSummary(summary).build() - assertThat(gmm.isGroupSummary(summary)).isTrue() + assertThat(underTest.isGroupSummary(summary)).isTrue() } @Test - fun testIsGroupSummary_child() { - featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, true) - + fun isGroupSummary_child() { val groupKey = "group" val summary = NotificationEntryBuilder() @@ -130,20 +86,17 @@ class GroupMembershipManagerTest : SysuiTestCase() { val entry = NotificationEntryBuilder().setGroup(mContext, groupKey).build() GroupEntryBuilder().setKey(groupKey).setSummary(summary).addChild(entry).build() - assertThat(gmm.isGroupSummary(entry)).isFalse() + assertThat(underTest.isGroupSummary(entry)).isFalse() } @Test - fun testGetGroupSummary_topLevelEntry() { - featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, true) + fun getGroupSummary_topLevelEntry() { val entry = NotificationEntryBuilder().setParent(GroupEntry.ROOT_ENTRY).build() - assertThat(gmm.getGroupSummary(entry)).isNull() + assertThat(underTest.getGroupSummary(entry)).isNull() } @Test - fun testGetGroupSummary_summary() { - featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, true) - + fun getGroupSummary_summary() { val groupKey = "group" val summary = NotificationEntryBuilder() @@ -152,13 +105,11 @@ class GroupMembershipManagerTest : SysuiTestCase() { .build() GroupEntryBuilder().setKey(groupKey).setSummary(summary).build() - assertThat(gmm.getGroupSummary(summary)).isEqualTo(summary) + assertThat(underTest.getGroupSummary(summary)).isEqualTo(summary) } @Test - fun testGetGroupSummary_child() { - featureFlags.set(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE, true) - + fun getGroupSummary_child() { val groupKey = "group" val summary = NotificationEntryBuilder() @@ -168,6 +119,6 @@ class GroupMembershipManagerTest : SysuiTestCase() { val entry = NotificationEntryBuilder().setGroup(mContext, groupKey).build() GroupEntryBuilder().setKey(groupKey).setSummary(summary).addChild(entry).build() - assertThat(gmm.getGroupSummary(entry)).isEqualTo(summary) + assertThat(underTest.getGroupSummary(entry)).isEqualTo(summary) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt index 00a86ffc5a8f..cc4ebd4aa4c3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ActivityStarterImplTest.kt @@ -1,15 +1,17 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file - * except in compliance with the License. You may obtain a copy of the License at + * 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. + * 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.phone @@ -18,9 +20,9 @@ import android.app.PendingIntent import android.content.Intent import android.os.RemoteException import android.os.UserHandle -import android.testing.AndroidTestingRunner import android.view.View import android.widget.FrameLayout +import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.ActivityIntentHelper @@ -66,7 +68,7 @@ import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @SmallTest -@RunWith(AndroidTestingRunner::class) +@RunWith(AndroidJUnit4::class) class ActivityStarterImplTest : SysuiTestCase() { @Mock private lateinit var centralSurfaces: CentralSurfaces @Mock private lateinit var assistManager: AssistManager @@ -139,7 +141,7 @@ class ActivityStarterImplTest : SysuiTestCase() { } @Test - fun startPendingIntentMaybeDismissingKeyguard_keyguardShowing_showOverLockscreen_activityLaunchAnimator() { + fun startPendingIntentMaybeDismissingKeyguard_keyguardShowing_showOverLs_launchAnimator() { val pendingIntent = mock(PendingIntent::class.java) val parent = FrameLayout(context) val view = @@ -214,7 +216,7 @@ class ActivityStarterImplTest : SysuiTestCase() { mainExecutor.runAllReady() verify(deviceProvisionedController).isDeviceProvisioned - verify(shadeController).runPostCollapseRunnables() + verify(shadeController).collapseShadeForActivityStart() } @Test @@ -226,7 +228,7 @@ class ActivityStarterImplTest : SysuiTestCase() { mainExecutor.runAllReady() verify(deviceProvisionedController).isDeviceProvisioned - verify(shadeController, never()).runPostCollapseRunnables() + verify(shadeController, never()).collapseShadeForActivityStart() } @Test diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java index 3d9645a3d983..b1736b16875d 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java @@ -227,5 +227,10 @@ public interface VolumeDialogController { void onCaptionEnabledStateChanged(Boolean isEnabled, Boolean checkBeforeSwitch); // requires version 2 void onShowCsdWarning(@AudioManager.CsdWarning int csdWarning, int durationMs); + + /** + * Callback function for when the volume changed due to a physical key press. + */ + void onVolumeChangedFromKey(); } } diff --git a/packages/SystemUI/res/layout/internet_connectivity_dialog.xml b/packages/SystemUI/res/layout/internet_connectivity_dialog.xml index 16eba220cf5d..1365a11d7a56 100644 --- a/packages/SystemUI/res/layout/internet_connectivity_dialog.xml +++ b/packages/SystemUI/res/layout/internet_connectivity_dialog.xml @@ -17,6 +17,7 @@ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" android:id="@+id/internet_connectivity_dialog" android:layout_width="@dimen/large_dialog_width" @@ -386,9 +387,8 @@ </LinearLayout> </LinearLayout> - <LinearLayout + <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/button_layout" - android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="8dp" @@ -398,53 +398,58 @@ android:clickable="false" android:focusable="false"> - <LinearLayout + <Button + android:id="@+id/apm_button" + style="@style/Widget.Dialog.Button.BorderButton" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_weight="1" - android:layout_gravity="start|center_vertical" - android:orientation="horizontal"> - <Button - android:id="@+id/apm_button" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="@string/turn_off_airplane_mode" - android:ellipsize="end" - android:maxLines="1" - style="@style/Widget.Dialog.Button.BorderButton" - android:clickable="true" - android:focusable="true"/> - - <Button - android:id="@+id/share_wifi_button" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="@string/share_wifi_button_text" - style="?android:attr/buttonBarNeutralButtonStyle" - android:maxLines="1" - android:ellipsize="end" - android:clickable="true" - android:focusable="true" - android:visibility="gone"/> - </LinearLayout> - - <LinearLayout + android:layout_marginEnd="10dp" + android:clickable="true" + android:ellipsize="end" + android:focusable="true" + android:maxLines="1" + android:text="@string/turn_off_airplane_mode" + app:layout_constrainedWidth="true" + app:layout_constraintHorizontal_bias="0" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@id/share_wifi_button" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <Button + android:id="@+id/share_wifi_button" + style="?android:attr/buttonBarNeutralButtonStyle" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginStart="16dp" - android:layout_gravity="end|center_vertical"> - <Button - android:id="@+id/done_button" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:text="@string/inline_done_button" - style="@style/Widget.Dialog.Button" - android:maxLines="1" - android:ellipsize="end" - android:clickable="true" - android:focusable="true"/> - </LinearLayout> - </LinearLayout> + android:layout_marginEnd="10dp" + android:clickable="true" + android:ellipsize="end" + android:focusable="true" + android:maxLines="1" + android:visibility="gone" + app:layout_constraintHorizontal_bias="0" + android:text="@string/share_wifi_button_text" + app:layout_constrainedWidth="true" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@id/done_button" + app:layout_constraintStart_toEndOf="@id/apm_button" + app:layout_constraintTop_toTopOf="parent" /> + + <Button + android:id="@+id/done_button" + style="@style/Widget.Dialog.Button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:clickable="true" + android:ellipsize="end" + android:focusable="true" + android:maxLines="1" + android:text="@string/inline_done_button" + app:layout_constrainedWidth="true" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + </androidx.constraintlayout.widget.ConstraintLayout> </LinearLayout> </androidx.core.widget.NestedScrollView> diff --git a/packages/SystemUI/res/layout/ongoing_privacy_chip.xml b/packages/SystemUI/res/layout/ongoing_privacy_chip.xml index 2c7467d726b4..fab7840a6a51 100644 --- a/packages/SystemUI/res/layout/ongoing_privacy_chip.xml +++ b/packages/SystemUI/res/layout/ongoing_privacy_chip.xml @@ -27,7 +27,7 @@ tools:parentTag="com.android.systemui.privacy.OngoingPrivacyChip"> > - <LinearLayout + <com.android.systemui.animation.view.LaunchableLinearLayout android:id="@+id/icons_container" android:layout_height="@dimen/ongoing_appops_chip_height" android:layout_width="wrap_content" diff --git a/packages/SystemUI/res/layout/super_notification_shade.xml b/packages/SystemUI/res/layout/super_notification_shade.xml index dca84b9fab7c..b792acc8b097 100644 --- a/packages/SystemUI/res/layout/super_notification_shade.xml +++ b/packages/SystemUI/res/layout/super_notification_shade.xml @@ -27,10 +27,11 @@ android:fitsSystemWindows="true"> <!-- Placeholder for the communal UI that will be replaced if the feature is enabled. --> - <ViewStub + <View android:id="@+id/communal_ui_stub" android:layout_width="match_parent" - android:layout_height="match_parent" /> + android:layout_height="match_parent" + android:visibility="gone" /> <com.android.systemui.scrim.ScrimView android:id="@+id/scrim_behind" diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index c630a7ff09b6..4209c1f6a732 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1728,6 +1728,12 @@ <dimen name="communal_grid_height">630dp</dimen> <!-- Number of columns for each communal card --> <integer name="communal_grid_columns_per_card">6</integer> + <!-- Width of area on right edge of screen in which swipes will open the communal hub --> + <dimen name="communal_right_edge_swipe_region_width">16dp</dimen> + <!-- Height of area at top of communal hub where swipes should open the notification shade --> + <dimen name="communal_top_edge_swipe_region_height">32dp</dimen> + <!-- Height of area at bottom of communal hub where swipes should open the bouncer --> + <dimen name="communal_bottom_edge_swipe_region_height">32dp</dimen> <dimen name="drag_and_drop_icon_size">70dp</dimen> @@ -1799,6 +1805,9 @@ <dimen name="dream_overlay_complication_smartspace_padding">24dp</dimen> <dimen name="dream_overlay_complication_smartspace_max_width">408dp</dimen> + <!-- The width of the swipe target to initiate opening communal hub over dreams. --> + <dimen name="communal_gesture_initiation_width">48dp</dimen> + <!-- The position of the end guide, which dream overlay complications can align their start with if their end is aligned with the parent end. Represented as the percentage over from the start of the parent container. --> @@ -1932,5 +1941,9 @@ <dimen name="bouncer_user_switcher_view_mode_view_flipper_bottom_margin">0dp</dimen> <!-- UDFPS view attributes --> - <dimen name="udfps_icon_size">6mm</dimen> + <!-- UDFPS icon size in microns/um --> + <dimen name="udfps_icon_size" format="float">6000</dimen> + <!-- Microns/ums (1000 um = 1mm) per pixel for the given device. If unspecified, UI that + relies on this value will not be sized correctly. --> + <item name="pixel_pitch" format="float" type="dimen">-1</item> </resources> diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml index ec4c7d5bf67e..8ec5ccd7a080 100644 --- a/packages/SystemUI/res/values/ids.xml +++ b/packages/SystemUI/res/values/ids.xml @@ -227,6 +227,7 @@ <item type="id" name="ambient_indication_container" /> <item type="id" name="status_view_media_container" /> <item type="id" name="smart_space_barrier_bottom" /> + <item type="id" name="weather_clock_date_and_icons_barrier_bottom" /> <!-- Privacy dialog --> <item type="id" name="privacy_dialog_close_app_button" /> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 9bc7681665f1..2b43360f0689 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -1089,6 +1089,8 @@ <string name="cta_label_to_open_widget_picker">Add more widgets</string> <!-- Text for the popup to be displayed after dismissing the CTA tile. [CHAR LIMIT=50] --> <string name="popup_on_dismiss_cta_tile_text">Long press to customize widgets</string> + <!-- Text for the button to configure widgets after long press. [CHAR LIMIT=50] --> + <string name="button_to_configure_widgets_text">Customize widgets</string> <!-- Label for the button which configures widgets [CHAR LIMIT=NONE] --> <string name="edit_widget">Edit widget</string> <!-- Description for the button that removes a widget on click. [CHAR LIMIT=50] --> diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/InputChannelCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/InputChannelCompat.java index 259cca8c01e2..9e92c939dbbc 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/InputChannelCompat.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/InputChannelCompat.java @@ -16,8 +16,11 @@ package com.android.systemui.shared.system; -import android.graphics.Matrix; +import static android.os.Trace.TRACE_TAG_INPUT; + import android.os.Looper; +import android.os.Trace; +import android.util.Log; import android.view.BatchedInputEventReceiver; import android.view.Choreographer; import android.view.InputChannel; @@ -52,23 +55,24 @@ public class InputChannelCompat { return target.addBatch(src); } - /** @see MotionEvent#createRotateMatrix */ - public static Matrix createRotationMatrix( - /*@Surface.Rotation*/ int rotation, int displayW, int displayH) { - return MotionEvent.createRotateMatrix(rotation, displayW, displayH); - } - /** * @see BatchedInputEventReceiver */ public static class InputEventReceiver { + private final String mName; private final BatchedInputEventReceiver mReceiver; + @Deprecated public InputEventReceiver(InputChannel inputChannel, Looper looper, Choreographer choreographer, final InputEventListener listener) { - mReceiver = new BatchedInputEventReceiver(inputChannel, looper, choreographer) { + this("unknown", inputChannel, looper, choreographer, listener); + } + public InputEventReceiver(String name, InputChannel inputChannel, Looper looper, + Choreographer choreographer, final InputEventListener listener) { + mName = name; + mReceiver = new BatchedInputEventReceiver(inputChannel, looper, choreographer) { @Override public void onInputEvent(InputEvent event) { listener.onInputEvent(event); @@ -89,6 +93,9 @@ public class InputChannelCompat { */ public void dispose() { mReceiver.dispose(); + Trace.instant(TRACE_TAG_INPUT, "InputMonitorCompat-" + mName + " receiver disposed"); + Log.d(InputMonitorCompat.TAG, "Input event receiver for monitor (" + mName + + ") disposed"); } } } diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/InputMonitorCompat.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/InputMonitorCompat.java index c4aac111f24c..78beaf76ea78 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/InputMonitorCompat.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/InputMonitorCompat.java @@ -17,8 +17,13 @@ package com.android.systemui.shared.system; import android.hardware.input.InputManagerGlobal; import android.os.Looper; +import android.os.Trace; +import android.util.Log; import android.view.Choreographer; import android.view.InputMonitor; +import android.view.SurfaceControl; + +import androidx.annotation.NonNull; import com.android.systemui.shared.system.InputChannelCompat.InputEventListener; import com.android.systemui.shared.system.InputChannelCompat.InputEventReceiver; @@ -27,14 +32,20 @@ import com.android.systemui.shared.system.InputChannelCompat.InputEventReceiver; * @see android.view.InputMonitor */ public class InputMonitorCompat { + static final String TAG = "InputMonitorCompat"; private final InputMonitor mInputMonitor; + private final String mName; /** * Monitor input on the specified display for gestures. */ - public InputMonitorCompat(String name, int displayId) { + public InputMonitorCompat(@NonNull String name, int displayId) { + mName = name + "-disp" + displayId; mInputMonitor = InputManagerGlobal.getInstance() .monitorGestureInput(name, displayId); + Trace.instant(Trace.TRACE_TAG_INPUT, "InputMonitorCompat-" + mName + " created"); + Log.d(TAG, "Input monitor (" + mName + ") created"); + } /** @@ -45,10 +56,19 @@ public class InputMonitorCompat { } /** + * @see InputMonitor#getSurface() + */ + public SurfaceControl getSurface() { + return mInputMonitor.getSurface(); + } + + /** * @see InputMonitor#dispose() */ public void dispose() { mInputMonitor.dispose(); + Trace.instant(Trace.TRACE_TAG_INPUT, "InputMonitorCompat-" + mName + " disposed"); + Log.d(TAG, "Input monitor (" + mName + ") disposed"); } /** @@ -56,7 +76,9 @@ public class InputMonitorCompat { */ public InputEventReceiver getInputReceiver(Looper looper, Choreographer choreographer, InputEventListener listener) { - return new InputEventReceiver(mInputMonitor.getInputChannel(), looper, choreographer, + Trace.instant(Trace.TRACE_TAG_INPUT, "InputMonitorCompat-" + mName + " receiver created"); + Log.d(TAG, "Input event receiver for monitor (" + mName + ") created"); + return new InputEventReceiver(mName, mInputMonitor.getInputChannel(), looper, choreographer, listener); } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java index 8e5d0dac7bef..ecce22315c50 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java @@ -37,6 +37,7 @@ import android.app.ActivityManager; import android.app.admin.DevicePolicyManager; import android.content.Intent; import android.content.res.ColorStateList; +import android.content.res.Configuration; import android.content.res.Resources; import android.hardware.biometrics.BiometricRequestConstants; import android.media.AudioManager; @@ -390,6 +391,11 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard mSecurityViewFlipperController.updateConstraints(useSplitBouncer); } } + + @Override + public void onConfigChanged(Configuration newConfig) { + configureMode(); + } }; private final KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback = new KeyguardUpdateMonitorCallback() { diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index fe96099b0824..536f3afdd575 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java @@ -114,7 +114,6 @@ import com.android.settingslib.WirelessUtils; import com.android.settingslib.fuelgauge.BatteryStatus; import com.android.systemui.CoreStartable; import com.android.systemui.Dumpable; -import com.android.systemui.Flags; import com.android.systemui.biometrics.AuthController; import com.android.systemui.biometrics.FingerprintInteractiveToAuthProvider; import com.android.systemui.broadcast.BroadcastDispatcher; @@ -383,7 +382,6 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab private List<SubscriptionInfo> mSubscriptionInfo; @VisibleForTesting protected int mFingerprintRunningState = BIOMETRIC_STATE_STOPPED; - private boolean mFingerprintDetectRunning; private boolean mIsDreaming; private boolean mLogoutEnabled; private int mActiveMobileDataSubscription = SubscriptionManager.INVALID_SUBSCRIPTION_ID; @@ -1005,7 +1003,6 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab final boolean wasCancellingRestarting = mFingerprintRunningState == BIOMETRIC_STATE_CANCELLING_RESTARTING; mFingerprintRunningState = BIOMETRIC_STATE_STOPPED; - mFingerprintDetectRunning = false; if (wasCancellingRestarting) { KeyguardUpdateMonitor.this.updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE); } else { @@ -1114,9 +1111,6 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab boolean wasRunning = mFingerprintRunningState == BIOMETRIC_STATE_RUNNING; boolean isRunning = fingerprintRunningState == BIOMETRIC_STATE_RUNNING; mFingerprintRunningState = fingerprintRunningState; - if (mFingerprintRunningState == BIOMETRIC_STATE_STOPPED) { - mFingerprintDetectRunning = false; - } mLogger.logFingerprintRunningState(mFingerprintRunningState); // Clients of KeyguardUpdateMonitor don't care about the internal state about the // asynchronousness of the cancel cycle. So only notify them if the actually running state @@ -1644,11 +1638,11 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab void setAssistantVisible(boolean assistantVisible) { mAssistantVisible = assistantVisible; mLogger.logAssistantVisible(mAssistantVisible); - if (getFaceAuthInteractor() != null) { - getFaceAuthInteractor().onAssistantTriggeredOnLockScreen(); - } updateFingerprintListeningState(BIOMETRIC_ACTION_UPDATE); if (mAssistantVisible) { + if (getFaceAuthInteractor() != null) { + getFaceAuthInteractor().onAssistantTriggeredOnLockScreen(); + } requestActiveUnlock( ActiveUnlockConfig.ActiveUnlockRequestOrigin.ASSISTANT, "assistant", @@ -2105,7 +2099,6 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab @VisibleForTesting void resetBiometricListeningState() { mFingerprintRunningState = BIOMETRIC_STATE_STOPPED; - mFingerprintDetectRunning = false; } @VisibleForTesting @@ -2544,10 +2537,8 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab return; } final boolean shouldListenForFingerprint = shouldListenForFingerprint(isUdfpsSupported()); - final boolean running = mFingerprintRunningState == BIOMETRIC_STATE_RUNNING; - final boolean runningOrRestarting = running + final boolean runningOrRestarting = mFingerprintRunningState == BIOMETRIC_STATE_RUNNING || mFingerprintRunningState == BIOMETRIC_STATE_CANCELLING_RESTARTING; - final boolean runDetect = shouldRunFingerprintDetect(); if (runningOrRestarting && !shouldListenForFingerprint) { if (action == BIOMETRIC_ACTION_START) { mLogger.v("Ignoring stopListeningForFingerprint()"); @@ -2559,24 +2550,10 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab mLogger.v("Ignoring startListeningForFingerprint()"); return; } - startListeningForFingerprint(runDetect); - } else if (running && runDetect && !mFingerprintDetectRunning) { - if (action == BIOMETRIC_ACTION_STOP) { - mLogger.v("Ignoring startListeningForFingerprint(detect)"); - return; - } - // stop running authentication and start running fingerprint detection - stopListeningForFingerprint(); - startListeningForFingerprint(true); + startListeningForFingerprint(); } } - private boolean shouldRunFingerprintDetect() { - return !isUnlockingWithFingerprintAllowed() - || (Flags.runFingerprintDetectOnDismissibleKeyguard() - && getUserCanSkipBouncer(mSelectedUserInteractor.getSelectedUserId())); - } - /** * If a user is encrypted or not. * This is NOT related to the lock screen being visible or not. @@ -2832,6 +2809,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab && biometricEnabledForUser && !isUserInLockdown(user); final boolean strongerAuthRequired = !isUnlockingWithFingerprintAllowed(); + final boolean isSideFps = isSfpsSupported() && isSfpsEnrolled(); final boolean shouldListenBouncerState = !strongerAuthRequired || !mPrimaryBouncerIsOrWillBeShowing; @@ -2894,7 +2872,7 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab } } - private void startListeningForFingerprint(boolean runDetect) { + private void startListeningForFingerprint() { final int userId = mSelectedUserInteractor.getSelectedUserId(); final boolean unlockPossible = isUnlockWithFingerprintPossible(userId); if (mFingerprintCancelSignal != null) { @@ -2924,20 +2902,18 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab mFingerprintInteractiveToAuthProvider.getVendorExtension(userId)); } - if (runDetect) { + if (!isUnlockingWithFingerprintAllowed()) { mLogger.v("startListeningForFingerprint - detect"); mFpm.detectFingerprint( mFingerprintCancelSignal, mFingerprintDetectionCallback, fingerprintAuthenticateOptions); - mFingerprintDetectRunning = true; } else { mLogger.v("startListeningForFingerprint"); mFpm.authenticate(null /* crypto */, mFingerprintCancelSignal, mFingerprintAuthenticationCallback, null /* handler */, fingerprintAuthenticateOptions); - mFingerprintDetectRunning = false; } setFingerprintRunningState(BIOMETRIC_STATE_RUNNING); } @@ -3962,7 +3938,6 @@ public class KeyguardUpdateMonitor implements TrustManager.TrustListener, Dumpab mSelectedUserInteractor.getSelectedUserId())); pw.println(" getUserUnlockedWithBiometric()=" + getUserUnlockedWithBiometric(mSelectedUserInteractor.getSelectedUserId())); - pw.println(" mFingerprintDetectRunning=" + mFingerprintDetectRunning); pw.println(" SIM States:"); for (SimData data : mSimDatas.values()) { pw.println(" " + data.toString()); diff --git a/packages/SystemUI/src/com/android/systemui/GuestResumeSessionReceiver.java b/packages/SystemUI/src/com/android/systemui/GuestResumeSessionReceiver.java index 0f5f869cba5d..43728260248a 100644 --- a/packages/SystemUI/src/com/android/systemui/GuestResumeSessionReceiver.java +++ b/packages/SystemUI/src/com/android/systemui/GuestResumeSessionReceiver.java @@ -20,11 +20,12 @@ import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.pm.UserInfo; +import android.os.HandlerExecutor; +import android.os.HandlerThread; import android.os.UserHandle; import androidx.annotation.NonNull; -import com.android.systemui.res.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.UiEventLogger; import com.android.systemui.GuestResetOrExitSessionReceiver.ResetSessionDialogFactory; @@ -32,6 +33,7 @@ import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.qs.QSUserSwitcherEvent; +import com.android.systemui.res.R; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.phone.SystemUIDialog; import com.android.systemui.statusbar.policy.UserSwitcherController; @@ -61,6 +63,7 @@ public class GuestResumeSessionReceiver { private final SecureSettings mSecureSettings; private final ResetSessionDialogFactory mResetSessionDialogFactory; private final GuestSessionNotification mGuestSessionNotification; + private final HandlerThread mHandlerThread; @VisibleForTesting public final UserTracker.Callback mUserChangedCallback = @@ -111,13 +114,16 @@ public class GuestResumeSessionReceiver { mSecureSettings = secureSettings; mGuestSessionNotification = guestSessionNotification; mResetSessionDialogFactory = resetSessionDialogFactory; + mHandlerThread = new HandlerThread("GuestResumeSessionReceiver"); + mHandlerThread.start(); } /** * Register this receiver with the {@link BroadcastDispatcher} */ public void register() { - mUserTracker.addCallback(mUserChangedCallback, mMainExecutor); + mUserTracker.addCallback(mUserChangedCallback, + new HandlerExecutor(mHandlerThread.getThreadHandler())); } private void cancelDialog() { diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityModule.kt b/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityModule.kt index 8c2d221e3f97..35f9344ae897 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityModule.kt +++ b/packages/SystemUI/src/com/android/systemui/accessibility/AccessibilityModule.kt @@ -16,6 +16,8 @@ package com.android.systemui.accessibility +import com.android.systemui.accessibility.data.repository.AccessibilityQsShortcutsRepository +import com.android.systemui.accessibility.data.repository.AccessibilityQsShortcutsRepositoryImpl import com.android.systemui.accessibility.data.repository.ColorCorrectionRepository import com.android.systemui.accessibility.data.repository.ColorCorrectionRepositoryImpl import com.android.systemui.accessibility.data.repository.ColorInversionRepository @@ -31,4 +33,9 @@ interface AccessibilityModule { @Binds fun colorInversionRepository(impl: ColorInversionRepositoryImpl): ColorInversionRepository + + @Binds + fun accessibilityQsShortcutsRepository( + impl: AccessibilityQsShortcutsRepositoryImpl + ): AccessibilityQsShortcutsRepository } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/AccessibilityQsShortcutsRepository.kt b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/AccessibilityQsShortcutsRepository.kt new file mode 100644 index 000000000000..401ac0f9337b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/AccessibilityQsShortcutsRepository.kt @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.accessibility.data.repository + +import android.util.SparseArray +import androidx.annotation.GuardedBy +import com.android.systemui.dagger.SysUISingleton +import javax.inject.Inject +import kotlinx.coroutines.flow.SharedFlow + +/** Provides data related to accessibility quick setting shortcut option. */ +interface AccessibilityQsShortcutsRepository { + /** + * Observable for the a11y features the user chooses in the Settings app to use the quick + * setting option. + */ + fun a11yQsShortcutTargets(userId: Int): SharedFlow<Set<String>> +} + +@SysUISingleton +class AccessibilityQsShortcutsRepositoryImpl +@Inject +constructor( + private val userA11yQsShortcutsRepositoryFactory: UserA11yQsShortcutsRepository.Factory, +) : AccessibilityQsShortcutsRepository { + + @GuardedBy("userA11yQsShortcutsRepositories") + private val userA11yQsShortcutsRepositories = SparseArray<UserA11yQsShortcutsRepository>() + + override fun a11yQsShortcutTargets(userId: Int): SharedFlow<Set<String>> { + return synchronized(userA11yQsShortcutsRepositories) { + if (userId !in userA11yQsShortcutsRepositories) { + val userA11yQsShortcutsRepository = + userA11yQsShortcutsRepositoryFactory.create(userId) + userA11yQsShortcutsRepositories.put(userId, userA11yQsShortcutsRepository) + } + userA11yQsShortcutsRepositories.get(userId).targets + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/UserA11yQsShortcutsRepository.kt b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/UserA11yQsShortcutsRepository.kt new file mode 100644 index 000000000000..ed91f03cc56e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/accessibility/data/repository/UserA11yQsShortcutsRepository.kt @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.accessibility.data.repository + +import android.provider.Settings +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository +import com.android.systemui.util.settings.SecureSettings +import com.android.systemui.util.settings.SettingsProxyExt.observerFlow +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.shareIn + +/** + * Single user version of [AccessibilityQsShortcutsRepository]. It provides a similar interface as + * [TileSpecRepository], but focusing solely on the user it was created for. It observes the changes + * on the [Settings.Secure.ACCESSIBILITY_QS_TARGETS] for a given user + */ +class UserA11yQsShortcutsRepository +@AssistedInject +constructor( + @Assisted private val userId: Int, + private val secureSettings: SecureSettings, + @Application private val applicationScope: CoroutineScope, + @Background private val backgroundDispatcher: CoroutineDispatcher, +) { + val targets = + secureSettings + .observerFlow(userId, SETTING_NAME) + // Force an update + .onStart { emit(Unit) } + .map { getA11yQsShortcutTargets(userId) } + .flowOn(backgroundDispatcher) + .shareIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + replay = 1 + ) + + private fun getA11yQsShortcutTargets(userId: Int): Set<String> { + val settingValue = secureSettings.getStringForUser(SETTING_NAME, userId) ?: "" + return settingValue.split(SETTING_SEPARATOR).filterNot { it.isEmpty() }.toSet() + } + + companion object { + const val SETTING_NAME = Settings.Secure.ACCESSIBILITY_QS_TARGETS + const val SETTING_SEPARATOR = ":" + } + + @AssistedFactory + interface Factory { + fun create( + userId: Int, + ): UserA11yQsShortcutsRepository + } +} diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt index 86f372a94848..71d0e7d6081a 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt @@ -35,7 +35,6 @@ import com.android.systemui.Flags.lightRevealMigration import com.android.systemui.biometrics.data.repository.FacePropertyRepository import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.log.core.LogLevel import com.android.systemui.plugins.statusbar.StatusBarStateController @@ -51,7 +50,6 @@ import com.android.systemui.statusbar.phone.BiometricUnlockController import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.ViewController -import kotlinx.coroutines.ExperimentalCoroutinesApi import java.io.PrintWriter import javax.inject.Inject import javax.inject.Provider @@ -64,7 +62,6 @@ import javax.inject.Provider * * The ripple uses the accent color of the current theme. */ -@ExperimentalCoroutinesApi @SysUISingleton class AuthRippleController @Inject constructor( private val sysuiContext: Context, @@ -316,18 +313,6 @@ class AuthRippleController @Inject constructor( mView.fadeDwellRipple() } } - - override fun onBiometricDetected( - userId: Int, - biometricSourceType: BiometricSourceType, - isStrongBiometric: Boolean - ) { - // TODO (b/309804148): add support detect auth ripple for deviceEntryUdfpsRefactor - if (!DeviceEntryUdfpsRefactor.isEnabled && - keyguardUpdateMonitor.getUserCanSkipBouncer(userId)) { - showUnlockRipple(biometricSourceType) - } - } } private val configurationChangedListener = diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepository.kt b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepository.kt index ad2136af4b86..d28dbc0ae06f 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/data/repository/BiometricStatusRepository.kt @@ -94,6 +94,10 @@ constructor( override fun onAuthenticationStopped() { updateFingerprintAuthenticateReason(AuthenticationReason.NotRunning) } + + override fun onAuthenticationSucceeded(requestReason: Int, userId: Int) {} + + override fun onAuthenticationFailed(requestReason: Int, userId: Int) {} } updateFingerprintAuthenticateReason(AuthenticationReason.NotRunning) diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractor.kt index 348b54e0c7f6..c3dc2d406cbc 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractor.kt @@ -26,20 +26,24 @@ import com.android.systemui.biometrics.shared.model.DisplayRotation import com.android.systemui.biometrics.shared.model.FingerprintSensorType import com.android.systemui.biometrics.shared.model.isDefaultOrientation import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.log.SideFpsLogger import com.android.systemui.res.R import java.util.Optional import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach +@ExperimentalCoroutinesApi @SysUISingleton class SideFpsSensorInteractor @Inject @@ -49,6 +53,7 @@ constructor( windowManager: WindowManager, displayStateInteractor: DisplayStateInteractor, fingerprintInteractiveToAuthProvider: Optional<FingerprintInteractiveToAuthProvider>, + biometricSettingsRepository: BiometricSettingsRepository, keyguardTransitionInteractor: KeyguardTransitionInteractor, private val logger: SideFpsLogger, ) { @@ -84,13 +89,24 @@ constructor( .map { it ?: 0L } .onEach { logger.authDurationChanged(it) } + private val isSettingEnabled: Flow<Boolean> = + biometricSettingsRepository.isFingerprintEnrolledAndEnabled + .flatMapLatest { enabledAndEnrolled -> + if (!enabledAndEnrolled || fingerprintInteractiveToAuthProvider.isEmpty) { + flowOf(false) + } else { + fingerprintInteractiveToAuthProvider.get().enabledForCurrentUser + } + } + .onEach { logger.restToUnlockSettingEnabledChanged(it) } + val isProlongedTouchRequiredForAuthentication: Flow<Boolean> = - if (fingerprintInteractiveToAuthProvider.isEmpty || !isProlongedTouchEnabledForDevice) { + if (!isProlongedTouchEnabledForDevice) { flowOf(false) } else { combine( isAvailable, - fingerprintInteractiveToAuthProvider.get().enabledForCurrentUser + isSettingEnabled, ) { sfpsAvailable, isSettingEnabled -> sfpsAvailable && isSettingEnabled } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractor.kt index 4fc1b5841047..a77cc1fea6a6 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractor.kt @@ -17,6 +17,7 @@ package com.android.systemui.biometrics.domain.interactor import android.content.Context +import android.util.Log import android.view.MotionEvent import com.android.systemui.biometrics.AuthController import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams @@ -42,12 +43,23 @@ import kotlinx.coroutines.flow.stateIn class UdfpsOverlayInteractor @Inject constructor( - @Application context: Context, + @Application private val context: Context, private val authController: AuthController, private val selectedUserInteractor: SelectedUserInteractor, @Application scope: CoroutineScope ) { - private val iconSize: Int = context.resources.getDimensionPixelSize(R.dimen.udfps_icon_size) + private fun calculateIconSize(): Int { + val pixelPitch = context.resources.getFloat(R.dimen.pixel_pitch) + if (pixelPitch <= 0) { + Log.e( + "UdfpsOverlayInteractor", + "invalid pixelPitch: $pixelPitch. Pixel pitch must be updated per device." + ) + } + return (context.resources.getFloat(R.dimen.udfps_icon_size) / pixelPitch).toInt() + } + + private var iconSize: Int = calculateIconSize() /** Whether a touch is within the under-display fingerprint sensor area */ fun isTouchWithinUdfpsArea(ev: MotionEvent): Boolean { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricCustomizedViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricCustomizedViewBinder.kt index 16e7f05fae37..96582cb56dd7 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricCustomizedViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricCustomizedViewBinder.kt @@ -114,7 +114,7 @@ private fun createNewRowLayout(inflater: LayoutInflater): LinearLayout { private fun PromptContentItem.doesExceedMaxLinesIfTwoColumn( resources: Resources, ): Boolean { - val passedInText: CharSequence = + val passedInText: String = when (this) { is PromptContentItemPlainText -> text is PromptContentItemBulletedText -> text diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt index dca0338dc8e7..0f1340a63032 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt @@ -21,6 +21,7 @@ import android.graphics.Rect import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable import android.hardware.biometrics.BiometricPrompt +import android.hardware.biometrics.Flags.customBiometricPrompt import android.hardware.biometrics.PromptContentView import android.util.Log import android.view.HapticFeedbackConstants @@ -240,7 +241,7 @@ constructor( promptSelectorInteractor.prompt .map { when { - it == null -> null + !customBiometricPrompt() || it == null -> null it.logoRes != -1 -> context.resources.getDrawable(it.logoRes, context.theme) it.logoBitmap != null -> BitmapDrawable(context.resources, it.logoBitmap) else -> context.packageManager.getApplicationIcon(it.opPackageName) @@ -258,7 +259,9 @@ constructor( /** Custom content view for the prompt. */ val contentView: Flow<PromptContentView?> = - promptSelectorInteractor.prompt.map { it?.contentView }.distinctUntilChanged() + promptSelectorInteractor.prompt + .map { if (customBiometricPrompt()) it?.contentView else null } + .distinctUntilChanged() private val originalDescription = promptSelectorInteractor.prompt.map { it?.description ?: "" }.distinctUntilChanged() diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/domain/interactor/ConfigurationInteractor.kt b/packages/SystemUI/src/com/android/systemui/common/ui/domain/interactor/ConfigurationInteractor.kt index 13539850a598..5f6ff82c6038 100644 --- a/packages/SystemUI/src/com/android/systemui/common/ui/domain/interactor/ConfigurationInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/common/ui/domain/interactor/ConfigurationInteractor.kt @@ -72,6 +72,9 @@ class ConfigurationInteractor @Inject constructor(private val repository: Config val onAnyConfigurationChange: Flow<Unit> = repository.onAnyConfigurationChange.onStart { emit(Unit) } + /** Emits the new configuration on any configuration change */ + val configurationValues: Flow<Configuration> = repository.configurationValues + /** Emits the current resolution scaling factor */ val scaleForResolution: Flow<Float> = repository.scaleForResolution } diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt index 1f4be4060223..addd880f2079 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalRepository.kt @@ -20,13 +20,18 @@ import com.android.systemui.Flags.communalHub import com.android.systemui.communal.shared.model.CommunalSceneKey import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.flags.FeatureFlagsClassic import com.android.systemui.flags.Flags import com.android.systemui.scene.data.repository.SceneContainerRepository import com.android.systemui.scene.shared.flag.SceneContainerFlags import com.android.systemui.scene.shared.model.SceneKey +import com.android.systemui.user.data.repository.UserRepository +import com.android.systemui.util.settings.SecureSettings +import com.android.systemui.util.settings.SettingsProxyExt.observerFlow import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow @@ -34,16 +39,26 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.withContext /** Encapsulates the state of communal mode. */ interface CommunalRepository { /** Whether communal features are enabled. */ val isCommunalEnabled: Boolean + /** + * A {@link StateFlow} that tracks whether communal hub is enabled (it can be disabled in + * settings). + */ + val communalEnabledState: StateFlow<Boolean> + /** Whether the communal hub is showing. */ val isCommunalHubShowing: Flow<Boolean> @@ -72,13 +87,36 @@ interface CommunalRepository { class CommunalRepositoryImpl @Inject constructor( + @Application private val applicationScope: CoroutineScope, @Background backgroundScope: CoroutineScope, + @Background private val backgroundDispatcher: CoroutineDispatcher, private val featureFlagsClassic: FeatureFlagsClassic, sceneContainerFlags: SceneContainerFlags, sceneContainerRepository: SceneContainerRepository, + userRepository: UserRepository, + private val secureSettings: SecureSettings ) : CommunalRepository { + + private val communalEnabledSettingState: Flow<Boolean> = + userRepository.selectedUserInfo + .flatMapLatest { userInfo -> observeSettings(userInfo.id) } + .shareIn(scope = applicationScope, started = SharingStarted.WhileSubscribed()) + + override val communalEnabledState: StateFlow<Boolean> = + if (featureFlagsClassic.isEnabled(Flags.COMMUNAL_SERVICE_ENABLED) && communalHub()) { + communalEnabledSettingState + .filterNotNull() + .stateIn( + scope = applicationScope, + started = SharingStarted.Eagerly, + initialValue = true + ) + } else { + MutableStateFlow(false) + } + override val isCommunalEnabled: Boolean - get() = featureFlagsClassic.isEnabled(Flags.COMMUNAL_SERVICE_ENABLED) && communalHub() + get() = communalEnabledState.value private val _desiredScene: MutableStateFlow<CommunalSceneKey> = MutableStateFlow(CommunalSceneKey.DEFAULT) @@ -115,4 +153,26 @@ constructor( } else { desiredScene.map { sceneKey -> sceneKey == CommunalSceneKey.Communal } } + + private fun observeSettings(userId: Int): Flow<Boolean> = + secureSettings + .observerFlow( + userId = userId, + names = + arrayOf( + GLANCEABLE_HUB_ENABLED, + ) + ) + // Force an update + .onStart { emit(Unit) } + .map { readFromSettings(userId) } + + private suspend fun readFromSettings(userId: Int): Boolean = + withContext(backgroundDispatcher) { + secureSettings.getIntForUser(GLANCEABLE_HUB_ENABLED, 1, userId) == 1 + } + + companion object { + private const val GLANCEABLE_HUB_ENABLED = "glanceable_hub_enabled" + } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt index 44b0383e12c6..c36f7fa22c82 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt @@ -68,11 +68,14 @@ constructor( private val appWidgetHost: CommunalAppWidgetHost, private val editWidgetsActivityStarter: EditWidgetsActivityStarter ) { - /** Whether communal features are enabled. */ val isCommunalEnabled: Boolean get() = communalRepository.isCommunalEnabled + /** A {@link StateFlow} that tracks whether communal features are enabled. */ + val communalEnabledState: StateFlow<Boolean> + get() = communalRepository.communalEnabledState + /** Whether communal features are enabled and available. */ val isCommunalAvailable: StateFlow<Boolean> = flowOf(isCommunalEnabled) @@ -81,8 +84,12 @@ constructor( combine( keyguardInteractor.isEncryptedOrLockdown, userRepository.selectedUserInfo, - ) { isEncryptedOrLockdown, selectedUserInfo -> - !isEncryptedOrLockdown && selectedUserInfo.isMain + keyguardInteractor.isKeyguardVisible, + keyguardInteractor.isDreaming, + ) { isEncryptedOrLockdown, selectedUserInfo, isKeyguardVisible, isDreaming -> + !isEncryptedOrLockdown && + selectedUserInfo.isMain && + (isKeyguardVisible || isDreaming) } else flowOf(false) } @@ -154,8 +161,6 @@ constructor( it is ObservableCommunalTransitionState.Idle && it.scene == CommunalSceneKey.Communal } - val isKeyguardVisible: Flow<Boolean> = keyguardInteractor.isKeyguardVisible - /** Callback received whenever the [SceneTransitionLayout] finishes a scene transition. */ fun onSceneChanged(newScene: CommunalSceneKey) { communalRepository.setDesiredScene(newScene) diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractor.kt index 4e5be9b8aa5e..309c84e4e955 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalTutorialInteractor.kt @@ -27,12 +27,15 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch /** Encapsulates business-logic related to communal tutorial state. */ @@ -48,7 +51,7 @@ constructor( communalInteractor: CommunalInteractor, ) { /** An observable for whether the tutorial is available. */ - val isTutorialAvailable: Flow<Boolean> = + val isTutorialAvailable: StateFlow<Boolean> = combine( communalInteractor.isCommunalAvailable, keyguardInteractor.isKeyguardVisible, @@ -58,7 +61,11 @@ constructor( isKeyguardVisible && tutorialSettingState != Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED } - .distinctUntilChanged() + .stateIn( + scope = scope, + started = SharingStarted.WhileSubscribed(), + initialValue = false, + ) /** * A flow of the new tutorial state after transitioning. The new state will be calculated based diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/binder/CommunalTutorialIndicatorViewBinder.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/binder/CommunalTutorialIndicatorViewBinder.kt index 4dfc371aaeef..0120b5c87f0a 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/binder/CommunalTutorialIndicatorViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/binder/CommunalTutorialIndicatorViewBinder.kt @@ -18,7 +18,6 @@ package com.android.systemui.communal.ui.binder import android.widget.TextView -import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle @@ -32,16 +31,14 @@ object CommunalTutorialIndicatorViewBinder { fun bind( view: TextView, viewModel: CommunalTutorialIndicatorViewModel, + isPreviewMode: Boolean = false, ): DisposableHandle { val disposableHandle = view.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.STARTED) { launch { - viewModel.showIndicator.collect { isVisible -> - updateView( - view = view, - isIndicatorVisible = isVisible, - ) + viewModel.showIndicator(isPreviewMode).collect { showIndicator -> + view.isVisible = showIndicator } } @@ -51,18 +48,4 @@ object CommunalTutorialIndicatorViewBinder { return disposableHandle } - - private fun updateView( - isIndicatorVisible: Boolean, - view: TextView, - ) { - if (!isIndicatorVisible) { - view.isGone = true - return - } - - if (!view.isVisible) { - view.isVisible = true - } - } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/view/layout/sections/CommunalTutorialIndicatorSection.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/view/layout/sections/CommunalTutorialIndicatorSection.kt index 027cc96350f5..2d9dd50b6d11 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/view/layout/sections/CommunalTutorialIndicatorSection.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/view/layout/sections/CommunalTutorialIndicatorSection.kt @@ -120,6 +120,7 @@ constructor( ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM ) + setVisibility(tutorialIndicatorId, View.GONE) } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt index acc7981dd460..1e64d3f9cedc 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt @@ -35,8 +35,6 @@ abstract class BaseCommunalViewModel( ) { val isCommunalAvailable: StateFlow<Boolean> = communalInteractor.isCommunalAvailable - val isKeyguardVisible: Flow<Boolean> = communalInteractor.isKeyguardVisible - val currentScene: StateFlow<CommunalSceneKey> = communalInteractor.desiredScene /** Whether widgets are currently being re-ordered. */ diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalTutorialIndicatorViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalTutorialIndicatorViewModel.kt index 274e61a7499f..63a497213255 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalTutorialIndicatorViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalTutorialIndicatorViewModel.kt @@ -20,17 +20,30 @@ import com.android.systemui.communal.domain.interactor.CommunalTutorialInteracto import com.android.systemui.keyguard.domain.interactor.KeyguardBottomAreaInteractor import javax.inject.Inject import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.distinctUntilChanged /** View model for communal tutorial indicator on keyguard */ class CommunalTutorialIndicatorViewModel @Inject constructor( - communalTutorialInteractor: CommunalTutorialInteractor, + private val communalTutorialInteractor: CommunalTutorialInteractor, bottomAreaInteractor: KeyguardBottomAreaInteractor, ) { - /** An observable for whether the tutorial indicator view should be visible. */ - val showIndicator: Flow<Boolean> = communalTutorialInteractor.isTutorialAvailable + /** + * An observable for whether the tutorial indicator view should be visible. + * + * @param isPreviewMode Whether for preview keyguard mode in wallpaper settings. + */ + fun showIndicator(isPreviewMode: Boolean): StateFlow<Boolean> { + return if (isPreviewMode) { + MutableStateFlow(false).asStateFlow() + } else { + communalTutorialInteractor.isTutorialAvailable + } + } /** An observable for the alpha level for the tutorial indicator. */ val alpha: Flow<Float> = bottomAreaInteractor.alpha.distinctUntilChanged() diff --git a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt index 641064becf24..947cb024f7fe 100644 --- a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt +++ b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt @@ -17,6 +17,7 @@ package com.android.systemui.compose +import android.app.Dialog import android.content.Context import android.view.View import android.view.WindowInsets @@ -26,11 +27,14 @@ import com.android.systemui.bouncer.ui.BouncerDialogFactory import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel import com.android.systemui.communal.widgets.WidgetConfigurator +import com.android.systemui.keyboard.stickykeys.ui.viewmodel.StickyKeysIndicatorViewModel import com.android.systemui.people.ui.viewmodel.PeopleViewModel import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel import com.android.systemui.scene.shared.model.Scene import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel +import com.android.systemui.statusbar.phone.SystemUIDialogFactory +import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelViewModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.StateFlow @@ -70,6 +74,12 @@ interface BaseComposeFacade { onEditDone: () -> Unit, ) + fun setVolumePanelActivityContent( + activity: ComponentActivity, + viewModel: VolumePanelViewModel, + onDismissAnimationFinished: () -> Unit, + ) + /** Create a [View] to represent [viewModel] on screen. */ fun createFooterActionsView( context: Context, @@ -86,6 +96,12 @@ interface BaseComposeFacade { sceneByKey: Map<SceneKey, Scene>, ): View + /** Creates sticky key dialog presenting provided [viewModel] */ + fun createStickyKeysDialog( + dialogFactory: SystemUIDialogFactory, + viewModel: StickyKeysIndicatorViewModel + ): Dialog + /** Create a [View] to represent [viewModel] on screen. */ fun createCommunalView( context: Context, diff --git a/packages/SystemUI/src/com/android/systemui/doze/dagger/DozeModule.java b/packages/SystemUI/src/com/android/systemui/doze/dagger/DozeModule.java index db0c3c68107a..0fd688760a32 100644 --- a/packages/SystemUI/src/com/android/systemui/doze/dagger/DozeModule.java +++ b/packages/SystemUI/src/com/android/systemui/doze/dagger/DozeModule.java @@ -18,10 +18,7 @@ package com.android.systemui.doze.dagger; import android.content.Context; import android.hardware.Sensor; -import android.os.Handler; -import com.android.systemui.res.R; -import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dagger.qualifiers.UiBackground; import com.android.systemui.doze.DozeAuthRemover; import com.android.systemui.doze.DozeBrightnessHostForwarder; @@ -40,6 +37,7 @@ import com.android.systemui.doze.DozeTransitionListener; import com.android.systemui.doze.DozeTriggers; import com.android.systemui.doze.DozeUi; import com.android.systemui.doze.DozeWallpaperState; +import com.android.systemui.res.R; import com.android.systemui.statusbar.phone.DozeParameters; import com.android.systemui.statusbar.policy.DevicePostureController; import com.android.systemui.util.sensors.AsyncSensorManager; @@ -75,9 +73,8 @@ public abstract class DozeModule { @Provides @DozeScope - static WakeLock providesDozeWakeLock(DelayedWakeLock.Builder delayedWakeLockBuilder, - @Main Handler handler) { - return delayedWakeLockBuilder.setHandler(handler).setTag("Doze").build(); + static WakeLock providesDozeWakeLock(DelayedWakeLock.Factory delayedWakeLockFactory) { + return delayedWakeLockFactory.create("Doze"); } @Provides diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java new file mode 100644 index 000000000000..c9b56a2ebd9a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.dreams.touch; + +import static com.android.systemui.dreams.touch.dagger.ShadeModule.COMMUNAL_GESTURE_INITIATION_WIDTH; + +import android.graphics.Rect; +import android.graphics.Region; +import android.view.GestureDetector; +import android.view.MotionEvent; + +import com.android.systemui.statusbar.NotificationShadeWindowController; +import com.android.systemui.statusbar.phone.CentralSurfaces; + +import java.util.Optional; + +import javax.inject.Inject; +import javax.inject.Named; + +/** {@link DreamTouchHandler} responsible for handling touches to open communal hub. **/ +public class CommunalTouchHandler implements DreamTouchHandler { + private final int mInitiationWidth; + private final NotificationShadeWindowController mNotificationShadeWindowController; + private final Optional<CentralSurfaces> mCentralSurfaces; + + @Inject + public CommunalTouchHandler( + Optional<CentralSurfaces> centralSurfaces, + NotificationShadeWindowController notificationShadeWindowController, + @Named(COMMUNAL_GESTURE_INITIATION_WIDTH) int initiationWidth) { + mInitiationWidth = initiationWidth; + mCentralSurfaces = centralSurfaces; + mNotificationShadeWindowController = notificationShadeWindowController; + } + + @Override + public void onSessionStart(TouchSession session) { + mCentralSurfaces.ifPresent(surfaces -> handleSessionStart(surfaces, session)); + } + + @Override + public void getTouchInitiationRegion(Rect bounds, Region region) { + final Rect outBounds = new Rect(bounds); + outBounds.inset(outBounds.width() - mInitiationWidth, 0, 0, 0); + region.op(outBounds, Region.Op.UNION); + } + + private void handleSessionStart(CentralSurfaces surfaces, TouchSession session) { + // Force the notification shade window open (otherwise the hub won't show while swiping). + mNotificationShadeWindowController.setForcePluginOpen(true, this); + + session.registerInputListener(ev -> { + surfaces.handleDreamTouch((MotionEvent) ev); + if (ev != null && ((MotionEvent) ev).getAction() == MotionEvent.ACTION_UP) { + var unused = session.pop(); + } + }); + + session.registerGestureListener(new GestureDetector.SimpleOnGestureListener() { + @Override + public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, + float distanceY) { + return true; + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, + float velocityY) { + return true; + } + }); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/ShadeModule.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/ShadeModule.java index 94fe4bd04dab..0f08d376f37c 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/ShadeModule.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/dagger/ShadeModule.java @@ -18,11 +18,13 @@ package com.android.systemui.dreams.touch.dagger; import android.content.res.Resources; -import com.android.systemui.res.R; import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.dreams.touch.CommunalTouchHandler; import com.android.systemui.dreams.touch.DreamTouchHandler; import com.android.systemui.dreams.touch.ShadeTouchHandler; +import com.android.systemui.res.R; +import dagger.Binds; import dagger.Module; import dagger.Provides; import dagger.multibindings.IntoSet; @@ -33,7 +35,7 @@ import javax.inject.Named; * Dependencies for swipe down to notification over dream. */ @Module -public class ShadeModule { +public abstract class ShadeModule { /** * The height, defined in pixels, of the gesture initiation region at the top of the screen for * swiping down notifications. @@ -41,15 +43,22 @@ public class ShadeModule { public static final String NOTIFICATION_SHADE_GESTURE_INITIATION_HEIGHT = "notification_shade_gesture_initiation_height"; + /** Width of swipe gesture edge to show communal hub. */ + public static final String COMMUNAL_GESTURE_INITIATION_WIDTH = + "communal_gesture_initiation_width"; + /** * Provides {@link ShadeTouchHandler} to handle notification swipe down over dream. */ - @Provides + @Binds @IntoSet - public static DreamTouchHandler providesNotificationShadeTouchHandler( - ShadeTouchHandler touchHandler) { - return touchHandler; - } + public abstract DreamTouchHandler providesNotificationShadeTouchHandler( + ShadeTouchHandler touchHandler); + + /** Provides {@link CommunalTouchHandler}. */ + @Binds + @IntoSet + public abstract DreamTouchHandler bindCommunalTouchHandler(CommunalTouchHandler touchHandler); /** * Provides the height of the gesture area for notification swipe down. @@ -59,4 +68,13 @@ public class ShadeModule { public static int providesNotificationShadeGestureRegionHeight(@Main Resources resources) { return resources.getDimensionPixelSize(R.dimen.dream_overlay_status_bar_height); } + + /** + * Provides the width of the gesture area for swiping open communal hub. + */ + @Provides + @Named(COMMUNAL_GESTURE_INITIATION_WIDTH) + public static int providesCommunalGestureInitiationWidth(@Main Resources resources) { + return resources.getDimensionPixelSize(R.dimen.communal_gesture_initiation_width); + } } diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index 846736c04d98..d2883cce06c2 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -44,11 +44,11 @@ object Flags { // 100 - notification // TODO(b/297792660): Tracking Bug @JvmField val UNCLEARED_TRANSIENT_HUN_FIX = - unreleasedFlag("uncleared_transient_hun_fix", teamfood = true) + releasedFlag("uncleared_transient_hun_fix") // TODO(b/298308067): Tracking Bug @JvmField val SWIPE_UNCLEARED_TRANSIENT_VIEW_FIX = - unreleasedFlag("swipe_uncleared_transient_view_fix", teamfood = true) + releasedFlag("swipe_uncleared_transient_view_fix") // TODO(b/254512751): Tracking Bug val NOTIFICATION_PIPELINE_DEVELOPER_LOGGING = @@ -102,12 +102,6 @@ object Flags { default = true ) - /** Only notify group expansion listeners when a change happens. */ - // TODO(b/292213543): Tracking Bug - @JvmField - val NOTIFICATION_GROUP_EXPANSION_CHANGE = - releasedFlag("notification_group_expansion_change") - // TODO(b/301955929) @JvmField val NOTIF_LS_BACKGROUND_THREAD = @@ -574,9 +568,6 @@ object Flags { @JvmField val ENABLE_NEW_PRIVACY_DIALOG = releasedFlag("enable_new_privacy_dialog") - // TODO(b/289573946): Tracking Bug - @JvmField val PRECOMPUTED_TEXT = releasedFlag("precomputed_text") - // TODO(b/302087895): Tracking Bug @JvmField val CALL_LAYOUT_ASYNC_SET_DATA = unreleasedFlag("call_layout_async_set_data", teamfood = true) diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java index 3de9e68909cf..a95ddb5a0201 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/GlobalActionsDialogLite.java @@ -2437,6 +2437,7 @@ public class GlobalActionsDialogLite implements DialogInterface.OnDismissListene return true; } }); + mGlobalActionsLayout.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); mGlobalActionsLayout.setRotationListener(this::onRotate); mGlobalActionsLayout.setAdapter(mAdapter); mContainer = findViewById(com.android.systemui.res.R.id.global_actions_container); diff --git a/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderHapticPlugin.kt b/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderHapticPlugin.kt new file mode 100644 index 000000000000..58fb6a95b872 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/haptics/slider/SeekableSliderHapticPlugin.kt @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.haptics.slider + +import android.view.MotionEvent +import android.view.VelocityTracker +import android.widget.SeekBar +import androidx.annotation.VisibleForTesting +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.statusbar.VibratorHelper +import com.android.systemui.util.time.SystemClock +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +/** + * A plugin added to a manager of a [android.widget.SeekBar] that adds dynamic haptic feedback. + * + * A [SeekableSliderEventProducer] is used as the producer of slider events, a + * [SliderHapticFeedbackProvider] is used as the listener of slider states to play haptic feedback + * depending on the state, and a [SeekableSliderTracker] is used as the state machine handler that + * tracks and manipulates the slider state. + */ +class SeekableSliderHapticPlugin +@JvmOverloads +constructor( + vibratorHelper: VibratorHelper, + systemClock: SystemClock, + @Main private val mainDispatcher: CoroutineDispatcher, + @Application private val applicationScope: CoroutineScope, + sliderHapticFeedbackConfig: SliderHapticFeedbackConfig = SliderHapticFeedbackConfig(), + sliderTrackerConfig: SeekableSliderTrackerConfig = SeekableSliderTrackerConfig(), +) { + + private val velocityTracker = VelocityTracker.obtain() + + private val sliderEventProducer = SeekableSliderEventProducer() + + private val sliderHapticFeedbackProvider = + SliderHapticFeedbackProvider( + vibratorHelper, + velocityTracker, + sliderHapticFeedbackConfig, + systemClock, + ) + + private val sliderTracker = + SeekableSliderTracker( + sliderHapticFeedbackProvider, + sliderEventProducer, + mainDispatcher, + sliderTrackerConfig, + ) + + val isTracking: Boolean + get() = sliderTracker.isTracking + + val trackerState: SliderState + get() = sliderTracker.currentState + + /** + * A waiting [Job] for a timer that estimates the key-up event when a key-down event is + * received. + * + * This is useful for the cases where the slider is being operated by an external key, but the + * release of the key is not easily accessible (e.g., the volume keys) + */ + private var keyUpJob: Job? = null + + @VisibleForTesting + val isKeyUpTimerWaiting: Boolean + get() = keyUpJob != null && keyUpJob?.isActive == true + + /** + * Start the plugin. + * + * This starts the tracking of slider states, events and triggering of haptic feedback. + */ + fun start() { + if (!isTracking) { + sliderTracker.startTracking() + } + } + + /** + * Stop the plugin + * + * This stops the tracking of slider states, events and triggers of haptic feedback. + */ + fun stop() = sliderTracker.stopTracking() + + /** React to a touch event */ + fun onTouchEvent(event: MotionEvent?) { + when (event?.actionMasked) { + MotionEvent.ACTION_UP, + MotionEvent.ACTION_CANCEL -> velocityTracker.clear() + MotionEvent.ACTION_DOWN, + MotionEvent.ACTION_MOVE -> velocityTracker.addMovement(event) + } + } + + /** onStartTrackingTouch event from the slider's [android.widget.SeekBar] */ + fun onStartTrackingTouch(seekBar: SeekBar) { + if (isTracking) { + sliderEventProducer.onStartTrackingTouch(seekBar) + } + } + + /** onProgressChanged event from the slider's [android.widget.SeekBar] */ + fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { + if (isTracking) { + sliderEventProducer.onProgressChanged(seekBar, progress, fromUser) + } + } + + /** onStopTrackingTouch event from the slider's [android.widget.SeekBar] */ + fun onStopTrackingTouch(seekBar: SeekBar) { + if (isTracking) { + sliderEventProducer.onStopTrackingTouch(seekBar) + } + } + + /** onArrowUp event recorded */ + fun onArrowUp() { + if (isTracking) { + sliderEventProducer.onArrowUp() + } + } + + /** + * An external key was pressed (e.g., a volume key). + * + * This event is used to estimate the key-up event based on by running a timer as a waiting + * coroutine in the [keyUpTimerScope]. A key-up event in a slider corresponds to an onArrowUp + * event. Therefore, [onArrowUp] must be called after the timeout. + */ + fun onKeyDown() { + if (!isTracking) return + + if (isKeyUpTimerWaiting) { + // Cancel the ongoing wait + keyUpJob?.cancel() + } + keyUpJob = + applicationScope.launch { + delay(KEY_UP_TIMEOUT) + onArrowUp() + } + } + + companion object { + const val KEY_UP_TIMEOUT = 100L + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/PhysicalKeyboardCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/keyboard/PhysicalKeyboardCoreStartable.kt index d078688e5944..26e83a3f2179 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/PhysicalKeyboardCoreStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/PhysicalKeyboardCoreStartable.kt @@ -17,11 +17,14 @@ package com.android.systemui.keyboard +import com.android.hardware.input.Flags.keyboardA11yStickyKeysFlag import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.keyboard.backlight.ui.KeyboardBacklightDialogCoordinator +import com.android.systemui.keyboard.stickykeys.ui.StickyKeysIndicatorCoordinator +import dagger.Lazy import javax.inject.Inject /** A [CoreStartable] that launches components interested in physical keyboard interaction. */ @@ -29,12 +32,16 @@ import javax.inject.Inject class PhysicalKeyboardCoreStartable @Inject constructor( - private val keyboardBacklightDialogCoordinator: KeyboardBacklightDialogCoordinator, + private val keyboardBacklightDialogCoordinator: Lazy<KeyboardBacklightDialogCoordinator>, + private val stickyKeysIndicatorCoordinator: Lazy<StickyKeysIndicatorCoordinator>, private val featureFlags: FeatureFlags, ) : CoreStartable { override fun start() { if (featureFlags.isEnabled(Flags.KEYBOARD_BACKLIGHT_INDICATOR)) { - keyboardBacklightDialogCoordinator.startListening() + keyboardBacklightDialogCoordinator.get().startListening() + } + if (keyboardA11yStickyKeysFlag()) { + stickyKeysIndicatorCoordinator.get().startListening() } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/StickyKeysLogger.kt b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/StickyKeysLogger.kt index 37034f63aca7..4ed812010100 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/StickyKeysLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/StickyKeysLogger.kt @@ -26,11 +26,20 @@ import javax.inject.Inject private const val TAG = "stickyKeys" class StickyKeysLogger @Inject constructor(@KeyboardLog private val buffer: LogBuffer) { - fun logNewStickyKeysReceived(linkedHashMap: Map<ModifierKey, Locked>) { + fun logNewStickyKeysReceived(stickyKeys: Map<ModifierKey, Locked>) { buffer.log( TAG, LogLevel.VERBOSE, - { str1 = linkedHashMap.toString() }, + { str1 = stickyKeys.toString() }, + { "new sticky keys state received: $str1" } + ) + } + + fun logNewUiState(stickyKeys: Map<ModifierKey, Locked>) { + buffer.log( + TAG, + LogLevel.INFO, + { str1 = stickyKeys.toString() }, { "new sticky keys state received: $str1" } ) } diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/ui/StickyKeysIndicatorCoordinator.kt b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/ui/StickyKeysIndicatorCoordinator.kt new file mode 100644 index 000000000000..b68551bf93b3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/stickykeys/ui/StickyKeysIndicatorCoordinator.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyboard.stickykeys.ui + +import android.app.Dialog +import android.util.Log +import android.view.Gravity +import android.view.ViewGroup.LayoutParams.WRAP_CONTENT +import android.view.Window +import android.view.WindowManager +import com.android.systemui.compose.ComposeFacade +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.keyboard.stickykeys.StickyKeysLogger +import com.android.systemui.keyboard.stickykeys.ui.viewmodel.StickyKeysIndicatorViewModel +import com.android.systemui.statusbar.phone.SystemUIDialogFactory +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import javax.inject.Inject + +@SysUISingleton +class StickyKeysIndicatorCoordinator +@Inject +constructor( + @Application private val applicationScope: CoroutineScope, + private val dialogFactory: SystemUIDialogFactory, + private val viewModel: StickyKeysIndicatorViewModel, + private val stickyKeysLogger: StickyKeysLogger, +) { + + private var dialog: Dialog? = null + + fun startListening() { + // this check needs to be moved to PhysicalKeyboardCoreStartable + if (!ComposeFacade.isComposeAvailable()) { + Log.e("StickyKeysIndicatorCoordinator", "Compose is required for this UI") + return + } + applicationScope.launch { + viewModel.indicatorContent.collect { stickyKeys -> + stickyKeysLogger.logNewUiState(stickyKeys) + if (stickyKeys.isEmpty()) { + dialog?.dismiss() + dialog = null + } else if (dialog == null) { + dialog = ComposeFacade.createStickyKeysDialog(dialogFactory, viewModel).apply { + setCanceledOnTouchOutside(false) + window?.setAttributes() + show() + } + } + } + } + } + + private fun Window.setAttributes() { + setType(WindowManager.LayoutParams.TYPE_STATUS_BAR_SUB_PANEL) + addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED) + clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND) + setGravity(Gravity.TOP or Gravity.END) + attributes = WindowManager.LayoutParams().apply { + copyFrom(attributes) + width = WRAP_CONTENT + title = "StickyKeysIndicator" + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index 5cebd96249b6..50caf17f71dd 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -2507,8 +2507,18 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, String message = ""; switch (msg.what) { case SHOW: - message = "SHOW"; - handleShow((Bundle) msg.obj); + // There is a potential race condition when SysUI starts up. CentralSurfaces + // must invoke #registerCentralSurfaces on this class before any messages can be + // processed. If this happens, repost the message with a small delay and try + // again. + if (mCentralSurfaces == null) { + message = "DELAYING SHOW"; + Message newMsg = mHandler.obtainMessage(SHOW, (Bundle) msg.obj); + mHandler.sendMessageDelayed(newMsg, 100); + } else { + message = "SHOW"; + handleShow((Bundle) msg.obj); + } break; case HIDE: message = "HIDE"; @@ -2595,8 +2605,18 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, Trace.endSection(); break; case SYSTEM_READY: - message = "SYSTEM_READY"; - handleSystemReady(); + // There is a potential race condition when SysUI starts up. CentralSurfaces + // must invoke #registerCentralSurfaces on this class before any messages can be + // processed. If this happens, repost the message with a small delay and try + // again. + if (mCentralSurfaces == null) { + message = "DELAYING SYSTEM_READY"; + Message newMsg = mHandler.obtainMessage(SYSTEM_READY); + mHandler.sendMessageDelayed(newMsg, 100); + } else { + message = "SYSTEM_READY"; + handleSystemReady(); + } break; } Log.d(TAG, "KeyguardViewMediator queue processing message: " + message); 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 7b1466cd1fc9..a97c152ba7e3 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromAlternateBouncerTransitionInteractor.kt @@ -17,14 +17,14 @@ package com.android.systemui.keyguard.domain.interactor import android.animation.ValueAnimator +import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.power.domain.interactor.PowerInteractor -import com.android.systemui.util.kotlin.Utils.Companion.toQuad -import com.android.systemui.util.kotlin.Utils.Companion.toQuint +import com.android.systemui.util.kotlin.Utils.Companion.sample import com.android.systemui.util.kotlin.sample import com.android.wm.shell.animation.Interpolators import javax.inject.Inject @@ -32,7 +32,6 @@ import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch @@ -46,6 +45,7 @@ constructor( @Background bgDispatcher: CoroutineDispatcher, @Main mainDispatcher: CoroutineDispatcher, private val keyguardInteractor: KeyguardInteractor, + private val communalInteractor: CommunalInteractor, private val powerInteractor: PowerInteractor, ) : TransitionInteractor( @@ -57,12 +57,12 @@ constructor( override fun start() { listenForAlternateBouncerToGone() - listenForAlternateBouncerToLockscreenAodOrDozing() + listenForAlternateBouncerToLockscreenHubAodOrDozing() listenForAlternateBouncerToPrimaryBouncer() listenForTransitionToCamera(scope, keyguardInteractor) } - private fun listenForAlternateBouncerToLockscreenAodOrDozing() { + private fun listenForAlternateBouncerToLockscreenHubAodOrDozing() { scope.launch { keyguardInteractor.alternateBouncerShowing // Add a slight delay, as alternateBouncer and primaryBouncer showing event changes @@ -70,14 +70,11 @@ constructor( // happening prematurely. .onEach { delay(50) } .sample( - combine( - keyguardInteractor.primaryBouncerShowing, - startedKeyguardTransitionStep, - powerInteractor.isAwake, - keyguardInteractor.isAodAvailable, - ::toQuad - ), - ::toQuint + keyguardInteractor.primaryBouncerShowing, + startedKeyguardTransitionStep, + powerInteractor.isAwake, + keyguardInteractor.isAodAvailable, + communalInteractor.isIdleOnCommunal ) .collect { ( @@ -85,7 +82,8 @@ constructor( isPrimaryBouncerShowing, lastStartedTransitionStep, isAwake, - isAodAvailable) -> + isAodAvailable, + isIdleOnCommunal) -> if ( !isAlternateBouncerShowing && !isPrimaryBouncerShowing && @@ -99,7 +97,11 @@ constructor( KeyguardState.DOZING } } else { - KeyguardState.LOCKSCREEN + if (isIdleOnCommunal) { + KeyguardState.GLANCEABLE_HUB + } else { + KeyguardState.LOCKSCREEN + } } startTransitionTo(to) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt index 3292ea85639c..9d38be941077 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt @@ -18,6 +18,7 @@ package com.android.systemui.keyguard.domain.interactor import android.animation.ValueAnimator import com.android.app.animation.Interpolators +import com.android.app.tracing.coroutines.launch import com.android.systemui.Flags import com.android.systemui.communal.shared.model.CommunalSceneKey import com.android.systemui.dagger.SysUISingleton @@ -39,12 +40,13 @@ class FromGlanceableHubTransitionInteractor @Inject constructor( @Background private val scope: CoroutineScope, + @Main mainDispatcher: CoroutineDispatcher, + @Background bgDispatcher: CoroutineDispatcher, private val glanceableHubTransitions: GlanceableHubTransitions, + private val keyguardInteractor: KeyguardInteractor, override val transitionRepository: KeyguardTransitionRepository, transitionInteractor: KeyguardTransitionInteractor, private val powerInteractor: PowerInteractor, - @Main mainDispatcher: CoroutineDispatcher, - @Background bgDispatcher: CoroutineDispatcher, ) : TransitionInteractor( fromState = KeyguardState.GLANCEABLE_HUB, @@ -58,6 +60,10 @@ constructor( } listenForHubToLockscreen() listenForHubToDozing() + listenForHubToPrimaryBouncer() + listenForHubToAlternateBouncer() + listenForHubToOccluded() + listenForHubToGone() } override fun getDefaultAnimatorForTransitionsToState(toState: KeyguardState): ValueAnimator { @@ -75,10 +81,42 @@ constructor( glanceableHubTransitions.listenForLockscreenAndHubTransition( transitionName = "listenForHubToLockscreen", transitionOwnerName = TAG, - toScene = CommunalSceneKey.Blank + toScene = CommunalSceneKey.Blank, ) } + private fun listenForHubToPrimaryBouncer() { + scope.launch("$TAG#listenForHubToPrimaryBouncer") { + keyguardInteractor.primaryBouncerShowing + .sample(startedKeyguardTransitionStep, ::Pair) + .collect { pair -> + val (isBouncerShowing, lastStartedTransitionStep) = pair + if ( + isBouncerShowing && + lastStartedTransitionStep.to == KeyguardState.GLANCEABLE_HUB + ) { + startTransitionTo(KeyguardState.PRIMARY_BOUNCER) + } + } + } + } + + private fun listenForHubToAlternateBouncer() { + scope.launch("$TAG#listenForHubToAlternateBouncer") { + keyguardInteractor.alternateBouncerShowing + .sample(startedKeyguardTransitionStep, ::Pair) + .collect { pair -> + val (isAlternateBouncerShowing, lastStartedTransitionStep) = pair + if ( + isAlternateBouncerShowing && + lastStartedTransitionStep.to == KeyguardState.GLANCEABLE_HUB + ) { + startTransitionTo(KeyguardState.ALTERNATE_BOUNCER) + } + } + } + } + private fun listenForHubToDozing() { scope.launch { powerInteractor.isAsleep.sample(startedKeyguardTransitionStep, ::Pair).collect { @@ -93,8 +131,32 @@ constructor( } } + private fun listenForHubToOccluded() { + scope.launch { + keyguardInteractor.isKeyguardOccluded.sample(startedKeyguardState, ::Pair).collect { + (isOccluded, keyguardState) -> + if (isOccluded && keyguardState == fromState) { + startTransitionTo(KeyguardState.OCCLUDED) + } + } + } + } + + private fun listenForHubToGone() { + scope.launch { + keyguardInteractor.isKeyguardGoingAway + .sample(startedKeyguardTransitionStep, ::Pair) + .collect { (isKeyguardGoingAway, lastStartedStep) -> + if (isKeyguardGoingAway && lastStartedStep.to == fromState) { + startTransitionTo(KeyguardState.GONE) + } + } + } + } + companion object { const val TAG = "FromGlanceableHubTransitionInteractor" - val DEFAULT_DURATION = 500.milliseconds + val DEFAULT_DURATION = 400.milliseconds + val TO_LOCKSCREEN_DURATION = DEFAULT_DURATION } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt index 742790eeaedb..7477624f52d8 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt @@ -18,6 +18,7 @@ package com.android.systemui.keyguard.domain.interactor import android.animation.ValueAnimator import com.android.app.animation.Interpolators +import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main @@ -25,6 +26,7 @@ import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepositor import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.util.kotlin.Utils.Companion.sample import com.android.systemui.util.kotlin.Utils.Companion.toTriple import com.android.systemui.util.kotlin.sample import javax.inject.Inject @@ -45,6 +47,7 @@ constructor( @Main mainDispatcher: CoroutineDispatcher, private val keyguardInteractor: KeyguardInteractor, private val powerInteractor: PowerInteractor, + private val communalInteractor: CommunalInteractor, ) : TransitionInteractor( fromState = KeyguardState.GONE, @@ -56,18 +59,27 @@ constructor( override fun start() { listenForGoneToAodOrDozing() listenForGoneToDreaming() - listenForGoneToLockscreen() + listenForGoneToLockscreenOrHub() listenForGoneToDreamingLockscreenHosted() } // Primarily for when the user chooses to lock down the device - private fun listenForGoneToLockscreen() { + private fun listenForGoneToLockscreenOrHub() { scope.launch { keyguardInteractor.isKeyguardShowing - .sample(startedKeyguardTransitionStep, ::Pair) - .collect { (isKeyguardShowing, lastStartedStep) -> + .sample( + startedKeyguardTransitionStep, + communalInteractor.isIdleOnCommunal, + ) + .collect { (isKeyguardShowing, lastStartedStep, isIdleOnCommunal) -> if (isKeyguardShowing && lastStartedStep.to == KeyguardState.GONE) { - startTransitionTo(KeyguardState.LOCKSCREEN) + val to = + if (isIdleOnCommunal) { + KeyguardState.GLANCEABLE_HUB + } else { + KeyguardState.LOCKSCREEN + } + startTransitionTo(to) } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt index 40061f410456..efb604d5dadc 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt @@ -18,12 +18,14 @@ package com.android.systemui.keyguard.domain.interactor import android.animation.ValueAnimator import com.android.app.animation.Interpolators +import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.util.kotlin.Utils.Companion.sample import com.android.systemui.util.kotlin.Utils.Companion.toTriple import com.android.systemui.util.kotlin.sample import javax.inject.Inject @@ -44,6 +46,7 @@ constructor( @Main mainDispatcher: CoroutineDispatcher, private val keyguardInteractor: KeyguardInteractor, private val powerInteractor: PowerInteractor, + private val communalInteractor: CommunalInteractor, ) : TransitionInteractor( fromState = KeyguardState.OCCLUDED, @@ -53,7 +56,7 @@ constructor( ) { override fun start() { - listenForOccludedToLockscreen() + listenForOccludedToLockscreenOrHub() listenForOccludedToDreaming() listenForOccludedToAodOrDozing() listenForOccludedToGone() @@ -86,18 +89,15 @@ constructor( } } - private fun listenForOccludedToLockscreen() { + private fun listenForOccludedToLockscreenOrHub() { scope.launch { keyguardInteractor.isKeyguardOccluded .sample( - combine( - keyguardInteractor.isKeyguardShowing, - startedKeyguardTransitionStep, - ::Pair - ), - ::toTriple + keyguardInteractor.isKeyguardShowing, + startedKeyguardTransitionStep, + communalInteractor.isIdleOnCommunal, ) - .collect { (isOccluded, isShowing, lastStartedKeyguardState) -> + .collect { (isOccluded, isShowing, lastStartedKeyguardState, isIdleOnCommunal) -> // Occlusion signals come from the framework, and should interrupt any // existing transition if ( @@ -105,7 +105,13 @@ constructor( isShowing && lastStartedKeyguardState.to == KeyguardState.OCCLUDED ) { - startTransitionTo(KeyguardState.LOCKSCREEN) + val to = + if (isIdleOnCommunal) { + KeyguardState.GLANCEABLE_HUB + } else { + KeyguardState.LOCKSCREEN + } + startTransitionTo(to) } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt index c62055f83517..33b6373d5876 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractor.kt @@ -18,6 +18,7 @@ package com.android.systemui.keyguard.domain.interactor import android.animation.ValueAnimator import com.android.keyguard.KeyguardSecurityModel +import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main @@ -29,8 +30,8 @@ import com.android.systemui.keyguard.shared.model.KeyguardSurfaceBehindModel import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.user.domain.interactor.SelectedUserInteractor +import com.android.systemui.util.kotlin.Utils.Companion.sample import com.android.systemui.util.kotlin.Utils.Companion.toQuad -import com.android.systemui.util.kotlin.Utils.Companion.toQuint import com.android.systemui.util.kotlin.Utils.Companion.toTriple import com.android.systemui.util.kotlin.sample import com.android.wm.shell.animation.Interpolators @@ -54,6 +55,7 @@ constructor( @Background bgDispatcher: CoroutineDispatcher, @Main mainDispatcher: CoroutineDispatcher, private val keyguardInteractor: KeyguardInteractor, + private val communalInteractor: CommunalInteractor, private val flags: FeatureFlags, private val keyguardSecurityModel: KeyguardSecurityModel, private val selectedUserInteractor: SelectedUserInteractor, @@ -69,7 +71,7 @@ constructor( override fun start() { listenForPrimaryBouncerToGone() listenForPrimaryBouncerToAodOrDozing() - listenForPrimaryBouncerToLockscreenOrOccluded() + listenForPrimaryBouncerToLockscreenHubOrOccluded() listenForPrimaryBouncerToDreamingLockscreenHosted() listenForTransitionToCamera(scope, keyguardInteractor) } @@ -125,18 +127,15 @@ constructor( scope.launch { startTransitionTo(KeyguardState.GONE) } } - private fun listenForPrimaryBouncerToLockscreenOrOccluded() { + private fun listenForPrimaryBouncerToLockscreenHubOrOccluded() { scope.launch { keyguardInteractor.primaryBouncerShowing .sample( - combine( - powerInteractor.isAwake, - startedKeyguardTransitionStep, - keyguardInteractor.isKeyguardOccluded, - keyguardInteractor.isActiveDreamLockscreenHosted, - ::toQuad - ), - ::toQuint + powerInteractor.isAwake, + startedKeyguardTransitionStep, + keyguardInteractor.isKeyguardOccluded, + keyguardInteractor.isActiveDreamLockscreenHosted, + communalInteractor.isIdleOnCommunal ) .collect { ( @@ -144,16 +143,23 @@ constructor( isAwake, lastStartedTransitionStep, occluded, - isActiveDreamLockscreenHosted) -> + isActiveDreamLockscreenHosted, + isIdleOnCommunal) -> if ( !isBouncerShowing && lastStartedTransitionStep.to == KeyguardState.PRIMARY_BOUNCER && isAwake && !isActiveDreamLockscreenHosted ) { - startTransitionTo( - if (occluded) KeyguardState.OCCLUDED else KeyguardState.LOCKSCREEN - ) + val toState = + if (occluded) { + KeyguardState.OCCLUDED + } else if (isIdleOnCommunal) { + KeyguardState.GLANCEABLE_HUB + } else { + KeyguardState.LOCKSCREEN + } + startTransitionTo(toState) } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitions.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitions.kt index cb50839331a2..ca661536d988 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitions.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitions.kt @@ -23,6 +23,7 @@ import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.communal.domain.interactor.CommunalTransitionProgress import com.android.systemui.communal.shared.model.CommunalSceneKey import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionInfo @@ -30,12 +31,15 @@ import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.util.kotlin.sample import java.util.UUID import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.flowOn class GlanceableHubTransitions @Inject constructor( @Application private val scope: CoroutineScope, + @Background private val bgDispatcher: CoroutineDispatcher, private val transitionInteractor: KeyguardTransitionInteractor, private val transitionRepository: KeyguardTransitionRepository, private val communalInteractor: CommunalInteractor, @@ -66,7 +70,10 @@ constructor( scope.launch("$transitionOwnerName#$transitionName") { communalInteractor .transitionProgressToScene(toScene) - .sample(transitionInteractor.startedKeyguardTransitionStep, ::Pair) + .sample( + transitionInteractor.startedKeyguardTransitionStep.flowOn(bgDispatcher), + ::Pair + ) .collect { pair -> val (transitionProgress, lastStartedStep) = pair diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt index 91747e0f69a0..36bd905d23ac 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt @@ -98,7 +98,7 @@ constructor( val dozeAmount: Flow<Float> = repository.linearDozeAmount /** Whether the system is in doze mode. */ - val isDozing: Flow<Boolean> = repository.isDozing + val isDozing: StateFlow<Boolean> = repository.isDozing /** Receive an event for doze time tick */ val dozeTimeTick: Flow<Long> = repository.dozeTimeTick diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt index 3dd3e0762c7c..8d6493f9a278 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt @@ -24,6 +24,7 @@ import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle +import com.android.systemui.Flags.keyguardBottomAreaRefactor import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.BaseBlueprintTransition import com.android.systemui.keyguard.ui.viewmodel.KeyguardBlueprintViewModel import com.android.systemui.lifecycle.repeatWhenAttached @@ -53,7 +54,8 @@ class KeyguardBlueprintViewBinder { } // Apply transition. - if (prevBluePrint != null && prevBluePrint != blueprint) { + if (!keyguardBottomAreaRefactor() && prevBluePrint != null && + prevBluePrint != blueprint) { TransitionManager.beginDelayedTransition( constraintLayout, BaseBlueprintTransition() diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt index 400b8bfff9b0..3c3ebdfc066b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt @@ -89,6 +89,12 @@ object KeyguardClockViewBinder { } } } + launch { + if (!migrateClocksToBlueprint()) return@launch + viewModel.isAodIconsVisible.collect { + applyConstraints(clockSection, keyguardRootView, true) + } + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt index eb3afb7c9eec..841bad4c15cc 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt @@ -39,6 +39,7 @@ import android.view.View import android.view.ViewGroup import android.view.WindowManager import android.widget.FrameLayout +import android.widget.TextView import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.view.isInvisible import com.android.keyguard.ClockEventController @@ -48,6 +49,8 @@ import com.android.systemui.animation.view.LaunchableImageView import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.common.ui.ConfigurationState +import com.android.systemui.communal.ui.binder.CommunalTutorialIndicatorViewBinder +import com.android.systemui.communal.ui.viewmodel.CommunalTutorialIndicatorViewModel import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main @@ -133,6 +136,7 @@ constructor( private val screenOffAnimationController: ScreenOffAnimationController, private val shadeInteractor: ShadeInteractor, private val secureSettings: SecureSettings, + private val communalTutorialViewModel: CommunalTutorialIndicatorViewModel, ) { val hostToken: IBinder? = bundle.getBinder(KEY_HOST_TOKEN) private val width: Int = bundle.getInt(KEY_VIEW_WIDTH) @@ -408,6 +412,8 @@ constructor( smartSpaceView?.let { KeyguardPreviewSmartspaceViewBinder.bind(it, smartspaceViewModel) } + + setupCommunalTutorialIndicator(keyguardRootView) } ) } @@ -601,6 +607,17 @@ constructor( } } + private fun setupCommunalTutorialIndicator(keyguardRootView: ConstraintLayout) { + keyguardRootView.findViewById<TextView>(R.id.communal_tutorial_indicator)?.let { + indicatorView -> + CommunalTutorialIndicatorViewBinder.bind( + indicatorView, + communalTutorialViewModel, + isPreviewMode = true, + ) + } + } + private suspend fun fetchThemeStyleFromSetting(): Style { val overlayPackageJson = withContext(backgroundDispatcher) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt index ed7abff555e7..ad589dfcff9e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AodNotificationIconsSection.kt @@ -57,7 +57,6 @@ constructor( private var nicBindingDisposable: DisposableHandle? = null private val nicId = R.id.aod_notification_icon_container private lateinit var nic: NotificationIconContainer - private val smartSpaceBarrier = View.generateViewId() override fun addViews(constraintLayout: ConstraintLayout) { if (!KeyguardShadeMigrationNssl.isEnabled) { @@ -121,8 +120,20 @@ constructor( } else { connect(nicId, TOP, R.id.keyguard_status_view, topAlignment, bottomMargin) } - connect(nicId, START, PARENT_ID, START) - connect(nicId, END, PARENT_ID, END) + connect( + nicId, + START, + PARENT_ID, + START, + context.resources.getDimensionPixelSize(R.dimen.status_view_margin_horizontal) + ) + connect( + nicId, + END, + PARENT_ID, + END, + context.resources.getDimensionPixelSize(R.dimen.status_view_margin_horizontal) + ) constrainHeight( nicId, context.resources.getDimensionPixelSize(R.dimen.notification_shelf_height) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt index b344d3b9afea..fe4f07d022dd 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt @@ -19,6 +19,7 @@ package com.android.systemui.keyguard.ui.view.layout.sections import android.content.Context import android.view.View +import androidx.constraintlayout.widget.Barrier import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.constraintlayout.widget.ConstraintSet.BOTTOM @@ -30,11 +31,13 @@ import androidx.constraintlayout.widget.ConstraintSet.TOP import androidx.constraintlayout.widget.ConstraintSet.VISIBLE import androidx.constraintlayout.widget.ConstraintSet.WRAP_CONTENT import com.android.systemui.Flags +import com.android.systemui.customization.R as customizationR import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.keyguard.ui.binder.KeyguardClockViewBinder import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel +import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel import com.android.systemui.plugins.clocks.ClockController import com.android.systemui.plugins.clocks.ClockFaceLayout import com.android.systemui.res.R @@ -61,6 +64,7 @@ constructor( protected val keyguardClockViewModel: KeyguardClockViewModel, private val context: Context, private val splitShadeStateController: SplitShadeStateController, + val smartspaceViewModel: KeyguardSmartspaceViewModel, val blueprintInteractor: Lazy<KeyguardBlueprintInteractor>, ) : KeyguardSection() { override fun addViews(constraintLayout: ConstraintLayout) {} @@ -117,6 +121,35 @@ constructor( private fun getLargeClockFace(clock: ClockController): ClockFaceLayout = clock.largeClock.layout private fun getSmallClockFace(clock: ClockController): ClockFaceLayout = clock.smallClock.layout + + fun constrainWeatherClockDateIconsBarrier(constraints: ConstraintSet) { + constraints.apply { + if (keyguardClockViewModel.isAodIconsVisible.value) { + createBarrier( + R.id.weather_clock_date_and_icons_barrier_bottom, + Barrier.BOTTOM, + 0, + *intArrayOf(sharedR.id.bc_smartspace_view, R.id.aod_notification_icon_container) + ) + } else { + if (smartspaceViewModel.bcSmartspaceVisibility.value == VISIBLE) { + createBarrier( + R.id.weather_clock_date_and_icons_barrier_bottom, + Barrier.BOTTOM, + 0, + (sharedR.id.bc_smartspace_view) + ) + } else { + createBarrier( + R.id.weather_clock_date_and_icons_barrier_bottom, + Barrier.BOTTOM, + getDimen(ENHANCED_SMARTSPACE_HEIGHT), + (R.id.lockscreen_clock_view) + ) + } + } + } + } open fun applyDefaultConstraints(constraints: ConstraintSet) { val guideline = if (keyguardClockViewModel.clockShouldBeCentered.value) PARENT_ID @@ -128,16 +161,14 @@ constructor( var largeClockTopMargin = context.resources.getDimensionPixelSize(R.dimen.status_bar_height) + context.resources.getDimensionPixelSize( - com.android.systemui.customization.R.dimen.small_clock_padding_top + customizationR.dimen.small_clock_padding_top ) + context.resources.getDimensionPixelSize(R.dimen.keyguard_smartspace_top_offset) largeClockTopMargin += getDimen(DATE_WEATHER_VIEW_HEIGHT) largeClockTopMargin += getDimen(ENHANCED_SMARTSPACE_HEIGHT) if (!keyguardClockViewModel.useLargeClock) { largeClockTopMargin -= - context.resources.getDimensionPixelSize( - com.android.systemui.customization.R.dimen.small_clock_height - ) + context.resources.getDimensionPixelSize(customizationR.dimen.small_clock_height) } connect(R.id.lockscreen_clock_view_large, TOP, PARENT_ID, TOP, largeClockTopMargin) constrainHeight(R.id.lockscreen_clock_view_large, WRAP_CONTENT) @@ -145,18 +176,15 @@ constructor( constrainWidth(R.id.lockscreen_clock_view, WRAP_CONTENT) constrainHeight( R.id.lockscreen_clock_view, - context.resources.getDimensionPixelSize( - com.android.systemui.customization.R.dimen.small_clock_height - ) + context.resources.getDimensionPixelSize(customizationR.dimen.small_clock_height) ) connect( R.id.lockscreen_clock_view, START, PARENT_ID, START, - context.resources.getDimensionPixelSize( - com.android.systemui.customization.R.dimen.clock_padding_start - ) + context.resources.getDimensionPixelSize(customizationR.dimen.clock_padding_start) + + context.resources.getDimensionPixelSize(R.dimen.status_view_margin_horizontal) ) var smallClockTopMargin = if (splitShadeStateController.shouldUseSplitNotificationShade(context.resources)) { @@ -167,12 +195,12 @@ constructor( } if (keyguardClockViewModel.useLargeClock) { smallClockTopMargin -= - context.resources.getDimensionPixelSize( - com.android.systemui.customization.R.dimen.small_clock_height - ) + context.resources.getDimensionPixelSize(customizationR.dimen.small_clock_height) } connect(R.id.lockscreen_clock_view, TOP, PARENT_ID, TOP, smallClockTopMargin) } + + constrainWeatherClockDateIconsBarrier(constraints) } private fun getDimen(name: String): Int { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt index 8c5e9b4c6817..d75a72f91061 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt @@ -18,7 +18,6 @@ package com.android.systemui.keyguard.ui.view.layout.sections import android.content.Context -import android.view.View import androidx.constraintlayout.widget.ConstraintSet import androidx.constraintlayout.widget.ConstraintSet.BOTTOM import androidx.constraintlayout.widget.ConstraintSet.END @@ -67,7 +66,6 @@ constructor( notificationStackSizeCalculator, mainDispatcher, ) { - private val smartSpaceBarrier = View.generateViewId() override fun applyConstraints(constraintSet: ConstraintSet) { if (!KeyguardShadeMigrationNssl.isEnabled) { return diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt index 37842a84c3d3..2f99719df36c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSection.kt @@ -31,7 +31,8 @@ import com.android.systemui.keyguard.shared.model.KeyguardSection import com.android.systemui.keyguard.ui.binder.KeyguardSmartspaceViewBinder import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel -import com.android.systemui.shared.R +import com.android.systemui.res.R as R +import com.android.systemui.shared.R as sharedR import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController import dagger.Lazy import javax.inject.Inject @@ -100,94 +101,94 @@ constructor( if (!migrateClocksToBlueprint()) { return } + val horizontalPaddingStart = + context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_start) + + context.resources.getDimensionPixelSize(R.dimen.status_view_margin_horizontal) + val horizontalPaddingEnd = + context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_end) + + context.resources.getDimensionPixelSize(R.dimen.status_view_margin_horizontal) constraintSet.apply { // migrate addDateWeatherView, addWeatherView from KeyguardClockSwitchController - constrainHeight(R.id.date_smartspace_view, ConstraintSet.WRAP_CONTENT) - constrainWidth(R.id.date_smartspace_view, ConstraintSet.WRAP_CONTENT) + constrainHeight(sharedR.id.date_smartspace_view, ConstraintSet.WRAP_CONTENT) + constrainWidth(sharedR.id.date_smartspace_view, ConstraintSet.WRAP_CONTENT) connect( - R.id.date_smartspace_view, + sharedR.id.date_smartspace_view, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START, - context.resources.getDimensionPixelSize( - com.android.systemui.res.R.dimen.below_clock_padding_start - ) + horizontalPaddingStart ) - constrainWidth(R.id.weather_smartspace_view, ConstraintSet.WRAP_CONTENT) + constrainWidth(sharedR.id.weather_smartspace_view, ConstraintSet.WRAP_CONTENT) connect( - R.id.weather_smartspace_view, + sharedR.id.weather_smartspace_view, ConstraintSet.TOP, - R.id.date_smartspace_view, + sharedR.id.date_smartspace_view, ConstraintSet.TOP ) connect( - R.id.weather_smartspace_view, + sharedR.id.weather_smartspace_view, ConstraintSet.BOTTOM, - R.id.date_smartspace_view, + sharedR.id.date_smartspace_view, ConstraintSet.BOTTOM ) connect( - R.id.weather_smartspace_view, + sharedR.id.weather_smartspace_view, ConstraintSet.START, - R.id.date_smartspace_view, + sharedR.id.date_smartspace_view, ConstraintSet.END, 4 ) // migrate addSmartspaceView from KeyguardClockSwitchController - constrainHeight(R.id.bc_smartspace_view, ConstraintSet.WRAP_CONTENT) + constrainHeight(sharedR.id.bc_smartspace_view, ConstraintSet.WRAP_CONTENT) connect( - R.id.bc_smartspace_view, + sharedR.id.bc_smartspace_view, ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START, - context.resources.getDimensionPixelSize( - com.android.systemui.res.R.dimen.below_clock_padding_start - ) + horizontalPaddingStart ) connect( - R.id.bc_smartspace_view, + sharedR.id.bc_smartspace_view, ConstraintSet.END, if (keyguardClockViewModel.clockShouldBeCentered.value) ConstraintSet.PARENT_ID - else com.android.systemui.res.R.id.split_shade_guideline, + else R.id.split_shade_guideline, ConstraintSet.END, - context.resources.getDimensionPixelSize( - com.android.systemui.res.R.dimen.below_clock_padding_end - ) + horizontalPaddingEnd ) if (keyguardClockViewModel.hasCustomWeatherDataDisplay.value) { - clear(R.id.date_smartspace_view, ConstraintSet.TOP) + clear(sharedR.id.date_smartspace_view, ConstraintSet.TOP) connect( - R.id.date_smartspace_view, + sharedR.id.date_smartspace_view, ConstraintSet.BOTTOM, - R.id.bc_smartspace_view, + sharedR.id.bc_smartspace_view, ConstraintSet.TOP ) } else { - clear(R.id.date_smartspace_view, ConstraintSet.BOTTOM) + clear(sharedR.id.date_smartspace_view, ConstraintSet.BOTTOM) connect( - R.id.date_smartspace_view, + sharedR.id.date_smartspace_view, ConstraintSet.TOP, - com.android.systemui.res.R.id.lockscreen_clock_view, + R.id.lockscreen_clock_view, ConstraintSet.BOTTOM ) connect( - R.id.bc_smartspace_view, + sharedR.id.bc_smartspace_view, ConstraintSet.TOP, - R.id.date_smartspace_view, + sharedR.id.date_smartspace_view, ConstraintSet.BOTTOM ) } createBarrier( - com.android.systemui.res.R.id.smart_space_barrier_bottom, + R.id.smart_space_barrier_bottom, Barrier.BOTTOM, 0, *intArrayOf( - R.id.bc_smartspace_view, - R.id.date_smartspace_view, - R.id.weather_smartspace_view, + sharedR.id.bc_smartspace_view, + sharedR.id.date_smartspace_view, + sharedR.id.weather_smartspace_view, ) ) } @@ -212,7 +213,7 @@ constructor( private fun updateVisibility(constraintSet: ConstraintSet) { constraintSet.apply { setVisibility( - R.id.weather_smartspace_view, + sharedR.id.weather_smartspace_view, when (keyguardClockViewModel.hasCustomWeatherDataDisplay.value) { true -> ConstraintSet.GONE false -> @@ -223,7 +224,7 @@ constructor( } ) setVisibility( - R.id.date_smartspace_view, + sharedR.id.date_smartspace_view, if (keyguardClockViewModel.hasCustomWeatherDataDisplay.value) ConstraintSet.GONE else ConstraintSet.VISIBLE ) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt index bc518210a067..6aa2ecabae75 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/GlanceableHubToLockscreenTransitionViewModel.kt @@ -17,7 +17,7 @@ package com.android.systemui.keyguard.ui.viewmodel import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.keyguard.domain.interactor.FromLockscreenTransitionInteractor +import com.android.systemui.keyguard.domain.interactor.FromGlanceableHubTransitionInteractor import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow import javax.inject.Inject @@ -37,7 +37,7 @@ constructor( ) { private val transitionAnimation = animationFlow.setup( - duration = FromLockscreenTransitionInteractor.TO_GLANCEABLE_HUB_DURATION, + duration = FromGlanceableHubTransitionInteractor.TO_LOCKSCREEN_DURATION, from = KeyguardState.GLANCEABLE_HUB, to = KeyguardState.LOCKSCREEN, ) @@ -45,10 +45,20 @@ constructor( // TODO(b/315205222): implement full animation spec instead of just a simple fade. val keyguardAlpha: Flow<Float> = transitionAnimation.sharedFlow( - duration = FromLockscreenTransitionInteractor.TO_GLANCEABLE_HUB_DURATION, + duration = FromGlanceableHubTransitionInteractor.TO_LOCKSCREEN_DURATION, onStep = { it }, onFinish = { 1f }, onCancel = { 0f }, name = "GLANCEABLE_HUB->LOCKSCREEN: keyguardAlpha", ) + + // TODO(b/315205216): implement full animation spec instead of just a simple fade. + val notificationAlpha: Flow<Float> = + transitionAnimation.sharedFlow( + duration = FromGlanceableHubTransitionInteractor.TO_LOCKSCREEN_DURATION, + onStep = { it }, + onFinish = { 1f }, + onCancel = { 0f }, + name = "GLANCEABLE_HUB->LOCKSCREEN: notificationAlpha", + ) } 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 f37d9f801db3..6763e0a1b798 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 @@ -27,6 +27,7 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.shared.model.SettingsClockSize import com.android.systemui.plugins.clocks.ClockController import com.android.systemui.res.R +import com.android.systemui.statusbar.notification.domain.interactor.NotificationsKeyguardInteractor import com.android.systemui.statusbar.policy.SplitShadeStateController import com.android.systemui.util.Utils import javax.inject.Inject @@ -44,6 +45,7 @@ constructor( private val keyguardClockInteractor: KeyguardClockInteractor, @Application private val applicationScope: CoroutineScope, private val splitShadeStateController: SplitShadeStateController, + notifsKeyguardInteractor: NotificationsKeyguardInteractor, ) { var burnInLayer: Layer? = null val useLargeClock: Boolean @@ -91,6 +93,13 @@ constructor( initialValue = false ) + val isAodIconsVisible: StateFlow<Boolean> = + notifsKeyguardInteractor.areNotificationsFullyHidden.stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = false + ) + // Needs to use a non application context to get display cutout. fun getSmallClockTopMargin(context: Context) = if (splitShadeStateController.shouldUseSplitNotificationShade(context.resources)) { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt index ea6670267270..709e184e6e52 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt @@ -21,6 +21,7 @@ import android.graphics.Point import android.view.View.VISIBLE import com.android.systemui.Flags.newAodTransition import com.android.systemui.common.shared.model.NotificationContainerBounds +import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor @@ -56,6 +57,7 @@ constructor( private val deviceEntryInteractor: DeviceEntryInteractor, private val dozeParameters: DozeParameters, private val keyguardInteractor: KeyguardInteractor, + communalInteractor: CommunalInteractor, keyguardTransitionInteractor: KeyguardTransitionInteractor, private val notificationsKeyguardInteractor: NotificationsKeyguardInteractor, aodToLockscreenTransitionViewModel: AodToLockscreenTransitionViewModel, @@ -88,11 +90,23 @@ constructor( /** An observable for the alpha level for the entire keyguard root view. */ val alpha: Flow<Float> = - merge( - aodAlphaViewModel.alpha, - lockscreenToGlanceableHubTransitionViewModel.keyguardAlpha, - glanceableHubToLockscreenTransitionViewModel.keyguardAlpha, - ) + combine( + communalInteractor.isIdleOnCommunal, + merge( + aodAlphaViewModel.alpha, + lockscreenToGlanceableHubTransitionViewModel.keyguardAlpha, + glanceableHubToLockscreenTransitionViewModel.keyguardAlpha, + ) + ) { isIdleOnCommunal, alpha -> + if (isIdleOnCommunal) { + // Keyguard should not show while the communal hub is fully visible. This check + // is added since at the moment, closing the notification shade will cause the + // keyguard alpha to be set back to 1. + 0f + } else { + alpha + } + } .distinctUntilChanged() /** Specific alpha value for elements visible during [KeyguardState.LOCKSCREEN] */ diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModel.kt index 3ea83ae21085..3afa49e50167 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenToGlanceableHubTransitionViewModel.kt @@ -51,4 +51,14 @@ constructor( onCancel = { 1f }, name = "LOCKSCREEN->GLANCEABLE_HUB: keyguardAlpha", ) + + // TODO(b/315205216): implement full animation spec instead of just a simple fade. + val notificationAlpha: Flow<Float> = + transitionAnimation.sharedFlow( + duration = FromLockscreenTransitionInteractor.TO_GLANCEABLE_HUB_DURATION, + onStep = { 1f - it }, + onFinish = { 0f }, + onCancel = { 1f }, + name = "LOCKSCREEN->GLANCEABLE_HUB: notificationAlpha", + ) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt index 693e3b7506fc..ca9c8571f6b9 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/SideFpsProgressBarViewModel.kt @@ -20,7 +20,7 @@ import android.animation.ValueAnimator import android.content.Context import android.graphics.Point import androidx.annotation.VisibleForTesting -import androidx.core.animation.doOnEnd +import androidx.core.animation.addListener import com.android.systemui.Flags import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor import com.android.systemui.biometrics.domain.interactor.SideFpsSensorInteractor @@ -30,15 +30,18 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFingerprintAuthInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.shared.model.AcquiredFingerprintAuthenticationStatus import com.android.systemui.keyguard.shared.model.ErrorFingerprintAuthenticationStatus import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus +import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.res.R import com.android.systemui.statusbar.phone.DozeServiceHost import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -46,13 +49,14 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onCompletion -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch +@ExperimentalCoroutinesApi @SysUISingleton class SideFpsProgressBarViewModel @Inject @@ -63,9 +67,11 @@ constructor( // todo (b/317432075) Injecting DozeServiceHost directly instead of using it through // DozeInteractor as DozeServiceHost already depends on DozeInteractor. private val dozeServiceHost: DozeServiceHost, + private val keyguardInteractor: KeyguardInteractor, displayStateInteractor: DisplayStateInteractor, @Main private val mainDispatcher: CoroutineDispatcher, @Application private val applicationScope: CoroutineScope, + private val powerInteractor: PowerInteractor, ) { private val _progress = MutableStateFlow(0.0f) private val _visible = MutableStateFlow(false) @@ -176,48 +182,54 @@ constructor( return@collectLatest } animatorJob = - combine( - sfpsSensorInteractor.authenticationDuration, - fpAuthRepository.authenticationStatus, - ::Pair - ) - .onEach { (authDuration, authStatus) -> - when (authStatus) { - is AcquiredFingerprintAuthenticationStatus -> { - if (authStatus.fingerprintCaptureStarted) { - _visible.value = true - dozeServiceHost.fireSideFpsAcquisitionStarted() - _animator?.cancel() - _animator = - ValueAnimator.ofFloat(0.0f, 1.0f) - .setDuration(authDuration) - .apply { - addUpdateListener { - _progress.value = it.animatedValue as Float - } - addListener( - doOnEnd { - if (_progress.value == 0.0f) { - _visible.value = false - } + sfpsSensorInteractor.authenticationDuration + .flatMapLatest { authDuration -> + _animator?.cancel() + fpAuthRepository.authenticationStatus.map { authStatus -> + when (authStatus) { + is AcquiredFingerprintAuthenticationStatus -> { + if (authStatus.fingerprintCaptureStarted) { + if (keyguardInteractor.isDozing.value) { + dozeServiceHost.fireSideFpsAcquisitionStarted() + } else { + powerInteractor + .wakeUpForSideFingerprintAcquisition() + } + _animator?.cancel() + _animator = + ValueAnimator.ofFloat(0.0f, 1.0f) + .setDuration(authDuration) + .apply { + addUpdateListener { + _progress.value = + it.animatedValue as Float } - ) - } - _animator?.start() - } else if (authStatus.fingerprintCaptureCompleted) { - onFingerprintCaptureCompleted() - } else { - // Abandoned FP Auth attempt - _animator?.reverse() + addListener( + onEnd = { + if (_progress.value == 0.0f) { + _visible.value = false + } + }, + onStart = { _visible.value = true }, + onCancel = { _visible.value = false } + ) + } + _animator?.start() + } else if (authStatus.fingerprintCaptureCompleted) { + onFingerprintCaptureCompleted() + } else { + // Abandoned FP Auth attempt + _animator?.reverse() + } } + is ErrorFingerprintAuthenticationStatus -> + onFingerprintCaptureCompleted() + is FailFingerprintAuthenticationStatus -> + onFingerprintCaptureCompleted() + is SuccessFingerprintAuthenticationStatus -> + onFingerprintCaptureCompleted() + else -> Unit } - is ErrorFingerprintAuthenticationStatus -> - onFingerprintCaptureCompleted() - is FailFingerprintAuthenticationStatus -> - onFingerprintCaptureCompleted() - is SuccessFingerprintAuthenticationStatus -> - onFingerprintCaptureCompleted() - else -> Unit } } .flowOn(mainDispatcher) diff --git a/packages/SystemUI/src/com/android/systemui/log/SideFpsLogger.kt b/packages/SystemUI/src/com/android/systemui/log/SideFpsLogger.kt index 171656a48e58..ce64ac1cbcd6 100644 --- a/packages/SystemUI/src/com/android/systemui/log/SideFpsLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/log/SideFpsLogger.kt @@ -117,4 +117,13 @@ class SideFpsLogger @Inject constructor(@BouncerLog private val buffer: LogBuffe { "SideFpsSensor auth duration changed: $long1" } ) } + + fun restToUnlockSettingEnabledChanged(enabled: Boolean) { + buffer.log( + TAG, + LogLevel.DEBUG, + { bool1 = enabled }, + { "restToUnlockSettingEnabled: $bool1" } + ) + } } diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaResumeListener.kt b/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaResumeListener.kt index 23ee00d88fdc..a3029b284934 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaResumeListener.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaResumeListener.kt @@ -146,7 +146,7 @@ constructor( null, UserHandle.ALL ) - userTracker.addCallback(userTrackerCallback, mainExecutor) + userTracker.addCallback(userTrackerCallback, backgroundExecutor) loadSavedComponents() } } diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java index 58e042868607..91c86dff34ea 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java @@ -84,6 +84,7 @@ import com.android.systemui.res.R; import com.android.systemui.settings.UserTracker; import com.android.systemui.shared.system.ActivityManagerWrapper; import com.android.systemui.shared.system.InputChannelCompat; +import com.android.systemui.shared.system.InputMonitorCompat; import com.android.systemui.shared.system.QuickStepContract; import com.android.systemui.shared.system.SysUiStatsLog; import com.android.systemui.shared.system.TaskStackChangeListener; @@ -261,7 +262,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack private boolean mIsTrackpadThreeFingerSwipe; private boolean mIsButtonForcedVisible; - private InputMonitor mInputMonitor; + private InputMonitorCompat mInputMonitor; private InputChannelCompat.InputEventReceiver mInputEventReceiver; private NavigationEdgeBackPlugin mEdgeBackPlugin; @@ -665,10 +666,8 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack } // Register input event receiver - mInputMonitor = mContext.getSystemService(InputManager.class).monitorGestureInput( - "edge-swipe", mDisplayId); - mInputEventReceiver = new InputChannelCompat.InputEventReceiver( - mInputMonitor.getInputChannel(), Looper.getMainLooper(), + mInputMonitor = new InputMonitorCompat("edge-swipe", mDisplayId); + mInputEventReceiver = mInputMonitor.getInputReceiver(Looper.getMainLooper(), Choreographer.getInstance(), this::onInputEvent); // Add a nav bar panel window diff --git a/packages/SystemUI/src/com/android/systemui/power/PowerUI.java b/packages/SystemUI/src/com/android/systemui/power/PowerUI.java index 958ace358816..21de185ee838 100644 --- a/packages/SystemUI/src/com/android/systemui/power/PowerUI.java +++ b/packages/SystemUI/src/com/android/systemui/power/PowerUI.java @@ -26,6 +26,8 @@ import android.content.res.Configuration; import android.database.ContentObserver; import android.os.BatteryManager; import android.os.Handler; +import android.os.HandlerExecutor; +import android.os.HandlerThread; import android.os.IThermalEventListener; import android.os.IThermalService; import android.os.PowerManager; @@ -95,6 +97,7 @@ public class PowerUI implements private Future mLastShowWarningTask; private boolean mEnableSkinTemperatureWarning; private boolean mEnableUsbTemperatureAlarm; + private final HandlerThread mHandlerThread; private int mLowBatteryAlertCloseLevel; private final int[] mLowBatteryReminderLevels = new int[2]; @@ -167,6 +170,8 @@ public class PowerUI implements mPowerManager = powerManager; mWakefulnessLifecycle = wakefulnessLifecycle; mUserTracker = userTracker; + mHandlerThread = new HandlerThread("PowerUI"); + mHandlerThread.start(); } public void start() { @@ -185,7 +190,8 @@ public class PowerUI implements false, obs, UserHandle.USER_ALL); updateBatteryWarningLevels(); mReceiver.init(); - mUserTracker.addCallback(mUserChangedCallback, mContext.getMainExecutor()); + mUserTracker.addCallback(mUserChangedCallback, + new HandlerExecutor(mHandlerThread.getThreadHandler())); mWakefulnessLifecycle.addObserver(mWakefulnessObserver); // Check to see if we need to let the user know that the phone previously shut down due diff --git a/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt b/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt index d9e3e55c1ad3..3f8834af3ea4 100644 --- a/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/power/domain/interactor/PowerInteractor.kt @@ -119,6 +119,11 @@ constructor( } } + /** Wakes up the device for the Side FPS acquisition event. */ + fun wakeUpForSideFingerprintAcquisition() { + repository.wakeUp("SFPS_FP_ACQUISITION_STARTED", PowerManager.WAKE_REASON_BIOMETRIC) + } + /** * Called from [KeyguardService] to inform us that the device has started waking up. This is the * canonical source of wakefulness information for System UI. This method should not be called diff --git a/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyChip.kt b/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyChip.kt index 8e1b00d825aa..7a4be3fdc93b 100644 --- a/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyChip.kt +++ b/packages/SystemUI/src/com/android/systemui/privacy/OngoingPrivacyChip.kt @@ -23,11 +23,11 @@ import android.view.Gravity.END import android.view.ViewGroup import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.view.ViewGroup.LayoutParams.WRAP_CONTENT +import android.widget.FrameLayout import android.widget.ImageView import android.widget.LinearLayout import com.android.settingslib.Utils import com.android.systemui.res.R -import com.android.systemui.animation.view.LaunchableFrameLayout import com.android.systemui.statusbar.events.BackgroundAnimatableView class OngoingPrivacyChip @JvmOverloads constructor( @@ -35,7 +35,7 @@ class OngoingPrivacyChip @JvmOverloads constructor( attrs: AttributeSet? = null, defStyleAttrs: Int = 0, defStyleRes: Int = 0 -) : LaunchableFrameLayout(context, attrs, defStyleAttrs, defStyleRes), BackgroundAnimatableView { +) : FrameLayout(context, attrs, defStyleAttrs, defStyleRes), BackgroundAnimatableView { private var configuration: Configuration private var iconMargin = 0 @@ -43,6 +43,8 @@ class OngoingPrivacyChip @JvmOverloads constructor( private var iconColor = 0 private val iconsContainer: LinearLayout + val launchableContentView + get() = iconsContainer var privacyList = emptyList<PrivacyItem>() set(value) { diff --git a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogControllerV2.kt b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogControllerV2.kt index 76ef8a2b813c..f121630d180e 100644 --- a/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogControllerV2.kt +++ b/packages/SystemUI/src/com/android/systemui/privacy/PrivacyDialogControllerV2.kt @@ -26,9 +26,9 @@ import android.content.pm.PackageManager import android.os.UserHandle import android.permission.PermissionGroupUsage import android.permission.PermissionManager -import android.view.View import androidx.annotation.MainThread import androidx.annotation.WorkerThread +import androidx.core.view.isVisible import com.android.internal.logging.UiEventLogger import com.android.systemui.animation.DialogLaunchAnimator import com.android.systemui.appops.AppOpsController @@ -214,7 +214,7 @@ class PrivacyDialogControllerV2( * @param context A context to use to create the dialog. * @see filterAndSelect */ - fun showDialog(context: Context, view: View? = null) { + fun showDialog(context: Context, privacyChip: OngoingPrivacyChip? = null) { dismissDialog() backgroundExecutor.execute { val usage = permGroupUsage() @@ -277,8 +277,8 @@ class PrivacyDialogControllerV2( ) d.setShowForAllUsers(true) d.addOnDismissListener(onDialogDismissed) - if (view != null) { - val controller = getPrivacyDialogController(view) + if (privacyChip != null) { + val controller = getPrivacyDialogController(privacyChip) if (controller == null) { d.show() } else { @@ -296,10 +296,13 @@ class PrivacyDialogControllerV2( } } - private fun getPrivacyDialogController(source: View): DialogLaunchAnimator.Controller? { - val delegate = DialogLaunchAnimator.Controller.fromView(source) ?: return null + private fun getPrivacyDialogController( + source: OngoingPrivacyChip + ): DialogLaunchAnimator.Controller? { + val delegate = + DialogLaunchAnimator.Controller.fromView(source.launchableContentView) ?: return null return object : DialogLaunchAnimator.Controller by delegate { - override fun shouldAnimateExit() = false + override fun shouldAnimateExit() = source.isVisible } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java index a3b92541d593..a2dfc0159c6e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImpl.java @@ -27,8 +27,11 @@ import android.graphics.PointF; import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; +import android.view.ViewGroup; import android.widget.FrameLayout; +import androidx.annotation.Nullable; + import com.android.systemui.Dumpable; import com.android.systemui.qs.customize.QSCustomizer; import com.android.systemui.res.R; @@ -53,6 +56,7 @@ public class QSContainerImpl extends FrameLayout implements Dumpable { private QuickStatusBarHeader mHeader; private float mQsExpansion; private QSCustomizer mQSCustomizer; + private QSPanel mQSPanel; private NonInterceptingScrollView mQSPanelContainer; private int mHorizontalMargins; @@ -72,6 +76,7 @@ public class QSContainerImpl extends FrameLayout implements Dumpable { protected void onFinishInflate() { super.onFinishInflate(); mQSPanelContainer = findViewById(R.id.expanded_qs_scroll_view); + mQSPanel = findViewById(R.id.quick_settings_panel); mHeader = findViewById(R.id.header); mQSCustomizer = findViewById(R.id.qs_customize); setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO); @@ -79,6 +84,13 @@ public class QSContainerImpl extends FrameLayout implements Dumpable { void setSceneContainerEnabled(boolean enabled) { mSceneContainerEnabled = enabled; + if (enabled) { + mQSPanelContainer.removeAllViews(); + removeView(mQSPanelContainer); + LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + addView(mQSPanel, 0, lp); + } } @Override @@ -97,20 +109,26 @@ public class QSContainerImpl extends FrameLayout implements Dumpable { protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { // QSPanel will show as many rows as it can (up to TileLayout.MAX_ROWS) such that the // bottom and footer are inside the screen. - MarginLayoutParams layoutParams = (MarginLayoutParams) mQSPanelContainer.getLayoutParams(); - int availableHeight = View.MeasureSpec.getSize(heightMeasureSpec); - int maxQs = availableHeight - layoutParams.topMargin - layoutParams.bottomMargin - - getPaddingBottom(); - int padding = mPaddingLeft + mPaddingRight + layoutParams.leftMargin - + layoutParams.rightMargin; - final int qsPanelWidthSpec = getChildMeasureSpec(widthMeasureSpec, padding, - layoutParams.width); - mQSPanelContainer.measure(qsPanelWidthSpec, - MeasureSpec.makeMeasureSpec(maxQs, MeasureSpec.AT_MOST)); - int width = mQSPanelContainer.getMeasuredWidth() + padding; - super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(availableHeight, MeasureSpec.EXACTLY)); + + if (!mSceneContainerEnabled) { + MarginLayoutParams layoutParams = + (MarginLayoutParams) mQSPanelContainer.getLayoutParams(); + int maxQs = availableHeight - layoutParams.topMargin - layoutParams.bottomMargin + - getPaddingBottom(); + int padding = mPaddingLeft + mPaddingRight + layoutParams.leftMargin + + layoutParams.rightMargin; + final int qsPanelWidthSpec = getChildMeasureSpec(widthMeasureSpec, padding, + layoutParams.width); + mQSPanelContainer.measure(qsPanelWidthSpec, + MeasureSpec.makeMeasureSpec(maxQs, MeasureSpec.AT_MOST)); + int width = mQSPanelContainer.getMeasuredWidth() + padding; + super.onMeasure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), + MeasureSpec.makeMeasureSpec(availableHeight, MeasureSpec.EXACTLY)); + } else { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + // QSCustomizer will always be the height of the screen, but do this after // other measuring to avoid changing the height of the QS. mQSCustomizer.measure(widthMeasureSpec, @@ -130,12 +148,15 @@ public class QSContainerImpl extends FrameLayout implements Dumpable { @Override protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed, int parentHeightMeasureSpec, int heightUsed) { - // Do not measure QSPanel again when doing super.onMeasure. - // This prevents the pages in PagedTileLayout to be remeasured with a different (incorrect) - // size to the one used for determining the number of rows and then the number of pages. - if (child != mQSPanelContainer) { - super.measureChildWithMargins(child, parentWidthMeasureSpec, widthUsed, - parentHeightMeasureSpec, heightUsed); + if (!mSceneContainerEnabled) { + // Do not measure QSPanel again when doing super.onMeasure. + // This prevents the pages in PagedTileLayout to be remeasured with a different + // (incorrect) size to the one used for determining the number of rows and then the + // number of pages. + if (child != mQSPanelContainer) { + super.measureChildWithMargins(child, parentWidthMeasureSpec, widthUsed, + parentHeightMeasureSpec, heightUsed); + } } } @@ -151,6 +172,7 @@ public class QSContainerImpl extends FrameLayout implements Dumpable { updateClippingPath(); } + @Nullable public NonInterceptingScrollView getQSPanelContainer() { return mQSPanelContainer; } @@ -172,11 +194,19 @@ public class QSContainerImpl extends FrameLayout implements Dumpable { .getDimensionPixelSize( R.dimen.large_screen_shade_header_height); } - mQSPanelContainer.setPaddingRelative( - mQSPanelContainer.getPaddingStart(), - mSceneContainerEnabled ? 0 : topPadding, - mQSPanelContainer.getPaddingEnd(), - mQSPanelContainer.getPaddingBottom()); + if (mQSPanelContainer != null) { + mQSPanelContainer.setPaddingRelative( + mQSPanelContainer.getPaddingStart(), + mSceneContainerEnabled ? 0 : topPadding, + mQSPanelContainer.getPaddingEnd(), + mQSPanelContainer.getPaddingBottom()); + } else { + mQSPanel.setPaddingRelative( + mQSPanel.getPaddingStart(), + mSceneContainerEnabled ? 0 : topPadding, + mQSPanel.getPaddingEnd(), + mQSPanel.getPaddingBottom()); + } int horizontalMargins = getResources().getDimensionPixelSize(R.dimen.qs_horizontal_margin); int horizontalPadding = getResources().getDimensionPixelSize( @@ -220,7 +250,9 @@ public class QSContainerImpl extends FrameLayout implements Dumpable { public void setExpansion(float expansion) { mQsExpansion = expansion; - mQSPanelContainer.setScrollingEnabled(expansion > 0f); + if (mQSPanelContainer != null) { + mQSPanelContainer.setScrollingEnabled(expansion > 0f); + } updateExpansion(); } @@ -239,7 +271,7 @@ public class QSContainerImpl extends FrameLayout implements Dumpable { lp.rightMargin = mHorizontalMargins; lp.leftMargin = mHorizontalMargins; } - if (view == mQSPanelContainer) { + if (view == mQSPanelContainer || view == mQSPanel) { // QS panel lays out some of its content full width qsPanelController.setContentMargins(mContentHorizontalPadding, mContentHorizontalPadding); diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java index 7b001c7b72f7..ffbc56098e26 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSContainerImplController.java @@ -81,6 +81,9 @@ public class QSContainerImplController extends ViewController<QSContainerImpl> { public void onInit() { mQuickStatusBarHeaderController.init(); mView.setSceneContainerEnabled(mSceneContainerEnabled); + if (mSceneContainerEnabled && mQsPanelController != null) { + mQSPanelContainer.setOnTouchListener(null); + } } public void setListening(boolean listening) { @@ -91,13 +94,17 @@ public class QSContainerImplController extends ViewController<QSContainerImpl> { protected void onViewAttached() { mView.updateResources(mQsPanelController, mQuickStatusBarHeaderController); mConfigurationController.addCallback(mConfigurationListener); - mQSPanelContainer.setOnTouchListener(mContainerTouchHandler); + if (!mSceneContainerEnabled && mQSPanelContainer != null) { + mQSPanelContainer.setOnTouchListener(mContainerTouchHandler); + } } @Override protected void onViewDetached() { mConfigurationController.removeCallback(mConfigurationListener); - mQSPanelContainer.setOnTouchListener(null); + if (mQSPanelContainer != null) { + mQSPanelContainer.setOnTouchListener(null); + } } public QSContainerImpl getView() { diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java index 7f91fd2ebb80..290821e4ab13 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java @@ -61,6 +61,7 @@ import com.android.systemui.qs.footer.ui.binder.FooterActionsViewBinder; import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.res.R; +import com.android.systemui.scene.shared.flag.SceneContainerFlags; import com.android.systemui.shade.transition.LargeScreenShadeInterpolator; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.StatusBarState; @@ -171,8 +172,11 @@ public class QSImpl implements QS, CommandQueue.Callbacks, StatusBarStateControl private CommandQueue mCommandQueue; private View mRootView; + @Nullable private View mFooterActionsView; + private final SceneContainerFlags mSceneContainerFlags; + @Inject public QSImpl(RemoteInputQuickSettingsDisabler remoteInputQsDisabler, SysuiStatusBarStateController statusBarStateController, CommandQueue commandQueue, @@ -185,7 +189,8 @@ public class QSImpl implements QS, CommandQueue.Callbacks, StatusBarStateControl FooterActionsViewModel.Factory footerActionsViewModelFactory, FooterActionsViewBinder footerActionsViewBinder, LargeScreenShadeInterpolator largeScreenShadeInterpolator, - FeatureFlags featureFlags) { + FeatureFlags featureFlags, + SceneContainerFlags sceneContainerFlags) { mRemoteInputQuickSettingsDisabler = remoteInputQsDisabler; mQsMediaHost = qsMediaHost; mQqsMediaHost = qqsMediaHost; @@ -201,6 +206,7 @@ public class QSImpl implements QS, CommandQueue.Callbacks, StatusBarStateControl mFooterActionsViewModelFactory = footerActionsViewModelFactory; mFooterActionsViewBinder = footerActionsViewBinder; mListeningAndVisibilityLifecycleOwner = new ListeningAndVisibilityLifecycleOwner(); + mSceneContainerFlags = sceneContainerFlags; } /** @@ -216,10 +222,17 @@ public class QSImpl implements QS, CommandQueue.Callbacks, StatusBarStateControl mQSPanelController.init(); mQuickQSPanelController.init(); - mQSFooterActionsViewModel = mFooterActionsViewModelFactory - .create(mListeningAndVisibilityLifecycleOwner); - bindFooterActionsView(mRootView); - mFooterActionsController.init(); + if (!mSceneContainerFlags.isEnabled()) { + mQSFooterActionsViewModel = mFooterActionsViewModelFactory + .create(mListeningAndVisibilityLifecycleOwner); + bindFooterActionsView(mRootView); + mFooterActionsController.init(); + } else { + View footerView = mRootView.findViewById(R.id.qs_footer_actions); + if (footerView != null) { + ((ViewGroup) footerView.getParent()).removeView(footerView); + } + } mQSPanelScrollView = mRootView.findViewById(R.id.expanded_qs_scroll_view); mQSPanelScrollView.addOnLayoutChangeListener( @@ -234,6 +247,7 @@ public class QSImpl implements QS, CommandQueue.Callbacks, StatusBarStateControl mScrollListener.onQsPanelScrollChanged(scrollY); } }); + mQSPanelScrollView.setScrollingEnabled(!mSceneContainerFlags.isEnabled()); mHeader = mRootView.findViewById(R.id.header); mFooter = qsComponent.getQSFooter(); @@ -481,7 +495,9 @@ public class QSImpl implements QS, CommandQueue.Callbacks, StatusBarStateControl boolean footerVisible = qsPanelVisible && (mQsExpanded || !keyguardShowing || mHeaderAnimating || mShowCollapsedOnKeyguard); mFooter.setVisibility(footerVisible ? View.VISIBLE : View.INVISIBLE); - mFooterActionsView.setVisibility(footerVisible ? View.VISIBLE : View.INVISIBLE); + if (mFooterActionsView != null) { + mFooterActionsView.setVisibility(footerVisible ? View.VISIBLE : View.INVISIBLE); + } mFooter.setExpanded((keyguardShowing && !mHeaderAnimating && !mShowCollapsedOnKeyguard) || (mQsExpanded && !mStackScrollerOverscrolling)); mQSPanelController.setVisibility(qsPanelVisible ? View.VISIBLE : View.INVISIBLE); @@ -622,8 +638,13 @@ public class QSImpl implements QS, CommandQueue.Callbacks, StatusBarStateControl @Override public int getHeightDiff() { - return mQSPanelScrollView.getBottom() - mHeader.getBottom() - + mHeader.getPaddingBottom(); + if (mSceneContainerFlags.isEnabled()) { + return mQSPanelController.getViewBottom() - mHeader.getBottom() + + mHeader.getPaddingBottom(); + } else { + return mQSPanelScrollView.getBottom() - mHeader.getBottom() + + mHeader.getPaddingBottom(); + } } @Override @@ -678,25 +699,29 @@ public class QSImpl implements QS, CommandQueue.Callbacks, StatusBarStateControl mFooter.setExpansion(onKeyguardAndExpanded ? 1 : expansion); float footerActionsExpansion = onKeyguardAndExpanded ? 1 : mInSplitShade ? alphaProgress : expansion; - mQSFooterActionsViewModel.onQuickSettingsExpansionChanged(footerActionsExpansion, - mInSplitShade); + if (mQSFooterActionsViewModel != null) { + mQSFooterActionsViewModel.onQuickSettingsExpansionChanged(footerActionsExpansion, + mInSplitShade); + } mQSPanelController.setRevealExpansion(expansion); mQSPanelController.getTileLayout().setExpansion(expansion, proposedTranslation); mQuickQSPanelController.getTileLayout().setExpansion(expansion, proposedTranslation); - float qsScrollViewTranslation = - onKeyguard && !mShowCollapsedOnKeyguard ? panelTranslationY : 0; - mQSPanelScrollView.setTranslationY(qsScrollViewTranslation); + if (!mSceneContainerFlags.isEnabled()) { + float qsScrollViewTranslation = + onKeyguard && !mShowCollapsedOnKeyguard ? panelTranslationY : 0; + mQSPanelScrollView.setTranslationY(qsScrollViewTranslation); - if (fullyCollapsed) { - mQSPanelScrollView.setScrollY(0); - } + if (fullyCollapsed) { + mQSPanelScrollView.setScrollY(0); + } - if (!fullyExpanded) { - // Set bounds on the QS panel so it doesn't run over the header when animating. - mQsBounds.top = (int) -mQSPanelScrollView.getTranslationY(); - mQsBounds.right = mQSPanelScrollView.getWidth(); - mQsBounds.bottom = mQSPanelScrollView.getHeight(); + if (!fullyExpanded) { + // Set bounds on the QS panel so it doesn't run over the header when animating. + mQsBounds.top = (int) -mQSPanelScrollView.getTranslationY(); + mQsBounds.right = mQSPanelScrollView.getWidth(); + mQsBounds.bottom = mQSPanelScrollView.getHeight(); + } } updateQsBounds(); @@ -786,15 +811,17 @@ public class QSImpl implements QS, CommandQueue.Callbacks, StatusBarStateControl mQsBounds.set(-sideMargin, 0, mQSPanelScrollView.getWidth() + sideMargin, mQSPanelScrollView.getHeight()); } - mQSPanelScrollView.setClipBounds(mQsBounds); - - mQSPanelScrollView.getLocationOnScreen(mLocationTemp); - int left = mLocationTemp[0]; - int top = mLocationTemp[1]; - mQsMediaHost.getCurrentClipping().set(left, top, - left + getView().getMeasuredWidth(), - top + mQSPanelScrollView.getMeasuredHeight() - - mQSPanelController.getPaddingBottom()); + if (!mSceneContainerFlags.isEnabled()) { + mQSPanelScrollView.setClipBounds(mQsBounds); + + mQSPanelScrollView.getLocationOnScreen(mLocationTemp); + int left = mLocationTemp[0]; + int top = mLocationTemp[1]; + mQsMediaHost.getCurrentClipping().set(left, top, + left + getView().getMeasuredWidth(), + top + mQSPanelScrollView.getMeasuredHeight() + - mQSPanelController.getPaddingBottom()); + } } private void updateMediaPositions() { @@ -867,9 +894,15 @@ public class QSImpl implements QS, CommandQueue.Callbacks, StatusBarStateControl // The customize state changed, so our height changed. mContainer.updateExpansion(); boolean customizing = isCustomizing(); - mQSPanelScrollView.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE); + if (mSceneContainerFlags.isEnabled()) { + mQSPanelController.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE); + } else { + mQSPanelScrollView.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE); + } mFooter.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE); - mFooterActionsView.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE); + if (mFooterActionsView != null) { + mFooterActionsView.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE); + } mHeader.setVisibility(!customizing ? View.VISIBLE : View.INVISIBLE); // Let the panel know the position changed and it needs to update where notifications // and whatnot are. diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java index 51b94dd983f3..7a7ee59fa63f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java @@ -387,7 +387,7 @@ public class QSPanel extends LinearLayout implements Tunable { setPaddingRelative(getPaddingStart(), mSceneContainerEnabled ? 0 : paddingTop, getPaddingEnd(), - paddingBottom); + mSceneContainerEnabled ? 0 : paddingBottom); } void addOnConfigurationChangedListener(OnConfigurationChangedListener listener) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java index ef58a608aa1f..c3f5086b0096 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelController.java @@ -278,5 +278,9 @@ public class QSPanelController extends QSPanelControllerBase<QSPanel> { public int getPaddingBottom() { return mView.getPaddingBottom(); } + + int getViewBottom() { + return mView.getBottom(); + } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/BaseAutoAddableModule.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/BaseAutoAddableModule.kt index adea26e5aa26..e1ec338cec6f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/BaseAutoAddableModule.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/dagger/BaseAutoAddableModule.kt @@ -18,6 +18,8 @@ package com.android.systemui.qs.pipeline.dagger import android.content.res.Resources import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.qs.pipeline.domain.autoaddable.A11yShortcutAutoAddable +import com.android.systemui.qs.pipeline.domain.autoaddable.A11yShortcutAutoAddableList import com.android.systemui.qs.pipeline.domain.autoaddable.AutoAddableSetting import com.android.systemui.qs.pipeline.domain.autoaddable.AutoAddableSettingList import com.android.systemui.qs.pipeline.domain.autoaddable.CastAutoAddable @@ -51,6 +53,16 @@ interface BaseAutoAddableModule { ) .toSet() } + + @Provides + @ElementsIntoSet + fun providesA11yShortcutAutoAddable( + a11yShortcutAutoAddableFactory: A11yShortcutAutoAddable.Factory + ): Set<AutoAddable> { + return A11yShortcutAutoAddableList.getA11yShortcutAutoAddables( + a11yShortcutAutoAddableFactory + ) + } } @Binds @IntoSet fun bindCastAutoAddable(impl: CastAutoAddable): AutoAddable diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/A11yShortcutAutoAddable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/A11yShortcutAutoAddable.kt new file mode 100644 index 000000000000..2cebbe3f372c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/A11yShortcutAutoAddable.kt @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.pipeline.domain.autoaddable + +import android.content.ComponentName +import android.provider.Settings +import com.android.systemui.accessibility.data.repository.AccessibilityQsShortcutsRepository +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.qs.pipeline.domain.model.AutoAddSignal +import com.android.systemui.qs.pipeline.domain.model.AutoAddTracking +import com.android.systemui.qs.pipeline.domain.model.AutoAddable +import com.android.systemui.qs.pipeline.shared.TileSpec +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import java.util.Objects +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map + +/** + * [A11yShortcutAutoAddable] will auto add/remove qs tile of the accessibility framework feature + * based on the user's choices in the Settings app. + * + * The a11y feature component is added to [Settings.Secure.ACCESSIBILITY_QS_TARGETS] when the user + * selects to use qs tile as a shortcut for the a11 feature in the Settings app. The accessibility + * feature component is removed from [Settings.Secure.ACCESSIBILITY_QS_TARGETS] when the user + * doesn't want to use qs tile as a shortcut for the a11y feature in the Settings app. + * + * [A11yShortcutAutoAddable] tracks a [Settings.Secure.ACCESSIBILITY_QS_TARGETS] and when its value + * changes, it will emit a [AutoAddSignal.Add] for the [spec] if the [componentName] is a substring + * of the value; it will emit a [AutoAddSignal.Remove] for the [spec] if the [componentName] is not + * a substring of the value. + */ +class A11yShortcutAutoAddable +@AssistedInject +constructor( + private val a11yQsShortcutsRepository: AccessibilityQsShortcutsRepository, + @Background private val bgDispatcher: CoroutineDispatcher, + @Assisted private val spec: TileSpec, + @Assisted private val componentName: ComponentName +) : AutoAddable { + + override fun autoAddSignal(userId: Int): Flow<AutoAddSignal> { + return a11yQsShortcutsRepository + .a11yQsShortcutTargets(userId) + .map { it.contains(componentName.flattenToString()) } + .filterNotNull() + .distinctUntilChanged() + .map { if (it) AutoAddSignal.Add(spec) else AutoAddSignal.Remove(spec) } + .flowOn(bgDispatcher) + } + + override val autoAddTracking = AutoAddTracking.Always + + override val description = + "A11yShortcutAutoAddableSetting: $spec:$componentName ($autoAddTracking)" + + override fun equals(other: Any?): Boolean { + return other is A11yShortcutAutoAddable && + spec == other.spec && + componentName == other.componentName + } + + override fun hashCode(): Int { + return Objects.hash(spec, componentName) + } + + override fun toString(): String { + return description + } + + @AssistedFactory + interface Factory { + fun create(spec: TileSpec, componentName: ComponentName): A11yShortcutAutoAddable + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/A11yShortcutAutoAddableList.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/A11yShortcutAutoAddableList.kt new file mode 100644 index 000000000000..08e39204386e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/A11yShortcutAutoAddableList.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.pipeline.domain.autoaddable + +import android.view.accessibility.Flags +import com.android.internal.accessibility.AccessibilityShortcutController +import com.android.systemui.qs.pipeline.domain.model.AutoAddable +import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.qs.tiles.ColorCorrectionTile +import com.android.systemui.qs.tiles.ColorInversionTile +import com.android.systemui.qs.tiles.OneHandedModeTile +import com.android.systemui.qs.tiles.ReduceBrightColorsTile + +object A11yShortcutAutoAddableList { + + /** + * Generate a collection of [A11yShortcutAutoAddable] for the framework tiles related to + * accessibility features with shortcut options + */ + fun getA11yShortcutAutoAddables(factory: A11yShortcutAutoAddable.Factory): Set<AutoAddable> { + return if (Flags.a11yQsShortcut()) { + setOf( + factory.create( + TileSpec.create(ColorCorrectionTile.TILE_SPEC), + AccessibilityShortcutController.DALTONIZER_COMPONENT_NAME + ), + factory.create( + TileSpec.create(ColorInversionTile.TILE_SPEC), + AccessibilityShortcutController.COLOR_INVERSION_COMPONENT_NAME + ), + factory.create( + TileSpec.create(OneHandedModeTile.TILE_SPEC), + AccessibilityShortcutController.ONE_HANDED_COMPONENT_NAME + ), + factory.create( + TileSpec.create(ReduceBrightColorsTile.TILE_SPEC), + AccessibilityShortcutController.REDUCE_BRIGHT_COLORS_COMPONENT_NAME + ), + ) + } else { + emptySet() + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt index 216d716b07a4..88863cbad1ee 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/RecordIssueTile.kt @@ -17,6 +17,8 @@ package com.android.systemui.qs.tiles import android.app.AlertDialog +import android.app.BroadcastOptions +import android.app.PendingIntent import android.content.Intent import android.os.Handler import android.os.Looper @@ -42,6 +44,8 @@ import com.android.systemui.qs.logging.QSLogger import com.android.systemui.qs.tileimpl.QSTileImpl import com.android.systemui.recordissue.RecordIssueDialogDelegate import com.android.systemui.res.R +import com.android.systemui.screenrecord.RecordingService +import com.android.systemui.settings.UserContextProvider import com.android.systemui.statusbar.phone.KeyguardDismissUtil import com.android.systemui.statusbar.policy.KeyguardStateController import javax.inject.Inject @@ -61,6 +65,7 @@ constructor( private val keyguardDismissUtil: KeyguardDismissUtil, private val keyguardStateController: KeyguardStateController, private val dialogLaunchAnimator: DialogLaunchAnimator, + private val userContextProvider: UserContextProvider, private val delegateFactory: RecordIssueDialogDelegate.Factory, ) : QSTileImpl<QSTile.BooleanState>( @@ -91,12 +96,22 @@ constructor( public override fun handleClick(view: View?) { if (isRecording) { isRecording = false + stopScreenRecord() } else { mUiHandler.post { showPrompt(view) } } refreshState() } + private fun stopScreenRecord() = + PendingIntent.getService( + userContextProvider.userContext, + RecordingService.REQUEST_CODE, + RecordingService.getStopIntent(userContextProvider.userContext), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + .send(BroadcastOptions.makeBasic().apply { isInteractive = true }.toBundle()) + private fun showPrompt(view: View?) { val dialog: AlertDialog = delegateFactory diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java index 592cb3b18e80..211b459471de 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogController.java @@ -192,6 +192,7 @@ public class InternetDialogController implements AccessPointController.AccessPoi private DialogLaunchAnimator mDialogLaunchAnimator; private boolean mHasWifiEntries; private WifiStateWorker mWifiStateWorker; + private boolean mHasActiveSubId; @VisibleForTesting static final float TOAST_PARAMS_HORIZONTAL_WEIGHT = 1.0f; @@ -299,6 +300,7 @@ public class InternetDialogController implements AccessPointController.AccessPoi mExecutor); // Listen the subscription changes mOnSubscriptionsChangedListener = new InternetOnSubscriptionChangedListener(); + refreshHasActiveSubId(); mSubscriptionManager.addOnSubscriptionsChangedListener(mExecutor, mOnSubscriptionsChangedListener); mDefaultDataSubId = getDefaultDataSubscriptionId(); @@ -901,18 +903,22 @@ public class InternetDialogController implements AccessPointController.AccessPoi * @return whether there is the carrier item in the slice. */ boolean hasActiveSubId() { - if (mSubscriptionManager == null) { - if (DEBUG) { - Log.d(TAG, "SubscriptionManager is null, can not check carrier."); - } + if (isAirplaneModeEnabled() || mTelephonyManager == null) { return false; } - if (isAirplaneModeEnabled() || mTelephonyManager == null - || mSubscriptionManager.getActiveSubscriptionIdList().length <= 0) { - return false; + return mHasActiveSubId; + } + + private void refreshHasActiveSubId() { + if (mSubscriptionManager == null) { + mHasActiveSubId = false; + Log.e(TAG, "SubscriptionManager is null, set mHasActiveSubId = false"); + return; } - return true; + + mHasActiveSubId = mSubscriptionManager.getActiveSubscriptionIdList().length > 0; + Log.i(TAG, "mHasActiveSubId:" + mHasActiveSubId); } /** @@ -1204,6 +1210,7 @@ public class InternetDialogController implements AccessPointController.AccessPoi @Override public void onSubscriptionsChanged() { + refreshHasActiveSubId(); updateListener(); } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt index ce840eec29d9..0d4339680dac 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt @@ -17,10 +17,13 @@ package com.android.systemui.qs.ui.adapter import android.content.Context +import android.content.pm.ActivityInfo import android.os.Bundle import android.view.View import androidx.annotation.VisibleForTesting import androidx.asynclayoutinflater.view.AsyncLayoutInflater +import com.android.settingslib.applications.InterestingConfigChanges +import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Main @@ -58,7 +61,7 @@ interface QSSceneAdapter { /** * Inflate an instance of [QSImpl] for this context. Once inflated, it will be available in - * [qsView] + * [qsView]. Re-inflations due to configuration changes will use the last used [context]. */ suspend fun inflate(context: Context) @@ -90,6 +93,7 @@ constructor( private val qsImplProvider: Provider<QSImpl>, @Main private val mainDispatcher: CoroutineDispatcher, @Application applicationScope: CoroutineScope, + private val configurationInteractor: ConfigurationInteractor, private val asyncLayoutInflaterFactory: (Context) -> AsyncLayoutInflater, ) : QSContainerController, QSSceneAdapter { @@ -99,7 +103,15 @@ constructor( qsImplProvider: Provider<QSImpl>, @Main dispatcher: CoroutineDispatcher, @Application scope: CoroutineScope, - ) : this(qsSceneComponentFactory, qsImplProvider, dispatcher, scope, ::AsyncLayoutInflater) + configurationInteractor: ConfigurationInteractor, + ) : this( + qsSceneComponentFactory, + qsImplProvider, + dispatcher, + scope, + configurationInteractor, + ::AsyncLayoutInflater, + ) private val state = MutableStateFlow<QSSceneAdapter.State>(QSSceneAdapter.State.CLOSED) private val _isCustomizing: MutableStateFlow<Boolean> = MutableStateFlow(false) @@ -109,14 +121,36 @@ constructor( val qsImpl = _qsImpl.asStateFlow() override val qsView: Flow<View> = _qsImpl.map { it?.view }.filterNotNull() + // Same config changes as in FragmentHostManager + private val interestingChanges = + InterestingConfigChanges( + ActivityInfo.CONFIG_FONT_SCALE or + ActivityInfo.CONFIG_LOCALE or + ActivityInfo.CONFIG_ASSETS_PATHS + ) + init { applicationScope.launch { - state.sample(_isCustomizing, ::Pair).collect { (state, customizing) -> - _qsImpl.value?.apply { - if (state != QSSceneAdapter.State.QS && customizing) { - this@apply.closeCustomizerImmediately() + launch { + state.sample(_isCustomizing, ::Pair).collect { (state, customizing) -> + _qsImpl.value?.apply { + if (state != QSSceneAdapter.State.QS && customizing) { + this@apply.closeCustomizerImmediately() + } + applyState(state) + } + } + } + launch { + configurationInteractor.configurationValues.collect { config -> + if (interestingChanges.applyNewConfig(config)) { + // Assumption: The context is always the same and with the same theme. + // If colors change they will be reflected as attributes in the theme. + qsImpl.value?.view?.let { inflate(it.context) } + } else { + qsImpl.value?.onConfigurationChanged(config) + qsImpl.value?.view?.dispatchConfigurationChanged(config) } - applyState(state) } } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt index e5e1e8445e94..8a900ece2750 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt @@ -16,7 +16,10 @@ package com.android.systemui.qs.ui.viewmodel +import androidx.lifecycle.LifecycleOwner import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.qs.FooterActionsController +import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel import com.android.systemui.qs.ui.adapter.QSSceneAdapter import com.android.systemui.scene.shared.model.Direction import com.android.systemui.scene.shared.model.SceneKey @@ -24,6 +27,7 @@ import com.android.systemui.scene.shared.model.SceneModel import com.android.systemui.scene.shared.model.UserAction import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel +import java.util.concurrent.atomic.AtomicBoolean import javax.inject.Inject import kotlinx.coroutines.flow.map @@ -35,6 +39,8 @@ constructor( val shadeHeaderViewModel: ShadeHeaderViewModel, val qsSceneAdapter: QSSceneAdapter, val notifications: NotificationsPlaceholderViewModel, + private val footerActionsViewModelFactory: FooterActionsViewModel.Factory, + private val footerActionsController: FooterActionsController, ) { val destinationScenes = qsSceneAdapter.isCustomizing.map { customizing -> @@ -47,4 +53,13 @@ constructor( ) } } + + private val footerActionsControllerInitialized = AtomicBoolean(false) + + fun getFooterActionsViewModel(lifecycleOwner: LifecycleOwner): FooterActionsViewModel { + if (footerActionsControllerInitialized.compareAndSet(false, true)) { + footerActionsController.init() + } + return footerActionsViewModelFactory.create(lifecycleOwner) + } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/WindowRootViewVisibilityInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/WindowRootViewVisibilityInteractor.kt index e2959fe834d4..1c37908235bd 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/WindowRootViewVisibilityInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/WindowRootViewVisibilityInteractor.kt @@ -23,16 +23,22 @@ import com.android.systemui.keyguard.data.repository.KeyguardRepository import com.android.systemui.keyguard.shared.model.StatusBarState import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.scene.data.repository.WindowRootViewVisibilityRepository +import com.android.systemui.scene.shared.flag.SceneContainerFlags +import com.android.systemui.scene.shared.model.ObservableTransitionState +import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.statusbar.NotificationPresenter import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.init.NotificationsController import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor import com.android.systemui.statusbar.policy.HeadsUpManager import javax.inject.Inject +import javax.inject.Provider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -47,6 +53,8 @@ constructor( private val headsUpManager: HeadsUpManager, private val powerInteractor: PowerInteractor, private val activeNotificationsInteractor: ActiveNotificationsInteractor, + sceneContainerFlags: SceneContainerFlags, + sceneInteractorProvider: Provider<SceneInteractor>, ) : CoreStartable { private var notificationPresenter: NotificationPresenter? = null @@ -58,11 +66,28 @@ constructor( /** * True if lockscreen (including AOD) or the shade is visible and false otherwise. Notably, * false if the bouncer is visible. - * - * TODO(b/297080059): Use [SceneInteractor] as the source of truth if the scene flag is on. */ val isLockscreenOrShadeVisible: StateFlow<Boolean> = - windowRootViewVisibilityRepository.isLockscreenOrShadeVisible + if (!sceneContainerFlags.isEnabled()) { + windowRootViewVisibilityRepository.isLockscreenOrShadeVisible + } else { + sceneInteractorProvider + .get() + .transitionState + .map { state -> + when (state) { + is ObservableTransitionState.Idle -> + state.scene == SceneKey.Shade || state.scene == SceneKey.Lockscreen + is ObservableTransitionState.Transition -> + state.toScene == SceneKey.Shade || + state.toScene == SceneKey.Lockscreen || + state.fromScene == SceneKey.Shade || + state.fromScene == SceneKey.Lockscreen + } + } + .distinctUntilChanged() + .stateIn(scope, SharingStarted.Eagerly, false) + } /** * True if lockscreen (including AOD) or the shade is visible **and** the user is currently 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 c96651c1057e..995059dfaa9f 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 @@ -313,7 +313,7 @@ constructor( } applicationScope.launch { - keyguardInteractor.isDozing.distinctUntilChanged().collect { isDozing -> + keyguardInteractor.isDozing.collect { isDozing -> falsingCollector.setShowingAod(isDozing) } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/BaseShadeControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/BaseShadeControllerImpl.kt new file mode 100644 index 000000000000..f71a401d1612 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/BaseShadeControllerImpl.kt @@ -0,0 +1,109 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shade + +import com.android.systemui.assist.AssistManager +import com.android.systemui.log.LogBuffer +import com.android.systemui.shade.TouchLogger.Companion.logTouchesTo +import com.android.systemui.statusbar.CommandQueue +import com.android.systemui.statusbar.NotificationPresenter +import com.android.systemui.statusbar.NotificationShadeWindowController +import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager +import dagger.Lazy +import kotlinx.coroutines.ExperimentalCoroutinesApi + +/** A base class for non-empty implementations of ShadeController. */ +@OptIn(ExperimentalCoroutinesApi::class) +abstract class BaseShadeControllerImpl( + private val touchLog: LogBuffer, + protected val commandQueue: CommandQueue, + protected val statusBarKeyguardViewManager: StatusBarKeyguardViewManager, + protected val notificationShadeWindowController: NotificationShadeWindowController, + protected val assistManagerLazy: Lazy<AssistManager> +) : ShadeController { + protected lateinit var notifPresenter: NotificationPresenter + /** Runnables to run after completing a collapse of the shade. */ + private val postCollapseActions = ArrayList<Runnable>() + + override fun start() { + logTouchesTo(touchLog) + } + + final override fun animateExpandShade() { + if (isShadeEnabled) { + expandToNotifications() + } + } + + /** Expand the shade with notifications visible. */ + protected abstract fun expandToNotifications() + + final override fun animateExpandQs() { + if (isShadeEnabled) { + expandToQs() + } + } + + /** Expand the shade showing only quick settings. */ + protected abstract fun expandToQs() + + final override fun addPostCollapseAction(action: Runnable) { + postCollapseActions.add(action) + } + + protected fun runPostCollapseActions() { + val clonedList: ArrayList<Runnable> = ArrayList(postCollapseActions) + postCollapseActions.clear() + for (r in clonedList) { + r.run() + } + statusBarKeyguardViewManager.readyForKeyguardDone() + } + + final override fun onLaunchAnimationEnd(launchIsFullScreen: Boolean) { + if (!this.notifPresenter.isCollapsing()) { + onClosingFinished() + } + if (launchIsFullScreen) { + instantCollapseShade() + } + } + final override fun onLaunchAnimationCancelled(isLaunchForActivity: Boolean) { + if ( + notifPresenter.isPresenterFullyCollapsed() && + !notifPresenter.isCollapsing() && + isLaunchForActivity + ) { + onClosingFinished() + } else { + collapseShade(true /* animate */) + } + } + + protected fun onClosingFinished() { + runPostCollapseActions() + if (!this.notifPresenter.isPresenterFullyCollapsed()) { + // if we set it not to be focusable when collapsing, we have to undo it when we aborted + // the closing + notificationShadeWindowController.setNotificationShadeFocusable(true) + } + } + + override fun setNotificationPresenter(presenter: NotificationPresenter) { + notifPresenter = presenter + } +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt index 782d6519468c..97ec3f98cf0c 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt @@ -22,6 +22,7 @@ import android.os.SystemClock import android.view.GestureDetector import android.view.MotionEvent import android.view.View +import android.view.ViewGroup import com.android.internal.annotations.VisibleForTesting import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.communal.ui.viewmodel.CommunalViewModel @@ -33,6 +34,7 @@ import com.android.systemui.res.R import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.util.kotlin.collectFlow import javax.inject.Inject +import kotlinx.coroutines.flow.StateFlow /** * Controller that's responsible for the glanceable hub container view and its touch handling. @@ -49,13 +51,28 @@ constructor( private val powerManager: PowerManager, ) { /** The container view for the hub. This will not be initialized until [initView] is called. */ - private lateinit var communalContainerView: View + private var communalContainerView: View? = null /** * The width of the area in which a right edge swipe can open the hub, in pixels. Read from * resources when [initView] is called. */ - private var edgeSwipeRegionWidth: Int = 0 + // TODO(b/320786721): support RTL layouts + private var rightEdgeSwipeRegionWidth: Int = 0 + + /** + * The height of the area in which a top edge swipe while the hub is open will not intercept + * touches, in pixels. This allows the top edge swipe to instead open the notification shade. + * Read from resources when [initView] is called. + */ + private var topEdgeSwipeRegionWidth: Int = 0 + + /** + * The height of the area in which a bottom edge swipe while the hub is open will not intercept + * touches, in pixels. This allows the bottom edge swipe to instead open the bouncer. Read from + * resources when [initView] is called. + */ + private var bottomEdgeSwipeRegionWidth: Int = 0 /** * True if we are currently tracking a gesture for opening the hub that started in the edge @@ -63,6 +80,9 @@ constructor( */ private var isTrackingOpenGesture = false + /** True if we are currently tracking a touch on the hub while it's open. */ + private var isTrackingHubTouch = false + /** * True if the hub UI is fully open, meaning it should receive touch input. * @@ -90,6 +110,11 @@ constructor( return communalInteractor.isCommunalEnabled && isComposeAvailable() } + /** Returns a {@link StateFlow} that tracks whether communal hub is enabled. */ + fun enabledState(): StateFlow<Boolean> { + return communalInteractor.communalEnabledState + } + /** * Creates the container view containing the glanceable hub UI. * @@ -107,32 +132,44 @@ constructor( if (!isEnabled()) { throw RuntimeException("Glanceable hub is not enabled") } - if (::communalContainerView.isInitialized) { + if (communalContainerView != null) { throw RuntimeException("Communal view has already been initialized") } communalContainerView = containerView - edgeSwipeRegionWidth = - communalContainerView.resources.getDimensionPixelSize(R.dimen.communal_grid_gutter_size) + rightEdgeSwipeRegionWidth = + containerView.resources.getDimensionPixelSize( + R.dimen.communal_right_edge_swipe_region_width + ) + topEdgeSwipeRegionWidth = + containerView.resources.getDimensionPixelSize( + R.dimen.communal_top_edge_swipe_region_height + ) + bottomEdgeSwipeRegionWidth = + containerView.resources.getDimensionPixelSize( + R.dimen.communal_bottom_edge_swipe_region_height + ) collectFlow( - communalContainerView, + containerView, keyguardTransitionInteractor.isFinishedInStateWhere(KeyguardState::isBouncerState), { anyBouncerShowing = it } ) - collectFlow( - communalContainerView, - communalInteractor.isCommunalShowing, - { hubShowing = it } - ) - collectFlow( - communalContainerView, - shadeInteractor.isAnyFullyExpanded, - { shadeShowing = it } - ) + collectFlow(containerView, communalInteractor.isCommunalShowing, { hubShowing = it }) + collectFlow(containerView, shadeInteractor.isAnyFullyExpanded, { shadeShowing = it }) + + communalContainerView = containerView - return communalContainerView + return containerView + } + + /** Removes the container view from its parent. */ + fun disposeView() { + communalContainerView?.let { + (it.parent as ViewGroup).removeView(it) + communalContainerView = null + } } /** @@ -145,10 +182,10 @@ constructor( * to be fully in control of its own touch handling. */ fun onTouchEvent(ev: MotionEvent): Boolean { - if (!::communalContainerView.isInitialized) { - return false - } + return communalContainerView?.let { handleTouchEventOnCommunalView(it, ev) } ?: false + } + private fun handleTouchEventOnCommunalView(view: View, ev: MotionEvent): Boolean { val isDown = ev.actionMasked == MotionEvent.ACTION_DOWN val isUp = ev.actionMasked == MotionEvent.ACTION_UP val isCancel = ev.actionMasked == MotionEvent.ACTION_CANCEL @@ -157,28 +194,48 @@ constructor( // fully showing state val hubOccluded = anyBouncerShowing || shadeShowing - // If the hub is fully visible, send all touch events to it. - val communalVisible = hubShowing && !hubOccluded - if (communalVisible) { - dispatchTouchEvent(ev) + // If the hub is fully visible, send all touch events to it, other than top and bottom edge + // swipes. + if (hubShowing && isDown) { + val y = ev.rawY + val topSwipe: Boolean = y <= topEdgeSwipeRegionWidth + val bottomSwipe = y >= view.height - bottomEdgeSwipeRegionWidth + + if (topSwipe || bottomSwipe) { + // Don't intercept touches at the top/bottom edge so that swipes can open the + // notification shade and bouncer. + return false + } + + if (!hubOccluded) { + isTrackingHubTouch = true + dispatchTouchEvent(view, ev) + // Return true regardless of dispatch result as some touches at the start of a + // gesture may return false from dispatchTouchEvent. + return true + } + } else if (isTrackingHubTouch) { + if (isUp || isCancel) { + isTrackingHubTouch = false + } + dispatchTouchEvent(view, ev) // Return true regardless of dispatch result as some touches at the start of a gesture // may return false from dispatchTouchEvent. return true } - if (edgeSwipeRegionWidth == 0) { - // If the edge region width has not been read yet or whatever reason, don't bother + if (rightEdgeSwipeRegionWidth == 0) { + // If the edge region width has not been read yet for whatever reason, don't bother // intercepting touches to open the hub. return false } if (!isTrackingOpenGesture && isDown) { val x = ev.rawX - val inOpeningSwipeRegion: Boolean = - x >= communalContainerView.width - edgeSwipeRegionWidth + val inOpeningSwipeRegion: Boolean = x >= view.width - rightEdgeSwipeRegionWidth if (inOpeningSwipeRegion && !hubOccluded) { isTrackingOpenGesture = true - dispatchTouchEvent(ev) + dispatchTouchEvent(view, ev) // Return true regardless of dispatch result as some touches at the start of a // gesture may return false from dispatchTouchEvent. return true @@ -187,7 +244,7 @@ constructor( if (isUp || isCancel) { isTrackingOpenGesture = false } - dispatchTouchEvent(ev) + dispatchTouchEvent(view, ev) // Return true regardless of dispatch result as some touches at the start of a gesture // may return false from dispatchTouchEvent. return true @@ -200,8 +257,8 @@ constructor( * Dispatches the touch event to the communal container and sends a user activity event to reset * the screen timeout. */ - private fun dispatchTouchEvent(ev: MotionEvent) { - communalContainerView.dispatchTouchEvent(ev) + private fun dispatchTouchEvent(view: View, ev: MotionEvent) { + view.dispatchTouchEvent(ev) powerManager.userActivity( SystemClock.uptimeMillis(), PowerManager.USER_ACTIVITY_EVENT_TOUCH, diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index aeccf0031419..c0ceba3c1d4f 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -2883,7 +2883,9 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump private void onTrackingStarted() { endClosing(); mShadeRepository.setLegacyShadeTracking(true); - mTrackingStartedListener.onTrackingStarted(); + if (mTrackingStartedListener != null) { + mTrackingStartedListener.onTrackingStarted(); + } notifyExpandingStarted(); updateExpansionAndVisibility(); mScrimController.onTrackingStarted(); diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java index 8c852cd04738..5ecc54b09806 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java @@ -272,6 +272,14 @@ public class NotificationShadeWindowViewController implements Dumpable { return result; } + /** + * Handle a touch event while dreaming by forwarding the event to the content view. + * @param event The event to forward. + */ + public void handleDreamTouch(MotionEvent event) { + mView.dispatchTouchEvent(event); + } + /** Inflates the {@link R.layout#status_bar_expanded} layout and sets it up. */ public void setupExpandedStatusBar() { mStackScrollLayout = mView.findViewById(R.id.notification_stack_scroller); @@ -597,16 +605,21 @@ public class NotificationShadeWindowViewController implements Dumpable { * The layout lives in {@link R.id.communal_ui_stub}. */ public void setupCommunalHubLayout() { - if (!mGlanceableHubContainerController.isEnabled()) { - return; - } - - // Replace the placeholder view with the communal UI. - View communalPlaceholder = mView.findViewById(R.id.communal_ui_stub); - int index = mView.indexOfChild(communalPlaceholder); - mView.removeView(communalPlaceholder); - - mView.addView(mGlanceableHubContainerController.initView(mView.getContext()), index); + collectFlow( + mView, + mGlanceableHubContainerController.enabledState(), + isEnabled -> { + if (isEnabled) { + View communalPlaceholder = mView.findViewById(R.id.communal_ui_stub); + int index = mView.indexOfChild(communalPlaceholder); + mView.addView( + mGlanceableHubContainerController.initView(mView.getContext()), + index); + } else { + mGlanceableHubContainerController.disposeView(); + } + } + ); } private boolean didNotificationPanelInterceptEvent(MotionEvent ev) { diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeController.java b/packages/SystemUI/src/com/android/systemui/shade/ShadeController.java index 2c4b0b990e6d..ec4b23a56483 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeController.java @@ -33,10 +33,20 @@ import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager; * {@link com.android.systemui.keyguard.KeyguardViewMediator} and others. */ public interface ShadeController extends CoreStartable { - /** True if the shade UI is enabled on this particular Android variant and false otherwise. */ + /** + * True if the shade UI is enabled on this particular Android variant and false otherwise. + * + * @deprecated use ShadeInteractor instead + */ + @Deprecated boolean isShadeEnabled(); - /** Make our window larger and the shade expanded */ + /** + * Make our window larger and the shade expanded + * + * @deprecated will no longer be needed when keyguard is a sibling view to the shade + */ + @Deprecated void instantExpandShade(); /** Collapse the shade instantly with no animation. */ @@ -74,13 +84,28 @@ public interface ShadeController extends CoreStartable { /** Expand the shade with quick settings expanded with an animation. */ void animateExpandQs(); - /** Posts a request to collapse the shade. */ + /** + * Posts a request to collapse the shade. + * + * @deprecated use #animateCollapseShade + */ + @Deprecated void postAnimateCollapseShade(); - /** Posts a request to force collapse the shade. */ + /** + * Posts a request to force collapse the shade. + * + * @deprecated use #animateForceCollapseShade + */ + @Deprecated void postAnimateForceCollapseShade(); - /** Posts a request to expand the shade to quick settings. */ + /** + * Posts a request to expand the shade to quick settings. + * + * @deprecated use #animateExpandQs + */ + @Deprecated void postAnimateExpandQs(); /** Cancels any ongoing expansion touch handling and collapses the shade. */ @@ -90,26 +115,29 @@ public interface ShadeController extends CoreStartable { * If the shade is not fully expanded, collapse it animated. * * @return Seems to always return false + * @deprecated use {@link #collapseShade()} instead */ + @Deprecated boolean closeShadeIfOpen(); /** - * Returns whether the shade state is the keyguard or not. - */ - boolean isKeyguard(); - - /** * Returns whether the shade is currently open. * Even though in the current implementation shade is in expanded state on keyguard, this * method makes distinction between shade being truly open and plain keyguard state: * - if QS and notifications are visible on the screen, return true * - for any other state, including keyguard, return false + * + * @deprecated will be replaced by ShadeInteractor once scene container launches */ + @Deprecated boolean isShadeFullyOpen(); /** * Returns whether shade or QS are currently opening or collapsing. + * + * @deprecated will be replaced by ShadeInteractor once scene container launches */ + @Deprecated boolean isExpandingOrCollapsing(); /** @@ -127,37 +155,67 @@ public interface ShadeController extends CoreStartable { */ void addPostCollapseAction(Runnable action); - /** Run all of the runnables added by {@link #addPostCollapseAction}. */ - void runPostCollapseRunnables(); - /** * Close the shade if it was open * * @return true if the shade was open, else false */ - boolean collapseShade(); + void collapseShade(); /** * If animate is true, does the same as {@link #collapseShade()}. Otherwise, instantly collapse * the shade. Post collapse runnables will be executed * * @param animate true to animate the collapse, false for instantaneous collapse + * @deprecated call either #animateCollapseShade or #instantCollapseShade */ + @Deprecated void collapseShade(boolean animate); - /** Calls #collapseShade if already on the main thread. If not, posts a call to it. */ + /** + * Calls #collapseShade if already on the main thread. If not, posts a call to it. + * @deprecated call #collapseShade + */ + @Deprecated void collapseOnMainThread(); - /** Makes shade expanded but not visible. */ + /** + * If necessary, instantly collapses the shade for an activity start, otherwise runs the + * post-collapse runnables. Instant collapse is ok here, because the purpose is to have the + * shade collapsed when the user returns to SysUI from the launched activity. + */ + void collapseShadeForActivityStart(); + + /** + * Makes shade expanded but not visible. + * + * @deprecated no longer needed once keyguard is a sibling view to the shade + */ + @Deprecated void makeExpandedInvisible(); - /** Makes shade expanded and visible. */ + /** + * Makes shade expanded and visible. + * + * @deprecated no longer needed once keyguard is a sibling view to the shade + */ + @Deprecated void makeExpandedVisible(boolean force); - /** Returns whether the shade is expanded and visible. */ + /** + * Returns whether the shade is expanded and visible. + * + * @deprecated no longer needed once keyguard is a sibling view to the shade + */ + @Deprecated boolean isExpandedVisible(); - /** Handle status bar touch event. */ + /** + * Handle status bar touch event. + * + * @deprecated only called by CentralSurfaces, which is being deleted + */ + @Deprecated void onStatusBarTouch(MotionEvent event); /** Called when a launch animation was cancelled. */ diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerEmptyImpl.kt index 82959eea562b..08a0c934702d 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerEmptyImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerEmptyImpl.kt @@ -42,9 +42,6 @@ open class ShadeControllerEmptyImpl @Inject constructor() : ShadeController { override fun closeShadeIfOpen(): Boolean { return false } - override fun isKeyguard(): Boolean { - return false - } override fun isShadeFullyOpen(): Boolean { return false } @@ -53,12 +50,10 @@ open class ShadeControllerEmptyImpl @Inject constructor() : ShadeController { } override fun postOnShadeExpanded(action: Runnable?) {} override fun addPostCollapseAction(action: Runnable?) {} - override fun runPostCollapseRunnables() {} - override fun collapseShade(): Boolean { - return false - } + override fun collapseShade() {} override fun collapseShade(animate: Boolean) {} override fun collapseOnMainThread() {} + override fun collapseShadeForActivityStart() {} override fun makeExpandedInvisible() {} override fun makeExpandedVisible(force: Boolean) {} override fun isExpandedVisible(): Boolean { diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java index fdc7eecd9b15..e8d9c35a893b 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java @@ -33,7 +33,6 @@ import com.android.systemui.log.dagger.ShadeTouchLog; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.scene.domain.interactor.WindowRootViewVisibilityInteractor; import com.android.systemui.statusbar.CommandQueue; -import com.android.systemui.statusbar.NotificationPresenter; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.StatusBarState; import com.android.systemui.statusbar.notification.row.NotificationGutsManager; @@ -44,14 +43,13 @@ import com.android.systemui.statusbar.window.StatusBarWindowController; import dagger.Lazy; -import java.util.ArrayList; import java.util.concurrent.Executor; import javax.inject.Inject; /** An implementation of {@link ShadeController}. */ @SysUISingleton -public final class ShadeControllerImpl implements ShadeController { +public final class ShadeControllerImpl extends BaseShadeControllerImpl { private static final String TAG = "ShadeControllerImpl"; private static final boolean SPEW = false; @@ -60,7 +58,6 @@ public final class ShadeControllerImpl implements ShadeController { private final CommandQueue mCommandQueue; private final Executor mMainExecutor; - private final LogBuffer mTouchLog; private final WindowRootViewVisibilityInteractor mWindowRootViewVisibilityInteractor; private final KeyguardStateController mKeyguardStateController; private final NotificationShadeWindowController mNotificationShadeWindowController; @@ -73,12 +70,9 @@ public final class ShadeControllerImpl implements ShadeController { private final Lazy<AssistManager> mAssistManagerLazy; private final Lazy<NotificationGutsManager> mGutsManager; - private final ArrayList<Runnable> mPostCollapseRunnables = new ArrayList<>(); - private boolean mExpandedVisible; private boolean mLockscreenOrShadeVisible; - private NotificationPresenter mPresenter; private NotificationShadeWindowViewController mNotificationShadeWindowViewController; private ShadeVisibilityListener mShadeVisibilityListener; @@ -99,9 +93,13 @@ public final class ShadeControllerImpl implements ShadeController { Lazy<AssistManager> assistManagerLazy, Lazy<NotificationGutsManager> gutsManager ) { + super(touchLog, + commandQueue, + statusBarKeyguardViewManager, + notificationShadeWindowController, + assistManagerLazy); mCommandQueue = commandQueue; mMainExecutor = mainExecutor; - mTouchLog = touchLog; mWindowRootViewVisibilityInteractor = windowRootViewVisibilityInteractor; mShadeViewControllerLazy = shadeViewControllerLazy; mStatusBarStateController = statusBarStateController; @@ -117,7 +115,7 @@ public final class ShadeControllerImpl implements ShadeController { @Override public boolean isShadeEnabled() { - return true; + return mCommandQueue.panelsEnabled() && mDeviceProvisionedController.isCurrentUserSetup(); } @Override @@ -125,20 +123,16 @@ public final class ShadeControllerImpl implements ShadeController { // Make our window larger and the panel expanded. makeExpandedVisible(true /* force */); getShadeViewController().expand(false /* animate */); - mCommandQueue.recomputeDisableFlags(mDisplayId, false /* animate */); + getCommandQueue().recomputeDisableFlags(mDisplayId, false /* animate */); } @Override public void animateCollapseShade(int flags, boolean force, boolean delayed, float speedUpFactor) { if (!force && mStatusBarStateController.getState() != StatusBarState.SHADE) { - runPostCollapseRunnables(); + runPostCollapseActions(); return; } - if (SPEW) { - Log.d(TAG, - "animateCollapse(): mExpandedVisible=" + mExpandedVisible + "flags=" + flags); - } if (getNotificationShadeWindowView() != null && getShadeViewController().canBeCollapsed() && (flags & CommandQueue.FLAG_EXCLUDE_NOTIFICATION_PANEL) == 0) { @@ -151,28 +145,19 @@ public final class ShadeControllerImpl implements ShadeController { } @Override - public void animateExpandShade() { - if (!mCommandQueue.panelsEnabled()) { - return; - } + protected void expandToNotifications() { getShadeViewController().expandToNotifications(); } @Override - public void animateExpandQs() { - if (!mCommandQueue.panelsEnabled()) { - return; - } - // Settings are not available in setup - if (!mDeviceProvisionedController.isCurrentUserSetup()) return; - + protected void expandToQs() { getShadeViewController().expandToQs(); } @Override public boolean closeShadeIfOpen() { if (!getShadeViewController().isFullyCollapsed()) { - mCommandQueue.animateCollapsePanels( + getCommandQueue().animateCollapsePanels( CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL, true /* force */); notifyVisibilityChanged(false); mAssistManagerLazy.get().hideAssist(); @@ -181,11 +166,6 @@ public final class ShadeControllerImpl implements ShadeController { } @Override - public boolean isKeyguard() { - return mStatusBarStateController.getState() == StatusBarState.KEYGUARD; - } - - @Override public boolean isShadeFullyOpen() { return getShadeViewController().isShadeFullyExpanded(); } @@ -224,46 +204,34 @@ public final class ShadeControllerImpl implements ShadeController { } @Override - public void addPostCollapseAction(Runnable action) { - mPostCollapseRunnables.add(action); + public void collapseShade() { + collapseShadeInternal(); } - @Override - public void runPostCollapseRunnables() { - ArrayList<Runnable> clonedList = new ArrayList<>(mPostCollapseRunnables); - mPostCollapseRunnables.clear(); - int size = clonedList.size(); - for (int i = 0; i < size; i++) { - clonedList.get(i).run(); - } - mStatusBarKeyguardViewManager.readyForKeyguardDone(); - } - - @Override - public boolean collapseShade() { + private boolean collapseShadeInternal() { if (!getShadeViewController().isFullyCollapsed()) { // close the shade if it was open animateCollapseShadeForcedDelayed(); notifyVisibilityChanged(false); - return true; } else { return false; } } + @Override public void collapseShade(boolean animate) { if (animate) { - boolean willCollapse = collapseShade(); + boolean willCollapse = collapseShadeInternal(); if (!willCollapse) { - runPostCollapseRunnables(); + runPostCollapseActions(); } - } else if (!mPresenter.isPresenterFullyCollapsed()) { + } else if (!getNotifPresenter().isPresenterFullyCollapsed()) { instantCollapseShade(); notifyVisibilityChanged(false); } else { - runPostCollapseRunnables(); + runPostCollapseActions(); } } @@ -296,46 +264,16 @@ public final class ShadeControllerImpl implements ShadeController { } } - private void onClosingFinished() { - runPostCollapseRunnables(); - if (!mPresenter.isPresenterFullyCollapsed()) { - // if we set it not to be focusable when collapsing, we have to undo it when we aborted - // the closing - mNotificationShadeWindowController.setNotificationShadeFocusable(true); - } - } - - @Override - public void onLaunchAnimationCancelled(boolean isLaunchForActivity) { - if (mPresenter.isPresenterFullyCollapsed() - && !mPresenter.isCollapsing() - && isLaunchForActivity) { - onClosingFinished(); - } else { - collapseShade(true /* animate */); - } - } - - @Override - public void onLaunchAnimationEnd(boolean launchIsFullScreen) { - if (!mPresenter.isCollapsing()) { - onClosingFinished(); - } - if (launchIsFullScreen) { - instantCollapseShade(); - } - } - @Override public void instantCollapseShade() { getShadeViewController().instantCollapse(); - runPostCollapseRunnables(); + runPostCollapseActions(); } @Override public void makeExpandedVisible(boolean force) { if (SPEW) Log.d(TAG, "Make expanded visible: expanded visible=" + mExpandedVisible); - if (!force && (mExpandedVisible || !mCommandQueue.panelsEnabled())) { + if (!force && (mExpandedVisible || !getCommandQueue().panelsEnabled())) { return; } @@ -346,7 +284,7 @@ public final class ShadeControllerImpl implements ShadeController { mNotificationShadeWindowController.setPanelVisible(true); notifyVisibilityChanged(true); - mCommandQueue.recomputeDisableFlags(mDisplayId, !force /* animate */); + getCommandQueue().recomputeDisableFlags(mDisplayId, !force /* animate */); notifyExpandedVisibleChanged(true); } @@ -377,9 +315,9 @@ public final class ShadeControllerImpl implements ShadeController { -1 /* y */, true /* resetMenu */); - runPostCollapseRunnables(); + runPostCollapseActions(); notifyExpandedVisibleChanged(false); - mCommandQueue.recomputeDisableFlags( + getCommandQueue().recomputeDisableFlags( mDisplayId, getShadeViewController().shouldHideStatusBarIconsWhenExpanded()); @@ -421,11 +359,6 @@ public final class ShadeControllerImpl implements ShadeController { } @Override - public void setNotificationPresenter(NotificationPresenter presenter) { - mPresenter = presenter; - } - - @Override public void setNotificationShadeWindowViewController( NotificationShadeWindowViewController controller) { mNotificationShadeWindowViewController = controller; @@ -441,8 +374,8 @@ public final class ShadeControllerImpl implements ShadeController { @Override public void start() { - TouchLogger.logTouchesTo(mTouchLog); - getShadeViewController().setTrackingStartedListener(this::runPostCollapseRunnables); + super.start(); + getShadeViewController().setTrackingStartedListener(this::runPostCollapseActions); getShadeViewController().setOpenCloseListener( new OpenCloseListener() { @Override @@ -456,4 +389,16 @@ public final class ShadeControllerImpl implements ShadeController { } }); } + + @Override + public void collapseShadeForActivityStart() { + if (isExpandedVisible() && !mStatusBarKeyguardViewManager.isBouncerShowing()) { + animateCollapseShadeForcedDelayed(); + } else { + // Do it after DismissAction has been processed to conserve the + // needed ordering. + mMainExecutor.execute(this::runPostCollapseActions); + } + } + } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt new file mode 100644 index 000000000000..10b9db0a349b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt @@ -0,0 +1,245 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shade + +import android.view.MotionEvent +import com.android.systemui.assist.AssistManager +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.dagger.ShadeTouchLog +import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.scene.shared.model.SceneKey +import com.android.systemui.scene.shared.model.SceneModel +import com.android.systemui.shade.ShadeController.ShadeVisibilityListener +import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.statusbar.CommandQueue +import com.android.systemui.statusbar.NotificationShadeWindowController +import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout +import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager +import dagger.Lazy +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch + +/** + * Implementation of ShadeController backed by scenes instead of NPVC. + * + * TODO(b/300258424) rename to ShadeControllerImpl and inline/delete all the deprecated methods + */ +@OptIn(ExperimentalCoroutinesApi::class) +@SysUISingleton +class ShadeControllerSceneImpl +@Inject +constructor( + @Background private val scope: CoroutineScope, + private val shadeInteractor: ShadeInteractor, + private val sceneInteractor: SceneInteractor, + private val deviceEntryInteractor: DeviceEntryInteractor, + private val notificationStackScrollLayout: NotificationStackScrollLayout, + @ShadeTouchLog private val touchLog: LogBuffer, + commandQueue: CommandQueue, + statusBarKeyguardViewManager: StatusBarKeyguardViewManager, + notificationShadeWindowController: NotificationShadeWindowController, + assistManagerLazy: Lazy<AssistManager>, +) : + BaseShadeControllerImpl( + touchLog, + commandQueue, + statusBarKeyguardViewManager, + notificationShadeWindowController, + assistManagerLazy, + ) { + + init { + scope.launch { + shadeInteractor.isAnyExpanded.collect { + if (!it) { + runPostCollapseActions() + } + } + } + } + + override fun isShadeEnabled() = shadeInteractor.isShadeEnabled.value + + override fun isShadeFullyOpen(): Boolean = shadeInteractor.isAnyFullyExpanded.value + + override fun isExpandingOrCollapsing(): Boolean = + shadeInteractor.anyExpansion.value > 0f && shadeInteractor.anyExpansion.value < 1f + + override fun instantExpandShade() { + // Do nothing + } + + override fun instantCollapseShade() { + // TODO(b/315921512) add support for instant transition + sceneInteractor.changeScene( + SceneModel(getCollapseDestinationScene(), "instant"), + "hide shade" + ) + } + + override fun animateCollapseShade( + flags: Int, + force: Boolean, + delayed: Boolean, + speedUpFactor: Float + ) { + if (!force && !shadeInteractor.isAnyExpanded.value) { + runPostCollapseActions() + return + } + if ( + shadeInteractor.isAnyExpanded.value && + flags and CommandQueue.FLAG_EXCLUDE_NOTIFICATION_PANEL == 0 + ) { + // release focus immediately to kick off focus change transition + notificationShadeWindowController.setNotificationShadeFocusable(false) + notificationStackScrollLayout.cancelExpandHelper() + sceneInteractor.changeScene( + SceneModel(SceneKey.Shade, null), + "ShadeController.animateExpandShade" + ) + if (delayed) { + scope.launch { + delay(125) + animateCollapseShadeInternal() + } + } else { + animateCollapseShadeInternal() + } + } + } + + private fun animateCollapseShadeInternal() { + sceneInteractor.changeScene( + SceneModel(getCollapseDestinationScene(), "ShadeController.animateCollapseShade"), + "ShadeController.animateCollapseShade" + ) + } + + private fun getCollapseDestinationScene(): SceneKey { + return if (deviceEntryInteractor.isDeviceEntered.value) { + SceneKey.Gone + } else { + SceneKey.Lockscreen + } + } + + override fun cancelExpansionAndCollapseShade() { + // TODO do we need to actually cancel the touch session? + animateCollapseShade() + } + + override fun closeShadeIfOpen(): Boolean { + if (shadeInteractor.isAnyExpanded.value) { + commandQueue.animateCollapsePanels( + CommandQueue.FLAG_EXCLUDE_RECENTS_PANEL, + true /* force */ + ) + assistManagerLazy.get().hideAssist() + } + return false + } + + override fun collapseShade() { + animateCollapseShadeForcedDelayed() + } + + override fun collapseShade(animate: Boolean) { + if (animate) { + animateCollapseShade() + } else { + instantCollapseShade() + } + } + + override fun collapseOnMainThread() { + // TODO if this works with delegation alone, we can deprecate and delete + collapseShade() + } + + override fun expandToNotifications() { + sceneInteractor.changeScene( + SceneModel(SceneKey.Shade, null), + "ShadeController.animateExpandShade" + ) + } + + override fun expandToQs() { + sceneInteractor.changeScene( + SceneModel(SceneKey.QuickSettings, null), + "ShadeController.animateExpandQs" + ) + } + + override fun setVisibilityListener(listener: ShadeVisibilityListener) { + scope.launch { sceneInteractor.isVisible.collect { listener.expandedVisibleChanged(it) } } + } + + @ExperimentalCoroutinesApi + override fun collapseShadeForActivityStart() { + if (shadeInteractor.isAnyExpanded.value) { + animateCollapseShadeForcedDelayed() + } else { + runPostCollapseActions() + } + } + + override fun postAnimateCollapseShade() { + animateCollapseShade() + } + + override fun postAnimateForceCollapseShade() { + animateCollapseShadeForced() + } + + override fun postAnimateExpandQs() { + expandToQs() + } + + override fun postOnShadeExpanded(action: Runnable) { + // TODO verify that clicking "reply" in a work profile notification launches the app + // TODO verify that there's not a way to replace and deprecate this method + scope.launch { + shadeInteractor.isAnyFullyExpanded.first { it } + action.run() + } + } + + override fun makeExpandedInvisible() { + // Do nothing + } + + override fun makeExpandedVisible(force: Boolean) { + // Do nothing + } + + override fun isExpandedVisible(): Boolean { + return sceneInteractor.desiredScene.value.key != SceneKey.Gone + } + + override fun onStatusBarTouch(event: MotionEvent) { + // The only call to this doesn't happen with KeyguardShadeMigrationNssl enabled + throw UnsupportedOperationException() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt index c057147b022a..fc2c3ee14206 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt @@ -53,6 +53,20 @@ abstract class ShadeModule { @Provides @SysUISingleton + fun provideShadeController( + sceneContainerFlags: SceneContainerFlags, + sceneContainerOn: Provider<ShadeControllerSceneImpl>, + sceneContainerOff: Provider<ShadeControllerImpl> + ): ShadeController { + return if (sceneContainerFlags.isEnabled()) { + sceneContainerOn.get() + } else { + sceneContainerOff.get() + } + } + + @Provides + @SysUISingleton fun provideShadeAnimationInteractor( sceneContainerFlags: SceneContainerFlags, sceneContainerOn: Provider<ShadeAnimationInteractorSceneContainerImpl>, @@ -79,8 +93,4 @@ abstract class ShadeModule { abstract fun bindsShadeViewController( notificationPanelViewController: NotificationPanelViewController ): ShadeViewController - - @Binds - @SysUISingleton - abstract fun bindsShadeController(shadeControllerImpl: ShadeControllerImpl): ShadeController } diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt index 31a4de4d630e..43ede2aa288d 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt @@ -29,7 +29,7 @@ interface ShadeInteractor : BaseShadeInteractor { val isShadeEnabled: StateFlow<Boolean> /** Whether either the shade or QS is fully expanded. */ - val isAnyFullyExpanded: Flow<Boolean> + val isAnyFullyExpanded: StateFlow<Boolean> /** Whether the Shade is fully expanded. */ val isShadeFullyExpanded: Flow<Boolean> diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt index 6defbcf29a6c..55dd6744d44e 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorEmptyImpl.kt @@ -34,7 +34,7 @@ class ShadeInteractorEmptyImpl @Inject constructor() : ShadeInteractor { override val isQsBypassingShade: Flow<Boolean> = inactiveFlowBoolean override val isQsFullscreen: Flow<Boolean> = inactiveFlowBoolean override val anyExpansion: StateFlow<Float> = inactiveFlowFloat - override val isAnyFullyExpanded: Flow<Boolean> = inactiveFlowBoolean + override val isAnyFullyExpanded: StateFlow<Boolean> = inactiveFlowBoolean override val isShadeFullyExpanded: Flow<Boolean> = inactiveFlowBoolean override val isAnyExpanded: StateFlow<Boolean> = inactiveFlowBoolean override val isUserInteractingWithShade: Flow<Boolean> = inactiveFlowBoolean 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 6407b5a4d16c..a71cf950cbe1 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 @@ -55,12 +55,20 @@ constructor( private val baseShadeInteractor: BaseShadeInteractor, ) : ShadeInteractor, BaseShadeInteractor by baseShadeInteractor { override val isShadeEnabled: StateFlow<Boolean> = - disableFlagsRepository.disableFlags - .map { it.isShadeEnabled() } + combine( + deviceProvisioningRepository.isFactoryResetProtectionActive, + disableFlagsRepository.disableFlags, + ) { isFrpActive, isDisabledByFlags -> + isDisabledByFlags.isShadeEnabled() && !isFrpActive + } + .distinctUntilChanged() .stateIn(scope, SharingStarted.Eagerly, initialValue = false) - override val isAnyFullyExpanded: Flow<Boolean> = - anyExpansion.map { it >= 1f }.distinctUntilChanged() + override val isAnyFullyExpanded: StateFlow<Boolean> = + anyExpansion + .map { it >= 1f } + .distinctUntilChanged() + .stateIn(scope, SharingStarted.Eagerly, initialValue = false) override val isShadeFullyExpanded: Flow<Boolean> = baseShadeInteractor.shadeExpansion.map { it >= 1f }.distinctUntilChanged() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java index 909cff37b67d..e5982428fe14 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java @@ -542,7 +542,19 @@ public final class KeyboardShortcutListSearch { new ShortcutKeyGroupMultiMappingInfo( context.getString(R.string.group_system_access_google_assistant), Arrays.asList( - Pair.create(KeyEvent.KEYCODE_A, KeyEvent.META_META_ON))) + Pair.create(KeyEvent.KEYCODE_A, KeyEvent.META_META_ON))), + /* Lock screen: Meta + L */ + new ShortcutKeyGroupMultiMappingInfo( + context.getString(R.string.group_system_lock_screen), + Arrays.asList( + Pair.create(KeyEvent.KEYCODE_L, KeyEvent.META_META_ON))), + /* Pull up Notes app for quick memo: Meta + Ctrl + N */ + new ShortcutKeyGroupMultiMappingInfo( + context.getString(R.string.group_system_quick_memo), + Arrays.asList( + Pair.create( + KeyEvent.KEYCODE_N, + KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON))) ); for (ShortcutKeyGroupMultiMappingInfo info : infoList) { systemGroup.addItem(info.getShortcutMultiMappingInfo()); @@ -584,11 +596,17 @@ public final class KeyboardShortcutListSearch { new ArrayList<>()); // System multitasking shortcuts: + // Enter Split screen with current app to RHS: Meta + Ctrl + Right arrow + // Enter Split screen with current app to LHS: Meta + Ctrl + Left arrow // Switch from Split screen to full screen: Meta + Ctrl + Up arrow String[] shortcutLabels = { + context.getString(R.string.system_multitasking_rhs), + context.getString(R.string.system_multitasking_lhs), context.getString(R.string.system_multitasking_full_screen), }; int[] keyCodes = { + KeyEvent.KEYCODE_DPAD_RIGHT, + KeyEvent.KEYCODE_DPAD_LEFT, KeyEvent.KEYCODE_DPAD_UP, }; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java index fc84973c46bd..28d4457b264b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java @@ -35,7 +35,6 @@ import android.net.wifi.WifiManager; import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; -import android.os.HandlerExecutor; import android.os.Looper; import android.provider.Settings; import android.telephony.CarrierConfigManager; @@ -61,7 +60,6 @@ import com.android.settingslib.mobile.MobileStatusTracker.SubscriptionDefaults; import com.android.settingslib.mobile.TelephonyIcons; import com.android.settingslib.net.DataUsageController; import com.android.systemui.Dumpable; -import com.android.systemui.res.R; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; @@ -73,6 +71,7 @@ import com.android.systemui.log.LogBuffer; import com.android.systemui.log.core.LogLevel; import com.android.systemui.log.dagger.StatusBarNetworkControllerLog; import com.android.systemui.qs.tiles.dialog.InternetDialogFactory; +import com.android.systemui.res.R; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags; import com.android.systemui.statusbar.policy.ConfigurationController; @@ -85,6 +84,8 @@ import com.android.systemui.util.CarrierConfigTracker; import dalvik.annotation.optimization.NeverCompile; +import kotlin.Unit; + import java.io.PrintWriter; import java.text.SimpleDateFormat; import java.util.ArrayList; @@ -99,8 +100,6 @@ import java.util.stream.Collectors; import javax.inject.Inject; -import kotlin.Unit; - /** Platform implementation of the network controller. **/ @SysUISingleton public class NetworkControllerImpl extends BroadcastReceiver @@ -350,7 +349,7 @@ public class NetworkControllerImpl extends BroadcastReceiver // AIRPLANE_MODE_CHANGED is sent at boot; we've probably already missed it updateAirplaneMode(true /* force callback */); mUserTracker = userTracker; - mUserTracker.addCallback(mUserChangedCallback, new HandlerExecutor(mMainHandler)); + mUserTracker.addCallback(mUserChangedCallback, mBgExecutor); deviceProvisionedController.addCallback(new DeviceProvisionedListener() { @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java index 2d5afd56da72..3cdb2cd9b5c6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupExpansionManagerImpl.java @@ -21,8 +21,6 @@ import androidx.annotation.NonNull; import com.android.systemui.Dumpable; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dump.DumpManager; -import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.flags.Flags; import com.android.systemui.statusbar.notification.collection.GroupEntry; import com.android.systemui.statusbar.notification.collection.ListEntry; import com.android.systemui.statusbar.notification.collection.NotifPipeline; @@ -53,14 +51,11 @@ public class GroupExpansionManagerImpl implements GroupExpansionManager, Dumpabl */ private final Set<NotificationEntry> mExpandedGroups = new HashSet<>(); - private final FeatureFlags mFeatureFlags; - @Inject public GroupExpansionManagerImpl(DumpManager dumpManager, - GroupMembershipManager groupMembershipManager, FeatureFlags featureFlags) { + GroupMembershipManager groupMembershipManager) { mDumpManager = dumpManager; mGroupMembershipManager = groupMembershipManager; - mFeatureFlags = featureFlags; } /** @@ -86,10 +81,8 @@ public class GroupExpansionManagerImpl implements GroupExpansionManager, Dumpabl }; public void attach(NotifPipeline pipeline) { - if (mFeatureFlags.isEnabled(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE)) { - mDumpManager.registerDumpable(this); - pipeline.addOnBeforeRenderListListener(mNotifTracker); - } + mDumpManager.registerDumpable(this); + pipeline.addOnBeforeRenderListListener(mNotifTracker); } @Override @@ -105,8 +98,7 @@ public class GroupExpansionManagerImpl implements GroupExpansionManager, Dumpabl @Override public void setGroupExpanded(NotificationEntry entry, boolean expanded) { NotificationEntry groupSummary = mGroupMembershipManager.getGroupSummary(entry); - if (mFeatureFlags.isEnabled(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE) - && entry.getParent() == null) { + if (entry.getParent() == null) { if (expanded) { throw new IllegalArgumentException("Cannot expand group that is not attached"); } else { @@ -124,7 +116,7 @@ public class GroupExpansionManagerImpl implements GroupExpansionManager, Dumpabl } // Only notify listeners if something changed. - if (!mFeatureFlags.isEnabled(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE) || changed) { + if (changed) { sendOnGroupExpandedChange(entry, expanded); } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerImpl.java index cb7935369564..da1247953c4c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/GroupMembershipManagerImpl.java @@ -22,8 +22,6 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.android.systemui.dagger.SysUISingleton; -import com.android.systemui.flags.FeatureFlagsClassic; -import com.android.systemui.flags.Flags; import com.android.systemui.statusbar.notification.collection.GroupEntry; import com.android.systemui.statusbar.notification.collection.ListEntry; import com.android.systemui.statusbar.notification.collection.NotificationEntry; @@ -38,25 +36,17 @@ import javax.inject.Inject; */ @SysUISingleton public class GroupMembershipManagerImpl implements GroupMembershipManager { - FeatureFlagsClassic mFeatureFlags; - @Inject - public GroupMembershipManagerImpl(FeatureFlagsClassic featureFlags) { - mFeatureFlags = featureFlags; - } + public GroupMembershipManagerImpl() {} @Override public boolean isGroupSummary(@NonNull NotificationEntry entry) { - if (mFeatureFlags.isEnabled(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE)) { - if (entry.getParent() == null) { - // The entry is not attached, so it doesn't count. - return false; - } - // If entry is a summary, its parent is a GroupEntry with summary = entry. - return entry.getParent().getSummary() == entry; - } else { - return getGroupSummary(entry) == entry; + if (entry.getParent() == null) { + // The entry is not attached, so it doesn't count. + return false; } + // If entry is a summary, its parent is a GroupEntry with summary = entry. + return entry.getParent().getSummary() == entry; } @Nullable @@ -70,12 +60,8 @@ public class GroupMembershipManagerImpl implements GroupMembershipManager { @Override public boolean isChildInGroup(@NonNull NotificationEntry entry) { - if (mFeatureFlags.isEnabled(Flags.NOTIFICATION_GROUP_EXPANSION_CHANGE)) { - // An entry is a child if it's not a summary or top level entry, but it is attached. - return !isGroupSummary(entry) && !isTopLevelEntry(entry) && entry.getParent() != null; - } else { - return !isTopLevelEntry(entry); - } + // An entry is a child if it's not a summary or top level entry, but it is attached. + return !isGroupSummary(entry) && !isTopLevelEntry(entry) && entry.getParent() != null; } @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt index aca8b64c05d2..342828c4b5d3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt @@ -6,6 +6,7 @@ import android.database.ContentObserver import android.net.Uri import android.os.Handler import android.os.HandlerExecutor +import android.os.HandlerThread import android.os.UserHandle import android.provider.Settings import com.android.keyguard.KeyguardUpdateMonitor @@ -87,6 +88,7 @@ class KeyguardNotificationVisibilityProviderImpl @Inject constructor( secureSettings.getUriFor(Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS) private val onStateChangedListeners = ListenerSet<Consumer<String>>() private var hideSilentNotificationsOnLockscreen: Boolean = false + private val handlerThread: HandlerThread = HandlerThread("KeyguardNotificationVis") private val userTrackerCallback = object : UserTracker.Callback { override fun onUserChanged(newUser: Int, userContext: Context) { @@ -154,7 +156,9 @@ class KeyguardNotificationVisibilityProviderImpl @Inject constructor( notifyStateChanged("onStatusBarUpcomingStateChanged") } }) - userTracker.addCallback(userTrackerCallback, HandlerExecutor(handler)) + handlerThread.start() + userTracker.addCallback(userTrackerCallback, + HandlerExecutor(handlerThread.getThreadHandler())) } override fun addOnStateChangedListener(listener: Consumer<String>) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java index 4349b3b5aeb3..c6832bc20e6d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/logging/NotificationLogger.java @@ -247,6 +247,9 @@ public class NotificationLogger implements StateListener, CoreStartable, public void setUpWithContainer(NotificationListContainer listContainer) { mListContainer = listContainer; + if (mLogging) { + mListContainer.setChildLocationsChangedListener(this::onChildLocationsChanged); + } } @Override @@ -294,7 +297,9 @@ public class NotificationLogger implements StateListener, CoreStartable, lockscreen = mLockscreen != null && mLockscreen; } mNotificationPanelLogger.logPanelShown(lockscreen, getVisibleNotifications()); - mListContainer.setChildLocationsChangedListener(this::onChildLocationsChanged); + if (mListContainer != null) { + mListContainer.setChildLocationsChangedListener(this::onChildLocationsChanged); + } // Sometimes, the transition from lockscreenOrShadeVisible=false -> // lockscreenOrShadeVisible=true doesn't cause the scroller to emit child location // events. Hence generate one ourselves to guarantee that we're reporting visible diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifLayoutInflaterFactory.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifLayoutInflaterFactory.kt index d10b556de0fb..8bc8e8cc49a0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifLayoutInflaterFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifLayoutInflaterFactory.kt @@ -22,11 +22,9 @@ import android.util.Log import android.view.LayoutInflater import android.view.View import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag -import com.android.systemui.statusbar.notification.row.NotificationRowModule.NOTIF_REMOTEVIEWS_FACTORIES import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject -import javax.inject.Named /** * Implementation of [NotifLayoutInflaterFactory]. This class uses a set of @@ -37,8 +35,7 @@ class NotifLayoutInflaterFactory constructor( @Assisted private val row: ExpandableNotificationRow, @Assisted @InflationFlag val layoutType: Int, - @Named(NOTIF_REMOTEVIEWS_FACTORIES) - private val remoteViewsFactories: Set<@JvmSuppressWildcards NotifRemoteViewsFactory> + private val notifRemoteViewsFactoryContainer: NotifRemoteViewsFactoryContainer ) : LayoutInflater.Factory2 { override fun onCreateView( @@ -49,7 +46,7 @@ constructor( ): View? { var handledFactory: NotifRemoteViewsFactory? = null var result: View? = null - for (layoutFactory in remoteViewsFactories) { + for (layoutFactory in notifRemoteViewsFactoryContainer.factories) { layoutFactory.instantiate(row, layoutType, parent, name, context, attrs)?.run { check(handledFactory == null) { "$layoutFactory tries to produce name:$name with type:$layoutType. " + diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifRemoteViewsFactoryContainer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifRemoteViewsFactoryContainer.kt new file mode 100644 index 000000000000..99177c270b32 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotifRemoteViewsFactoryContainer.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.row + +import com.android.systemui.flags.FeatureFlags +import com.android.systemui.flags.Flags +import javax.inject.Inject + +interface NotifRemoteViewsFactoryContainer { + val factories: Set<NotifRemoteViewsFactory> +} + +class NotifRemoteViewsFactoryContainerImpl +@Inject +constructor( + featureFlags: FeatureFlags, + precomputedTextViewFactory: PrecomputedTextViewFactory, + bigPictureLayoutInflaterFactory: BigPictureLayoutInflaterFactory, + callLayoutSetDataAsyncFactory: CallLayoutSetDataAsyncFactory, +) : NotifRemoteViewsFactoryContainer { + override val factories: Set<NotifRemoteViewsFactory> = buildSet { + add(precomputedTextViewFactory) + if (featureFlags.isEnabled(Flags.BIGPICTURE_NOTIFICATION_LAZY_LOADING)) { + add(bigPictureLayoutInflaterFactory) + } + if (featureFlags.isEnabled(Flags.CALL_LAYOUT_ASYNC_SET_DATA)) { + add(callLayoutSetDataAsyncFactory) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowModule.java index 3a59978d415c..200a08a2ea65 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowModule.java @@ -17,26 +17,15 @@ package com.android.systemui.statusbar.notification.row; import com.android.systemui.dagger.SysUISingleton; -import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.flags.Flags; import dagger.Binds; import dagger.Module; -import dagger.Provides; -import dagger.multibindings.ElementsIntoSet; - -import java.util.HashSet; -import java.util.Set; - -import javax.inject.Named; /** * Dagger Module containing notification row and view inflation implementations. */ @Module public abstract class NotificationRowModule { - public static final String NOTIF_REMOTEVIEWS_FACTORIES = - "notif_remoteviews_factories"; /** * Provides notification row content binder instance. @@ -54,26 +43,11 @@ public abstract class NotificationRowModule { public abstract NotifRemoteViewCache provideNotifRemoteViewCache( NotifRemoteViewCacheImpl cacheImpl); - /** Provides view factories to be inflated in notification content. */ - @Provides - @ElementsIntoSet - @Named(NOTIF_REMOTEVIEWS_FACTORIES) - static Set<NotifRemoteViewsFactory> provideNotifRemoteViewsFactories( - FeatureFlags featureFlags, - PrecomputedTextViewFactory precomputedTextViewFactory, - BigPictureLayoutInflaterFactory bigPictureLayoutInflaterFactory, - CallLayoutSetDataAsyncFactory callLayoutSetDataAsyncFactory - ) { - final Set<NotifRemoteViewsFactory> replacementFactories = new HashSet<>(); - if (featureFlags.isEnabled(Flags.PRECOMPUTED_TEXT)) { - replacementFactories.add(precomputedTextViewFactory); - } - if (featureFlags.isEnabled(Flags.BIGPICTURE_NOTIFICATION_LAZY_LOADING)) { - replacementFactories.add(bigPictureLayoutInflaterFactory); - } - if (featureFlags.isEnabled(Flags.CALL_LAYOUT_ASYNC_SET_DATA)) { - replacementFactories.add(callLayoutSetDataAsyncFactory); - } - return replacementFactories; - } + /** + * Provides notification remote view factory container + */ + @Binds + @SysUISingleton + public abstract NotifRemoteViewsFactoryContainer provideNotifRemoteViewsFactoryContainer( + NotifRemoteViewsFactoryContainerImpl containerImpl); } 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 9a8cc0ae33cb..1143481863f5 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 @@ -355,6 +355,12 @@ public class NotificationStackScrollLayoutController implements Dumpable { private float mMaxAlphaForExpansion = 1.0f; private float mMaxAlphaForUnhide = 1.0f; + /** + * Maximum alpha when to and from or sitting idle on the glanceable hub. Will be 1.0f when the + * hub is not visible or transitioning. + */ + private float mMaxAlphaForGlanceableHub = 1.0f; + private final NotificationListViewBinder mViewBinder; private void updateResources() { @@ -1285,9 +1291,19 @@ public class NotificationStackScrollLayoutController implements Dumpable { updateAlpha(); } + /** + * Sets the max alpha value for notifications when idle on the glanceable hub or when + * transitioning to/from the glanceable hub. + */ + public void setMaxAlphaForGlanceableHub(float alpha) { + mMaxAlphaForGlanceableHub = alpha; + updateAlpha(); + } + private void updateAlpha() { if (mView != null) { - mView.setAlpha(Math.min(mMaxAlphaForExpansion, mMaxAlphaForUnhide)); + mView.setAlpha(Math.min(mMaxAlphaForExpansion, + Math.min(mMaxAlphaForUnhide, mMaxAlphaForGlanceableHub))); } } @@ -1779,6 +1795,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { public void dump(@NonNull PrintWriter pw, @NonNull String[] args) { pw.println("mMaxAlphaForExpansion=" + mMaxAlphaForExpansion); pw.println("mMaxAlphaForUnhide=" + mMaxAlphaForUnhide); + pw.println("mMaxAlphaForGlanceableHub=" + mMaxAlphaForGlanceableHub); } /** diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt index 12927b87630e..f842e304ffdf 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt @@ -18,7 +18,6 @@ package com.android.systemui.statusbar.notification.stack.ui.viewbinder import android.animation.Animator import android.animation.AnimatorListenerAdapter -import android.animation.ValueAnimator import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.systemui.dagger.qualifiers.Main @@ -125,7 +124,14 @@ object SharedNotificationContainerBinder { launch { viewModel.translationY.collect { controller.setTranslationY(it) } } - launch { viewModel.alpha.collect { controller.setMaxAlphaForExpansion(it) } } + launch { + viewModel.expansionAlpha.collect { controller.setMaxAlphaForExpansion(it) } + } + launch { + viewModel.glanceableHubAlpha.collect { + controller.setMaxAlphaForGlanceableHub(it) + } + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt index a48fb45861d2..99cd89b84c14 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt @@ -20,6 +20,7 @@ package com.android.systemui.statusbar.notification.stack.ui.viewmodel import com.android.systemui.common.shared.model.NotificationContainerBounds +import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor @@ -27,6 +28,8 @@ import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE_LOCKED import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING import com.android.systemui.keyguard.shared.model.TransitionState.STARTED +import com.android.systemui.keyguard.ui.viewmodel.GlanceableHubToLockscreenTransitionViewModel +import com.android.systemui.keyguard.ui.viewmodel.LockscreenToGlanceableHubTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.LockscreenToOccludedTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel import com.android.systemui.shade.domain.interactor.ShadeInteractor @@ -61,8 +64,11 @@ constructor( private val keyguardInteractor: KeyguardInteractor, keyguardTransitionInteractor: KeyguardTransitionInteractor, private val shadeInteractor: ShadeInteractor, + communalInteractor: CommunalInteractor, occludedToLockscreenTransitionViewModel: OccludedToLockscreenTransitionViewModel, lockscreenToOccludedTransitionViewModel: LockscreenToOccludedTransitionViewModel, + glanceableHubToLockscreenTransitionViewModel: GlanceableHubToLockscreenTransitionViewModel, + lockscreenToGlanceableHubTransitionViewModel: LockscreenToGlanceableHubTransitionViewModel ) { private val statesForConstrainedNotifications = setOf( @@ -87,6 +93,20 @@ constructor( .distinctUntilChanged() .onStart { emit(false) } + private val lockscreenToGlanceableHubRunning = + keyguardTransitionInteractor + .transition(KeyguardState.LOCKSCREEN, KeyguardState.GLANCEABLE_HUB) + .map { it.transitionState == STARTED || it.transitionState == RUNNING } + .distinctUntilChanged() + .onStart { emit(false) } + + private val glanceableHubToLockscreenRunning = + keyguardTransitionInteractor + .transition(KeyguardState.GLANCEABLE_HUB, KeyguardState.LOCKSCREEN) + .map { it.transitionState == STARTED || it.transitionState == RUNNING } + .distinctUntilChanged() + .onStart { emit(false) } + val shadeCollapseFadeInComplete = MutableStateFlow(false) val configurationBasedDimensions: Flow<ConfigurationBasedDimensions> = @@ -144,6 +164,24 @@ constructor( initialValue = false, ) + /** Are we purely on the glanceable hub without the shade/qs? */ + internal val isOnGlanceableHubWithoutShade: Flow<Boolean> = + combine( + communalInteractor.isIdleOnCommunal, + // Shade with notifications + shadeInteractor.shadeExpansion.map { it > 0f }, + // Shade without notifications, quick settings only (pull down from very top on + // lockscreen) + shadeInteractor.qsExpansion.map { it > 0f }, + ) { isIdleOnCommunal, isShadeVisible, qsExpansion -> + isIdleOnCommunal && !(isShadeVisible || qsExpansion) + } + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = false, + ) + /** Fade in only for use after the shade collapses */ val shadeCollpaseFadeIn: Flow<Boolean> = flow { @@ -201,7 +239,7 @@ constructor( initialValue = NotificationContainerBounds(), ) - val alpha: Flow<Float> = + val expansionAlpha: Flow<Float> = // Due to issues with the legacy shade, some shade expansion events are sent incorrectly, // such as when the shade resets. This can happen while the LOCKSCREEN<->OCCLUDED transition // is running. Therefore use a series of flatmaps to prevent unwanted interruptions while @@ -235,6 +273,43 @@ constructor( } /** + * Returns a flow of the expected alpha while running a LOCKSCREEN<->GLANCEABLE_HUB transition + * or idle on the glanceable hub. + * + * Must return 1.0f when not controlling the alpha since notifications does a min of all the + * alpha sources. + */ + val glanceableHubAlpha: Flow<Float> = + isOnGlanceableHubWithoutShade.flatMapLatest { isOnGlanceableHubWithoutShade -> + combineTransform( + lockscreenToGlanceableHubRunning, + glanceableHubToLockscreenRunning, + merge( + lockscreenToGlanceableHubTransitionViewModel.notificationAlpha, + glanceableHubToLockscreenTransitionViewModel.notificationAlpha, + ) + .onStart { + // Transition flows don't emit a value on start, kick things off so the + // combine starts. + emit(1f) + } + ) { lockscreenToGlanceableHubRunning, glanceableHubToLockscreenRunning, alpha -> + if (isOnGlanceableHubWithoutShade) { + // Notifications should not be visible on the glanceable hub. + // TODO(b/321075734): implement a way to actually set the notifications to gone + // while on the hub instead of just adjusting alpha + emit(0f) + } else if (lockscreenToGlanceableHubRunning || glanceableHubToLockscreenRunning) { + emit(alpha) + } else { + // Not on the hub and no transitions running, return full visibility so we don't + // block the notifications from showing. + emit(1f) + } + } + } + + /** * Under certain scenarios, such as swiping up on the lockscreen, the container will need to be * translated as the keyguard fades out. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt index 63194c37bbe1..8a56da3dafab 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ActivityStarterImpl.kt @@ -797,18 +797,7 @@ constructor( } } if (dismissShade) { - if ( - shadeControllerLazy.get().isExpandedVisible && - !statusBarKeyguardViewManagerLazy.get().isBouncerShowing - ) { - shadeControllerLazy.get().animateCollapseShadeForcedDelayed() - } else { - // Do it after DismissAction has been processed to conserve the - // needed ordering. - postOnUiThread { - shadeControllerLazy.get().runPostCollapseRunnables() - } - } + shadeControllerLazy.get().collapseShadeForActivityStart() } return deferred } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java index 90cba409a787..40194361e12b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfaces.java @@ -25,6 +25,7 @@ import android.content.Intent; import android.content.pm.PackageManager; import android.os.Bundle; import android.os.UserHandle; +import android.view.MotionEvent; import android.view.RemoteAnimationAdapter; import android.view.View; import android.window.RemoteTransition; @@ -277,6 +278,13 @@ public interface CentralSurfaces extends Dumpable, LifecycleOwner { void awakenDreams(); + /** + * Handle a touch event while dreaming when the touch was initiated within a prescribed + * swipeable area. This method is provided for cases where swiping in certain areas of a dream + * should be handled by CentralSurfaces instead (e.g. swiping communal hub open). + */ + void handleDreamTouch(MotionEvent event); + boolean isBouncerShowing(); boolean isBouncerShowingScrimmed(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesEmptyImpl.kt index 7dc4b96ea154..60dfaa790ec9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesEmptyImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesEmptyImpl.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.phone import android.content.Intent +import android.view.MotionEvent import androidx.lifecycle.LifecycleRegistry import com.android.keyguard.AuthKeyguardMessageArea import com.android.systemui.animation.ActivityLaunchAnimator @@ -78,6 +79,7 @@ abstract class CentralSurfacesEmptyImpl : CentralSurfaces { override fun updateScrimController() {} override fun shouldIgnoreTouch() = false override fun isDeviceInteractive() = false + override fun handleDreamTouch(event: MotionEvent?) {} override fun awakenDreams() {} override fun isBouncerShowing() = false override fun isBouncerShowingScrimmed() = false diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index 6e3aabf7c754..266c19c09941 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -80,6 +80,7 @@ import android.util.Log; import android.view.Display; import android.view.IRemoteAnimationRunner; import android.view.IWindowManager; +import android.view.MotionEvent; import android.view.ThreadedRenderer; import android.view.View; import android.view.WindowInsets; @@ -2903,6 +2904,11 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { }; @Override + public void handleDreamTouch(MotionEvent event) { + getNotificationShadeWindowViewController().handleDreamTouch(event); + } + + @Override public void awakenDreams() { mUiBgExecutor.execute(() -> { try { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java index 459b368b5ac9..aabe4a0d66f9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java @@ -300,7 +300,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump DozeParameters dozeParameters, AlarmManager alarmManager, KeyguardStateController keyguardStateController, - DelayedWakeLock.Builder delayedWakeLockBuilder, + DelayedWakeLock.Factory delayedWakeLockFactory, Handler handler, KeyguardUpdateMonitor keyguardUpdateMonitor, DockManager dockManager, @@ -331,7 +331,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump mScreenOffAnimationController = screenOffAnimationController; mTimeTicker = new AlarmTimeout(alarmManager, this::onHideWallpaperTimeout, "hide_aod_wallpaper", mHandler); - mWakeLock = delayedWakeLockBuilder.setHandler(mHandler).setTag("Scrims").build(); + mWakeLock = delayedWakeLockFactory.create("Scrims"); // Scrim alpha is initially set to the value on the resource but might be changed // to make sure that text on top of it is legible. mDozeParameters = dozeParameters; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt index fe49c0791370..6b303263d4b0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractor.kt @@ -17,6 +17,7 @@ package com.android.systemui.statusbar.pipeline.mobile.domain.interactor import android.content.Context +import com.android.internal.telephony.flags.Flags import com.android.settingslib.SignalIcon.MobileIconGroup import com.android.settingslib.graph.SignalDrawable import com.android.settingslib.mobile.MobileIconCarrierIdOverrides @@ -32,14 +33,18 @@ import com.android.systemui.statusbar.pipeline.mobile.domain.model.NetworkTypeIc import com.android.systemui.statusbar.pipeline.mobile.domain.model.NetworkTypeIconModel.DefaultIcon import com.android.systemui.statusbar.pipeline.mobile.domain.model.NetworkTypeIconModel.OverriddenIcon import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel +import com.android.systemui.statusbar.pipeline.satellite.ui.model.SatelliteIconModel import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityModel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi 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.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn @@ -79,6 +84,9 @@ interface MobileIconInteractor { /** Whether or not to show the slice attribution */ val showSliceAttribution: StateFlow<Boolean> + /** True if this connection is satellite-based */ + val isNonTerrestrial: StateFlow<Boolean> + /** * Provider name for this network connection. The name can be one of 3 values: * 1. The default network name, if one is configured @@ -244,6 +252,13 @@ class MobileIconInteractorImpl( override val showSliceAttribution: StateFlow<Boolean> = connectionRepository.hasPrioritizedNetworkCapabilities + override val isNonTerrestrial: StateFlow<Boolean> = + if (Flags.carrierEnabledSatelliteFlag()) { + connectionRepository.isNonTerrestrial + } else { + MutableStateFlow(false).asStateFlow() + } + override val isRoaming: StateFlow<Boolean> = combine( connectionRepository.carrierNetworkChangeActive, @@ -313,26 +328,45 @@ class MobileIconInteractorImpl( } .stateIn(scope, SharingStarted.WhileSubscribed(), 0) - override val signalLevelIcon: StateFlow<SignalIconModel> = run { - val initial = - SignalIconModel( - level = shownLevel.value, - numberOfLevels = numberOfLevels.value, - showExclamationMark = showExclamationMark.value, - carrierNetworkChange = carrierNetworkChangeActive.value, - ) + private val cellularIcon: Flow<SignalIconModel.Cellular> = combine( + shownLevel, + numberOfLevels, + showExclamationMark, + carrierNetworkChangeActive, + ) { shownLevel, numberOfLevels, showExclamationMark, carrierNetworkChange -> + SignalIconModel.Cellular( shownLevel, numberOfLevels, showExclamationMark, - carrierNetworkChangeActive, - ) { shownLevel, numberOfLevels, showExclamationMark, carrierNetworkChange -> - SignalIconModel( - shownLevel, - numberOfLevels, - showExclamationMark, - carrierNetworkChange, - ) + carrierNetworkChange, + ) + } + + private val satelliteIcon: Flow<SignalIconModel.Satellite> = + shownLevel.map { + SignalIconModel.Satellite( + level = it, + icon = SatelliteIconModel.fromSignalStrength(it) + ?: SatelliteIconModel.fromSignalStrength(0)!! + ) + } + + override val signalLevelIcon: StateFlow<SignalIconModel> = run { + val initial = + SignalIconModel.Cellular( + shownLevel.value, + numberOfLevels.value, + showExclamationMark.value, + carrierNetworkChangeActive.value, + ) + isNonTerrestrial + .flatMapLatest { ntn -> + if (ntn) { + satelliteIcon + } else { + cellularIcon + } } .distinctUntilChanged() .logDiffsForTable( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/model/SignalIconModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/model/SignalIconModel.kt index e58f08183157..d6b8fd4fec87 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/model/SignalIconModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/domain/model/SignalIconModel.kt @@ -17,51 +17,94 @@ package com.android.systemui.statusbar.pipeline.mobile.domain.model import com.android.settingslib.graph.SignalDrawable +import com.android.systemui.common.shared.model.Icon import com.android.systemui.log.table.Diffable import com.android.systemui.log.table.TableRowLogger -/** A model that will be consumed by [SignalDrawable] to show the mobile triangle icon. */ -data class SignalIconModel( - val level: Int, - val numberOfLevels: Int, - val showExclamationMark: Boolean, - val carrierNetworkChange: Boolean, -) : Diffable<SignalIconModel> { - // TODO(b/267767715): Can we implement [logDiffs] and [logFull] generically for data classes? +sealed interface SignalIconModel : Diffable<SignalIconModel> { + val level: Int + override fun logDiffs(prevVal: SignalIconModel, row: TableRowLogger) { - if (prevVal.level != level) { - row.logChange(COL_LEVEL, level) + logPartial(prevVal, row) + } + + override fun logFull(row: TableRowLogger) = logFully(row) + + fun logFully(row: TableRowLogger) + + fun logPartial(prevVal: SignalIconModel, row: TableRowLogger) + + /** A model that will be consumed by [SignalDrawable] to show the mobile triangle icon. */ + data class Cellular( + override val level: Int, + val numberOfLevels: Int, + val showExclamationMark: Boolean, + val carrierNetworkChange: Boolean, + ) : SignalIconModel { + override fun logPartial(prevVal: SignalIconModel, row: TableRowLogger) { + if (prevVal !is Cellular) { + logFull(row) + } else { + if (prevVal.level != level) { + row.logChange(COL_LEVEL, level) + } + if (prevVal.numberOfLevels != numberOfLevels) { + row.logChange(COL_NUM_LEVELS, numberOfLevels) + } + if (prevVal.showExclamationMark != showExclamationMark) { + row.logChange(COL_SHOW_EXCLAMATION, showExclamationMark) + } + if (prevVal.carrierNetworkChange != carrierNetworkChange) { + row.logChange(COL_CARRIER_NETWORK_CHANGE, carrierNetworkChange) + } + } } - if (prevVal.numberOfLevels != numberOfLevels) { + + override fun logFully(row: TableRowLogger) { + row.logChange(COL_TYPE, "c") + row.logChange(COL_LEVEL, level) row.logChange(COL_NUM_LEVELS, numberOfLevels) - } - if (prevVal.showExclamationMark != showExclamationMark) { row.logChange(COL_SHOW_EXCLAMATION, showExclamationMark) - } - if (prevVal.carrierNetworkChange != carrierNetworkChange) { row.logChange(COL_CARRIER_NETWORK_CHANGE, carrierNetworkChange) } - } - override fun logFull(row: TableRowLogger) { - row.logChange(COL_LEVEL, level) - row.logChange(COL_NUM_LEVELS, numberOfLevels) - row.logChange(COL_SHOW_EXCLAMATION, showExclamationMark) - row.logChange(COL_CARRIER_NETWORK_CHANGE, carrierNetworkChange) + /** Convert this model to an [Int] consumable by [SignalDrawable]. */ + fun toSignalDrawableState(): Int = + if (carrierNetworkChange) { + SignalDrawable.getCarrierChangeState(numberOfLevels) + } else { + SignalDrawable.getState(level, numberOfLevels, showExclamationMark) + } } - /** Convert this model to an [Int] consumable by [SignalDrawable]. */ - fun toSignalDrawableState(): Int = - if (carrierNetworkChange) { - SignalDrawable.getCarrierChangeState(numberOfLevels) - } else { - SignalDrawable.getState(level, numberOfLevels, showExclamationMark) + /** + * For non-terrestrial networks, we can use a resource-backed icon instead of the + * [SignalDrawable]-backed version above + */ + data class Satellite( + override val level: Int, + val icon: Icon.Resource, + ) : SignalIconModel { + override fun logPartial(prevVal: SignalIconModel, row: TableRowLogger) { + if (prevVal !is Satellite) { + logFull(row) + } else { + if (prevVal.level != level) row.logChange(COL_LEVEL, level) + } } + override fun logFully(row: TableRowLogger) { + row.logChange("numLevels", "HELLO") + row.logChange(COL_TYPE, "s") + row.logChange(COL_LEVEL, level) + } + } + companion object { private const val COL_LEVEL = "level" private const val COL_NUM_LEVELS = "numLevels" private const val COL_SHOW_EXCLAMATION = "showExclamation" private const val COL_CARRIER_NETWORK_CHANGE = "carrierNetworkChange" + private const val COL_TYPE = "type" } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/VerboseMobileViewLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/VerboseMobileViewLogger.kt index a1a5370819f8..43cb38ff0108 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/VerboseMobileViewLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/VerboseMobileViewLogger.kt @@ -59,7 +59,7 @@ constructor( str1 = parentView.getIdForLogging() int1 = subId int2 = icon.level - bool1 = icon.showExclamationMark + bool1 = if (icon is SignalIconModel.Cellular) icon.showExclamationMark else false }, { "Binder[subId=$int1, viewId=$str1] received new signal icon: " + diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt index 5475528152ed..a0c5618041f6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/binder/MobileIconBinder.kt @@ -38,6 +38,7 @@ import com.android.systemui.plugins.DarkIconDispatcher import com.android.systemui.res.R import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.StatusBarIconView.STATE_HIDDEN +import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel import com.android.systemui.statusbar.pipeline.mobile.ui.MobileViewLogger import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.LocationBasedMobileViewModel import com.android.systemui.statusbar.pipeline.shared.ui.binder.ModernStatusBarViewBinding @@ -70,7 +71,7 @@ object MobileIconBinder { val networkTypeView = view.requireViewById<ImageView>(R.id.mobile_type) val networkTypeContainer = view.requireViewById<FrameLayout>(R.id.mobile_type_container) val iconView = view.requireViewById<ImageView>(R.id.mobile_signal) - val mobileDrawable = SignalDrawable(view.context).also { iconView.setImageDrawable(it) } + val mobileDrawable = SignalDrawable(view.context) val roamingView = view.requireViewById<ImageView>(R.id.mobile_roaming) val roamingSpace = view.requireViewById<Space>(R.id.mobile_roaming_space) val dotView = view.requireViewById<StatusBarIconView>(R.id.status_bar_dot) @@ -138,7 +139,12 @@ object MobileIconBinder { viewModel.subscriptionId, icon, ) - mobileDrawable.level = icon.toSignalDrawableState() + if (icon is SignalIconModel.Cellular) { + iconView.setImageDrawable(mobileDrawable) + mobileDrawable.level = icon.toSignalDrawableState() + } else if (icon is SignalIconModel.Satellite) { + IconViewBinder.bind(icon.icon, iconView) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt index 60c662da6aec..eda5c44b5c2f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModel.kt @@ -33,12 +33,15 @@ import com.android.systemui.statusbar.pipeline.shared.data.model.DataActivityMod import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.stateIn /** Common interface for all of the location-based mobile icon view models. */ @@ -71,7 +74,6 @@ interface MobileIconViewModelCommon { * model gets the exact same information, as well as allows us to log that unified state only once * per icon. */ -@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") @OptIn(ExperimentalCoroutinesApi::class) class MobileIconViewModel( override val subscriptionId: Int, @@ -81,6 +83,100 @@ class MobileIconViewModel( flags: FeatureFlagsClassic, scope: CoroutineScope, ) : MobileIconViewModelCommon { + private val cellProvider by lazy { + CellularIconViewModel( + subscriptionId, + iconInteractor, + airplaneModeInteractor, + constants, + flags, + scope, + ) + } + + private val satelliteProvider by lazy { + CarrierBasedSatelliteViewModelImpl( + subscriptionId, + iconInteractor, + ) + } + + /** + * Similar to repository switching, this allows us to split up the logic of satellite/cellular + * states, since they are different by nature + */ + private val vmProvider: Flow<MobileIconViewModelCommon> = + iconInteractor.isNonTerrestrial + .mapLatest { nonTerrestrial -> + if (nonTerrestrial) { + satelliteProvider + } else { + cellProvider + } + } + .stateIn(scope, SharingStarted.WhileSubscribed(), cellProvider) + + override val isVisible: StateFlow<Boolean> = + vmProvider + .flatMapLatest { it.isVisible } + .stateIn(scope, SharingStarted.WhileSubscribed(), false) + + override val icon: Flow<SignalIconModel> = vmProvider.flatMapLatest { it.icon } + + override val contentDescription: Flow<ContentDescription> = + vmProvider.flatMapLatest { it.contentDescription } + + override val roaming: Flow<Boolean> = vmProvider.flatMapLatest { it.roaming } + + override val networkTypeIcon: Flow<Icon.Resource?> = + vmProvider.flatMapLatest { it.networkTypeIcon } + + override val networkTypeBackground: StateFlow<Icon.Resource?> = + vmProvider + .flatMapLatest { it.networkTypeBackground } + .stateIn(scope, SharingStarted.WhileSubscribed(), null) + + override val activityInVisible: Flow<Boolean> = + vmProvider.flatMapLatest { it.activityInVisible } + + override val activityOutVisible: Flow<Boolean> = + vmProvider.flatMapLatest { it.activityOutVisible } + + override val activityContainerVisible: Flow<Boolean> = + vmProvider.flatMapLatest { it.activityContainerVisible } +} + +/** Representation of this network when it is non-terrestrial (e.g., satellite) */ +private class CarrierBasedSatelliteViewModelImpl( + override val subscriptionId: Int, + interactor: MobileIconInteractor, +) : MobileIconViewModelCommon { + override val isVisible: StateFlow<Boolean> = MutableStateFlow(true) + override val icon: Flow<SignalIconModel> = interactor.signalLevelIcon + + override val contentDescription: Flow<ContentDescription> = + MutableStateFlow(ContentDescription.Loaded("")) + + /** These fields are not used for satellite icons currently */ + override val roaming: Flow<Boolean> = flowOf(false) + override val networkTypeIcon: Flow<Icon.Resource?> = flowOf(null) + override val networkTypeBackground: StateFlow<Icon.Resource?> = MutableStateFlow(null) + override val activityInVisible: Flow<Boolean> = flowOf(false) + override val activityOutVisible: Flow<Boolean> = flowOf(false) + override val activityContainerVisible: Flow<Boolean> = flowOf(false) +} + +/** Terrestrial (cellular) icon. */ +@Suppress("EXPERIMENTAL_IS_NOT_ENABLED") +@OptIn(ExperimentalCoroutinesApi::class) +private class CellularIconViewModel( + override val subscriptionId: Int, + iconInteractor: MobileIconInteractor, + airplaneModeInteractor: AirplaneModeInteractor, + constants: ConnectivityConstants, + flags: FeatureFlagsClassic, + scope: CoroutineScope, +) : MobileIconViewModelCommon { override val isVisible: StateFlow<Boolean> = if (!constants.hasDataCapabilities) { flowOf(false) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/model/SatelliteIconModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/model/SatelliteIconModel.kt index 6938d667ca81..63566ee814ab 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/model/SatelliteIconModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/model/SatelliteIconModel.kt @@ -28,7 +28,7 @@ object SatelliteIconModel { fun fromConnectionState( connectionState: SatelliteConnectionState, signalStrength: Int, - ): Icon? = + ): Icon.Resource? = when (connectionState) { // TODO(b/316635648): check if this should be null SatelliteConnectionState.Unknown, @@ -41,9 +41,13 @@ object SatelliteIconModel { SatelliteConnectionState.Connected -> fromSignalStrength(signalStrength) } - private fun fromSignalStrength( + /** + * Satellite icon appropriate for when we are connected. Use [fromConnectionState] for a more + * generally correct representation. + */ + fun fromSignalStrength( signalStrength: Int, - ): Icon? = + ): Icon.Resource? = // TODO(b/316634365): these need content descriptions when (signalStrength) { // No signal diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/InternetTileViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/InternetTileViewModel.kt index ae58398753e8..352413ee568a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/InternetTileViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/InternetTileViewModel.kt @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.pipeline.shared.ui.viewmodel import android.content.Context import android.text.Html import com.android.systemui.common.shared.model.ContentDescription +import com.android.systemui.common.shared.model.ContentDescription.Companion.loadContentDescription import com.android.systemui.common.shared.model.Text import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application @@ -28,6 +29,7 @@ import com.android.systemui.res.R import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository import com.android.systemui.statusbar.pipeline.ethernet.domain.EthernetInteractor import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor +import com.android.systemui.statusbar.pipeline.mobile.domain.model.SignalIconModel import com.android.systemui.statusbar.pipeline.shared.data.repository.ConnectivityRepository import com.android.systemui.statusbar.pipeline.shared.ui.model.InternetTileModel import com.android.systemui.statusbar.pipeline.shared.ui.model.SignalIcon @@ -116,14 +118,31 @@ constructor( it.signalLevelIcon, mobileDataContentName, ) { networkNameModel, signalIcon, dataContentDescription -> - val secondary = - mobileDataContentConcat(networkNameModel.name, dataContentDescription) - InternetTileModel.Active( - secondaryTitle = secondary, - icon = SignalIcon(signalIcon.toSignalDrawableState()), - stateDescription = ContentDescription.Loaded(secondary.toString()), - contentDescription = ContentDescription.Loaded(internetLabel), - ) + when (signalIcon) { + is SignalIconModel.Cellular -> { + val secondary = + mobileDataContentConcat( + networkNameModel.name, + dataContentDescription + ) + InternetTileModel.Active( + secondaryTitle = secondary, + icon = SignalIcon(signalIcon.toSignalDrawableState()), + stateDescription = ContentDescription.Loaded(secondary.toString()), + contentDescription = ContentDescription.Loaded(internetLabel), + ) + } + is SignalIconModel.Satellite -> { + val secondary = + signalIcon.icon.contentDescription.loadContentDescription(context) + InternetTileModel.Active( + secondaryTitle = secondary, + iconId = signalIcon.icon.res, + stateDescription = ContentDescription.Loaded(secondary), + contentDescription = ContentDescription.Loaded(internetLabel), + ) + } + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java index 20d1fff91443..a2d8d1579e3d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java @@ -28,6 +28,8 @@ import android.icu.lang.UCharacter; import android.icu.text.DateTimePatternGenerator; import android.os.Bundle; import android.os.Handler; +import android.os.HandlerExecutor; +import android.os.HandlerThread; import android.os.Parcelable; import android.os.SystemClock; import android.os.UserHandle; @@ -48,11 +50,11 @@ import android.widget.TextView; import com.android.settingslib.Utils; import com.android.systemui.Dependency; import com.android.systemui.FontSizeUtils; -import com.android.systemui.res.R; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.demomode.DemoModeCommandReceiver; import com.android.systemui.plugins.DarkIconDispatcher; import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver; +import com.android.systemui.res.R; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.phone.StatusBarIconController; @@ -106,6 +108,7 @@ public class Clock extends TextView implements private final int mAmPmStyle; private boolean mShowSeconds; private Handler mSecondsHandler; + private HandlerThread mHandlerThread; // Fields to cache the width so the clock remains at an approximately constant width private int mCharsAtCurrentWidth = -1; @@ -146,6 +149,8 @@ public class Clock extends TextView implements } mBroadcastDispatcher = Dependency.get(BroadcastDispatcher.class); mUserTracker = Dependency.get(UserTracker.class); + mHandlerThread = new HandlerThread("Clock"); + mHandlerThread.start(); setIncludeFontPadding(false); } @@ -205,7 +210,8 @@ public class Clock extends TextView implements Dependency.get(TunerService.class).addTunable(this, CLOCK_SECONDS, StatusBarIconController.ICON_HIDE_LIST); mCommandQueue.addCallback(this); - mUserTracker.addCallback(mUserChangedCallback, mContext.getMainExecutor()); + mUserTracker.addCallback(mUserChangedCallback, + new HandlerExecutor(mHandlerThread.getThreadHandler())); mCurrentUserId = mUserTracker.getUserId(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NextAlarmControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NextAlarmControllerImpl.java index b7d8ee3943e3..a7440d6c200e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NextAlarmControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NextAlarmControllerImpl.java @@ -21,6 +21,8 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.os.HandlerExecutor; +import android.os.HandlerThread; import android.os.UserHandle; import androidx.annotation.NonNull; @@ -51,6 +53,7 @@ public class NextAlarmControllerImpl extends BroadcastReceiver private final UserTracker mUserTracker; private AlarmManager mAlarmManager; private AlarmManager.AlarmClockInfo mNextAlarm; + private HandlerThread mHandlerThread; private final UserTracker.Callback mUserChangedCallback = new UserTracker.Callback() { @@ -75,7 +78,10 @@ public class NextAlarmControllerImpl extends BroadcastReceiver IntentFilter filter = new IntentFilter(); filter.addAction(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED); broadcastDispatcher.registerReceiver(this, filter, null, UserHandle.ALL); - mUserTracker.addCallback(mUserChangedCallback, mainExecutor); + mHandlerThread = new HandlerThread("NextAlarmControllerImpl"); + mHandlerThread.start(); + mUserTracker.addCallback(mUserChangedCallback, + new HandlerExecutor(mHandlerThread.getThreadHandler())); updateNextAlarm(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java index 9f4a90658b2e..6a6efbc11362 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java @@ -157,7 +157,7 @@ public class SecurityControllerImpl implements SecurityController { // TODO: re-register network callback on user change. mConnectivityManager.registerNetworkCallback(REQUEST, mNetworkCallback); onUserSwitched(mUserTracker.getUserId()); - mUserTracker.addCallback(mUserChangedCallback, mMainExecutor); + mUserTracker.addCallback(mUserChangedCallback, mBgExecutor); } public void dump(PrintWriter pw, String[] args) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserInfoControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserInfoControllerImpl.java index 2ed9d1548007..0bc0e88114a5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserInfoControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserInfoControllerImpl.java @@ -36,9 +36,9 @@ import androidx.annotation.NonNull; import com.android.internal.util.UserIcons; import com.android.settingslib.drawable.UserIconDrawable; -import com.android.systemui.res.R; import com.android.systemui.dagger.SysUISingleton; -import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.dagger.qualifiers.Background; +import com.android.systemui.res.R; import com.android.systemui.settings.UserTracker; import java.util.ArrayList; @@ -66,11 +66,11 @@ public class UserInfoControllerImpl implements UserInfoController { /** */ @Inject - public UserInfoControllerImpl(Context context, @Main Executor mainExecutor, + public UserInfoControllerImpl(Context context, @Background Executor bgExecutor, UserTracker userTracker) { mContext = context; mUserTracker = userTracker; - mUserTracker.addCallback(mUserChangedCallback, mainExecutor); + mUserTracker.addCallback(mUserChangedCallback, bgExecutor); IntentFilter profileFilter = new IntentFilter(); profileFilter.addAction(ContactsContract.Intents.ACTION_PROFILE_CHANGED); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java index df210b073e77..f0b49307aad5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java @@ -29,6 +29,7 @@ import android.database.ContentObserver; import android.net.Uri; import android.os.Handler; import android.os.HandlerExecutor; +import android.os.HandlerThread; import android.os.Trace; import android.os.UserHandle; import android.os.UserManager; @@ -81,6 +82,7 @@ public class ZenModeControllerImpl implements ZenModeController, Dumpable { private volatile int mZenMode; private long mZenUpdateTime; private NotificationManager.Policy mConsolidatedNotificationPolicy; + private HandlerThread mHandlerThread; private final UserTracker.Callback mUserChangedCallback = new UserTracker.Callback() { @@ -133,6 +135,8 @@ public class ZenModeControllerImpl implements ZenModeController, Dumpable { } } }; + mHandlerThread = new HandlerThread("ZenModeControllerImpl"); + mHandlerThread.start(); mNoMan = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); globalSettings.registerContentObserver(Global.ZEN_MODE, modeContentObserver); updateZenMode(getModeSettingValueFromProvider()); @@ -143,7 +147,8 @@ public class ZenModeControllerImpl implements ZenModeController, Dumpable { mSetupObserver = new SetupObserver(handler); mSetupObserver.register(); mUserManager = context.getSystemService(UserManager.class); - mUserTracker.addCallback(mUserChangedCallback, new HandlerExecutor(handler)); + mUserTracker.addCallback(mUserChangedCallback, + new HandlerExecutor(mHandlerThread.getThreadHandler())); // This registers the alarm broadcast receiver for the current user mUserChangedCallback.onUserChanged(getCurrentUser(), context); diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java index 2b9ad50c1257..77518db9184c 100644 --- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java +++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java @@ -480,7 +480,7 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { return; } - mUserTracker.addCallback(mUserTrackerCallback, mMainExecutor); + mUserTracker.addCallback(mUserTrackerCallback, mBgExecutor); mDeviceProvisionedController.addCallback(mDeviceProvisionedListener); diff --git a/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java b/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java index 550a65c01bfc..f5b4d17ae7d3 100644 --- a/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java +++ b/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java @@ -26,6 +26,7 @@ import android.database.ContentObserver; import android.net.Uri; import android.os.Handler; import android.os.HandlerExecutor; +import android.os.HandlerThread; import android.os.Looper; import android.os.UserManager; import android.provider.Settings; @@ -38,11 +39,11 @@ import androidx.annotation.WorkerThread; import com.android.internal.util.ArrayUtils; import com.android.systemui.DejankUtils; -import com.android.systemui.res.R; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.demomode.DemoModeController; import com.android.systemui.qs.QSHost; +import com.android.systemui.res.R; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.phone.StatusBarIconController; import com.android.systemui.statusbar.phone.SystemUIDialog; @@ -98,6 +99,7 @@ public class TunerServiceImpl extends TunerService { private UserTracker.Callback mCurrentUserTracker; private UserTracker mUserTracker; private final ComponentName mTunerComponent; + private HandlerThread mHandlerThread; /** */ @@ -117,7 +119,8 @@ public class TunerServiceImpl extends TunerService { mDemoModeController = demoModeController; mUserTracker = userTracker; mTunerComponent = new ComponentName(mContext, TunerActivity.class); - + mHandlerThread = new HandlerThread("TunerServiceImpl"); + mHandlerThread.start(); for (UserInfo user : UserManager.get(mContext).getUsers()) { mCurrentUser = user.getUserHandle().getIdentifier(); if (getValue(TUNER_VERSION, 0) != CURRENT_TUNER_VERSION) { @@ -135,7 +138,7 @@ public class TunerServiceImpl extends TunerService { } }; mUserTracker.addCallback(mCurrentUserTracker, - new HandlerExecutor(mainHandler)); + new HandlerExecutor(mHandlerThread.getThreadHandler())); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt index cf76c0d2e696..74e133923378 100644 --- a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt @@ -190,7 +190,7 @@ constructor( } } - tracker.addCallback(callback, mainDispatcher.asExecutor()) + tracker.addCallback(callback, backgroundDispatcher.asExecutor()) send(currentSelectionStatus) awaitClose { tracker.removeCallback(callback) } diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/Utils.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/Utils.kt index 2336a8e46f07..6993c961fba8 100644 --- a/packages/SystemUI/src/com/android/systemui/util/kotlin/Utils.kt +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/Utils.kt @@ -28,9 +28,13 @@ class Utils { fun <A, B, C, D> toQuad(a: A, bcd: Triple<B, C, D>) = Quad(a, bcd.first, bcd.second, bcd.third) + fun <A, B, C, D, E> toQuint(a: A, b: B, c: C, d: D, e: E) = Quint(a, b, c, d, e) fun <A, B, C, D, E> toQuint(a: A, bcde: Quad<B, C, D, E>) = Quint(a, bcde.first, bcde.second, bcde.third, bcde.fourth) + fun <A, B, C, D, E, F> toSextuple(a: A, bcdef: Quint<B, C, D, E, F>) = + Sextuple(a, bcdef.first, bcdef.second, bcdef.third, bcdef.fourth, bcdef.fifth) + /** * Samples the provided flows, emitting a tuple of the original flow's value as well as each * of the combined flows' values. @@ -69,6 +73,22 @@ class Utils { ): Flow<Quint<A, B, C, D, E>> { return this.sample(combine(b, c, d, e, ::Quad), ::toQuint) } + + /** + * Samples the provided flows, emitting a tuple of the original flow's value as well as each + * of the combined flows' values. + * + * Flow<A>.sample(Flow<B>, Flow<C>, Flow<D>, Flow<E>, Flow<F>) -> (A, B, C, D, E, F) + */ + fun <A, B, C, D, E, F> Flow<A>.sample( + b: Flow<B>, + c: Flow<C>, + d: Flow<D>, + e: Flow<E>, + f: Flow<F>, + ): Flow<Sextuple<A, B, C, D, E, F>> { + return this.sample(combine(b, c, d, e, f, ::Quint), ::toSextuple) + } } } @@ -81,3 +101,12 @@ data class Quint<A, B, C, D, E>( val fourth: D, val fifth: E ) + +data class Sextuple<A, B, C, D, E, F>( + val first: A, + val second: B, + val third: C, + val fourth: D, + val fifth: E, + val sixth: F, +) diff --git a/packages/SystemUI/src/com/android/systemui/util/wakelock/DelayedWakeLock.java b/packages/SystemUI/src/com/android/systemui/util/wakelock/DelayedWakeLock.java index 972895d4a192..039109e9ddc6 100644 --- a/packages/SystemUI/src/com/android/systemui/util/wakelock/DelayedWakeLock.java +++ b/packages/SystemUI/src/com/android/systemui/util/wakelock/DelayedWakeLock.java @@ -19,7 +19,11 @@ package com.android.systemui.util.wakelock; import android.content.Context; import android.os.Handler; -import javax.inject.Inject; +import com.android.systemui.dagger.qualifiers.Background; + +import dagger.assisted.Assisted; +import dagger.assisted.AssistedFactory; +import dagger.assisted.AssistedInject; /** * A wake lock that has a built in delay when releasing to give the framebuffer time to update. @@ -32,9 +36,11 @@ public class DelayedWakeLock implements WakeLock { private final Handler mHandler; private final WakeLock mInner; - public DelayedWakeLock(Handler h, WakeLock inner) { - mHandler = h; - mInner = inner; + @AssistedInject + public DelayedWakeLock(@Background Handler handler, Context context, WakeLockLogger logger, + @Assisted String tag) { + mInner = WakeLock.createPartial(context, logger, tag); + mHandler = handler; } @Override @@ -58,46 +64,11 @@ public class DelayedWakeLock implements WakeLock { } /** - * An injectable builder for {@see DelayedWakeLock} that has the context already filled in. + * Factory to create the instance of DelayedWakeLock class. */ - public static class Builder { - private final Context mContext; - private final WakeLockLogger mLogger; - private String mTag; - private Handler mHandler; - - /** - * Constructor for DelayedWakeLock.Builder - */ - @Inject - public Builder(Context context, WakeLockLogger logger) { - mContext = context; - mLogger = logger; - } - - /** - * Set the tag for the WakeLock. - */ - public Builder setTag(String tag) { - mTag = tag; - - return this; - } - - /** - * Set the handler for the DelayedWakeLock. - */ - public Builder setHandler(Handler handler) { - mHandler = handler; - - return this; - } - - /** - * Build the DelayedWakeLock. - */ - public DelayedWakeLock build() { - return new DelayedWakeLock(mHandler, WakeLock.createPartial(mContext, mLogger, mTag)); - } + @AssistedFactory + public interface Factory { + /** creates the instance of DelayedWakeLock class. */ + DelayedWakeLock create(String tag); } -} +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java index 9ee3d220a79b..aee441a13a5d 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java @@ -535,6 +535,7 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa } if (changed && fromKey) { Events.writeEvent(Events.EVENT_KEY, stream, lastAudibleStreamVolume); + mCallbacks.onVolumeChangedFromKey(); } return changed; } @@ -1030,6 +1031,18 @@ public class VolumeDialogControllerImpl implements VolumeDialogController, Dumpa } @Override + public void onVolumeChangedFromKey() { + for (final Map.Entry<Callbacks, Handler> entry : mCallbackMap.entrySet()) { + entry.getValue().post(new Runnable() { + @Override + public void run() { + entry.getKey().onVolumeChangedFromKey(); + } + }); + } + } + + @Override public void onAccessibilityModeChanged(Boolean showA11yStream) { boolean show = showA11yStream != null && showA11yStream; for (final Map.Entry<Callbacks, Handler> entry : mCallbackMap.entrySet()) { diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java index 404621d1fe81..ce6d74092197 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java +++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java @@ -34,6 +34,7 @@ import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; import static com.android.internal.jank.InteractionJankMonitor.CUJ_VOLUME_CONTROL; import static com.android.internal.jank.InteractionJankMonitor.Configuration.Builder; +import static com.android.systemui.Flags.hapticVolumeSlider; import static com.android.systemui.volume.Events.DISMISS_REASON_POSTURE_CHANGED; import static com.android.systemui.volume.Events.DISMISS_REASON_SETTINGS_CLICKED; @@ -49,7 +50,6 @@ import android.app.KeyguardManager; import android.content.ContentResolver; import android.content.Context; import android.content.DialogInterface; -import android.content.Intent; import android.content.pm.PackageManager; import android.content.res.ColorStateList; import android.content.res.Configuration; @@ -76,7 +76,6 @@ import android.os.VibrationEffect; import android.provider.Settings; import android.provider.Settings.Global; import android.text.InputFilter; -import android.util.FeatureFlagUtils; import android.util.Log; import android.util.Slog; import android.util.SparseBooleanArray; @@ -117,14 +116,18 @@ import com.android.internal.view.RotationPolicy; import com.android.settingslib.Utils; import com.android.systemui.Dumpable; import com.android.systemui.Prefs; +import com.android.systemui.dagger.qualifiers.Application; +import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; +import com.android.systemui.haptics.slider.SeekableSliderHapticPlugin; +import com.android.systemui.haptics.slider.SliderHapticFeedbackConfig; import com.android.systemui.media.dialog.MediaOutputDialogFactory; -import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.VolumeDialog; import com.android.systemui.plugins.VolumeDialogController; import com.android.systemui.plugins.VolumeDialogController.State; import com.android.systemui.plugins.VolumeDialogController.StreamState; import com.android.systemui.res.R; +import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.DevicePostureController; @@ -132,6 +135,8 @@ import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.util.AlphaTintDrawableWrapper; import com.android.systemui.util.RoundedCornerProgressDrawable; import com.android.systemui.util.settings.SecureSettings; +import com.android.systemui.volume.domain.interactor.VolumePanelNavigationInteractor; +import com.android.systemui.volume.ui.navigation.VolumeNavigator; import dagger.Lazy; @@ -140,6 +145,9 @@ import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; +import kotlinx.coroutines.CoroutineDispatcher; +import kotlinx.coroutines.CoroutineScope; + /** * Visual presentation of the volume dialog. * @@ -262,9 +270,9 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, private final Accessibility mAccessibility = new Accessibility(); private final ConfigurationController mConfigurationController; private final MediaOutputDialogFactory mMediaOutputDialogFactory; - private final VolumePanelFactory mVolumePanelFactory; private final CsdWarningDialog.Factory mCsdWarningDialogFactory; - private final ActivityStarter mActivityStarter; + private final VolumePanelNavigationInteractor mVolumePanelNavigationInteractor; + private final VolumeNavigator mVolumeNavigator; private boolean mShowing; private boolean mShowA11yStream; private int mActiveStream; @@ -303,6 +311,10 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, private int mOrientation; private final Lazy<SecureSettings> mSecureSettings; private int mDialogTimeoutMillis; + private final CoroutineDispatcher mMainDispatcher; + private final CoroutineScope mApplicationScope; + private final VibratorHelper mVibratorHelper; + private final com.android.systemui.util.time.SystemClock mSystemClock; public VolumeDialogImpl( Context context, @@ -311,19 +323,26 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, DeviceProvisionedController deviceProvisionedController, ConfigurationController configurationController, MediaOutputDialogFactory mediaOutputDialogFactory, - VolumePanelFactory volumePanelFactory, - ActivityStarter activityStarter, InteractionJankMonitor interactionJankMonitor, + VolumePanelNavigationInteractor volumePanelNavigationInteractor, + VolumeNavigator volumeNavigator, boolean shouldListenForJank, CsdWarningDialog.Factory csdWarningDialogFactory, DevicePostureController devicePostureController, Looper looper, DumpManager dumpManager, - Lazy<SecureSettings> secureSettings) { + Lazy<SecureSettings> secureSettings, + VibratorHelper vibratorHelper, + @Main CoroutineDispatcher mainDispatcher, + @Application CoroutineScope applicationScope, + com.android.systemui.util.time.SystemClock systemClock) { mContext = new ContextThemeWrapper(context, R.style.volume_dialog_theme); mHandler = new H(looper); - + mMainDispatcher = mainDispatcher; + mApplicationScope = applicationScope; + mVibratorHelper = vibratorHelper; + mSystemClock = systemClock; mShouldListenForJank = shouldListenForJank; mController = volumeDialogController; mKeyguard = (KeyguardManager) mContext.getSystemService(Context.KEYGUARD_SERVICE); @@ -332,9 +351,7 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, mDeviceProvisionedController = deviceProvisionedController; mConfigurationController = configurationController; mMediaOutputDialogFactory = mediaOutputDialogFactory; - mVolumePanelFactory = volumePanelFactory; mCsdWarningDialogFactory = csdWarningDialogFactory; - mActivityStarter = activityStarter; mShowActiveStreamOnly = showActiveStreamOnly(); mHasSeenODICaptionsTooltip = Prefs.getBoolean(context, Prefs.Key.HAS_SEEN_ODI_CAPTIONS_TOOLTIP, false); @@ -349,6 +366,8 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, mUseBackgroundBlur = mContext.getResources().getBoolean(R.bool.config_volumeDialogUseBackgroundBlur); mInteractionJankMonitor = interactionJankMonitor; + mVolumePanelNavigationInteractor = volumePanelNavigationInteractor; + mVolumeNavigator = volumeNavigator; mSecureSettings = secureSettings; mDialogTimeoutMillis = DIALOG_TIMEOUT_MILLIS; @@ -839,6 +858,7 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, row.header.setFilters(new InputFilter[] {new InputFilter.LengthFilter(13)}); } row.slider = row.view.findViewById(R.id.volume_row_slider); + row.createPlugin(mVibratorHelper, mSystemClock, mMainDispatcher, mApplicationScope); row.slider.setOnSeekBarChangeListener(new VolumeSeekBarChangeListener(row)); row.number = row.view.findViewById(R.id.volume_number); @@ -1187,13 +1207,8 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, Events.writeEvent(Events.EVENT_SETTINGS_CLICK); dismissH(DISMISS_REASON_SETTINGS_CLICKED); mMediaOutputDialogFactory.dismiss(); - if (FeatureFlagUtils.isEnabled(mContext, - FeatureFlagUtils.SETTINGS_VOLUME_PANEL_IN_SYSTEMUI)) { - mVolumePanelFactory.create(true /* aboveStatusBar */, null); - } else { - mActivityStarter.startActivity(new Intent(Settings.Panel.ACTION_VOLUME), - true /* dismissShade */); - } + mVolumeNavigator.openVolumePanel( + mVolumePanelNavigationInteractor.getVolumePanelRoute()); }); } } @@ -1480,6 +1495,12 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, mController.getCaptionsComponentState(false); checkODICaptionsTooltip(false); updateBackgroundForDrawerClosedAmount(); + for (int i = 0; i < mRows.size(); i++) { + VolumeRow row = mRows.get(i); + if (row.slider.getVisibility() == VISIBLE) { + row.addHaptics(); + } + } Trace.endSection(); } @@ -1532,7 +1553,9 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, protected void dismissH(int reason) { Trace.beginSection("VolumeDialogImpl#dismissH"); - + for (int i = 0; i < mRows.size(); i++) { + mRows.get(i).removeHaptics(); + } Log.i(TAG, "mDialog.dismiss() reason: " + Events.DISMISS_REASONS[reason] + " from: " + Debug.getCaller()); @@ -2358,6 +2381,14 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, public void onCaptionEnabledStateChanged(Boolean isEnabled, Boolean checkForSwitchState) { updateCaptionsEnabledH(isEnabled, checkForSwitchState); } + + @Override + public void onVolumeChangedFromKey() { + VolumeRow activeRow = getActiveRow(); + if (activeRow.mHapticPlugin != null) { + activeRow.mHapticPlugin.onKeyDown(); + } + } }; @VisibleForTesting void onPostureChanged(int posture) { @@ -2459,6 +2490,15 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { if (mRow.ss == null) return; + if (getActiveRow().equals(mRow) + && mRow.slider.getVisibility() == VISIBLE + && mRow.mHapticPlugin != null) { + mRow.mHapticPlugin.onProgressChanged(seekBar, progress, fromUser); + if (!fromUser) { + // Consider a change from program as the volume key being continuously pressed + mRow.mHapticPlugin.onKeyDown(); + } + } if (D.BUG) Log.d(TAG, AudioSystem.streamToString(mRow.stream) + " onProgressChanged " + progress + " fromUser=" + fromUser); if (!fromUser) return; @@ -2485,6 +2525,9 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, @Override public void onStartTrackingTouch(SeekBar seekBar) { if (D.BUG) Log.d(TAG, "onStartTrackingTouch"+ " " + mRow.stream); + if (mRow.mHapticPlugin != null) { + mRow.mHapticPlugin.onStartTrackingTouch(seekBar); + } mController.setActiveStream(mRow.stream); mRow.tracking = true; } @@ -2492,6 +2535,9 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, @Override public void onStopTrackingTouch(SeekBar seekBar) { if (D.BUG) Log.d(TAG, "onStopTrackingTouch"+ " " + mRow.stream); + if (mRow.mHapticPlugin != null) { + mRow.mHapticPlugin.onStopTrackingTouch(seekBar); + } mRow.tracking = false; mRow.userAttempt = SystemClock.uptimeMillis(); final int userLevel = getImpliedLevel(seekBar, seekBar.getProgress()); @@ -2524,6 +2570,22 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, } private static class VolumeRow { + private static final SliderHapticFeedbackConfig sSliderHapticFeedbackConfig = + new SliderHapticFeedbackConfig( + /* velocityInterpolatorFactor= */ 1f, + /* progressInterpolatorFactor= */ 1f, + /* progressBasedDragMinScale= */ 0f, + /* progressBasedDragMaxScale= */ 0.2f, + /* additionalVelocityMaxBump= */ 0.15f, + /* deltaMillisForDragInterval= */ 0f, + /* deltaProgressForDragThreshold= */ 0.015f, + /* numberOfLowTicks= */ 5, + /* maxVelocityToScale= */ 300f, + /* velocityAxis= */ MotionEvent.AXIS_Y, + /* upperBookendScale= */ 1f, + /* lowerBookendScale= */ 0.05f, + /* exponent= */ 1f / 0.89f); + private View view; private TextView header; private ImageButton icon; @@ -2544,6 +2606,7 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, private ObjectAnimator anim; // slider progress animation for non-touch-related updates private int animTargetProgress; private int lastAudibleLevel = 1; + private SeekableSliderHapticPlugin mHapticPlugin; void setIcon(int iconRes, Resources.Theme theme) { if (icon != null) { @@ -2554,6 +2617,50 @@ public class VolumeDialogImpl implements VolumeDialog, Dumpable, sliderProgressIcon.setDrawable(view.getResources().getDrawable(iconRes, theme)); } } + + void createPlugin( + VibratorHelper vibratorHelper, + com.android.systemui.util.time.SystemClock systemClock, + CoroutineDispatcher mainDispatcher, + CoroutineScope applicationScope) { + if (!hapticVolumeSlider() || mHapticPlugin != null) return; + + mHapticPlugin = new SeekableSliderHapticPlugin( + vibratorHelper, + systemClock, + mainDispatcher, + applicationScope, + sSliderHapticFeedbackConfig); + } + + + @SuppressLint("ClickableViewAccessibility") + void addTouchListener() { + slider.setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View view, MotionEvent motionEvent) { + if (mHapticPlugin != null) { + mHapticPlugin.onTouchEvent(motionEvent); + } + return false; + } + }); + } + + void addHaptics() { + if (mHapticPlugin != null) { + addTouchListener(); + mHapticPlugin.start(); + } + } + + @SuppressLint("ClickableViewAccessibility") + void removeHaptics() { + slider.setOnTouchListener(null); + if (mHapticPlugin != null) { + mHapticPlugin.stop(); + } + } } /** diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java index 497c4cb070f0..b1bfbe0016e1 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java +++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java @@ -16,30 +16,36 @@ package com.android.systemui.volume.dagger; +import android.app.Activity; import android.content.Context; import android.media.AudioManager; import android.os.Looper; import com.android.internal.jank.InteractionJankMonitor; import com.android.systemui.CoreStartable; +import com.android.systemui.dagger.qualifiers.Application; +import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; import com.android.systemui.media.dialog.MediaOutputDialogFactory; -import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.VolumeDialog; import com.android.systemui.plugins.VolumeDialogController; +import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.DevicePostureController; import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.util.settings.SecureSettings; +import com.android.systemui.util.time.SystemClock; import com.android.systemui.volume.CsdWarningDialog; import com.android.systemui.volume.VolumeComponent; import com.android.systemui.volume.VolumeDialogComponent; import com.android.systemui.volume.VolumeDialogImpl; -import com.android.systemui.volume.VolumePanelFactory; import com.android.systemui.volume.VolumeUI; +import com.android.systemui.volume.domain.interactor.VolumePanelNavigationInteractor; import com.android.systemui.volume.panel.dagger.VolumePanelComponent; import com.android.systemui.volume.panel.dagger.factory.VolumePanelComponentFactory; +import com.android.systemui.volume.panel.ui.activity.VolumePanelActivity; +import com.android.systemui.volume.ui.navigation.VolumeNavigator; import dagger.Binds; import dagger.Lazy; @@ -49,6 +55,9 @@ import dagger.multibindings.ClassKey; import dagger.multibindings.IntoMap; import dagger.multibindings.IntoSet; +import kotlinx.coroutines.CoroutineDispatcher; +import kotlinx.coroutines.CoroutineScope; + /** Dagger Module for code in the volume package. */ @Module( subcomponents = { @@ -71,6 +80,12 @@ public interface VolumeModule { @Binds VolumeComponent provideVolumeComponent(VolumeDialogComponent volumeDialogComponent); + /** Inject into VolumePanelActivity. */ + @Binds + @IntoMap + @ClassKey(VolumePanelActivity.class) + Activity bindVolumePanelActivity(VolumePanelActivity activity); + /** */ @Binds VolumePanelComponentFactory bindVolumePanelComponentFactory(VolumePanelComponent.Factory impl); @@ -84,13 +99,17 @@ public interface VolumeModule { DeviceProvisionedController deviceProvisionedController, ConfigurationController configurationController, MediaOutputDialogFactory mediaOutputDialogFactory, - VolumePanelFactory volumePanelFactory, - ActivityStarter activityStarter, InteractionJankMonitor interactionJankMonitor, + VolumePanelNavigationInteractor volumePanelNavigationInteractor, + VolumeNavigator volumeNavigator, CsdWarningDialog.Factory csdFactory, DevicePostureController devicePostureController, DumpManager dumpManager, - Lazy<SecureSettings> secureSettings) { + Lazy<SecureSettings> secureSettings, + VibratorHelper vibratorHelper, + @Main CoroutineDispatcher mainDispatcher, + @Application CoroutineScope applicationScope, + SystemClock systemClock) { VolumeDialogImpl impl = new VolumeDialogImpl( context, volumeDialogController, @@ -98,15 +117,19 @@ public interface VolumeModule { deviceProvisionedController, configurationController, mediaOutputDialogFactory, - volumePanelFactory, - activityStarter, interactionJankMonitor, + volumePanelNavigationInteractor, + volumeNavigator, true, /* should listen for jank */ csdFactory, devicePostureController, Looper.getMainLooper(), dumpManager, - secureSettings); + secureSettings, + vibratorHelper, + mainDispatcher, + applicationScope, + systemClock); impl.setStreamImportant(AudioManager.STREAM_SYSTEM, false); impl.setAutomute(true); impl.setSilentMode(false); diff --git a/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/VolumePanelNavigationInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/VolumePanelNavigationInteractor.kt new file mode 100644 index 000000000000..d64bb03f04ed --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/VolumePanelNavigationInteractor.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.domain.interactor + +import android.content.Context +import android.util.FeatureFlagUtils +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.volume.domain.model.VolumePanelRoute +import com.android.systemui.volume.panel.shared.flag.VolumePanelFlag +import javax.inject.Inject + +/** Provides navigation routes for Volume space. */ +class VolumePanelNavigationInteractor +@Inject +constructor( + @Application private val context: Context, + private val volumePanelFlag: VolumePanelFlag, +) { + + fun getVolumePanelRoute(): VolumePanelRoute { + return when { + volumePanelFlag.canUseNewVolumePanel() -> VolumePanelRoute.COMPOSE_VOLUME_PANEL + FeatureFlagUtils.isEnabled( + context, + FeatureFlagUtils.SETTINGS_VOLUME_PANEL_IN_SYSTEMUI + ) -> VolumePanelRoute.SYSTEM_UI_VOLUME_PANEL + else -> VolumePanelRoute.SETTINGS_VOLUME_PANEL + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/domain/model/VolumePanelRoute.kt b/packages/SystemUI/src/com/android/systemui/volume/domain/model/VolumePanelRoute.kt new file mode 100644 index 000000000000..c85af152137b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/domain/model/VolumePanelRoute.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.domain.model + +enum class VolumePanelRoute { + COMPOSE_VOLUME_PANEL, + SETTINGS_VOLUME_PANEL, + SYSTEM_UI_VOLUME_PANEL, +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModel.kt new file mode 100644 index 000000000000..8ff2837c44ef --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/bottombar/ui/viewmodel/BottomBarViewModel.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.panel.component.bottombar.ui.viewmodel + +import android.content.Intent +import android.provider.Settings +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope +import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelViewModel +import javax.inject.Inject + +@VolumePanelScope +class BottomBarViewModel +@Inject +constructor( + private val activityStarter: ActivityStarter, + private val volumePanelViewModel: VolumePanelViewModel, +) { + + fun onDoneClicked() { + volumePanelViewModel.dismissPanel() + } + + fun onSettingsClicked() { + volumePanelViewModel.dismissPanel() + activityStarter.startActivity( + Intent(Settings.ACTION_SOUND_SETTINGS).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK), + true, + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/shared/model/VolumePanelComponents.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/shared/model/VolumePanelComponents.kt new file mode 100644 index 000000000000..1a4174afac65 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/shared/model/VolumePanelComponents.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.panel.component.shared.model + +import com.android.systemui.volume.panel.shared.model.VolumePanelComponentKey + +object VolumePanelComponents { + + const val BOTTOM_BAR: VolumePanelComponentKey = "bottom_bar" +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/dagger/DefaultMultibindsModule.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/dagger/DefaultMultibindsModule.kt index 3660ac166b12..d1d539003f93 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/dagger/DefaultMultibindsModule.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/dagger/DefaultMultibindsModule.kt @@ -16,8 +16,9 @@ package com.android.systemui.volume.panel.dagger -import com.android.systemui.volume.panel.VolumePanelComponentKey import com.android.systemui.volume.panel.domain.ComponentAvailabilityCriteria +import com.android.systemui.volume.panel.shared.model.VolumePanelComponentKey +import com.android.systemui.volume.panel.shared.model.VolumePanelUiComponent import dagger.Module import dagger.multibindings.Multibinds @@ -28,4 +29,6 @@ import dagger.multibindings.Multibinds interface DefaultMultibindsModule { @Multibinds fun criteriaMap(): Map<VolumePanelComponentKey, ComponentAvailabilityCriteria> + + @Multibinds fun components(): Map<VolumePanelComponentKey, VolumePanelUiComponent> } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/dagger/VolumePanelComponent.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/dagger/VolumePanelComponent.kt index 0a057eb5c927..0f19e9f2317b 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/dagger/VolumePanelComponent.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/dagger/VolumePanelComponent.kt @@ -16,12 +16,14 @@ package com.android.systemui.volume.panel.dagger +import com.android.systemui.volume.panel.component.bottombar.BottomBarModule import com.android.systemui.volume.panel.dagger.factory.VolumePanelComponentFactory import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope import com.android.systemui.volume.panel.domain.DomainModule import com.android.systemui.volume.panel.domain.interactor.ComponentsInteractor import com.android.systemui.volume.panel.ui.UiModule -import com.android.systemui.volume.panel.ui.viewmodel.ComponentsLayoutManager +import com.android.systemui.volume.panel.ui.composable.ComponentsFactory +import com.android.systemui.volume.panel.ui.layout.ComponentsLayoutManager import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelViewModel import dagger.BindsInstance import dagger.Subcomponent @@ -41,6 +43,7 @@ import kotlinx.coroutines.CoroutineScope DomainModule::class, UiModule::class, // Components modules + BottomBarModule::class, ] ) interface VolumePanelComponent { @@ -49,6 +52,8 @@ interface VolumePanelComponent { fun componentsInteractor(): ComponentsInteractor + fun componentsFactory(): ComponentsFactory + fun componentsLayoutManager(): ComponentsLayoutManager @Subcomponent.Factory diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/domain/DomainModule.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/domain/DomainModule.kt index 7817630d09d2..f785eb7e8e38 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/domain/DomainModule.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/domain/DomainModule.kt @@ -16,10 +16,11 @@ package com.android.systemui.volume.panel.domain -import com.android.systemui.volume.panel.VolumePanelComponentKey +import com.android.systemui.volume.panel.component.shared.model.VolumePanelComponents import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope import com.android.systemui.volume.panel.domain.interactor.ComponentsInteractor import com.android.systemui.volume.panel.domain.interactor.ComponentsInteractorImpl +import com.android.systemui.volume.panel.shared.model.VolumePanelComponentKey import dagger.Binds import dagger.Module import dagger.Provides @@ -47,6 +48,10 @@ interface DomainModule { */ @Provides @VolumePanelScope - fun provideEnabledComponents(): Collection<VolumePanelComponentKey> = setOf() + fun provideEnabledComponents(): Collection<VolumePanelComponentKey> { + return setOf( + VolumePanelComponents.BOTTOM_BAR, + ) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractor.kt index e5b52ea5eb88..5301b008bab7 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractor.kt @@ -16,10 +16,10 @@ package com.android.systemui.volume.panel.domain.interactor -import com.android.systemui.volume.panel.VolumePanelComponentKey import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope import com.android.systemui.volume.panel.domain.ComponentAvailabilityCriteria import com.android.systemui.volume.panel.domain.model.ComponentModel +import com.android.systemui.volume.panel.shared.model.VolumePanelComponentKey import javax.inject.Inject import javax.inject.Provider import kotlinx.coroutines.CoroutineScope diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/domain/model/ComponentModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/domain/model/ComponentModel.kt index 9765713f0a50..11a9916982ed 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/domain/model/ComponentModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/domain/model/ComponentModel.kt @@ -16,7 +16,7 @@ package com.android.systemui.volume.panel.domain.model -import com.android.systemui.volume.panel.VolumePanelComponentKey +import com.android.systemui.volume.panel.shared.model.VolumePanelComponentKey /** * Represents a current state of the Volume Panel component. diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/shared/flag/VolumePanelFlag.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/shared/flag/VolumePanelFlag.kt new file mode 100644 index 000000000000..d90a9c75deec --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/shared/flag/VolumePanelFlag.kt @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.panel.shared.flag + +import com.android.systemui.Flags +import com.android.systemui.compose.ComposeFacade +import com.android.systemui.flags.RefactorFlagUtils +import javax.inject.Inject + +/** Provides a flag to check for the new Compose based Volume Panel availability. */ +class VolumePanelFlag @Inject constructor() { + + /** + * Returns true when the new Volume Panel is available and false the otherwise. The new panel + * can only be available when [ComposeFacade.isComposeAvailable] is true. + */ + fun canUseNewVolumePanel(): Boolean { + return ComposeFacade.isComposeAvailable() && Flags.newVolumePanel() + } + + fun assertNewVolumePanel() { + require(ComposeFacade.isComposeAvailable()) + RefactorFlagUtils.assertInNewMode(Flags.newVolumePanel(), Flags.FLAG_NEW_VOLUME_PANEL) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/shared/model/VolumePanelComponentKey.kt index 22a74d27ba1f..4644ee7a3b6e 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/VolumePanelComponentKey.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/shared/model/VolumePanelComponentKey.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.volume.panel +package com.android.systemui.volume.panel.shared.model /** Uniquely identifies the [com.android.systemui.volume.panel.ui.VolumePanelComponent]. */ typealias VolumePanelComponentKey = String diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/shared/model/VolumePanelUiComponent.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/shared/model/VolumePanelUiComponent.kt new file mode 100644 index 000000000000..24de41f9be87 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/shared/model/VolumePanelUiComponent.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.panel.shared.model + +/** + * An element of a Volume Panel. This can be a button bar, group of sliders or something else. The + * only real implementation is Compose-based and located in `compose/features/`. + * + * Steps for adding an implementation in SystemUI: + * 1) Implement `ComposeVolumePanelUiComponent` in `compose/features/` + * 2) Add a module binding `ComposeVolumePanelUiComponent` into a map in compose/facade/enabled + * 3) Add an interface with the same name as the 2-step module in compose/facade/disabled to stub it + * when the Compose is disabled + * 4) Add the module to the VolumePanelComponent + */ +interface VolumePanelUiComponent diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/UiModule.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/UiModule.kt index bfa7ef2e42c2..1346c54cb4ac 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/UiModule.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/UiModule.kt @@ -16,8 +16,8 @@ package com.android.systemui.volume.panel.ui -import com.android.systemui.volume.panel.ui.viewmodel.ComponentsLayoutManager -import com.android.systemui.volume.panel.ui.viewmodel.DefaultComponentsLayoutManager +import com.android.systemui.volume.panel.ui.layout.ComponentsLayoutManager +import com.android.systemui.volume.panel.ui.layout.DefaultComponentsLayoutManager import dagger.Binds import dagger.Module @@ -25,5 +25,6 @@ import dagger.Module @Module interface UiModule { - @Binds fun bindSorter(impl: DefaultComponentsLayoutManager): ComponentsLayoutManager + @Binds + fun bindComponentsLayoutManager(impl: DefaultComponentsLayoutManager): ComponentsLayoutManager } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/activity/VolumePanelActivity.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/activity/VolumePanelActivity.kt new file mode 100644 index 000000000000..1b2265b9891e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/activity/VolumePanelActivity.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.panel.ui.activity + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.enableEdgeToEdge +import androidx.activity.viewModels +import androidx.core.view.WindowCompat +import com.android.systemui.compose.ComposeFacade +import com.android.systemui.volume.panel.shared.flag.VolumePanelFlag +import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelViewModel +import javax.inject.Inject +import javax.inject.Provider + +class VolumePanelActivity +@Inject +constructor( + private val volumePanelViewModelFactory: Provider<VolumePanelViewModel.Factory>, + private val volumePanelFlag: VolumePanelFlag, +) : ComponentActivity() { + + private val viewModel: VolumePanelViewModel by + viewModels(factoryProducer = { volumePanelViewModelFactory.get() }) + + override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() + super.onCreate(savedInstanceState) + + volumePanelFlag.assertNewVolumePanel() + + WindowCompat.setDecorFitsSystemWindows(window, false) + ComposeFacade.setVolumePanelActivityContent(this, viewModel) { finish() } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/composable/ComponentsFactory.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/composable/ComponentsFactory.kt new file mode 100644 index 000000000000..db1c12125056 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/composable/ComponentsFactory.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.panel.ui.composable + +import com.android.systemui.volume.panel.dagger.VolumePanelComponent +import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope +import com.android.systemui.volume.panel.shared.model.VolumePanelComponentKey +import com.android.systemui.volume.panel.shared.model.VolumePanelUiComponent +import javax.inject.Inject +import javax.inject.Provider + +/** Provides [VolumePanelComponent] implementation for each [VolumePanelComponentKey]. */ +@VolumePanelScope +class ComponentsFactory +@Inject +constructor( + private val componentByKey: + Map< + VolumePanelComponentKey, + @JvmSuppressWildcards + Provider<@JvmSuppressWildcards VolumePanelUiComponent> + > +) { + + fun createComponent(key: VolumePanelComponentKey): VolumePanelUiComponent { + require(componentByKey.containsKey(key)) { "Component for key=$key is not bound." } + return componentByKey.getValue(key).get() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/model/ComponentsLayout.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/layout/ComponentsLayout.kt index 5690ac31718f..25a95d88702a 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/model/ComponentsLayout.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/layout/ComponentsLayout.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,9 @@ * limitations under the License. */ -package com.android.systemui.volume.panel.ui.model +package com.android.systemui.volume.panel.ui.layout + +import com.android.systemui.volume.panel.ui.viewmodel.ComponentState /** Represents components grouping into the layout. */ data class ComponentsLayout( diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/ComponentsLayoutManager.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/layout/ComponentsLayoutManager.kt index f45401a0dd0a..71ca95c21bb6 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/ComponentsLayoutManager.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/layout/ComponentsLayoutManager.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,11 +14,10 @@ * limitations under the License. */ -package com.android.systemui.volume.panel.ui.viewmodel +package com.android.systemui.volume.panel.ui.layout -import com.android.systemui.volume.panel.ui.model.ComponentState -import com.android.systemui.volume.panel.ui.model.ComponentsLayout -import com.android.systemui.volume.panel.ui.model.VolumePanelState +import com.android.systemui.volume.panel.ui.viewmodel.ComponentState +import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelState /** * Lays out components to [ComponentsLayout], that UI uses to render the Volume Panel. diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/DefaultComponentsLayoutManager.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/layout/DefaultComponentsLayoutManager.kt index cedfaf36d300..ff485c29c1ff 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/DefaultComponentsLayoutManager.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/layout/DefaultComponentsLayoutManager.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2024 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,24 +14,29 @@ * limitations under the License. */ -package com.android.systemui.volume.panel.ui.viewmodel +package com.android.systemui.volume.panel.ui.layout +import com.android.systemui.volume.panel.component.shared.model.VolumePanelComponents import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope -import com.android.systemui.volume.panel.ui.model.ComponentState -import com.android.systemui.volume.panel.ui.model.ComponentsLayout -import com.android.systemui.volume.panel.ui.model.VolumePanelState +import com.android.systemui.volume.panel.ui.viewmodel.ComponentState +import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelState import javax.inject.Inject -/** - * Default [ComponentsLayoutManager]. It places [VolumePanelComponents.BOTTOM_BAR] to - * [ComponentsLayout.bottomBarComponent] and everything else to - * [ComponentsLayout.contentComponents]. - */ @VolumePanelScope class DefaultComponentsLayoutManager @Inject constructor() : ComponentsLayoutManager { override fun layout( volumePanelState: VolumePanelState, components: Collection<ComponentState> - ): ComponentsLayout = TODO("Unimplemented yet") + ): ComponentsLayout { + val bottomBarKey = VolumePanelComponents.BOTTOM_BAR + return ComponentsLayout( + components.filter { it.key != bottomBarKey }.sortedBy { it.key }, + components.find { it.key == bottomBarKey } + ?: error( + "VolumePanelComponents.BOTTOM_BAR must be present in the default " + + "components layout." + ) + ) + } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/model/ComponentState.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/ComponentState.kt index 0a226e28da19..5f4dbfb4235e 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/model/ComponentState.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/ComponentState.kt @@ -14,12 +14,14 @@ * limitations under the License. */ -package com.android.systemui.volume.panel.ui.model +package com.android.systemui.volume.panel.ui.viewmodel -import com.android.systemui.volume.panel.VolumePanelComponentKey +import com.android.systemui.volume.panel.shared.model.VolumePanelComponentKey +import com.android.systemui.volume.panel.shared.model.VolumePanelUiComponent /** - * State of the [VolumePanelComponent]. + * State of the [VolumePanelComponent]. It has everything the UI layer needs to layout a particular + * component. * * @property key uniquely identifies this component * @property component is an inflated component obtained be the View Model @@ -27,5 +29,6 @@ import com.android.systemui.volume.panel.VolumePanelComponentKey */ data class ComponentState( val key: VolumePanelComponentKey, + val component: VolumePanelUiComponent, val isVisible: Boolean, ) diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/model/VolumePanelState.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelState.kt index 399342f749a6..f67db965e28a 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/model/VolumePanelState.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelState.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.volume.panel.ui.model +package com.android.systemui.volume.panel.ui.viewmodel import android.content.res.Configuration import android.content.res.Configuration.Orientation diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModel.kt index dda361abb378..d87a79ed90f4 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModel.kt @@ -26,9 +26,9 @@ import com.android.systemui.statusbar.policy.onConfigChanged import com.android.systemui.volume.panel.dagger.VolumePanelComponent import com.android.systemui.volume.panel.dagger.factory.VolumePanelComponentFactory import com.android.systemui.volume.panel.domain.interactor.ComponentsInteractor -import com.android.systemui.volume.panel.ui.model.ComponentState -import com.android.systemui.volume.panel.ui.model.ComponentsLayout -import com.android.systemui.volume.panel.ui.model.VolumePanelState +import com.android.systemui.volume.panel.ui.composable.ComponentsFactory +import com.android.systemui.volume.panel.ui.layout.ComponentsLayout +import com.android.systemui.volume.panel.ui.layout.ComponentsLayoutManager import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.cancel @@ -38,9 +38,10 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.launch +import kotlinx.coroutines.flow.update class VolumePanelViewModel( resources: Resources, @@ -56,6 +57,9 @@ class VolumePanelViewModel( private val componentsInteractor: ComponentsInteractor get() = volumePanelComponent.componentsInteractor() + private val componentsFactory: ComponentsFactory + get() = volumePanelComponent.componentsFactory() + private val componentsLayoutManager: ComponentsLayoutManager get() = volumePanelComponent.componentsLayoutManager() @@ -63,20 +67,22 @@ class VolumePanelViewModel( val volumePanelState: StateFlow<VolumePanelState> = combine( - configurationController.onConfigChanged.distinctUntilChanged(), + configurationController.onConfigChanged + .onStart { emit(resources.configuration) } + .distinctUntilChanged(), mutablePanelVisibility, ) { configuration, isVisible -> VolumePanelState(orientation = configuration.orientation, isVisible = isVisible) } .stateIn( - volumePanelComponent.coroutineScope(), + scope, SharingStarted.Eagerly, VolumePanelState( orientation = resources.configuration.orientation, isVisible = mutablePanelVisibility.value, ), ) - val mComponentsLayout: Flow<ComponentsLayout> = + val componentsLayout: Flow<ComponentsLayout> = combine( componentsInteractor.components, volumePanelState, @@ -85,19 +91,20 @@ class VolumePanelViewModel( components.map { model -> ComponentState( model.key, + componentsFactory.createComponent(model.key), model.isAvailable, ) } componentsLayoutManager.layout(scope, componentStates) } .shareIn( - volumePanelComponent.coroutineScope(), + scope, SharingStarted.Eagerly, replay = 1, ) fun dismissPanel() { - scope.launch { mutablePanelVisibility.emit(false) } + mutablePanelVisibility.update { false } } override fun onCleared() { diff --git a/packages/SystemUI/src/com/android/systemui/volume/ui/navigation/VolumeNavigator.kt b/packages/SystemUI/src/com/android/systemui/volume/ui/navigation/VolumeNavigator.kt new file mode 100644 index 000000000000..790638c09790 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/ui/navigation/VolumeNavigator.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.volume.ui.navigation + +import android.content.Context +import android.content.Intent +import android.provider.Settings +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.plugins.ActivityStarter +import com.android.systemui.volume.VolumePanelFactory +import com.android.systemui.volume.domain.model.VolumePanelRoute +import com.android.systemui.volume.panel.ui.activity.VolumePanelActivity +import javax.inject.Inject + +class VolumeNavigator +@Inject +constructor( + @Application private val context: Context, + private val volumePanelFactory: VolumePanelFactory, + private val activityStarter: ActivityStarter, +) { + + fun openVolumePanel(route: VolumePanelRoute) { + when (route) { + VolumePanelRoute.COMPOSE_VOLUME_PANEL -> + activityStarter.startActivityDismissingKeyguard( + /* intent = */ Intent(context, VolumePanelActivity::class.java), + /* onlyProvisioned = */ false, + /* dismissShade= */ true, + /* disallowEnterPictureInPictureWhileLaunching = */ true, + /* callback= */ null, + /* flags= */ 0, + /* animationController= */ null, + /* userHandle= */ null, + ) + VolumePanelRoute.SETTINGS_VOLUME_PANEL -> + activityStarter.startActivity( + /* intent= */ Intent(Settings.Panel.ACTION_VOLUME), + /* dismissShade= */ true + ) + VolumePanelRoute.SYSTEM_UI_VOLUME_PANEL -> + volumePanelFactory.create(aboveStatusBar = true, view = null) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java index 1e801aeb5a29..7c6ad233d853 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java @@ -32,6 +32,8 @@ import android.content.pm.UserInfo; import android.content.res.Configuration; import android.graphics.Rect; import android.inputmethodservice.InputMethodService; +import android.os.HandlerExecutor; +import android.os.HandlerThread; import android.os.IBinder; import android.util.Log; import android.view.Display; @@ -125,6 +127,7 @@ public final class WMShell implements private final DisplayTracker mDisplayTracker; private final NoteTaskInitializer mNoteTaskInitializer; private final Executor mSysUiMainExecutor; + private HandlerThread mHandlerThread; // Listeners and callbacks. Note that we prefer member variable over anonymous class here to // avoid the situation that some implementations, like KeyguardUpdateMonitor, use WeakReference @@ -206,6 +209,8 @@ public final class WMShell implements mDisplayTracker = displayTracker; mNoteTaskInitializer = noteTaskInitializer; mSysUiMainExecutor = sysUiMainExecutor; + mHandlerThread = new HandlerThread("WMShell"); + mHandlerThread.start(); } @Override @@ -219,7 +224,8 @@ public final class WMShell implements mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateMonitorCallback); // Subscribe to user changes - mUserTracker.addCallback(mUserChangedCallback, mContext.getMainExecutor()); + mUserTracker.addCallback(mUserChangedCallback, + new HandlerExecutor(mHandlerThread.getThreadHandler())); mCommandQueue.addCallback(this); mPipOptional.ifPresent(this::initPip); diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java index d8eb05a43e40..be06cc5d3d1d 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardUpdateMonitorTest.java @@ -121,7 +121,6 @@ import com.android.internal.widget.LockPatternUtils; import com.android.keyguard.KeyguardUpdateMonitor.BiometricAuthenticated; import com.android.keyguard.logging.KeyguardUpdateMonitorLogger; import com.android.settingslib.fuelgauge.BatteryStatus; -import com.android.systemui.Flags; import com.android.systemui.SysuiTestCase; import com.android.systemui.biometrics.AuthController; import com.android.systemui.biometrics.FingerprintInteractiveToAuthProvider; @@ -930,7 +929,8 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Test public void trustAgentHasTrust() { // WHEN user has trust - givenSelectedUserCanSkipBouncerFromTrustedState(); + mKeyguardUpdateMonitor.onTrustChanged(true, true, + mSelectedUserInteractor.getSelectedUserId(), 0, null); // THEN user is considered as "having trust" and bouncer can be skipped Assert.assertTrue(mKeyguardUpdateMonitor.getUserHasTrust( @@ -954,7 +954,8 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { @Test public void trustAgentHasTrust_fingerprintLockout() { // GIVEN user has trust - givenSelectedUserCanSkipBouncerFromTrustedState(); + mKeyguardUpdateMonitor.onTrustChanged(true, true, + mSelectedUserInteractor.getSelectedUserId(), 0, null); Assert.assertTrue(mKeyguardUpdateMonitor.getUserHasTrust( mSelectedUserInteractor.getSelectedUserId())); @@ -1720,6 +1721,24 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { } @Test + public void assistantVisible_sendEventToFaceAuthInteractor() { + // WHEN the assistant is visible + mKeyguardUpdateMonitor.setAssistantVisible(true); + + // THEN send event to face auth interactor + verify(mFaceAuthInteractor).onAssistantTriggeredOnLockScreen(); + } + + @Test + public void assistantNotVisible_doesNotSendEventToFaceAuthInteractor() { + // WHEN the assistant is visible + mKeyguardUpdateMonitor.setAssistantVisible(false); + + // THEN never send event to face auth interactor + verify(mFaceAuthInteractor, never()).onAssistantTriggeredOnLockScreen(); + } + + @Test public void fingerprintFailure_requestActiveUnlock_dismissKeyguard() { // GIVEN shouldTriggerActiveUnlock bouncerFullyVisible(); @@ -1997,43 +2016,6 @@ public class KeyguardUpdateMonitorTest extends SysuiTestCase { } @Test - public void runFpDetectFlagDisabled_sideFps_keyguardDismissible_fingerprintAuthenticateRuns() { - mSetFlagsRule.disableFlags(Flags.FLAG_RUN_FINGERPRINT_DETECT_ON_DISMISSIBLE_KEYGUARD); - - // Clear invocations, since previous setup (e.g. registering BiometricManager callbacks) - // will trigger updateBiometricListeningState(); - clearInvocations(mFingerprintManager); - mKeyguardUpdateMonitor.resetBiometricListeningState(); - - // GIVEN the user can skip the bouncer - givenSelectedUserCanSkipBouncerFromTrustedState(); - when(mStrongAuthTracker.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true); - mKeyguardUpdateMonitor.dispatchStartedGoingToSleep(0 /* why */); - mTestableLooper.processAllMessages(); - - // WHEN verify authenticate runs - verifyFingerprintAuthenticateCall(); - } - - @Test - public void sideFps_keyguardDismissible_fingerprintDetectRuns() { - mSetFlagsRule.enableFlags(Flags.FLAG_RUN_FINGERPRINT_DETECT_ON_DISMISSIBLE_KEYGUARD); - // Clear invocations, since previous setup (e.g. registering BiometricManager callbacks) - // will trigger updateBiometricListeningState(); - clearInvocations(mFingerprintManager); - mKeyguardUpdateMonitor.resetBiometricListeningState(); - - // GIVEN the user can skip the bouncer - givenSelectedUserCanSkipBouncerFromTrustedState(); - when(mStrongAuthTracker.isUnlockingWithBiometricAllowed(anyBoolean())).thenReturn(true); - mKeyguardUpdateMonitor.dispatchStartedGoingToSleep(0 /* why */); - mTestableLooper.processAllMessages(); - - // WHEN verify detect runs - verifyFingerprintDetectCall(); - } - - @Test public void testFingerprintSensorProperties() throws RemoteException { mFingerprintAuthenticatorsRegisteredCallback.onAllAuthenticatorsRegistered( new ArrayList<>()); diff --git a/packages/SystemUI/tests/src/com/android/systemui/back/domain/interactor/BackActionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/back/domain/interactor/BackActionInteractorTest.kt index e0c6bbad5635..0ba9abe2d36c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/back/domain/interactor/BackActionInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/back/domain/interactor/BackActionInteractorTest.kt @@ -32,12 +32,16 @@ import com.android.internal.statusbar.IStatusBarService import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest import com.android.systemui.power.domain.interactor.PowerInteractorFactory import com.android.systemui.scene.data.repository.WindowRootViewVisibilityRepository import com.android.systemui.scene.domain.interactor.WindowRootViewVisibilityInteractor +import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.scene.shared.flag.sceneContainerFlags import com.android.systemui.scene.ui.view.WindowRootView import com.android.systemui.shade.QuickSettingsController import com.android.systemui.shade.ShadeController @@ -59,7 +63,6 @@ import junit.framework.Assert.assertFalse import junit.framework.Assert.assertTrue import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import org.junit.Before import org.junit.Rule @@ -76,7 +79,8 @@ import org.mockito.junit.MockitoJUnit @RunWith(AndroidJUnit4::class) @OptIn(ExperimentalCoroutinesApi::class) class BackActionInteractorTest : SysuiTestCase() { - private val testScope = TestScope() + private val kosmos = Kosmos() + private val testScope = kosmos.testScope private val executor = FakeExecutor(FakeSystemClock()) @JvmField @Rule var mockitoRule = MockitoJUnit.rule() @@ -105,6 +109,8 @@ class BackActionInteractorTest : SysuiTestCase() { headsUpManager, powerInteractor, activeNotificationsInteractor, + kosmos.sceneContainerFlags, + kosmos::sceneInteractor, ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt index 54dbd04fb367..3603c3c6c46a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinderTest.kt @@ -63,14 +63,20 @@ import com.android.systemui.keyguard.DismissCallbackRegistry import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository import com.android.systemui.keyguard.data.repository.FakeTrustRepository +import com.android.systemui.keyguard.data.repository.biometricSettingsRepository import com.android.systemui.keyguard.domain.interactor.DeviceEntrySideFpsOverlayInteractor +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor +import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.keyguard.ui.viewmodel.SideFpsProgressBarViewModel import com.android.systemui.log.SideFpsLogger import com.android.systemui.log.logcatLogBuffer import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.res.R import com.android.systemui.shared.Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR +import com.android.systemui.statusbar.phone.dozeServiceHost import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.testKosmos import com.android.systemui.unfold.compat.ScreenSizeFoldProvider import com.android.systemui.user.domain.interactor.SelectedUserInteractor import com.android.systemui.util.concurrency.FakeExecutor @@ -107,6 +113,8 @@ import org.mockito.junit.MockitoRule @RunWith(JUnit4::class) @TestableLooper.RunWithLooper(setAsMainLooper = true) class SideFpsOverlayViewBinderTest : SysuiTestCase() { + private val kosmos = testKosmos() + @JvmField @Rule var mockitoRule: MockitoRule = MockitoJUnit.rule() @Mock private lateinit var activityTaskManager: ActivityTaskManager @Mock private lateinit var displayManager: DisplayManager @@ -237,7 +245,8 @@ class SideFpsOverlayViewBinderTest : SysuiTestCase() { windowManager, displayStateInteractor, Optional.of(fingerprintInteractiveToAuthProvider), - mock(), + kosmos.biometricSettingsRepository, + kosmos.keyguardTransitionInteractor, SideFpsLogger(logcatLogBuffer("SfpsLogger")) ) @@ -246,10 +255,12 @@ class SideFpsOverlayViewBinderTest : SysuiTestCase() { mContext, mock(), sfpsSensorInteractor, - mock(), + kosmos.dozeServiceHost, + kosmos.keyguardInteractor, displayStateInteractor, UnconfinedTestDispatcher(), testScope.backgroundScope, + kosmos.powerInteractor, ) viewModel = diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt index 3888f2b940b3..6a9c88151dd0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt @@ -21,6 +21,7 @@ import android.content.res.Configuration import android.graphics.Bitmap import android.graphics.Point import android.graphics.drawable.BitmapDrawable +import android.hardware.biometrics.Flags.FLAG_CUSTOM_BIOMETRIC_PROMPT import android.hardware.biometrics.PromptContentItemBulletedText import android.hardware.biometrics.PromptContentView import android.hardware.biometrics.PromptInfo @@ -1225,6 +1226,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa @Test fun descriptionOverriddenByContentView() = runGenericTest(contentView = promptContentView, description = "test description") { + mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) val contentView by collectLastValue(viewModel.contentView) val description by collectLastValue(viewModel.description) @@ -1235,6 +1237,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa @Test fun descriptionWithoutContentView() = runGenericTest(description = "test description") { + mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) val contentView by collectLastValue(viewModel.contentView) val description by collectLastValue(viewModel.description) @@ -1244,6 +1247,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa @Test fun defaultLogoIfNoLogoSet() = runGenericTest { + mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) val logo by collectLastValue(viewModel.logo) assertThat(logo).isEqualTo(defaultLogoIcon) } @@ -1251,6 +1255,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa @Test fun logoResSetByApp() = runGenericTest(logoRes = logoResFromApp) { + mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) val logo by collectLastValue(viewModel.logo) assertThat(logo).isEqualTo(logoFromApp) } @@ -1258,6 +1263,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa @Test fun logoBitmapSetByApp() = runGenericTest(logoBitmap = logoBitmapFromApp) { + mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) val logo by collectLastValue(viewModel.logo) assertThat((logo as BitmapDrawable).bitmap).isEqualTo(logoBitmapFromApp) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt index 1fa60fc9044b..3c430316ffbe 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt @@ -56,19 +56,27 @@ import com.android.systemui.bouncer.ui.BouncerView import com.android.systemui.classifier.FalsingCollector import com.android.systemui.coroutines.collectLastValue import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor +import com.android.systemui.deviceentry.domain.interactor.deviceEntryFingerprintAuthInteractor import com.android.systemui.display.data.repository.FakeDisplayRepository import com.android.systemui.keyguard.DismissCallbackRegistry import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository import com.android.systemui.keyguard.data.repository.FakeTrustRepository +import com.android.systemui.keyguard.data.repository.biometricSettingsRepository import com.android.systemui.keyguard.domain.interactor.DeviceEntrySideFpsOverlayInteractor +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor +import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.keyguard.ui.viewmodel.SideFpsProgressBarViewModel +import com.android.systemui.kosmos.testDispatcher import com.android.systemui.log.SideFpsLogger import com.android.systemui.log.logcatLogBuffer import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.res.R import com.android.systemui.shared.Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR +import com.android.systemui.statusbar.phone.dozeServiceHost import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.testKosmos import com.android.systemui.unfold.compat.ScreenSizeFoldProvider import com.android.systemui.user.domain.interactor.SelectedUserInteractor import com.android.systemui.util.concurrency.FakeExecutor @@ -98,6 +106,7 @@ import org.mockito.junit.MockitoRule @SmallTest @RunWith(JUnit4::class) class SideFpsOverlayViewModelTest : SysuiTestCase() { + private val kosmos = testKosmos() @JvmField @Rule var mockitoRule: MockitoRule = MockitoJUnit.rule() @Mock private lateinit var activityTaskManager: ActivityTaskManager @@ -239,19 +248,22 @@ class SideFpsOverlayViewModelTest : SysuiTestCase() { windowManager, displayStateInteractor, Optional.of(fingerprintInteractiveToAuthProvider), - mock(), + kosmos.biometricSettingsRepository, + kosmos.keyguardTransitionInteractor, SideFpsLogger(logcatLogBuffer("SfpsLogger")) ) sideFpsProgressBarViewModel = SideFpsProgressBarViewModel( mContext, - mock(), + kosmos.deviceEntryFingerprintAuthInteractor, sfpsSensorInteractor, - mock(), + kosmos.dozeServiceHost, + kosmos.keyguardInteractor, displayStateInteractor, - StandardTestDispatcher(), + kosmos.testDispatcher, testScope.backgroundScope, + kosmos.powerInteractor, ) underTest = diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/StickyKeysIndicatorCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/StickyKeysIndicatorCoordinatorTest.kt new file mode 100644 index 000000000000..df73cc8f0212 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/StickyKeysIndicatorCoordinatorTest.kt @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyboard.stickykeys.ui + +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.compose.ComposeFacade +import com.android.systemui.keyboard.data.repository.FakeStickyKeysRepository +import com.android.systemui.keyboard.data.repository.keyboardRepository +import com.android.systemui.keyboard.stickykeys.StickyKeysLogger +import com.android.systemui.keyboard.stickykeys.shared.model.Locked +import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.SHIFT +import com.android.systemui.keyboard.stickykeys.ui.viewmodel.StickyKeysIndicatorViewModel +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.statusbar.phone.ComponentSystemUIDialog +import com.android.systemui.statusbar.phone.SystemUIDialogFactory +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import org.junit.Assume +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import org.mockito.Mockito.anyBoolean +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyZeroInteractions + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(JUnit4::class) +class StickyKeysIndicatorCoordinatorTest : SysuiTestCase() { + + private lateinit var coordinator: StickyKeysIndicatorCoordinator + private val testScope = TestScope(StandardTestDispatcher()) + private val stickyKeysRepository = FakeStickyKeysRepository() + private val dialog = mock<ComponentSystemUIDialog>() + + @Before + fun setup() { + Assume.assumeTrue(ComposeFacade.isComposeAvailable()) + val dialogFactory = mock<SystemUIDialogFactory> { + whenever(applicationContext).thenReturn(context) + whenever(create(any(), anyInt(), anyBoolean())).thenReturn(dialog) + } + val keyboardRepository = Kosmos().keyboardRepository + val viewModel = StickyKeysIndicatorViewModel( + stickyKeysRepository, + keyboardRepository, + testScope.backgroundScope) + coordinator = StickyKeysIndicatorCoordinator( + testScope.backgroundScope, + dialogFactory, + viewModel, + mock<StickyKeysLogger>()) + coordinator.startListening() + keyboardRepository.setIsAnyKeyboardConnected(true) + } + + @Test + fun dialogIsShownWhenStickyKeysAreEmitted() { + testScope.run { + verifyZeroInteractions(dialog) + + stickyKeysRepository.setStickyKeys(linkedMapOf(SHIFT to Locked(true))) + runCurrent() + + verify(dialog).show() + } + } + + @Test + fun dialogDisappearsWhenStickyKeysAreEmpty() { + testScope.run { + verifyZeroInteractions(dialog) + + stickyKeysRepository.setStickyKeys(linkedMapOf(SHIFT to Locked(true))) + runCurrent() + stickyKeysRepository.setStickyKeys(linkedMapOf()) + runCurrent() + + verify(dialog).dismiss() + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/viewmodel/StickyKeysIndicatorViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/viewmodel/StickyKeysIndicatorViewModelTest.kt index d397fc202637..8a713688acf9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/viewmodel/StickyKeysIndicatorViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/stickykeys/ui/viewmodel/StickyKeysIndicatorViewModelTest.kt @@ -34,6 +34,7 @@ import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey.SHIFT import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent @@ -46,6 +47,7 @@ import org.mockito.ArgumentCaptor import org.mockito.Mockito.verify import org.mockito.Mockito.verifyZeroInteractions +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(JUnit4::class) class StickyKeysIndicatorViewModelTest : SysuiTestCase() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java index 754a7fd81475..8a3a4342915b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java @@ -309,6 +309,28 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { @Test @TestableLooper.RunWithLooper(setAsMainLooper = true) + public void testRaceCondition_doNotRegisterCentralSurfacesImmediately() { + create(false); + + // GIVEN central surfaces is not registered with KeyguardViewMediator, but a call to enable + // keyguard comes in + mViewMediator.onSystemReady(); + mViewMediator.setKeyguardEnabled(true); + TestableLooper.get(this).processAllMessages(); + + // If this step has been reached, then system ui has not crashed. Now register + // CentralSurfaces + assertFalse(mViewMediator.isShowingAndNotOccluded()); + register(); + TestableLooper.get(this).moveTimeForward(100); + TestableLooper.get(this).processAllMessages(); + + // THEN keyguard is shown + assertTrue(mViewMediator.isShowingAndNotOccluded()); + } + + @Test + @TestableLooper.RunWithLooper(setAsMainLooper = true) public void onLockdown_showKeyguard_evenIfKeyguardIsNotEnabledExternally() { // GIVEN keyguard is not enabled and isn't showing mViewMediator.onSystemReady(); @@ -1139,6 +1161,11 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { } private void createAndStartViewMediator(boolean orderUnlockAndWake) { + create(orderUnlockAndWake); + register(); + } + + private void create(boolean orderUnlockAndWake) { mContext.getOrCreateTestableResources().addOverride( com.android.internal.R.bool.config_orderUnlockAndWake, orderUnlockAndWake); @@ -1189,7 +1216,9 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { mSelectedUserInteractor, mKeyguardInteractor); mViewMediator.start(); + } + private void register() { mViewMediator.registerCentralSurfaces(mCentralSurfaces, null, null, null, null, null); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorTest.kt index 809947d2fec7..6092b6b351af 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorTest.kt @@ -66,6 +66,7 @@ class FromPrimaryBouncerTransitionInteractorTest : KeyguardTransitionInteractorT bgDispatcher = super.testDispatcher, mainDispatcher = super.testDispatcher, keyguardInteractor = super.keyguardInteractor, + communalInteractor = super.communalInteractor, flags = FakeFeatureFlags(), keyguardSecurityModel = mock(), powerInteractor = PowerInteractorFactory.create().powerInteractor, diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTestCase.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTestCase.kt index 339fd22259db..a03aed04432b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTestCase.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTestCase.kt @@ -17,14 +17,18 @@ package com.android.systemui.keyguard.domain.interactor import com.android.systemui.SysuiTestCase +import com.android.systemui.communal.domain.interactor.CommunalInteractor +import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository +import com.android.systemui.testKosmos import com.android.systemui.util.mockito.mock import dagger.Lazy import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope open class KeyguardTransitionInteractorTestCase : SysuiTestCase() { + private val kosmos = testKosmos() val testDispatcher = StandardTestDispatcher() var testScope = TestScope(testDispatcher) @@ -32,6 +36,7 @@ open class KeyguardTransitionInteractorTestCase : SysuiTestCase() { lateinit var transitionRepository: FakeKeyguardTransitionRepository lateinit var keyguardInteractor: KeyguardInteractor + lateinit var communalInteractor: CommunalInteractor lateinit var transitionInteractor: KeyguardTransitionInteractor /** @@ -51,6 +56,8 @@ open class KeyguardTransitionInteractorTestCase : SysuiTestCase() { keyguardInteractor = KeyguardInteractorFactory.create(repository = keyguardRepository).keyguardInteractor + communalInteractor = kosmos.communalInteractor + transitionInteractor = KeyguardTransitionInteractorFactory.create( repository = transitionRepository, diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt index dafd9e64371b..4f3a63dd2829 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt @@ -156,10 +156,11 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { val glanceableHubTransitions = GlanceableHubTransitions( - testScope, - transitionInteractor, - transitionRepository, - communalInteractor + scope = testScope, + bgDispatcher = kosmos.testDispatcher, + transitionInteractor = transitionInteractor, + transitionRepository = transitionRepository, + communalInteractor = communalInteractor ) fromLockscreenTransitionInteractor = FromLockscreenTransitionInteractor( @@ -196,6 +197,7 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { flags = featureFlags, keyguardSecurityModel = keyguardSecurityModel, powerInteractor = powerInteractor, + communalInteractor = communalInteractor, selectedUserInteractor = mSelectedUserInteractor, ) .apply { start() } @@ -242,6 +244,7 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { transitionRepository = transitionRepository, transitionInteractor = transitionInteractor, powerInteractor = powerInteractor, + communalInteractor = communalInteractor, ) .apply { start() } @@ -267,6 +270,7 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { transitionRepository = transitionRepository, transitionInteractor = transitionInteractor, powerInteractor = powerInteractor, + communalInteractor = communalInteractor, ) .apply { start() } @@ -278,6 +282,7 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { keyguardInteractor = keyguardInteractor, transitionRepository = transitionRepository, transitionInteractor = transitionInteractor, + communalInteractor = communalInteractor, powerInteractor = powerInteractor, ) .apply { start() } @@ -288,6 +293,7 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { bgDispatcher = kosmos.testDispatcher, mainDispatcher = kosmos.testDispatcher, glanceableHubTransitions = glanceableHubTransitions, + keyguardInteractor = keyguardInteractor, transitionRepository = transitionRepository, transitionInteractor = transitionInteractor, powerInteractor = powerInteractor, @@ -902,6 +908,37 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { } @Test + fun goneToGlanceableHub() = + testScope.runTest { + // GIVEN a prior transition has run to GONE + runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.GONE) + + // GIVEN the device is idle on the glanceable hub + val idleTransitionState = + MutableStateFlow<ObservableCommunalTransitionState>( + ObservableCommunalTransitionState.Idle(CommunalSceneKey.Communal) + ) + communalInteractor.setTransitionState(idleTransitionState) + runCurrent() + + // WHEN the keyguard starts to show + keyguardRepository.setKeyguardShowing(true) + runCurrent() + + val info = + withArgCaptor<TransitionInfo> { + verify(transitionRepository).startTransition(capture()) + } + // THEN a transition to DOZING should occur + assertThat(info.ownerName).isEqualTo(FromGoneTransitionInteractor::class.simpleName) + assertThat(info.from).isEqualTo(KeyguardState.GONE) + assertThat(info.to).isEqualTo(KeyguardState.GLANCEABLE_HUB) + assertThat(info.animator).isNotNull() + + coroutineContext.cancelChildren() + } + + @Test fun alternateBouncerToPrimaryBouncer() = testScope.runTest { // GIVEN a prior transition has run to ALTERNATE_BOUNCER @@ -1023,6 +1060,45 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { } @Test + fun alternateBouncerToGlanceableHub() = + testScope.runTest { + // GIVEN a prior transition has run to ALTERNATE_BOUNCER + bouncerRepository.setAlternateVisible(true) + runTransitionAndSetWakefulness( + KeyguardState.LOCKSCREEN, + KeyguardState.ALTERNATE_BOUNCER + ) + + // GIVEN the primary bouncer isn't showing and device not sleeping + bouncerRepository.setPrimaryShow(false) + + // GIVEN the device is idle on the glanceable hub + val idleTransitionState = + MutableStateFlow<ObservableCommunalTransitionState>( + ObservableCommunalTransitionState.Idle(CommunalSceneKey.Communal) + ) + communalInteractor.setTransitionState(idleTransitionState) + runCurrent() + + // WHEN the alternateBouncer stops showing + bouncerRepository.setAlternateVisible(false) + advanceUntilIdle() + + val info = + withArgCaptor<TransitionInfo> { + verify(transitionRepository).startTransition(capture()) + } + // THEN a transition to LOCKSCREEN should occur + assertThat(info.ownerName) + .isEqualTo(FromAlternateBouncerTransitionInteractor::class.simpleName) + assertThat(info.from).isEqualTo(KeyguardState.ALTERNATE_BOUNCER) + assertThat(info.to).isEqualTo(KeyguardState.GLANCEABLE_HUB) + assertThat(info.animator).isNotNull() + + coroutineContext.cancelChildren() + } + + @Test fun primaryBouncerToAod() = testScope.runTest { // GIVEN a prior transition has run to PRIMARY_BOUNCER @@ -1085,7 +1161,7 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { bouncerRepository.setPrimaryShow(true) runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.PRIMARY_BOUNCER) - // WHEN the alternateBouncer stops showing + // WHEN the primaryBouncer stops showing bouncerRepository.setPrimaryShow(false) runCurrent() @@ -1103,6 +1179,39 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { } @Test + fun primaryBouncerToGlanceableHub() = + testScope.runTest { + // GIVEN a prior transition has run to PRIMARY_BOUNCER + bouncerRepository.setPrimaryShow(true) + runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.PRIMARY_BOUNCER) + + // GIVEN the device is idle on the glanceable hub + val idleTransitionState = + MutableStateFlow<ObservableCommunalTransitionState>( + ObservableCommunalTransitionState.Idle(CommunalSceneKey.Communal) + ) + communalInteractor.setTransitionState(idleTransitionState) + runCurrent() + + // WHEN the primaryBouncer stops showing + bouncerRepository.setPrimaryShow(false) + runCurrent() + + val info = + withArgCaptor<TransitionInfo> { + verify(transitionRepository).startTransition(capture()) + } + // THEN a transition to LOCKSCREEN should occur + assertThat(info.ownerName) + .isEqualTo(FromPrimaryBouncerTransitionInteractor::class.simpleName) + assertThat(info.from).isEqualTo(KeyguardState.PRIMARY_BOUNCER) + assertThat(info.to).isEqualTo(KeyguardState.GLANCEABLE_HUB) + assertThat(info.animator).isNotNull() + + coroutineContext.cancelChildren() + } + + @Test fun primaryBouncerToDreamingLockscreenHosted() = testScope.runTest { // GIVEN device dreaming with the lockscreen hosted dream and not dozing @@ -1193,6 +1302,43 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { } @Test + fun occludedToGlanceableHub() = + testScope.runTest { + // GIVEN a device on lockscreen + keyguardRepository.setKeyguardShowing(true) + runCurrent() + + // GIVEN the device is idle on the glanceable hub + val idleTransitionState = + MutableStateFlow<ObservableCommunalTransitionState>( + ObservableCommunalTransitionState.Idle(CommunalSceneKey.Communal) + ) + communalInteractor.setTransitionState(idleTransitionState) + runCurrent() + + // GIVEN a prior transition has run to OCCLUDED + runTransitionAndSetWakefulness(KeyguardState.GLANCEABLE_HUB, KeyguardState.OCCLUDED) + keyguardRepository.setKeyguardOccluded(true) + runCurrent() + + // WHEN occlusion ends + keyguardRepository.setKeyguardOccluded(false) + runCurrent() + + val info = + withArgCaptor<TransitionInfo> { + verify(transitionRepository).startTransition(capture()) + } + // THEN a transition to GLANCEABLE_HUB should occur + assertThat(info.ownerName).isEqualTo(FromOccludedTransitionInteractor::class.simpleName) + assertThat(info.from).isEqualTo(KeyguardState.OCCLUDED) + assertThat(info.to).isEqualTo(KeyguardState.GLANCEABLE_HUB) + assertThat(info.animator).isNotNull() + + coroutineContext.cancelChildren() + } + + @Test fun occludedToAlternateBouncer() = testScope.runTest { // GIVEN a prior transition has run to OCCLUDED @@ -1640,6 +1786,111 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { coroutineContext.cancelChildren() } + @Test + fun glanceableHubToPrimaryBouncer() = + testScope.runTest { + // GIVEN a prior transition has run to ALTERNATE_BOUNCER + runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.GLANCEABLE_HUB) + + // WHEN the primary bouncer shows + bouncerRepository.setPrimaryShow(true) + runCurrent() + + val info = + withArgCaptor<TransitionInfo> { + verify(transitionRepository).startTransition(capture()) + } + // THEN a transition to PRIMARY_BOUNCER should occur + assertThat(info.ownerName) + .isEqualTo(FromGlanceableHubTransitionInteractor::class.simpleName) + assertThat(info.from).isEqualTo(KeyguardState.GLANCEABLE_HUB) + assertThat(info.to).isEqualTo(KeyguardState.PRIMARY_BOUNCER) + assertThat(info.animator).isNotNull() + + coroutineContext.cancelChildren() + } + + @Test + fun glanceableHubToAlternateBouncer() = + testScope.runTest { + // GIVEN a prior transition has run to ALTERNATE_BOUNCER + runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.GLANCEABLE_HUB) + + // WHEN the primary bouncer shows + bouncerRepository.setAlternateVisible(true) + runCurrent() + + val info = + withArgCaptor<TransitionInfo> { + verify(transitionRepository).startTransition(capture()) + } + // THEN a transition to PRIMARY_BOUNCER should occur + assertThat(info.ownerName) + .isEqualTo(FromGlanceableHubTransitionInteractor::class.simpleName) + assertThat(info.from).isEqualTo(KeyguardState.GLANCEABLE_HUB) + assertThat(info.to).isEqualTo(KeyguardState.ALTERNATE_BOUNCER) + assertThat(info.animator).isNotNull() + + coroutineContext.cancelChildren() + } + + @Test + fun glanceableHubToOccluded() = + testScope.runTest { + // GIVEN a prior transition has run to GLANCEABLE_HUB + runTransitionAndSetWakefulness(KeyguardState.GONE, KeyguardState.GLANCEABLE_HUB) + runCurrent() + + // GIVEN the device is idle on the glanceable hub + val idleTransitionState = + MutableStateFlow<ObservableCommunalTransitionState>( + ObservableCommunalTransitionState.Idle(CommunalSceneKey.Communal) + ) + communalInteractor.setTransitionState(idleTransitionState) + runCurrent() + + // WHEN the keyguard is occluded + keyguardRepository.setKeyguardOccluded(true) + runCurrent() + + val info = + withArgCaptor<TransitionInfo> { + verify(transitionRepository).startTransition(capture()) + } + // THEN a transition to OCCLUDED should occur + assertThat(info.ownerName) + .isEqualTo(FromGlanceableHubTransitionInteractor::class.simpleName) + assertThat(info.from).isEqualTo(KeyguardState.GLANCEABLE_HUB) + assertThat(info.to).isEqualTo(KeyguardState.OCCLUDED) + assertThat(info.animator).isNotNull() + + coroutineContext.cancelChildren() + } + + @Test + fun glanceableHubToGone() = + testScope.runTest { + // GIVEN a prior transition has run to GLANCEABLE_HUB + runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.GLANCEABLE_HUB) + + // WHEN keyguard goes away + keyguardRepository.setKeyguardGoingAway(true) + runCurrent() + + val info = + withArgCaptor<TransitionInfo> { + verify(transitionRepository).startTransition(capture()) + } + // THEN a transition to DOZING should occur + assertThat(info.ownerName) + .isEqualTo(FromGlanceableHubTransitionInteractor::class.simpleName) + assertThat(info.from).isEqualTo(KeyguardState.GLANCEABLE_HUB) + assertThat(info.to).isEqualTo(KeyguardState.GONE) + assertThat(info.animator).isNotNull() + + coroutineContext.cancelChildren() + } + private fun createKeyguardInteractor(): KeyguardInteractor { return KeyguardInteractorFactory.create( featureFlags = featureFlags, diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt index 57b555989166..acb6ff0192e8 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSectionTest.kt @@ -19,12 +19,15 @@ package com.android.systemui.keyguard.ui.view.layout.sections import android.content.pm.PackageManager import android.content.res.Resources +import android.view.View.GONE +import android.view.View.VISIBLE import androidx.constraintlayout.widget.ConstraintSet import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardClockInteractor import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel +import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel import com.android.systemui.res.R import com.android.systemui.statusbar.policy.SplitShadeStateController import com.android.systemui.util.Utils @@ -49,8 +52,11 @@ class ClockSectionTest : SysuiTestCase() { @Mock private lateinit var keyguardClockInteractor: KeyguardClockInteractor @Mock private lateinit var keyguardClockViewModel: KeyguardClockViewModel @Mock private lateinit var splitShadeStateController: SplitShadeStateController + @Mock private lateinit var smartspaceViewModel: KeyguardSmartspaceViewModel @Mock private lateinit var blueprintInteractor: Lazy<KeyguardBlueprintInteractor> + private val bcSmartspaceVisibility: MutableStateFlow<Int> = MutableStateFlow(VISIBLE) private val clockShouldBeCentered: MutableStateFlow<Boolean> = MutableStateFlow(true) + private val isAodIconsVisible: MutableStateFlow<Boolean> = MutableStateFlow(true) private lateinit var underTest: ClockSection @@ -110,6 +116,8 @@ class ClockSectionTest : SysuiTestCase() { mContext.setMockPackageManager(packageManager) whenever(keyguardClockViewModel.clockShouldBeCentered).thenReturn(clockShouldBeCentered) + whenever(keyguardClockViewModel.isAodIconsVisible).thenReturn(isAodIconsVisible) + whenever(smartspaceViewModel.bcSmartspaceVisibility).thenReturn(bcSmartspaceVisibility) underTest = ClockSection( @@ -117,6 +125,7 @@ class ClockSectionTest : SysuiTestCase() { keyguardClockViewModel, mContext, splitShadeStateController, + smartspaceViewModel, blueprintInteractor ) } @@ -176,6 +185,40 @@ class ClockSectionTest : SysuiTestCase() { assetSmallClockTop(cs, expectedSmallClockTopMargin) } + @Test + fun testSmartspaceVisible_weatherClockDateAndIconsBarrierBottomBelowBCSmartspace() { + isAodIconsVisible.value = false + bcSmartspaceVisibility.value = VISIBLE + val cs = ConstraintSet() + underTest.applyDefaultConstraints(cs) + val referencedIds = cs.getReferencedIds(R.id.weather_clock_date_and_icons_barrier_bottom) + referencedIds.contentEquals(intArrayOf(com.android.systemui.shared.R.id.bc_smartspace_view)) + } + + @Test + fun testSmartspaceGone_weatherClockDateAndIconsBarrierBottomBelowSmartspaceDateWeather() { + isAodIconsVisible.value = false + bcSmartspaceVisibility.value = GONE + val cs = ConstraintSet() + underTest.applyDefaultConstraints(cs) + val referencedIds = cs.getReferencedIds(R.id.weather_clock_date_and_icons_barrier_bottom) + referencedIds.contentEquals(intArrayOf(R.id.lockscreen_clock_view)) + } + + @Test + fun testHasAodIcons_weatherClockDateAndIconsBarrierBottomBelowSmartspaceDateWeather() { + isAodIconsVisible.value = true + val cs = ConstraintSet() + underTest.applyDefaultConstraints(cs) + val referencedIds = cs.getReferencedIds(R.id.weather_clock_date_and_icons_barrier_bottom) + referencedIds.contentEquals( + intArrayOf( + com.android.systemui.shared.R.id.bc_smartspace_view, + R.id.aod_notification_icon_container + ) + ) + } + private fun setLargeClock(useLargeClock: Boolean) { whenever(keyguardClockViewModel.useLargeClock).thenReturn(useLargeClock) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt index 1b4573dafe5e..22a2e93eb10d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModelTest.kt @@ -34,12 +34,14 @@ import com.android.systemui.plugins.clocks.ClockController import com.android.systemui.plugins.clocks.ClockFaceConfig import com.android.systemui.plugins.clocks.ClockFaceController import com.android.systemui.shared.clocks.ClockRegistry +import com.android.systemui.statusbar.notification.domain.interactor.NotificationsKeyguardInteractor import com.android.systemui.statusbar.policy.SplitShadeStateController import com.android.systemui.util.mockito.whenever import com.android.systemui.util.settings.FakeSettings import com.google.common.truth.Truth.assertThat import kotlin.test.Test import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestCoroutineScheduler import kotlinx.coroutines.test.TestScope @@ -68,6 +70,8 @@ class KeyguardClockViewModelTest : SysuiTestCase() { @Mock private lateinit var clockFaceConfig: ClockFaceConfig @Mock private lateinit var eventController: ClockEventController @Mock private lateinit var splitShadeStateController: SplitShadeStateController + @Mock private lateinit var notifsKeyguardInteractor: NotificationsKeyguardInteractor + @Mock private lateinit var areNotificationsFullyHidden: Flow<Boolean> @Before fun setup() { @@ -90,12 +94,15 @@ class KeyguardClockViewModelTest : SysuiTestCase() { scope.backgroundScope ) keyguardClockInteractor = KeyguardClockInteractor(keyguardClockRepository) + whenever(notifsKeyguardInteractor.areNotificationsFullyHidden) + .thenReturn(areNotificationsFullyHidden) underTest = KeyguardClockViewModel( keyguardInteractor, keyguardClockInteractor, scope.backgroundScope, splitShadeStateController, + notifsKeyguardInteractor ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogControllerV2Test.kt b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogControllerV2Test.kt index e4432f3038bc..0636831c7c66 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogControllerV2Test.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/privacy/PrivacyDialogControllerV2Test.kt @@ -30,13 +30,11 @@ import android.os.UserHandle import android.permission.PermissionGroupUsage import android.permission.PermissionManager import android.testing.AndroidTestingRunner -import android.view.View import android.widget.LinearLayout import androidx.test.filters.SmallTest import com.android.internal.logging.UiEventLogger import com.android.systemui.SysuiTestCase import com.android.systemui.animation.DialogLaunchAnimator -import com.android.systemui.animation.LaunchableView import com.android.systemui.appops.AppOpsController import com.android.systemui.plugins.ActivityStarter import com.android.systemui.privacy.logging.PrivacyLogger @@ -206,10 +204,7 @@ class PrivacyDialogControllerV2Test : SysuiTestCase() { @Test fun testShowDialogShowsDialogWithView() { val parent = LinearLayout(context) - val view = - object : View(context), LaunchableView { - override fun setShouldBlockVisibilityChanges(block: Boolean) {} - } + val view = OngoingPrivacyChip(context) parent.addView(view) val usage = createMockPermGroupUsage() `when`(permissionManager.getIndicatorAppOpUsageData(anyBoolean())).thenReturn(listOf(usage)) diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/HeaderPrivacyIconsControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/HeaderPrivacyIconsControllerTest.kt index fa02e8cb3e54..f98b68f2309b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/HeaderPrivacyIconsControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/HeaderPrivacyIconsControllerTest.kt @@ -173,7 +173,7 @@ class HeaderPrivacyIconsControllerTest : SysuiTestCase() { captor.value.onClick(privacyChip) verify(privacyDialogController).showDialog(any(Context::class.java)) verify(privacyDialogControllerV2, never()) - .showDialog(any(Context::class.java), any(View::class.java)) + .showDialog(any(Context::class.java), any(OngoingPrivacyChip::class.java)) } @Test @@ -186,7 +186,7 @@ class HeaderPrivacyIconsControllerTest : SysuiTestCase() { captor.value.onClick(privacyChip) verify(privacyDialogController).showDialog(any(Context::class.java)) verify(privacyDialogControllerV2, never()) - .showDialog(any(Context::class.java), any(View::class.java)) + .showDialog(any(Context::class.java), any(OngoingPrivacyChip::class.java)) } @Test @@ -207,7 +207,7 @@ class HeaderPrivacyIconsControllerTest : SysuiTestCase() { captor.value.onClick(privacyChip) verify(privacyDialogController, never()).showDialog(any(Context::class.java)) verify(privacyDialogControllerV2, never()) - .showDialog(any(Context::class.java), any(View::class.java)) + .showDialog(any(Context::class.java), any(OngoingPrivacyChip::class.java)) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSImplTest.java index c8c134a9474a..563a3fe9fc7f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSImplTest.java @@ -35,6 +35,7 @@ import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import android.annotation.Nullable; @@ -47,6 +48,7 @@ import android.view.Display; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.FrameLayout; import androidx.lifecycle.Lifecycle; import androidx.test.filters.SmallTest; @@ -63,6 +65,7 @@ import com.android.systemui.qs.footer.ui.binder.FooterActionsViewBinder; import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel; import com.android.systemui.qs.logging.QSLogger; import com.android.systemui.res.R; +import com.android.systemui.scene.shared.flag.SceneContainerFlags; import com.android.systemui.settings.FakeDisplayTracker; import com.android.systemui.shade.transition.LargeScreenShadeInterpolator; import com.android.systemui.statusbar.CommandQueue; @@ -111,7 +114,8 @@ public class QSImplTest extends SysuiTestCase { @Mock private FooterActionsViewBinder mFooterActionsViewBinder; @Mock private LargeScreenShadeInterpolator mLargeScreenShadeInterpolator; @Mock private FeatureFlagsClassic mFeatureFlags; - private View mQsView; + @Mock private SceneContainerFlags mSceneContainerFlags; + private ViewGroup mQsView; private final CommandQueue mCommandQueue = new CommandQueue(mContext, new FakeDisplayTracker(mContext)); @@ -121,6 +125,9 @@ public class QSImplTest extends SysuiTestCase { @Before public void setup() { + MockitoAnnotations.initMocks(this); + when(mSceneContainerFlags.isEnabled()).thenReturn(false); + mUnderTest = instantiate(); mUnderTest.onComponentCreated(mQsComponent, null); @@ -487,9 +494,24 @@ public class QSImplTest extends SysuiTestCase { verify(mQSAnimator).setOnKeyguard(true); } - private QSImpl instantiate() { - MockitoAnnotations.initMocks(this); + @Test + public void testSceneContainerFlagsEnabled_FooterActionsRemoved_controllerNotStarted() { + when(mSceneContainerFlags.isEnabled()).thenReturn(true); + clearInvocations( + mFooterActionsViewBinder, mFooterActionsViewModel, mFooterActionsViewModelFactory); + QSImpl other = instantiate(); + + other.onComponentCreated(mQsComponent, null); + assertThat((View) other.getView().findViewById(R.id.qs_footer_actions)).isNull(); + verifyZeroInteractions( + mFooterActionsViewModel, + mFooterActionsViewBinder, + mFooterActionsViewModelFactory + ); + } + + private QSImpl instantiate() { setupQsComponent(); setUpViews(); setUpInflater(); @@ -514,7 +536,8 @@ public class QSImplTest extends SysuiTestCase { mFooterActionsViewModelFactory, mFooterActionsViewBinder, mLargeScreenShadeInterpolator, - mFeatureFlags); + mFeatureFlags, + mSceneContainerFlags); } private void setUpOther() { @@ -533,14 +556,23 @@ public class QSImplTest extends SysuiTestCase { } private void setUpViews() { - mQsView = spy(new View(mContext)); + mQsView = spy(new FrameLayout(mContext)); when(mQsComponent.getRootView()).thenReturn(mQsView); - when(mQsView.findViewById(R.id.expanded_qs_scroll_view)) + + when(mQSPanelScrollView.findViewById(R.id.expanded_qs_scroll_view)) .thenReturn(mQSPanelScrollView); - when(mQsView.findViewById(R.id.header)).thenReturn(mHeader); - when(mQsView.findViewById(android.R.id.edit)).thenReturn(new View(mContext)); - when(mQsView.findViewById(R.id.qs_footer_actions)).thenAnswer( - invocation -> new FooterActionsViewBinder().create(mContext)); + mQsView.addView(mQSPanelScrollView); + + when(mHeader.findViewById(R.id.header)).thenReturn(mHeader); + mQsView.addView(mHeader); + + View customizer = new View(mContext); + customizer.setId(android.R.id.edit); + mQsView.addView(customizer); + + View footerActionsView = new FooterActionsViewBinder().create(mContext); + footerActionsView.setId(R.id.qs_footer_actions); + mQsView.addView(footerActionsView); } private void setUpInflater() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt index c7479fd50db1..1ed8c3cdf0ba 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt @@ -32,6 +32,7 @@ import com.android.systemui.qs.QsEventLogger import com.android.systemui.qs.logging.QSLogger import com.android.systemui.recordissue.RecordIssueDialogDelegate import com.android.systemui.res.R +import com.android.systemui.settings.UserContextProvider import com.android.systemui.statusbar.phone.KeyguardDismissUtil import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.policy.KeyguardStateController @@ -65,6 +66,7 @@ class RecordIssueTileTest : SysuiTestCase() { @Mock private lateinit var keyguardDismissUtil: KeyguardDismissUtil @Mock private lateinit var keyguardStateController: KeyguardStateController @Mock private lateinit var dialogLauncherAnimator: DialogLaunchAnimator + @Mock private lateinit var userContextProvider: UserContextProvider @Mock private lateinit var delegateFactory: RecordIssueDialogDelegate.Factory @Mock private lateinit var dialogDelegate: RecordIssueDialogDelegate @Mock private lateinit var dialog: SystemUIDialog @@ -94,6 +96,7 @@ class RecordIssueTileTest : SysuiTestCase() { keyguardDismissUtil, keyguardStateController, dialogLauncherAnimator, + userContextProvider, delegateFactory, ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogControllerTest.java index b24b8773d600..c0ef50fa9072 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/InternetDialogControllerTest.java @@ -1069,6 +1069,22 @@ public class InternetDialogControllerTest extends SysuiTestCase { assertThat(mInternetDialogController.mCallback).isNull(); } + @Test + public void hasActiveSubId_activeSubIdListIsEmpty_returnFalse() { + when(mSubscriptionManager.getActiveSubscriptionIdList()).thenReturn(new int[]{}); + mInternetDialogController.mOnSubscriptionsChangedListener.onSubscriptionsChanged(); + + assertThat(mInternetDialogController.hasActiveSubId()).isFalse(); + } + + @Test + public void hasActiveSubId_activeSubIdListNotEmpty_returnTrue() { + when(mSubscriptionManager.getActiveSubscriptionIdList()).thenReturn(new int[]{SUB_ID}); + mInternetDialogController.mOnSubscriptionsChangedListener.onSubscriptionsChanged(); + + assertThat(mInternetDialogController.hasActiveSubId()).isTrue(); + } + private String getResourcesString(String name) { return mContext.getResources().getString(getResourcesId(name)); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt index 81ff817830ff..a6e240b1b701 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/GlanceableHubContainerControllerTest.kt @@ -22,6 +22,7 @@ import android.testing.TestableLooper import android.testing.ViewUtils import android.view.MotionEvent import android.view.View +import android.widget.FrameLayout import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.communal.data.repository.FakeCommunalRepository @@ -61,6 +62,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { @Mock private lateinit var shadeInteractor: ShadeInteractor @Mock private lateinit var powerManager: PowerManager + private lateinit var parentView: FrameLayout private lateinit var containerView: View private lateinit var testableLooper: TestableLooper @@ -94,7 +96,12 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { .thenReturn(bouncerShowingFlow) whenever(shadeInteractor.isAnyFullyExpanded).thenReturn(shadeShowingFlow) - overrideResource(R.dimen.communal_grid_gutter_size, SWIPE_REGION_WIDTH) + overrideResource(R.dimen.communal_right_edge_swipe_region_width, RIGHT_SWIPE_REGION_WIDTH) + overrideResource(R.dimen.communal_top_edge_swipe_region_height, TOP_SWIPE_REGION_WIDTH) + overrideResource( + R.dimen.communal_bottom_edge_swipe_region_height, + BOTTOM_SWIPE_REGION_WIDTH + ) } @Test @@ -135,7 +142,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { initAndAttachContainerView() // Touch events are intercepted. - assertThat(underTest.onTouchEvent(DOWN_IN_SWIPE_REGION_EVENT)).isTrue() + assertThat(underTest.onTouchEvent(DOWN_IN_RIGHT_SWIPE_REGION_EVENT)).isTrue() } @Test @@ -147,7 +154,7 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { // Initial touch down is intercepted, and so are touches outside of the region, until an up // event is received. - assertThat(underTest.onTouchEvent(DOWN_IN_SWIPE_REGION_EVENT)).isTrue() + assertThat(underTest.onTouchEvent(DOWN_IN_RIGHT_SWIPE_REGION_EVENT)).isTrue() assertThat(underTest.onTouchEvent(MOVE_EVENT)).isTrue() assertThat(underTest.onTouchEvent(UP_EVENT)).isTrue() assertThat(underTest.onTouchEvent(MOVE_EVENT)).isFalse() @@ -168,6 +175,28 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { } @Test + fun onTouchEvent_topSwipeWhenHubOpen_returnsFalse() { + // Communal is open. + communalRepository.setDesiredScene(CommunalSceneKey.Communal) + + initAndAttachContainerView() + + // Touch event in the top swipe reqgion is not intercepted. + assertThat(underTest.onTouchEvent(DOWN_IN_TOP_SWIPE_REGION_EVENT)).isFalse() + } + + @Test + fun onTouchEvent_bottomSwipeWhenHubOpen_returnsFalse() { + // Communal is open. + communalRepository.setDesiredScene(CommunalSceneKey.Communal) + + initAndAttachContainerView() + + // Touch event in the bottom swipe reqgion is not intercepted. + assertThat(underTest.onTouchEvent(DOWN_IN_BOTTOM_SWIPE_REGION_EVENT)).isFalse() + } + + @Test fun onTouchEvent_communalAndBouncerShowing_doesNotIntercept() { // Communal is open. communalRepository.setDesiredScene(CommunalSceneKey.Communal) @@ -198,26 +227,70 @@ class GlanceableHubContainerControllerTest : SysuiTestCase() { assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse() } + @Test + fun onTouchEvent_containerViewDisposed_doesNotIntercept() { + // Communal is open. + communalRepository.setDesiredScene(CommunalSceneKey.Communal) + + initAndAttachContainerView() + testableLooper.processAllMessages() + + // Touch events are intercepted. + assertThat(underTest.onTouchEvent(DOWN_EVENT)).isTrue() + + // Container view disposed. + underTest.disposeView() + + // Touch events are not intercepted. + assertThat(underTest.onTouchEvent(DOWN_EVENT)).isFalse() + } + private fun initAndAttachContainerView() { containerView = View(context) + + parentView = FrameLayout(context) + parentView.addView(containerView) + // Make view clickable so that dispatchTouchEvent returns true. containerView.isClickable = true underTest.initView(containerView) // Attach the view so that flows start collecting. - ViewUtils.attachView(containerView) + ViewUtils.attachView(parentView) // Give the view a size so that determining if a touch starts at the right edge works. + parentView.layout(0, 0, CONTAINER_WIDTH, CONTAINER_HEIGHT) containerView.layout(0, 0, CONTAINER_WIDTH, CONTAINER_HEIGHT) } companion object { private const val CONTAINER_WIDTH = 100 private const val CONTAINER_HEIGHT = 100 - private const val SWIPE_REGION_WIDTH = 20 - - private val DOWN_EVENT = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0) - private val DOWN_IN_SWIPE_REGION_EVENT = + private const val RIGHT_SWIPE_REGION_WIDTH = 20 + private const val TOP_SWIPE_REGION_WIDTH = 20 + private const val BOTTOM_SWIPE_REGION_WIDTH = 20 + + private val DOWN_EVENT = + MotionEvent.obtain( + 0L, + 0L, + MotionEvent.ACTION_DOWN, + CONTAINER_WIDTH.toFloat(), + CONTAINER_HEIGHT.toFloat(), + 0 + ) + private val DOWN_IN_RIGHT_SWIPE_REGION_EVENT = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, CONTAINER_WIDTH.toFloat(), 0f, 0) + private val DOWN_IN_TOP_SWIPE_REGION_EVENT = + MotionEvent.obtain( + 0L, + 0L, + MotionEvent.ACTION_DOWN, + 0f, + TOP_SWIPE_REGION_WIDTH.toFloat(), + 0 + ) + private val DOWN_IN_BOTTOM_SWIPE_REGION_EVENT = + MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, CONTAINER_HEIGHT.toFloat(), 0) private val MOVE_EVENT = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_MOVE, 0f, 0f, 0) private val UP_EVENT = MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_UP, 0f, 0f, 0) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java index 200e758245a5..461db8e6166c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java @@ -226,6 +226,7 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { powerInteractor, new GlanceableHubTransitions( mTestScope, + mKosmos.getTestDispatcher(), keyguardTransitionInteractor, keyguardTransitionRepository, communalInteractor @@ -247,6 +248,7 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { mKosmos.getTestDispatcher(), mKosmos.getTestDispatcher(), keyguardInteractor, + communalInteractor, featureFlags, mKeyguardSecurityModel, mSelectedUserInteractor, diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt index a11839c56b0f..6681ceee3d09 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationShadeWindowViewControllerTest.kt @@ -74,10 +74,12 @@ import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat import java.util.Optional import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.emptyFlow import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runTest import org.junit.Before +import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor @@ -480,6 +482,7 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { } @Test + @Ignore("b/321332798") fun setsUpCommunalHubLayout_whenFlagEnabled() { if (!isComposeAvailable()) { return @@ -511,6 +514,8 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { } whenever(mGlanceableHubContainerController.isEnabled()).thenReturn(false) + whenever(mGlanceableHubContainerController.enabledState()) + .thenReturn(MutableStateFlow(false)) val mockCommunalPlaceholder = mock(View::class.java) val fakeViewIndex = 20 @@ -520,8 +525,7 @@ class NotificationShadeWindowViewControllerTest : SysuiTestCase() { underTest.setupCommunalHubLayout() - // No adding or removing of views occurs. - verify(view, times(0)).removeView(mockCommunalPlaceholder) + // No adding of views occurs. verify(view, times(0)).addView(any(), eq(fakeViewIndex)) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java index 6fc88ce95325..3e0a647d464e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerBaseTest.java @@ -257,6 +257,7 @@ public class QuickSettingsControllerBaseTest extends SysuiTestCase { powerInteractor, new GlanceableHubTransitions( mTestScope, + mKosmos.getTestDispatcher(), keyguardTransitionInteractor, keyguardTransitionRepository, communalInteractor @@ -278,6 +279,7 @@ public class QuickSettingsControllerBaseTest extends SysuiTestCase { mKosmos.getTestDispatcher(), mKosmos.getTestDispatcher(), keyguardInteractor, + communalInteractor, featureFlags, mock(KeyguardSecurityModel.class), mSelectedUserInteractor, diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeControllerImplTest.kt index c4911a41b4a7..cc79ca4efaa1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/ShadeControllerImplTest.kt @@ -22,15 +22,18 @@ import android.view.Display import android.view.WindowManager import androidx.test.filters.SmallTest import com.android.internal.statusbar.IStatusBarService -import com.android.keyguard.TestScopeProvider import com.android.systemui.SysuiTestCase import com.android.systemui.assist.AssistManager import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testScope import com.android.systemui.log.LogBuffer import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.power.domain.interactor.PowerInteractorFactory import com.android.systemui.scene.data.repository.WindowRootViewVisibilityRepository import com.android.systemui.scene.domain.interactor.WindowRootViewVisibilityInteractor +import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.scene.shared.flag.sceneContainerFlags import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.NotificationShadeWindowController import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationListRepository @@ -64,6 +67,8 @@ class ShadeControllerImplTest : SysuiTestCase() { private val executor = FakeExecutor(FakeSystemClock()) private val testDispatcher = StandardTestDispatcher() private val activeNotificationsRepository = ActiveNotificationListRepository() + private val kosmos = Kosmos() + private val testScope = kosmos.testScope @Mock private lateinit var commandQueue: CommandQueue @Mock private lateinit var keyguardStateController: KeyguardStateController @@ -84,12 +89,14 @@ class ShadeControllerImplTest : SysuiTestCase() { private val windowRootViewVisibilityInteractor: WindowRootViewVisibilityInteractor by lazy { WindowRootViewVisibilityInteractor( - TestScopeProvider.getTestScope(), + testScope, WindowRootViewVisibilityRepository(iStatusBarService, executor), FakeKeyguardRepository(), headsUpManager, PowerInteractorFactory.create().powerInteractor, - ActiveNotificationsInteractor(activeNotificationsRepository, testDispatcher) + ActiveNotificationsInteractor(activeNotificationsRepository, testDispatcher), + kosmos.sceneContainerFlags, + kosmos::sceneInteractor, ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt index 13934dac2401..8cb064dd39a6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt @@ -158,6 +158,7 @@ class StatusBarStateControllerImplTest : SysuiTestCase() { powerInteractor, GlanceableHubTransitions( testScope, + testDispatcher, keyguardTransitionInteractor, keyguardTransitionRepository, communalInteractor @@ -180,6 +181,7 @@ class StatusBarStateControllerImplTest : SysuiTestCase() { testDispatcher, testDispatcher, keyguardInteractor, + communalInteractor, featureFlags, mock(), mock(), diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/ConversationCoordinatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/ConversationCoordinatorTest.kt index 65697b736cbd..36f643ab9cca 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/ConversationCoordinatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/ConversationCoordinatorTest.kt @@ -24,7 +24,6 @@ import android.testing.AndroidTestingRunner import android.testing.TestableLooper import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.flags.FakeFeatureFlagsClassic import com.android.systemui.statusbar.notification.collection.GroupEntry import com.android.systemui.statusbar.notification.collection.GroupEntryBuilder import com.android.systemui.statusbar.notification.collection.NotifPipeline @@ -53,8 +52,8 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.verify -import org.mockito.MockitoAnnotations import org.mockito.Mockito.`when` as whenever +import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidTestingRunner::class) @@ -78,28 +77,22 @@ class ConversationCoordinatorTest : SysuiTestCase() { private lateinit var coordinator: ConversationCoordinator - private val featureFlags = FakeFeatureFlagsClassic() - @Before fun setUp() { MockitoAnnotations.initMocks(this) - coordinator = ConversationCoordinator( - peopleNotificationIdentifier, - conversationIconManager, - HighPriorityProvider( + coordinator = + ConversationCoordinator( peopleNotificationIdentifier, - GroupMembershipManagerImpl(featureFlags) - ), - headerController - ) + conversationIconManager, + HighPriorityProvider(peopleNotificationIdentifier, GroupMembershipManagerImpl()), + headerController + ) whenever(channel.isImportantConversation).thenReturn(true) coordinator.attach(pipeline) // capture arguments: - promoter = withArgCaptor { - verify(pipeline).addPromoter(capture()) - } + promoter = withArgCaptor { verify(pipeline).addPromoter(capture()) } beforeRenderListListener = withArgCaptor { verify(pipeline).addOnBeforeRenderListListener(capture()) } @@ -111,10 +104,10 @@ class ConversationCoordinatorTest : SysuiTestCase() { entry = NotificationEntryBuilder().setChannel(channel).build() val section = NotifSection(peopleAlertingSectioner, 0) - entryA = NotificationEntryBuilder().setChannel(channel) - .setSection(section).setTag("A").build() - entryB = NotificationEntryBuilder().setChannel(channel) - .setSection(section).setTag("B").build() + entryA = + NotificationEntryBuilder().setChannel(channel).setSection(section).setTag("A").build() + entryB = + NotificationEntryBuilder().setChannel(channel).setSection(section).setTag("B").build() } @Test @@ -129,11 +122,12 @@ class ConversationCoordinatorTest : SysuiTestCase() { val altChildA = NotificationEntryBuilder().setTag("A").build() val altChildB = NotificationEntryBuilder().setTag("B").build() val summary = NotificationEntryBuilder().setId(2).setChannel(channel).build() - val groupEntry = GroupEntryBuilder() - .setParent(GroupEntry.ROOT_ENTRY) - .setSummary(summary) - .setChildren(listOf(entry, altChildA, altChildB)) - .build() + val groupEntry = + GroupEntryBuilder() + .setParent(GroupEntry.ROOT_ENTRY) + .setSummary(summary) + .setChildren(listOf(entry, altChildA, altChildB)) + .build() assertTrue(promoter.shouldPromoteToTopLevel(entry)) assertFalse(promoter.shouldPromoteToTopLevel(altChildA)) assertFalse(promoter.shouldPromoteToTopLevel(altChildB)) @@ -146,41 +140,42 @@ class ConversationCoordinatorTest : SysuiTestCase() { @Test fun testInAlertingPeopleSectionWhenTheImportanceIsAtLeastDefault() { // GIVEN - val alertingEntry = NotificationEntryBuilder().setChannel(channel) - .setImportance(IMPORTANCE_DEFAULT).build() + val alertingEntry = + NotificationEntryBuilder().setChannel(channel).setImportance(IMPORTANCE_DEFAULT).build() whenever(peopleNotificationIdentifier.getPeopleNotificationType(alertingEntry)) - .thenReturn(TYPE_PERSON) + .thenReturn(TYPE_PERSON) // put alerting people notifications in this section assertThat(peopleAlertingSectioner.isInSection(alertingEntry)).isTrue() - } + } @Test fun testInSilentPeopleSectionWhenTheImportanceIsLowerThanDefault() { // GIVEN - val silentEntry = NotificationEntryBuilder().setChannel(channel) - .setImportance(IMPORTANCE_LOW).build() + val silentEntry = + NotificationEntryBuilder().setChannel(channel).setImportance(IMPORTANCE_LOW).build() whenever(peopleNotificationIdentifier.getPeopleNotificationType(silentEntry)) - .thenReturn(TYPE_PERSON) + .thenReturn(TYPE_PERSON) // THEN put silent people notifications in this section assertThat(peopleSilentSectioner.isInSection(silentEntry)).isTrue() // People Alerting sectioning happens before the silent one. - // It claims high important conversations and rest of conversations will be considered as silent. + // It claims high important conversations and rest of conversations will be considered as + // silent. assertThat(peopleAlertingSectioner.isInSection(silentEntry)).isFalse() } @Test fun testNotInPeopleSection() { // GIVEN - val entry = NotificationEntryBuilder().setChannel(channel) - .setImportance(IMPORTANCE_LOW).build() - val importantEntry = NotificationEntryBuilder().setChannel(channel) - .setImportance(IMPORTANCE_HIGH).build() + val entry = + NotificationEntryBuilder().setChannel(channel).setImportance(IMPORTANCE_LOW).build() + val importantEntry = + NotificationEntryBuilder().setChannel(channel).setImportance(IMPORTANCE_HIGH).build() whenever(peopleNotificationIdentifier.getPeopleNotificationType(entry)) - .thenReturn(TYPE_NON_PERSON) + .thenReturn(TYPE_NON_PERSON) whenever(peopleNotificationIdentifier.getPeopleNotificationType(importantEntry)) - .thenReturn(TYPE_NON_PERSON) + .thenReturn(TYPE_NON_PERSON) // THEN - only put people notification either silent or alerting assertThat(peopleSilentSectioner.isInSection(entry)).isFalse() @@ -190,19 +185,23 @@ class ConversationCoordinatorTest : SysuiTestCase() { @Test fun testInAlertingPeopleSectionWhenThereIsAnImportantChild() { // GIVEN - val altChildA = NotificationEntryBuilder().setTag("A") - .setImportance(IMPORTANCE_DEFAULT).build() - val altChildB = NotificationEntryBuilder().setTag("B") - .setImportance(IMPORTANCE_LOW).build() - val summary = NotificationEntryBuilder().setId(2) - .setImportance(IMPORTANCE_LOW).setChannel(channel).build() - val groupEntry = GroupEntryBuilder() + val altChildA = + NotificationEntryBuilder().setTag("A").setImportance(IMPORTANCE_DEFAULT).build() + val altChildB = NotificationEntryBuilder().setTag("B").setImportance(IMPORTANCE_LOW).build() + val summary = + NotificationEntryBuilder() + .setId(2) + .setImportance(IMPORTANCE_LOW) + .setChannel(channel) + .build() + val groupEntry = + GroupEntryBuilder() .setParent(GroupEntry.ROOT_ENTRY) .setSummary(summary) .setChildren(listOf(altChildA, altChildB)) .build() whenever(peopleNotificationIdentifier.getPeopleNotificationType(summary)) - .thenReturn(TYPE_PERSON) + .thenReturn(TYPE_PERSON) // THEN assertThat(peopleAlertingSectioner.isInSection(groupEntry)).isTrue() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationLoggerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationLoggerTest.java index ff02ef3d4e62..b01281c4daa3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationLoggerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/logging/NotificationLoggerTest.java @@ -42,9 +42,9 @@ import androidx.test.filters.SmallTest; import com.android.internal.logging.InstanceId; import com.android.internal.statusbar.IStatusBarService; import com.android.internal.statusbar.NotificationVisibility; -import com.android.keyguard.TestScopeProvider; import com.android.systemui.SysuiTestCase; import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository; +import com.android.systemui.kosmos.KosmosJavaAdapter; import com.android.systemui.power.domain.interactor.PowerInteractor; import com.android.systemui.power.domain.interactor.PowerInteractorFactory; import com.android.systemui.scene.data.repository.WindowRootViewVisibilityRepository; @@ -110,7 +110,8 @@ public class NotificationLoggerTest extends SysuiTestCase { private FakeExecutor mUiBgExecutor = new FakeExecutor(new FakeSystemClock()); private NotificationPanelLoggerFake mNotificationPanelLoggerFake = new NotificationPanelLoggerFake(); - private final TestScope mTestScope = TestScopeProvider.getTestScope(); + private final KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this); + private final TestScope mTestScope = mKosmos.getTestScope(); private final FakeKeyguardRepository mKeyguardRepository = new FakeKeyguardRepository(); private final PowerInteractor mPowerInteractor = PowerInteractorFactory.create().getPowerInteractor(); @@ -133,7 +134,9 @@ public class NotificationLoggerTest extends SysuiTestCase { mKeyguardRepository, mHeadsUpManager, mPowerInteractor, - mActiveNotificationsInteractor); + mActiveNotificationsInteractor, + mKosmos.getFakeSceneContainerFlags(), + () -> mKosmos.getSceneInteractor()); mWindowRootViewVisibilityInteractor.setIsLockscreenOrShadeVisible(true); mEntry = new NotificationEntryBuilder() diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotifLayoutInflaterFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotifLayoutInflaterFactoryTest.kt index 3f7fc979b1e3..fd4192151c57 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotifLayoutInflaterFactoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotifLayoutInflaterFactoryTest.kt @@ -62,7 +62,7 @@ class NotifLayoutInflaterFactoryTest : SysuiTestCase() { fun onCreateView_noMatchingViewForName_returnNull() { // GIVEN we have ViewFactories that replaces TextViews in expanded and collapsed layouts val layoutType = FLAG_CONTENT_VIEW_EXPANDED - inflaterFactory = NotifLayoutInflaterFactory(row, layoutType, viewFactorySpies) + inflaterFactory = createNotifLayoutInflaterFactory(row, layoutType, viewFactorySpies) // WHEN we try to inflate an ImageView for the expanded layout val createdView = inflaterFactory.onCreateView("ImageView", context, attrs) @@ -78,7 +78,7 @@ class NotifLayoutInflaterFactoryTest : SysuiTestCase() { fun onCreateView_noMatchingViewForLayoutType_returnNull() { // GIVEN we have ViewFactories that replaces TextViews in expanded and collapsed layouts val layoutType = FLAG_CONTENT_VIEW_HEADS_UP - inflaterFactory = NotifLayoutInflaterFactory(row, layoutType, viewFactorySpies) + inflaterFactory = createNotifLayoutInflaterFactory(row, layoutType, viewFactorySpies) // WHEN we try to inflate a TextView for the heads-up layout val createdView = inflaterFactory.onCreateView("TextView", context, attrs) @@ -94,7 +94,7 @@ class NotifLayoutInflaterFactoryTest : SysuiTestCase() { fun onCreateView_matchingViews_returnReplacementView() { // GIVEN we have ViewFactories that replaces TextViews in expanded and collapsed layouts val layoutType = FLAG_CONTENT_VIEW_EXPANDED - inflaterFactory = NotifLayoutInflaterFactory(row, layoutType, viewFactorySpies) + inflaterFactory = createNotifLayoutInflaterFactory(row, layoutType, viewFactorySpies) // WHEN we try to inflate a TextView for the expanded layout val createdView = inflaterFactory.onCreateView("TextView", context, attrs) @@ -110,7 +110,7 @@ class NotifLayoutInflaterFactoryTest : SysuiTestCase() { // GIVEN we have two factories that replaces TextViews in expanded layouts val layoutType = FLAG_CONTENT_VIEW_EXPANDED inflaterFactory = - NotifLayoutInflaterFactory( + createNotifLayoutInflaterFactory( row, layoutType, setOf( @@ -147,4 +147,18 @@ class NotifLayoutInflaterFactoryTest : SysuiTestCase() { null } } + + private fun createNotifLayoutInflaterFactory( + row: ExpandableNotificationRow, + layoutType: Int, + notifRemoteViewsFactoryContainer: Set<NotifRemoteViewsFactory> + ) = + NotifLayoutInflaterFactory( + row, + layoutType, + object : NotifRemoteViewsFactoryContainer { + override val factories: Set<NotifRemoteViewsFactory> = + notifRemoteViewsFactoryContainer + } + ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java index 71613edb8737..65491937c285 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerTest.java @@ -69,9 +69,9 @@ import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.UiEventLogger; import com.android.internal.logging.testing.UiEventLoggerFake; import com.android.internal.statusbar.IStatusBarService; -import com.android.keyguard.TestScopeProvider; import com.android.systemui.SysuiTestCase; import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository; +import com.android.systemui.kosmos.KosmosJavaAdapter; import com.android.systemui.people.widget.PeopleSpaceWidgetManager; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin; @@ -124,7 +124,8 @@ public class NotificationGutsManagerTest extends SysuiTestCase { private NotificationChannel mTestNotificationChannel = new NotificationChannel( TEST_CHANNEL_ID, TEST_CHANNEL_ID, IMPORTANCE_DEFAULT); - private TestScope mTestScope = TestScopeProvider.getTestScope(); + private KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this); + private TestScope mTestScope = mKosmos.getTestScope(); private JavaAdapter mJavaAdapter = new JavaAdapter(mTestScope.getBackgroundScope()); private FakeExecutor mExecutor = new FakeExecutor(new FakeSystemClock()); private TestableLooper mTestableLooper; @@ -182,7 +183,10 @@ public class NotificationGutsManagerTest extends SysuiTestCase { new FakeKeyguardRepository(), mHeadsUpManager, PowerInteractorFactory.create().getPowerInteractor(), - mActiveNotificationsInteractor); + mActiveNotificationsInteractor, + mKosmos.getFakeSceneContainerFlags(), + () -> mKosmos.getSceneInteractor() + ); mGutsManager = new NotificationGutsManager( mContext, 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 new file mode 100644 index 000000000000..446b9d0bd434 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationGutsManagerWithScenesTest.kt @@ -0,0 +1,631 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.systemui.statusbar.notification.row + +import android.R +import android.app.AppOpsManager +import android.app.INotificationManager +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Intent +import android.content.pm.PackageManager +import android.content.pm.ShortcutManager +import android.content.pm.launcherApps +import android.graphics.Color +import android.os.Binder +import android.os.Handler +import android.os.userManager +import android.provider.Settings +import android.service.notification.NotificationListenerService.Ranking +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.testing.TestableLooper.RunWithLooper +import android.util.ArraySet +import android.view.View +import android.view.accessibility.accessibilityManager +import androidx.test.filters.SmallTest +import com.android.internal.logging.MetricsLogger +import com.android.internal.logging.UiEventLogger +import com.android.internal.logging.metricsLogger +import com.android.internal.logging.testing.UiEventLoggerFake +import com.android.internal.statusbar.statusBarService +import com.android.systemui.SysuiTestCase +import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.kosmos.testScope +import com.android.systemui.people.widget.PeopleSpaceWidgetManager +import com.android.systemui.plugins.activityStarter +import com.android.systemui.plugins.statusbar.NotificationMenuRowPlugin +import com.android.systemui.plugins.statusbar.statusBarStateController +import com.android.systemui.power.domain.interactor.PowerInteractorFactory.create +import com.android.systemui.scene.data.repository.WindowRootViewVisibilityRepository +import com.android.systemui.scene.domain.interactor.WindowRootViewVisibilityInteractor +import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags +import com.android.systemui.scene.shared.model.ObservableTransitionState +import com.android.systemui.scene.shared.model.SceneKey +import com.android.systemui.scene.shared.model.SceneModel +import com.android.systemui.settings.UserContextProvider +import com.android.systemui.shade.shadeControllerSceneImpl +import com.android.systemui.statusbar.NotificationEntryHelper +import com.android.systemui.statusbar.NotificationPresenter +import com.android.systemui.statusbar.notification.AssistantFeedbackController +import com.android.systemui.statusbar.notification.NotificationActivityStarter +import com.android.systemui.statusbar.notification.collection.provider.HighPriorityProvider +import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor +import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier +import com.android.systemui.statusbar.notification.stack.NotificationListContainer +import com.android.systemui.statusbar.notificationLockscreenUserManager +import com.android.systemui.statusbar.policy.deviceProvisionedController +import com.android.systemui.statusbar.policy.headsUpManager +import com.android.systemui.testKosmos +import com.android.systemui.util.concurrency.FakeExecutor +import com.android.systemui.util.kotlin.JavaAdapter +import com.android.systemui.util.time.FakeSystemClock +import com.android.systemui.wmshell.BubblesManager +import java.util.Optional +import junit.framework.Assert +import kotlin.test.assertEquals +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.runCurrent +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations +import org.mockito.invocation.InvocationOnMock + +/** Tests for [NotificationGutsManager] with the scene container enabled. */ +@SmallTest +@RunWith(AndroidTestingRunner::class) +@RunWithLooper +class NotificationGutsManagerWithScenesTest : SysuiTestCase() { + private val testNotificationChannel = + NotificationChannel( + TEST_CHANNEL_ID, + TEST_CHANNEL_ID, + NotificationManager.IMPORTANCE_DEFAULT + ) + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val javaAdapter = JavaAdapter(testScope.backgroundScope) + private val executor = FakeExecutor(FakeSystemClock()) + private lateinit var testableLooper: TestableLooper + private lateinit var handler: Handler + private lateinit var helper: NotificationTestHelper + private lateinit var gutsManager: NotificationGutsManager + private lateinit var windowRootViewVisibilityInteractor: WindowRootViewVisibilityInteractor + + private val metricsLogger = kosmos.metricsLogger + private val deviceProvisionedController = kosmos.deviceProvisionedController + private val accessibilityManager = kosmos.accessibilityManager + private val mBarService = kosmos.statusBarService + private val launcherApps = kosmos.launcherApps + private val shadeController = kosmos.shadeControllerSceneImpl + private val notificationLockscreenUserManager = kosmos.notificationLockscreenUserManager + private val statusBarStateController = kosmos.statusBarStateController + private val headsUpManager = kosmos.headsUpManager + private val activityStarter = kosmos.activityStarter + private val userManager = kosmos.userManager + private val activeNotificationsInteractor = kosmos.activeNotificationsInteractor + private val sceneInteractor = kosmos.sceneInteractor + + @Mock private lateinit var onUserInteractionCallback: OnUserInteractionCallback + @Mock private lateinit var presenter: NotificationPresenter + @Mock private lateinit var notificationActivityStarter: NotificationActivityStarter + @Mock private lateinit var notificationListContainer: NotificationListContainer + @Mock + private lateinit var onSettingsClickListener: NotificationGutsManager.OnSettingsClickListener + @Mock private lateinit var highPriorityProvider: HighPriorityProvider + @Mock private lateinit var notificationManager: INotificationManager + @Mock private lateinit var shortcutManager: ShortcutManager + @Mock private lateinit var channelEditorDialogController: ChannelEditorDialogController + @Mock private lateinit var peopleNotificationIdentifier: PeopleNotificationIdentifier + @Mock private lateinit var contextTracker: UserContextProvider + @Mock private lateinit var bubblesManager: BubblesManager + @Mock private lateinit var peopleSpaceWidgetManager: PeopleSpaceWidgetManager + @Mock private lateinit var assistantFeedbackController: AssistantFeedbackController + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + val sceneContainerFlags = kosmos.fakeSceneContainerFlags + sceneContainerFlags.enabled = true + testableLooper = TestableLooper.get(this) + allowTestableLooperAsMainThread() + handler = Handler.createAsync(testableLooper.getLooper()) + helper = NotificationTestHelper(mContext, mDependency, TestableLooper.get(this)) + Mockito.`when`(accessibilityManager.isTouchExplorationEnabled).thenReturn(false) + windowRootViewVisibilityInteractor = + WindowRootViewVisibilityInteractor( + testScope.backgroundScope, + WindowRootViewVisibilityRepository(mBarService, executor), + FakeKeyguardRepository(), + headsUpManager, + create().powerInteractor, + activeNotificationsInteractor, + sceneContainerFlags, + { sceneInteractor }, + ) + gutsManager = + NotificationGutsManager( + mContext, + handler, + handler, + javaAdapter, + accessibilityManager, + highPriorityProvider, + notificationManager, + userManager, + peopleSpaceWidgetManager, + launcherApps, + shortcutManager, + channelEditorDialogController, + contextTracker, + assistantFeedbackController, + Optional.of(bubblesManager), + UiEventLoggerFake(), + onUserInteractionCallback, + shadeController, + windowRootViewVisibilityInteractor, + notificationLockscreenUserManager, + statusBarStateController, + mBarService, + deviceProvisionedController, + metricsLogger, + headsUpManager, + activityStarter + ) + gutsManager.setUpWithPresenter( + presenter, + notificationListContainer, + onSettingsClickListener + ) + gutsManager.setNotificationActivityStarter(notificationActivityStarter) + gutsManager.start() + } + + @Test + fun testOpenAndCloseGuts() { + val guts = Mockito.spy(NotificationGuts(mContext)) + Mockito.`when`(guts.post(ArgumentMatchers.any())).thenAnswer { invocation: InvocationOnMock + -> + handler.post((invocation.arguments[0] as Runnable)) + null + } + + // Test doesn't support animation since the guts view is not attached. + Mockito.doNothing() + .`when`(guts) + .openControls( + ArgumentMatchers.anyInt(), + ArgumentMatchers.anyInt(), + ArgumentMatchers.anyBoolean(), + ArgumentMatchers.any(Runnable::class.java) + ) + val realRow = createTestNotificationRow() + val menuItem = createTestMenuItem(realRow) + val row = Mockito.spy(realRow) + Mockito.`when`(row!!.windowToken).thenReturn(Binder()) + Mockito.`when`(row.guts).thenReturn(guts) + Assert.assertTrue(gutsManager.openGutsInternal(row, 0, 0, menuItem)) + assertEquals(View.INVISIBLE.toLong(), guts.visibility.toLong()) + testableLooper.processAllMessages() + verify(guts) + .openControls( + ArgumentMatchers.anyInt(), + ArgumentMatchers.anyInt(), + ArgumentMatchers.anyBoolean(), + ArgumentMatchers.any(Runnable::class.java) + ) + verify(headsUpManager).setGutsShown(realRow!!.entry, true) + assertEquals(View.VISIBLE.toLong(), guts.visibility.toLong()) + gutsManager.closeAndSaveGuts(false, false, true, 0, 0, false) + verify(guts) + .closeControls( + ArgumentMatchers.anyBoolean(), + ArgumentMatchers.anyBoolean(), + ArgumentMatchers.anyInt(), + ArgumentMatchers.anyInt(), + ArgumentMatchers.anyBoolean() + ) + verify(row, Mockito.times(1)).setGutsView(ArgumentMatchers.any()) + testableLooper.processAllMessages() + verify(headsUpManager).setGutsShown(realRow.entry, false) + } + + @Test + fun testLockscreenShadeVisible_visible_gutsNotClosed() { + // First, start out lockscreen or shade as not visible + setIsLockscreenOrShadeVisible(false) + testScope.testScheduler.runCurrent() + val guts = Mockito.mock(NotificationGuts::class.java) + gutsManager.exposedGuts = guts + + // WHEN the lockscreen or shade becomes visible + setIsLockscreenOrShadeVisible(true) + testScope.testScheduler.runCurrent() + + // THEN the guts are not closed + verify(guts, Mockito.never()).removeCallbacks(ArgumentMatchers.any()) + verify(guts, Mockito.never()) + .closeControls( + ArgumentMatchers.anyBoolean(), + ArgumentMatchers.anyBoolean(), + ArgumentMatchers.anyInt(), + ArgumentMatchers.anyInt(), + ArgumentMatchers.anyBoolean() + ) + } + + @Test + fun testLockscreenShadeVisible_notVisible_gutsClosed() { + // First, start out lockscreen or shade as visible + setIsLockscreenOrShadeVisible(true) + testScope.testScheduler.runCurrent() + val guts = Mockito.mock(NotificationGuts::class.java) + gutsManager.exposedGuts = guts + + // WHEN the lockscreen or shade is no longer visible + setIsLockscreenOrShadeVisible(false) + testScope.testScheduler.runCurrent() + + // THEN the guts are closed + verify(guts).removeCallbacks(ArgumentMatchers.any()) + verify(guts) + .closeControls( + /* leavebehinds= */ ArgumentMatchers.eq(true), + /* controls= */ ArgumentMatchers.eq(true), + /* x= */ ArgumentMatchers.anyInt(), + /* y= */ ArgumentMatchers.anyInt(), + /* force= */ ArgumentMatchers.eq(true) + ) + } + + @Test + fun testLockscreenShadeVisible_notVisible_listContainerReset() { + // First, start out lockscreen or shade as visible + setIsLockscreenOrShadeVisible(true) + testScope.testScheduler.runCurrent() + + // WHEN the lockscreen or shade is no longer visible + setIsLockscreenOrShadeVisible(false) + testScope.testScheduler.runCurrent() + + // THEN the list container is reset + verify(notificationListContainer) + .resetExposedMenuView(ArgumentMatchers.anyBoolean(), ArgumentMatchers.anyBoolean()) + } + + @Test + fun testChangeDensityOrFontScale() { + val guts = Mockito.spy(NotificationGuts(mContext)) + Mockito.`when`(guts.post(ArgumentMatchers.any())).thenAnswer { invocation: InvocationOnMock + -> + handler.post((invocation.arguments[0] as Runnable)) + null + } + + // Test doesn't support animation since the guts view is not attached. + Mockito.doNothing() + .`when`(guts) + .openControls( + ArgumentMatchers.anyInt(), + ArgumentMatchers.anyInt(), + ArgumentMatchers.anyBoolean(), + ArgumentMatchers.any(Runnable::class.java) + ) + val realRow = createTestNotificationRow() + val menuItem = createTestMenuItem(realRow) + val row = Mockito.spy(realRow) + Mockito.`when`(row!!.windowToken).thenReturn(Binder()) + Mockito.`when`(row.guts).thenReturn(guts) + Mockito.doNothing().`when`(row).ensureGutsInflated() + val realEntry = realRow!!.entry + val entry = Mockito.spy(realEntry) + Mockito.`when`(entry.row).thenReturn(row) + Mockito.`when`(entry.getGuts()).thenReturn(guts) + Assert.assertTrue(gutsManager.openGutsInternal(row, 0, 0, menuItem)) + testableLooper.processAllMessages() + verify(guts) + .openControls( + ArgumentMatchers.anyInt(), + ArgumentMatchers.anyInt(), + ArgumentMatchers.anyBoolean(), + ArgumentMatchers.any(Runnable::class.java) + ) + + // called once by mGutsManager.bindGuts() in mGutsManager.openGuts() + verify(row).setGutsView(ArgumentMatchers.any()) + row.onDensityOrFontScaleChanged() + gutsManager.onDensityOrFontScaleChanged(entry) + testableLooper.processAllMessages() + gutsManager.closeAndSaveGuts(false, false, false, 0, 0, false) + verify(guts) + .closeControls( + ArgumentMatchers.anyBoolean(), + ArgumentMatchers.anyBoolean(), + ArgumentMatchers.anyInt(), + ArgumentMatchers.anyInt(), + ArgumentMatchers.anyBoolean() + ) + + // called again by mGutsManager.bindGuts(), in mGutsManager.onDensityOrFontScaleChanged() + verify(row, Mockito.times(2)).setGutsView(ArgumentMatchers.any()) + } + + @Test + fun testAppOpsSettingsIntent_camera() { + val ops = ArraySet<Int>() + ops.add(AppOpsManager.OP_CAMERA) + gutsManager.startAppOpsSettingsActivity("", 0, ops, null) + val captor = ArgumentCaptor.forClass(Intent::class.java) + verify(notificationActivityStarter, Mockito.times(1)) + .startNotificationGutsIntent( + captor.capture(), + ArgumentMatchers.anyInt(), + ArgumentMatchers.any() + ) + assertEquals(Intent.ACTION_MANAGE_APP_PERMISSIONS, captor.value.action) + } + + @Test + fun testAppOpsSettingsIntent_mic() { + val ops = ArraySet<Int>() + ops.add(AppOpsManager.OP_RECORD_AUDIO) + gutsManager.startAppOpsSettingsActivity("", 0, ops, null) + val captor = ArgumentCaptor.forClass(Intent::class.java) + verify(notificationActivityStarter, Mockito.times(1)) + .startNotificationGutsIntent( + captor.capture(), + ArgumentMatchers.anyInt(), + ArgumentMatchers.any() + ) + assertEquals(Intent.ACTION_MANAGE_APP_PERMISSIONS, captor.value.action) + } + + @Test + fun testAppOpsSettingsIntent_camera_mic() { + val ops = ArraySet<Int>() + ops.add(AppOpsManager.OP_CAMERA) + ops.add(AppOpsManager.OP_RECORD_AUDIO) + gutsManager.startAppOpsSettingsActivity("", 0, ops, null) + val captor = ArgumentCaptor.forClass(Intent::class.java) + verify(notificationActivityStarter, Mockito.times(1)) + .startNotificationGutsIntent( + captor.capture(), + ArgumentMatchers.anyInt(), + ArgumentMatchers.any() + ) + assertEquals(Intent.ACTION_MANAGE_APP_PERMISSIONS, captor.value.action) + } + + @Test + fun testAppOpsSettingsIntent_overlay() { + val ops = ArraySet<Int>() + ops.add(AppOpsManager.OP_SYSTEM_ALERT_WINDOW) + gutsManager.startAppOpsSettingsActivity("", 0, ops, null) + val captor = ArgumentCaptor.forClass(Intent::class.java) + verify(notificationActivityStarter, Mockito.times(1)) + .startNotificationGutsIntent( + captor.capture(), + ArgumentMatchers.anyInt(), + ArgumentMatchers.any() + ) + assertEquals(Settings.ACTION_MANAGE_APP_OVERLAY_PERMISSION, captor.value.action) + } + + @Test + fun testAppOpsSettingsIntent_camera_mic_overlay() { + val ops = ArraySet<Int>() + ops.add(AppOpsManager.OP_CAMERA) + ops.add(AppOpsManager.OP_RECORD_AUDIO) + ops.add(AppOpsManager.OP_SYSTEM_ALERT_WINDOW) + gutsManager.startAppOpsSettingsActivity("", 0, ops, null) + val captor = ArgumentCaptor.forClass(Intent::class.java) + verify(notificationActivityStarter, Mockito.times(1)) + .startNotificationGutsIntent( + captor.capture(), + ArgumentMatchers.anyInt(), + ArgumentMatchers.any() + ) + assertEquals(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, captor.value.action) + } + + @Test + fun testAppOpsSettingsIntent_camera_overlay() { + val ops = ArraySet<Int>() + ops.add(AppOpsManager.OP_CAMERA) + ops.add(AppOpsManager.OP_SYSTEM_ALERT_WINDOW) + gutsManager.startAppOpsSettingsActivity("", 0, ops, null) + val captor = ArgumentCaptor.forClass(Intent::class.java) + verify(notificationActivityStarter, Mockito.times(1)) + .startNotificationGutsIntent( + captor.capture(), + ArgumentMatchers.anyInt(), + ArgumentMatchers.any() + ) + assertEquals(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, captor.value.action) + } + + @Test + fun testAppOpsSettingsIntent_mic_overlay() { + val ops = ArraySet<Int>() + ops.add(AppOpsManager.OP_RECORD_AUDIO) + ops.add(AppOpsManager.OP_SYSTEM_ALERT_WINDOW) + gutsManager.startAppOpsSettingsActivity("", 0, ops, null) + val captor = ArgumentCaptor.forClass(Intent::class.java) + verify(notificationActivityStarter, Mockito.times(1)) + .startNotificationGutsIntent( + captor.capture(), + ArgumentMatchers.anyInt(), + ArgumentMatchers.any() + ) + assertEquals(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, captor.value.action) + } + + @Test + @Throws(Exception::class) + fun testInitializeNotificationInfoView_highPriority() { + val notificationInfoView = Mockito.mock(NotificationInfo::class.java) + val row = Mockito.spy(helper.createRow()) + val entry = row.entry + NotificationEntryHelper.modifyRanking(entry) + .setUserSentiment(Ranking.USER_SENTIMENT_NEGATIVE) + .setImportance(NotificationManager.IMPORTANCE_HIGH) + .build() + Mockito.`when`(row.getIsNonblockable()).thenReturn(false) + Mockito.`when`(highPriorityProvider.isHighPriority(entry)).thenReturn(true) + val statusBarNotification = entry.sbn + gutsManager.initializeNotificationInfo(row, notificationInfoView) + verify(notificationInfoView) + .bindNotification( + ArgumentMatchers.any(PackageManager::class.java), + ArgumentMatchers.any(INotificationManager::class.java), + ArgumentMatchers.eq(onUserInteractionCallback), + ArgumentMatchers.eq(channelEditorDialogController), + ArgumentMatchers.eq(statusBarNotification.packageName), + ArgumentMatchers.any(NotificationChannel::class.java), + ArgumentMatchers.eq(entry), + ArgumentMatchers.any(NotificationInfo.OnSettingsClickListener::class.java), + ArgumentMatchers.any(NotificationInfo.OnAppSettingsClickListener::class.java), + ArgumentMatchers.any(UiEventLogger::class.java), + ArgumentMatchers.eq(true), + ArgumentMatchers.eq(false), + ArgumentMatchers.eq(true), /* wasShownHighPriority */ + ArgumentMatchers.eq(assistantFeedbackController), + ArgumentMatchers.any(MetricsLogger::class.java) + ) + } + + @Test + @Throws(Exception::class) + fun testInitializeNotificationInfoView_PassesAlongProvisionedState() { + val notificationInfoView = Mockito.mock(NotificationInfo::class.java) + val row = Mockito.spy(helper.createRow()) + NotificationEntryHelper.modifyRanking(row.entry) + .setUserSentiment(Ranking.USER_SENTIMENT_NEGATIVE) + .build() + Mockito.`when`(row.getIsNonblockable()).thenReturn(false) + val statusBarNotification = row.entry.sbn + val entry = row.entry + gutsManager.initializeNotificationInfo(row, notificationInfoView) + verify(notificationInfoView) + .bindNotification( + ArgumentMatchers.any(PackageManager::class.java), + ArgumentMatchers.any(INotificationManager::class.java), + ArgumentMatchers.eq(onUserInteractionCallback), + ArgumentMatchers.eq(channelEditorDialogController), + ArgumentMatchers.eq(statusBarNotification.packageName), + ArgumentMatchers.any(NotificationChannel::class.java), + ArgumentMatchers.eq(entry), + ArgumentMatchers.any(NotificationInfo.OnSettingsClickListener::class.java), + ArgumentMatchers.any(NotificationInfo.OnAppSettingsClickListener::class.java), + ArgumentMatchers.any(UiEventLogger::class.java), + ArgumentMatchers.eq(true), + ArgumentMatchers.eq(false), + ArgumentMatchers.eq(false), /* wasShownHighPriority */ + ArgumentMatchers.eq(assistantFeedbackController), + ArgumentMatchers.any(MetricsLogger::class.java) + ) + } + + @Test + @Throws(Exception::class) + fun testInitializeNotificationInfoView_withInitialAction() { + val notificationInfoView = Mockito.mock(NotificationInfo::class.java) + val row = Mockito.spy(helper.createRow()) + NotificationEntryHelper.modifyRanking(row.entry) + .setUserSentiment(Ranking.USER_SENTIMENT_NEGATIVE) + .build() + Mockito.`when`(row.getIsNonblockable()).thenReturn(false) + val statusBarNotification = row.entry.sbn + val entry = row.entry + gutsManager.initializeNotificationInfo(row, notificationInfoView) + verify(notificationInfoView) + .bindNotification( + ArgumentMatchers.any(PackageManager::class.java), + ArgumentMatchers.any(INotificationManager::class.java), + ArgumentMatchers.eq(onUserInteractionCallback), + ArgumentMatchers.eq(channelEditorDialogController), + ArgumentMatchers.eq(statusBarNotification.packageName), + ArgumentMatchers.any(NotificationChannel::class.java), + ArgumentMatchers.eq(entry), + ArgumentMatchers.any(NotificationInfo.OnSettingsClickListener::class.java), + ArgumentMatchers.any(NotificationInfo.OnAppSettingsClickListener::class.java), + ArgumentMatchers.any(UiEventLogger::class.java), + ArgumentMatchers.eq(true), + ArgumentMatchers.eq(false), + ArgumentMatchers.eq(false), /* wasShownHighPriority */ + ArgumentMatchers.eq(assistantFeedbackController), + ArgumentMatchers.any(MetricsLogger::class.java) + ) + } + + private fun createTestNotificationRow(): ExpandableNotificationRow? { + val nb = + Notification.Builder(mContext, testNotificationChannel.id) + .setContentTitle("foo") + .setColorized(true) + .setColor(Color.RED) + .setFlag(Notification.FLAG_CAN_COLORIZE, true) + .setSmallIcon(R.drawable.sym_def_app_icon) + return try { + val row = helper.createRow(nb.build()) + NotificationEntryHelper.modifyRanking(row.entry) + .setChannel(testNotificationChannel) + .build() + row + } catch (e: Exception) { + org.junit.Assert.fail() + null + } + } + + private fun setIsLockscreenOrShadeVisible(isVisible: Boolean) { + val key = + if (isVisible) { + SceneKey.Lockscreen + } else { + SceneKey.Bouncer + } + sceneInteractor.changeScene(SceneModel(key), "test") + sceneInteractor.setTransitionState( + MutableStateFlow<ObservableTransitionState>(ObservableTransitionState.Idle(key)) + ) + testScope.runCurrent() + } + + private fun createTestMenuItem( + row: ExpandableNotificationRow? + ): NotificationMenuRowPlugin.MenuItem { + val menuRow: NotificationMenuRowPlugin = + NotificationMenuRow(mContext, peopleNotificationIdentifier) + menuRow.createMenu(row, row!!.entry.sbn) + val menuItem = menuRow.getLongpressMenuItem(mContext) + Assert.assertNotNull(menuItem) + return menuItem + } + + companion object { + private const val TEST_CHANNEL_ID = "NotificationManagerServiceTestChannelId" + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt index a824bc4f803f..06298b78ae57 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt @@ -25,6 +25,9 @@ import com.android.systemui.Flags.FLAG_CENTRALIZED_STATUS_BAR_DIMENS_REFACTOR import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.NotificationContainerBounds import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository +import com.android.systemui.communal.domain.interactor.communalInteractor +import com.android.systemui.communal.shared.model.CommunalSceneKey +import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.Flags import com.android.systemui.flags.fakeFeatureFlagsClassic @@ -33,20 +36,19 @@ import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepos import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.StatusBarState -import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE_LOCKED import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.keyguard.ui.viewmodel.keyguardRootViewModel import com.android.systemui.kosmos.testScope import com.android.systemui.res.R import com.android.systemui.shade.data.repository.shadeRepository -import com.android.systemui.shade.largeScreenHeaderHelper import com.android.systemui.shade.mockLargeScreenHeaderHelper import com.android.systemui.statusbar.notification.stack.domain.interactor.sharedNotificationContainerInteractor import com.android.systemui.testKosmos import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest @@ -68,6 +70,7 @@ class SharedNotificationContainerViewModelTest : SysuiTestCase() { val keyguardInteractor = kosmos.keyguardInteractor val keyguardRootViewModel = kosmos.keyguardRootViewModel val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository + val communalInteractor = kosmos.communalInteractor val shadeRepository = kosmos.shadeRepository val sharedNotificationContainerInteractor = kosmos.sharedNotificationContainerInteractor val largeScreenHeaderHelper = kosmos.mockLargeScreenHeaderHelper @@ -214,6 +217,61 @@ class SharedNotificationContainerViewModelTest : SysuiTestCase() { } @Test + fun glanceableHubAlpha() = + testScope.runTest { + val alpha by collectLastValue(underTest.glanceableHubAlpha) + + // Start on lockscreen + showLockscreen() + assertThat(alpha).isEqualTo(1f) + + // Start transitioning to glanceable hub + val progress = 0.6f + keyguardTransitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.STARTED, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GLANCEABLE_HUB, + value = 0f, + ) + ) + runCurrent() + keyguardTransitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.RUNNING, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GLANCEABLE_HUB, + value = progress, + ) + ) + runCurrent() + // Expected alpha is inverse of progress as notifications are fading away + assertThat(alpha).isEqualTo(1 - progress) + + // Finish transition to glanceable hub + keyguardTransitionRepository.sendTransitionStep( + TransitionStep( + transitionState = TransitionState.FINISHED, + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.GLANCEABLE_HUB, + value = 1f, + ) + ) + val idleTransitionState = + MutableStateFlow<ObservableCommunalTransitionState>( + ObservableCommunalTransitionState.Idle(CommunalSceneKey.Communal) + ) + communalInteractor.setTransitionState(idleTransitionState) + runCurrent() + assertThat(alpha).isEqualTo(0f) + + // While state is GLANCEABLE_HUB, verify alpha is restored to full if glanceable hub is + // not fully visible. + shadeRepository.setLockscreenShadeExpansion(0.1f) + assertThat(alpha).isEqualTo(1f) + } + + @Test fun validateMarginTop() = testScope.runTest { overrideResource(R.bool.config_use_large_screen_shade_header, false) @@ -302,6 +360,43 @@ class SharedNotificationContainerViewModelTest : SysuiTestCase() { } @Test + fun isOnGlanceableHubWithoutShade() = + testScope.runTest { + val isOnGlanceableHubWithoutShade by + collectLastValue(underTest.isOnGlanceableHubWithoutShade) + + // Start on lockscreen + showLockscreen() + assertThat(isOnGlanceableHubWithoutShade).isFalse() + + // Move to glanceable hub + val idleTransitionState = + MutableStateFlow<ObservableCommunalTransitionState>( + ObservableCommunalTransitionState.Idle(CommunalSceneKey.Communal) + ) + communalInteractor.setTransitionState(idleTransitionState) + runCurrent() + assertThat(isOnGlanceableHubWithoutShade).isTrue() + + // While state is GLANCEABLE_HUB, validate variations of both shade and qs expansion + shadeRepository.setLockscreenShadeExpansion(0.1f) + shadeRepository.setQsExpansion(0f) + assertThat(isOnGlanceableHubWithoutShade).isFalse() + + shadeRepository.setLockscreenShadeExpansion(0.1f) + shadeRepository.setQsExpansion(0.1f) + assertThat(isOnGlanceableHubWithoutShade).isFalse() + + shadeRepository.setLockscreenShadeExpansion(0f) + shadeRepository.setQsExpansion(0.1f) + assertThat(isOnGlanceableHubWithoutShade).isFalse() + + shadeRepository.setLockscreenShadeExpansion(0f) + shadeRepository.setQsExpansion(0f) + assertThat(isOnGlanceableHubWithoutShade).isTrue() + } + + @Test fun boundsOnLockscreenNotInSplitShade() = testScope.runTest { val bounds by collectLastValue(underTest.bounds) diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java index d9eaea1367cd..b3a47d77a307 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java @@ -48,7 +48,6 @@ import android.content.Context; import android.content.res.ColorStateList; import android.content.res.TypedArray; import android.graphics.Color; -import android.os.Handler; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.util.MathUtils; @@ -135,7 +134,7 @@ public class ScrimControllerTest extends SysuiTestCase { @Mock private AlarmManager mAlarmManager; @Mock private DozeParameters mDozeParameters; @Mock private LightBarController mLightBarController; - @Mock private DelayedWakeLock.Builder mDelayedWakeLockBuilder; + @Mock private DelayedWakeLock.Factory mDelayedWakeLockFactory; @Mock private DelayedWakeLock mWakeLock; @Mock private KeyguardStateController mKeyguardStateController; @Mock private KeyguardUpdateMonitor mKeyguardUpdateMonitor; @@ -262,11 +261,7 @@ public class ScrimControllerTest extends SysuiTestCase { }).when(mLightBarController).setScrimState( any(ScrimState.class), anyFloat(), any(GradientColors.class)); - when(mDelayedWakeLockBuilder.setHandler(any(Handler.class))) - .thenReturn(mDelayedWakeLockBuilder); - when(mDelayedWakeLockBuilder.setTag(any(String.class))) - .thenReturn(mDelayedWakeLockBuilder); - when(mDelayedWakeLockBuilder.build()).thenReturn(mWakeLock); + when(mDelayedWakeLockFactory.create(any(String.class))).thenReturn(mWakeLock); when(mDockManager.isDocked()).thenReturn(false); when(mKeyguardTransitionInteractor.transition(any(), any())) @@ -281,7 +276,7 @@ public class ScrimControllerTest extends SysuiTestCase { mDozeParameters, mAlarmManager, mKeyguardStateController, - mDelayedWakeLockBuilder, + mDelayedWakeLockFactory, new FakeHandler(mLooper.getLooper()), mKeyguardUpdateMonitor, mDockManager, @@ -990,7 +985,7 @@ public class ScrimControllerTest extends SysuiTestCase { mDozeParameters, mAlarmManager, mKeyguardStateController, - mDelayedWakeLockBuilder, + mDelayedWakeLockFactory, new FakeHandler(mLooper.getLooper()), mKeyguardUpdateMonitor, mDockManager, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt index 20d5c5de3ffa..49953a1176fd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/MobileIconInteractorTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.statusbar.pipeline.mobile.domain.interactor +import android.platform.test.annotations.EnableFlags import android.telephony.CellSignalStrength import android.telephony.SubscriptionManager.PROFILE_CLASS_UNSET import android.telephony.TelephonyManager.NETWORK_TYPE_UNKNOWN @@ -159,10 +160,13 @@ class MobileIconInteractorTest : SysuiTestCase() { } @Test - fun numberOfLevels_comesFromRepo() = + fun numberOfLevels_comesFromRepo_whenApplicable() = testScope.runTest { var latest: Int? = null - val job = underTest.signalLevelIcon.onEach { latest = it.numberOfLevels }.launchIn(this) + val job = + underTest.signalLevelIcon + .onEach { latest = (it as? SignalIconModel.Cellular)?.numberOfLevels } + .launchIn(this) connectionRepository.numberOfLevels.value = 5 assertThat(latest).isEqualTo(5) @@ -491,14 +495,19 @@ class MobileIconInteractorTest : SysuiTestCase() { } @Test - fun iconId_correctLevel_notCutout() = + fun cellBasedIconId_correctLevel_notCutout() = testScope.runTest { + connectionRepository.isNonTerrestrial.value = false connectionRepository.isInService.value = true connectionRepository.primaryLevel.value = 1 connectionRepository.setDataEnabled(false) + connectionRepository.isNonTerrestrial.value = false - var latest: SignalIconModel? = null - val job = underTest.signalLevelIcon.onEach { latest = it }.launchIn(this) + var latest: SignalIconModel.Cellular? = null + val job = + underTest.signalLevelIcon + .onEach { latest = it as? SignalIconModel.Cellular } + .launchIn(this) assertThat(latest?.level).isEqualTo(1) assertThat(latest?.showExclamationMark).isFalse() @@ -509,6 +518,7 @@ class MobileIconInteractorTest : SysuiTestCase() { @Test fun icon_usesLevelFromInteractor() = testScope.runTest { + connectionRepository.isNonTerrestrial.value = false connectionRepository.isInService.value = true var latest: SignalIconModel? = null @@ -524,10 +534,15 @@ class MobileIconInteractorTest : SysuiTestCase() { } @Test - fun icon_usesNumberOfLevelsFromInteractor() = + fun cellBasedIcon_usesNumberOfLevelsFromInteractor() = testScope.runTest { - var latest: SignalIconModel? = null - val job = underTest.signalLevelIcon.onEach { latest = it }.launchIn(this) + connectionRepository.isNonTerrestrial.value = false + + var latest: SignalIconModel.Cellular? = null + val job = + underTest.signalLevelIcon + .onEach { latest = it as? SignalIconModel.Cellular } + .launchIn(this) connectionRepository.numberOfLevels.value = 5 assertThat(latest!!.numberOfLevels).isEqualTo(5) @@ -539,12 +554,16 @@ class MobileIconInteractorTest : SysuiTestCase() { } @Test - fun icon_defaultDataDisabled_showExclamationTrue() = + fun cellBasedIcon_defaultDataDisabled_showExclamationTrue() = testScope.runTest { + connectionRepository.isNonTerrestrial.value = false mobileIconsInteractor.activeDataConnectionHasDataEnabled.value = false - var latest: SignalIconModel? = null - val job = underTest.signalLevelIcon.onEach { latest = it }.launchIn(this) + var latest: SignalIconModel.Cellular? = null + val job = + underTest.signalLevelIcon + .onEach { latest = it as? SignalIconModel.Cellular } + .launchIn(this) assertThat(latest!!.showExclamationMark).isTrue() @@ -552,12 +571,16 @@ class MobileIconInteractorTest : SysuiTestCase() { } @Test - fun icon_defaultConnectionFailed_showExclamationTrue() = + fun cellBasedIcon_defaultConnectionFailed_showExclamationTrue() = testScope.runTest { + connectionRepository.isNonTerrestrial.value = false mobileIconsInteractor.isDefaultConnectionFailed.value = true - var latest: SignalIconModel? = null - val job = underTest.signalLevelIcon.onEach { latest = it }.launchIn(this) + var latest: SignalIconModel.Cellular? = null + val job = + underTest.signalLevelIcon + .onEach { latest = it as? SignalIconModel.Cellular } + .launchIn(this) assertThat(latest!!.showExclamationMark).isTrue() @@ -565,14 +588,18 @@ class MobileIconInteractorTest : SysuiTestCase() { } @Test - fun icon_enabledAndNotFailed_showExclamationFalse() = + fun cellBasedIcon_enabledAndNotFailed_showExclamationFalse() = testScope.runTest { + connectionRepository.isNonTerrestrial.value = false connectionRepository.isInService.value = true mobileIconsInteractor.activeDataConnectionHasDataEnabled.value = true mobileIconsInteractor.isDefaultConnectionFailed.value = false - var latest: SignalIconModel? = null - val job = underTest.signalLevelIcon.onEach { latest = it }.launchIn(this) + var latest: SignalIconModel.Cellular? = null + val job = + underTest.signalLevelIcon + .onEach { latest = it as? SignalIconModel.Cellular } + .launchIn(this) assertThat(latest!!.showExclamationMark).isFalse() @@ -580,11 +607,15 @@ class MobileIconInteractorTest : SysuiTestCase() { } @Test - fun icon_usesEmptyState_whenNotInService() = + fun cellBasedIcon_usesEmptyState_whenNotInService() = testScope.runTest { - var latest: SignalIconModel? = null - val job = underTest.signalLevelIcon.onEach { latest = it }.launchIn(this) + var latest: SignalIconModel.Cellular? = null + val job = + underTest.signalLevelIcon + .onEach { latest = it as? SignalIconModel.Cellular } + .launchIn(this) + connectionRepository.isNonTerrestrial.value = false connectionRepository.isInService.value = false assertThat(latest?.level).isEqualTo(0) @@ -604,11 +635,15 @@ class MobileIconInteractorTest : SysuiTestCase() { } @Test - fun icon_usesCarrierNetworkState_whenInCarrierNetworkChangeMode() = + fun cellBasedIcon_usesCarrierNetworkState_whenInCarrierNetworkChangeMode() = testScope.runTest { - var latest: SignalIconModel? = null - val job = underTest.signalLevelIcon.onEach { latest = it }.launchIn(this) + var latest: SignalIconModel.Cellular? = null + val job = + underTest.signalLevelIcon + .onEach { latest = it as? SignalIconModel.Cellular? } + .launchIn(this) + connectionRepository.isNonTerrestrial.value = false connectionRepository.isInService.value = true connectionRepository.carrierNetworkChangeActive.value = true connectionRepository.primaryLevel.value = 1 @@ -626,6 +661,20 @@ class MobileIconInteractorTest : SysuiTestCase() { job.cancel() } + @EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG) + @Test + fun satBasedIcon_isUsedWhenNonTerrestrial() = + testScope.runTest { + val latest by collectLastValue(underTest.signalLevelIcon) + + // Start off using cellular + assertThat(latest).isInstanceOf(SignalIconModel.Cellular::class.java) + + connectionRepository.isNonTerrestrial.value = true + + assertThat(latest).isInstanceOf(SignalIconModel.Satellite::class.java) + } + private fun createInteractor( overrides: MobileIconCarrierIdOverrides = MobileIconCarrierIdOverridesImpl() ) = diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/model/SignalIconModelParameterizedTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/model/SignalIconModelParameterizedTest.kt index 90a894648c9f..ebec00380cf4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/model/SignalIconModelParameterizedTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/model/SignalIconModelParameterizedTest.kt @@ -32,7 +32,7 @@ internal class SignalIconModelParameterizedTest(private val testCase: TestCase) @Test fun drawableFromModel_level0_numLevels4_noExclamation_notCarrierNetworkChange() { val model = - SignalIconModel( + SignalIconModel.Cellular( level = 0, numberOfLevels = 4, showExclamationMark = false, @@ -59,7 +59,7 @@ internal class SignalIconModelParameterizedTest(private val testCase: TestCase) val expected: Int, ) { fun toSignalIconModel() = - SignalIconModel( + SignalIconModel.Cellular( level = level, numberOfLevels = numberOfLevels, showExclamationMark = showExclamation, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelTest.kt index 889130f47820..deb9fcf4b56e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/LocationBasedMobileIconViewModelTest.kt @@ -190,7 +190,7 @@ class LocationBasedMobileIconViewModelTest : SysuiTestCase() { /** Convenience constructor for these tests */ fun defaultSignal(level: Int = 1): SignalIconModel { - return SignalIconModel( + return SignalIconModel.Cellular( level, NUM_LEVELS, showExclamationMark = false, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt index 147efcbd67c4..83d0fe8f9c4b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/ui/viewmodel/MobileIconViewModelTest.kt @@ -55,6 +55,7 @@ import com.android.systemui.util.CarrierConfigTracker import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.test.TestScope @@ -709,6 +710,87 @@ class MobileIconViewModelTest : SysuiTestCase() { .isEqualTo(Icon.Resource(R.drawable.mobile_network_type_background, null)) } + @EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG) + @Test + fun nonTerrestrial_defaultProperties() = + testScope.runTest { + repository.isNonTerrestrial.value = true + + val roaming by collectLastValue(underTest.roaming) + val networkTypeIcon by collectLastValue(underTest.networkTypeIcon) + val networkTypeBackground by collectLastValue(underTest.networkTypeBackground) + val activityInVisible by collectLastValue(underTest.activityInVisible) + val activityOutVisible by collectLastValue(underTest.activityOutVisible) + val activityContainerVisible by collectLastValue(underTest.activityContainerVisible) + + assertThat(roaming).isFalse() + assertThat(networkTypeIcon).isNull() + assertThat(networkTypeBackground).isNull() + assertThat(activityInVisible).isFalse() + assertThat(activityOutVisible).isFalse() + assertThat(activityContainerVisible).isFalse() + } + + @EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG) + @Test + fun nonTerrestrial_ignoresDefaultProperties() = + testScope.runTest { + repository.isNonTerrestrial.value = true + + val roaming by collectLastValue(underTest.roaming) + val networkTypeIcon by collectLastValue(underTest.networkTypeIcon) + val networkTypeBackground by collectLastValue(underTest.networkTypeBackground) + val activityInVisible by collectLastValue(underTest.activityInVisible) + val activityOutVisible by collectLastValue(underTest.activityOutVisible) + val activityContainerVisible by collectLastValue(underTest.activityContainerVisible) + + repository.setAllRoaming(true) + repository.setNetworkTypeKey(connectionsRepository.LTE_KEY) + // sets the background on cellular + repository.hasPrioritizedNetworkCapabilities.value = true + repository.dataActivityDirection.value = + DataActivityModel( + hasActivityIn = true, + hasActivityOut = true, + ) + + assertThat(roaming).isFalse() + assertThat(networkTypeIcon).isNull() + assertThat(networkTypeBackground).isNull() + assertThat(activityInVisible).isFalse() + assertThat(activityOutVisible).isFalse() + assertThat(activityContainerVisible).isFalse() + } + + @EnableFlags(com.android.internal.telephony.flags.Flags.FLAG_CARRIER_ENABLED_SATELLITE_FLAG) + @Test + fun nonTerrestrial_usesSatelliteIcon() = + testScope.runTest { + repository.isNonTerrestrial.value = true + repository.setAllLevels(0) + + val latest by + collectLastValue(underTest.icon.filterIsInstance(SignalIconModel.Satellite::class)) + + // Level 0 -> no connection + assertThat(latest).isNotNull() + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_0) + + // 1-2 -> 1 bar + repository.setAllLevels(1) + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1) + + repository.setAllLevels(2) + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_1) + + // 3-4 -> 2 bars + repository.setAllLevels(3) + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2) + + repository.setAllLevels(4) + assertThat(latest!!.icon.res).isEqualTo(R.drawable.ic_satellite_connected_2) + } + private fun createAndSetViewModel() { underTest = MobileIconViewModel( @@ -723,24 +805,5 @@ class MobileIconViewModelTest : SysuiTestCase() { companion object { private const val SUB_1_ID = 1 - private const val NUM_LEVELS = 4 - - /** Convenience constructor for these tests */ - fun defaultSignal(level: Int = 1): SignalIconModel { - return SignalIconModel( - level, - NUM_LEVELS, - showExclamationMark = false, - carrierNetworkChange = false, - ) - } - - fun emptySignal(): SignalIconModel = - SignalIconModel( - level = 0, - numberOfLevels = NUM_LEVELS, - showExclamationMark = true, - carrierNetworkChange = false, - ) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt index 0e0d4897d667..5b5819d649b4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/data/prod/DeviceBasedSatelliteRepositoryImplTest.kt @@ -125,22 +125,6 @@ class DeviceBasedSatelliteRepositoryImplTest : SysuiTestCase() { } @Test - fun satelliteManagerThrows_doesNotCrash() = - testScope.runTest { - setupDefaultRepo() - - whenever(satelliteManager.registerForNtnSignalStrengthChanged(any(), any())) - .thenThrow(SatelliteException(13)) - - val conn by collectLastValue(underTest.connectionState) - val strength by collectLastValue(underTest.signalStrength) - - // Flows have not emitted, we haven't crashed - assertThat(conn).isNull() - assertThat(strength).isNull() - } - - @Test fun connectionState_mapsFromSatelliteModemState() = testScope.runTest { setupDefaultRepo() diff --git a/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java index b58a41c89a4e..457acd214222 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java @@ -190,7 +190,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { mWakefulnessLifecycle.dispatchFinishedWakingUp(); mThemeOverlayController.start(); - verify(mUserTracker).addCallback(mUserTrackerCallback.capture(), eq(mMainExecutor)); + verify(mUserTracker).addCallback(mUserTrackerCallback.capture(), eq(mBgExecutor)); verify(mWallpaperManager).addOnColorsChangedListener(mColorsListener.capture(), eq(null), eq(UserHandle.USER_ALL)); verify(mBroadcastDispatcher).registerReceiver(mBroadcastReceiver.capture(), any(), diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java index 8c823b2376c3..7a8dce8fcd90 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java @@ -32,6 +32,7 @@ import static junit.framework.Assert.assertTrue; import static org.junit.Assume.assumeNotNull; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; @@ -63,15 +64,16 @@ import androidx.test.filters.SmallTest; import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.logging.testing.UiEventLoggerFake; +import com.android.keyguard.TestScopeProvider; import com.android.systemui.Prefs; import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.AnimatorTestRule; import com.android.systemui.dump.DumpManager; import com.android.systemui.media.dialog.MediaOutputDialogFactory; -import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.plugins.VolumeDialogController; import com.android.systemui.plugins.VolumeDialogController.State; import com.android.systemui.res.R; +import com.android.systemui.statusbar.VibratorHelper; import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.statusbar.policy.DevicePostureController; @@ -79,6 +81,9 @@ import com.android.systemui.statusbar.policy.DeviceProvisionedController; import com.android.systemui.statusbar.policy.FakeConfigurationController; import com.android.systemui.util.settings.FakeSettings; import com.android.systemui.util.settings.SecureSettings; +import com.android.systemui.util.time.FakeSystemClock; +import com.android.systemui.volume.domain.interactor.VolumePanelNavigationInteractor; +import com.android.systemui.volume.ui.navigation.VolumeNavigator; import dagger.Lazy; @@ -97,6 +102,8 @@ import org.mockito.MockitoAnnotations; import java.util.Arrays; import java.util.function.Predicate; +import kotlinx.coroutines.Dispatchers; + @SmallTest @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper(setAsMainLooper = true) @@ -126,10 +133,6 @@ public class VolumeDialogImplTest extends SysuiTestCase { @Mock MediaOutputDialogFactory mMediaOutputDialogFactory; @Mock - VolumePanelFactory mVolumePanelFactory; - @Mock - ActivityStarter mActivityStarter; - @Mock InteractionJankMonitor mInteractionJankMonitor; @Mock private DumpManager mDumpManager; @@ -138,6 +141,10 @@ public class VolumeDialogImplTest extends SysuiTestCase { DevicePostureController mPostureController; @Mock private Lazy<SecureSettings> mLazySecureSettings; + @Mock + private VolumePanelNavigationInteractor mVolumePanelNavigationInteractor; + @Mock + private VolumeNavigator mVolumeNavigator; private final CsdWarningDialog.Factory mCsdWarningDialogFactory = new CsdWarningDialog.Factory() { @@ -146,6 +153,8 @@ public class VolumeDialogImplTest extends SysuiTestCase { return mCsdWarningDialog; } }; + @Mock + private VibratorHelper mVibratorHelper; private int mLongestHideShowAnimationDuration = 250; private FakeSettings mSecureSettings; @@ -180,6 +189,8 @@ public class VolumeDialogImplTest extends SysuiTestCase { when(mLazySecureSettings.get()).thenReturn(mSecureSettings); + when(mVibratorHelper.getPrimitiveDurations(anyInt())).thenReturn(new int[]{0}); + mDialog = new VolumeDialogImpl( getContext(), mVolumeDialogController, @@ -187,15 +198,19 @@ public class VolumeDialogImplTest extends SysuiTestCase { mDeviceProvisionedController, mConfigurationController, mMediaOutputDialogFactory, - mVolumePanelFactory, - mActivityStarter, mInteractionJankMonitor, + mVolumePanelNavigationInteractor, + mVolumeNavigator, false, mCsdWarningDialogFactory, mPostureController, mTestableLooper.getLooper(), mDumpManager, - mLazySecureSettings); + mLazySecureSettings, + mVibratorHelper, + Dispatchers.getUnconfined(), + TestScopeProvider.getTestScope(), + new FakeSystemClock()); mDialog.init(0, null); State state = createShellState(); mDialog.onStateChangedH(state); diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java index 93d8dcc8f092..98f3ede850f4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java @@ -456,6 +456,7 @@ public class BubblesTest extends SysuiTestCase { powerInteractor, new GlanceableHubTransitions( mTestScope, + mKosmos.getTestDispatcher(), keyguardTransitionInteractor, keyguardTransitionRepository, communalInteractor @@ -477,6 +478,7 @@ public class BubblesTest extends SysuiTestCase { mKosmos.getTestDispatcher(), mKosmos.getTestDispatcher(), keyguardInteractor, + communalInteractor, featureFlags, mock(KeyguardSecurityModel.class), mSelectedUserInteractor, diff --git a/packages/SystemUI/tests/utils/src/android/app/KeyguardManagerKosmos.kt b/packages/SystemUI/tests/utils/src/android/app/KeyguardManagerKosmos.kt new file mode 100644 index 000000000000..e5121d5de6dd --- /dev/null +++ b/packages/SystemUI/tests/utils/src/android/app/KeyguardManagerKosmos.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.util.mockito.mock + +val Kosmos.keyguardManager by Kosmos.Fixture { mock<KeyguardManager>() } diff --git a/packages/SystemUI/tests/utils/src/android/os/HandlerKosmos.kt b/packages/SystemUI/tests/utils/src/android/os/HandlerKosmos.kt new file mode 100644 index 000000000000..4e2683bd7a2f --- /dev/null +++ b/packages/SystemUI/tests/utils/src/android/os/HandlerKosmos.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os + +import com.android.systemui.concurrency.fakeExecutor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.util.concurrency.mockExecutorHandler + +val Kosmos.fakeExecutorHandler by Kosmos.Fixture { mockExecutorHandler(fakeExecutor) } diff --git a/packages/SystemUI/tests/utils/src/android/service/dream/DreamManagerKosmos.kt b/packages/SystemUI/tests/utils/src/android/service/dream/DreamManagerKosmos.kt new file mode 100644 index 000000000000..fb51f0fec997 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/android/service/dream/DreamManagerKosmos.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.dream + +import android.service.dreams.IDreamManager +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.util.mockito.mock + +var Kosmos.dreamManager by Kosmos.Fixture { mock<IDreamManager>() } diff --git a/packages/SystemUI/tests/utils/src/android/view/WindowManagerKosmos.kt b/packages/SystemUI/tests/utils/src/android/view/WindowManagerKosmos.kt new file mode 100644 index 000000000000..2a0559869c86 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/android/view/WindowManagerKosmos.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view + +import com.android.systemui.kosmos.Kosmos +import org.mockito.Mockito.mock + +val Kosmos.windowManager by Kosmos.Fixture<WindowManager> { mock(WindowManager::class.java) } diff --git a/packages/SystemUI/tests/utils/src/com/android/internal/widget/LockPatternUtilsKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/internal/widget/LockPatternUtilsKosmos.kt new file mode 100644 index 000000000000..d9ea5e92710c --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/internal/widget/LockPatternUtilsKosmos.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.widget + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.util.mockito.mock + +var Kosmos.lockPatternUtils by Kosmos.Fixture { mock<LockPatternUtils>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/ActivityIntentHelperKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/ActivityIntentHelperKosmos.kt new file mode 100644 index 000000000000..7185b7cd0ac6 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/ActivityIntentHelperKosmos.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui + +import android.content.applicationContext +import com.android.systemui.kosmos.Kosmos + +val Kosmos.activityIntentHelper by Kosmos.Fixture { ActivityIntentHelper(applicationContext) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeAccessibilityQsShortcutsRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeAccessibilityQsShortcutsRepository.kt new file mode 100644 index 000000000000..e547da1b92dd --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/accessibility/data/repository/FakeAccessibilityQsShortcutsRepository.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.accessibility.data.repository + +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow + +class FakeAccessibilityQsShortcutsRepository : AccessibilityQsShortcutsRepository { + + private val targetsPerUser = mutableMapOf<Int, MutableSharedFlow<Set<String>>>() + + override fun a11yQsShortcutTargets(userId: Int): SharedFlow<Set<String>> { + return getFlow(userId).asSharedFlow() + } + + /** + * Set the a11y qs shortcut targets. In real world, the A11y QS Shortcut targets are set by the + * Settings app not in SysUi + */ + suspend fun setA11yQsShortcutTargets(userId: Int, targets: Set<String>) { + getFlow(userId).emit(targets) + } + + private fun getFlow(userId: Int): MutableSharedFlow<Set<String>> = + targetsPerUser.getOrPut(userId) { MutableSharedFlow(replay = 1) } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt new file mode 100644 index 000000000000..128f58bf9751 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/animation/ActivityLaunchAnimatorKosmos.kt @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.animation + +import com.android.systemui.kosmos.Kosmos + +val Kosmos.activityLaunchAnimator by Kosmos.Fixture { ActivityLaunchAnimator() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/assist/AssistManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/assist/AssistManagerKosmos.kt new file mode 100644 index 000000000000..b7d6f3a5f91f --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/assist/AssistManagerKosmos.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.assist + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.util.mockito.mock + +var Kosmos.assistManager by Kosmos.Fixture { mock<AssistManager>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/FakeFingerprintInteractiveToAuthProvider.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/FakeFingerprintInteractiveToAuthProvider.kt new file mode 100644 index 000000000000..8fcb60cec838 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/FakeFingerprintInteractiveToAuthProvider.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.biometrics + +import android.hardware.biometrics.common.AuthenticateReason +import kotlinx.coroutines.flow.MutableStateFlow + +class FakeFingerprintInteractiveToAuthProvider : FingerprintInteractiveToAuthProvider { + override val enabledForCurrentUser: MutableStateFlow<Boolean> = MutableStateFlow(false) + + private val userIdToExtension = mutableMapOf<Int, AuthenticateReason.Vendor>() + override fun getVendorExtension(userId: Int): AuthenticateReason.Vendor? = + userIdToExtension[userId] + + fun setVendorExtension(userId: Int, extension: AuthenticateReason.Vendor) { + userIdToExtension[userId] = extension + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/FingerprintInteractiveToAuthProviderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/FingerprintInteractiveToAuthProviderKosmos.kt new file mode 100644 index 000000000000..57dc37e3defa --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/FingerprintInteractiveToAuthProviderKosmos.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.biometrics + +import com.android.systemui.kosmos.Kosmos + +val Kosmos.fingerprintInteractiveToAuthProvider by + Kosmos.Fixture { fakeFingerprintInteractiveToAuthProvider } + +val Kosmos.fakeFingerprintInteractiveToAuthProvider by + Kosmos.Fixture { FakeFingerprintInteractiveToAuthProvider() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepositoryKosmos.kt index 8702e00b1e97..b5515c49cdbe 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FingerprintPropertyRepositoryKosmos.kt @@ -19,4 +19,6 @@ package com.android.systemui.biometrics.data.repository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture -val Kosmos.fingerprintPropertyRepository by Fixture { FakeFingerprintPropertyRepository() } +val Kosmos.fingerprintPropertyRepository by Fixture { fakeFingerprintPropertyRepository } + +val Kosmos.fakeFingerprintPropertyRepository by Fixture { FakeFingerprintPropertyRepository() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractorKosmos.kt new file mode 100644 index 000000000000..979a49b7e70f --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/SideFpsSensorInteractorKosmos.kt @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.biometrics.domain.interactor + +import android.content.applicationContext +import android.view.windowManager +import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository +import com.android.systemui.biometrics.fingerprintInteractiveToAuthProvider +import com.android.systemui.keyguard.data.repository.biometricSettingsRepository +import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.log.SideFpsLogger +import java.util.Optional +import org.mockito.Mockito.mock + +val Kosmos.sideFpsSensorInteractor by + Kosmos.Fixture { + SideFpsSensorInteractor( + applicationContext, + fingerprintPropertyRepository, + windowManager, + displayStateInteractor, + Optional.of(fingerprintInteractiveToAuthProvider), + biometricSettingsRepository, + keyguardTransitionInteractor, + mock(SideFpsLogger::class.java), + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalRepository.kt index 20fa545d54e0..cccd90832326 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalRepository.kt @@ -50,4 +50,11 @@ class FakeCommunalRepository( fun setIsCommunalHubShowing(isCommunalHubShowing: Boolean) { _isCommunalHubShowing.value = isCommunalHubShowing } + + private val _communalEnabledState: MutableStateFlow<Boolean> = MutableStateFlow(false) + override val communalEnabledState: StateFlow<Boolean> = _communalEnabledState + + fun setCommunalEnabledState(enabled: Boolean) { + _communalEnabledState.value = enabled + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/data/repository/FakeStickyKeysRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/data/repository/FakeStickyKeysRepository.kt new file mode 100644 index 000000000000..68e14573a9b2 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/data/repository/FakeStickyKeysRepository.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyboard.data.repository + +import com.android.systemui.keyboard.stickykeys.data.repository.StickyKeysRepository +import com.android.systemui.keyboard.stickykeys.shared.model.Locked +import com.android.systemui.keyboard.stickykeys.shared.model.ModifierKey +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow + +class FakeStickyKeysRepository : StickyKeysRepository { + override val settingEnabled: Flow<Boolean> = MutableStateFlow(true) + private val _stickyKeys: MutableStateFlow<LinkedHashMap<ModifierKey, Locked>> = + MutableStateFlow(LinkedHashMap()) + + override val stickyKeys: Flow<LinkedHashMap<ModifierKey, Locked>> = _stickyKeys + + fun setStickyKeys(keys: LinkedHashMap<ModifierKey, Locked>) { + _stickyKeys.value = keys + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryKosmos.kt new file mode 100644 index 000000000000..46f7355dfe75 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryKosmos.kt @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyboard.data.repository + +import com.android.systemui.kosmos.Kosmos + +val Kosmos.keyboardRepository by Kosmos.Fixture { FakeKeyboardRepository() }
\ No newline at end of file diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryKosmos.kt index 45d39b0d4bc4..cf8f8122dd67 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/BiometricSettingsRepositoryKosmos.kt @@ -19,4 +19,5 @@ package com.android.systemui.keyguard.data.repository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture -val Kosmos.biometricSettingsRepository by Fixture { FakeBiometricSettingsRepository() } +val Kosmos.fakeBiometricSettingsRepository by Fixture { FakeBiometricSettingsRepository() } +val Kosmos.biometricSettingsRepository by Fixture { fakeBiometricSettingsRepository } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryKosmos.kt index 6437ef3e50f4..0d20939530de 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryKosmos.kt @@ -19,6 +19,10 @@ package com.android.systemui.keyguard.data.repository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture -val Kosmos.deviceEntryFingerprintAuthRepository by Fixture { +val Kosmos.fakeDeviceEntryFingerprintAuthRepository by Fixture { FakeDeviceEntryFingerprintAuthRepository() } + +val Kosmos.deviceEntryFingerprintAuthRepository by Fixture { + fakeDeviceEntryFingerprintAuthRepository +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorKosmos.kt index 97536e20cb0a..719686e3d862 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/FromPrimaryBouncerTransitionInteractorKosmos.kt @@ -17,6 +17,7 @@ package com.android.systemui.keyguard.domain.interactor import com.android.keyguard.keyguardSecurityModel +import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.flags.featureFlagsClassic import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository import com.android.systemui.kosmos.Kosmos @@ -34,6 +35,7 @@ val Kosmos.fromPrimaryBouncerTransitionInteractor by bgDispatcher = testDispatcher, mainDispatcher = testDispatcher, keyguardInteractor = keyguardInteractor, + communalInteractor = communalInteractor, flags = featureFlagsClassic, keyguardSecurityModel = keyguardSecurityModel, selectedUserInteractor = selectedUserInteractor, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitionsKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitionsKosmos.kt index ec17c488e297..55885bf58acc 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitionsKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/GlanceableHubTransitionsKosmos.kt @@ -20,11 +20,13 @@ import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testDispatcher val Kosmos.glanceableHubTransitions by Kosmos.Fixture { GlanceableHubTransitions( scope = applicationCoroutineScope, + bgDispatcher = testDispatcher, transitionRepository = keyguardTransitionRepository, transitionInteractor = keyguardTransitionInteractor, communalInteractor = communalInteractor, 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 5ca0439c1313..4a85909ae996 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 @@ -20,6 +20,7 @@ import com.android.systemui.keyguard.domain.interactor.keyguardClockInteractor import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationsKeyguardInteractor import com.android.systemui.statusbar.policy.splitShadeStateController val Kosmos.keyguardClockViewModel by @@ -29,5 +30,6 @@ val Kosmos.keyguardClockViewModel by keyguardClockInteractor = keyguardClockInteractor, applicationScope = applicationCoroutineScope, splitShadeStateController = splitShadeStateController, + notifsKeyguardInteractor = notificationsKeyguardInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt index 5564767e0715..d376f126e2c9 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt @@ -18,6 +18,7 @@ package com.android.systemui.keyguard.ui.viewmodel +import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor @@ -33,6 +34,7 @@ val Kosmos.keyguardRootViewModel by Fixture { deviceEntryInteractor = deviceEntryInteractor, dozeParameters = dozeParameters, keyguardInteractor = keyguardInteractor, + communalInteractor = communalInteractor, keyguardTransitionInteractor = keyguardTransitionInteractor, notificationsKeyguardInteractor = notificationsKeyguardInteractor, aodToLockscreenTransitionViewModel = aodToLockscreenTransitionViewModel, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeControllerKosmos.kt new file mode 100644 index 000000000000..e13fa5207b33 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeControllerKosmos.kt @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.android.systemui.shade + +import android.view.WindowManager +import com.android.systemui.assist.AssistManager +import com.android.systemui.concurrency.fakeExecutor +import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.log.LogBuffer +import com.android.systemui.plugins.statusbar.statusBarStateController +import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.statusbar.CommandQueue +import com.android.systemui.statusbar.NotificationShadeWindowController +import com.android.systemui.statusbar.notification.row.NotificationGutsManager +import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout +import com.android.systemui.statusbar.notification.stack.ui.viewmodel.windowRootViewVisibilityInteractor +import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager +import com.android.systemui.statusbar.phone.statusBarKeyguardViewManager +import com.android.systemui.statusbar.policy.KeyguardStateController +import com.android.systemui.statusbar.policy.deviceProvisionedController +import com.android.systemui.statusbar.window.StatusBarWindowController +import com.android.systemui.util.mockito.mock +import kotlinx.coroutines.ExperimentalCoroutinesApi + +val Kosmos.shadeControllerSceneImpl by + Kosmos.Fixture { + ShadeControllerSceneImpl( + scope = applicationCoroutineScope, + shadeInteractor = shadeInteractor, + sceneInteractor = sceneInteractor, + notificationStackScrollLayout = mock<NotificationStackScrollLayout>(), + deviceEntryInteractor = deviceEntryInteractor, + touchLog = mock<LogBuffer>(), + commandQueue = mock<CommandQueue>(), + statusBarKeyguardViewManager = mock<StatusBarKeyguardViewManager>(), + notificationShadeWindowController = mock<NotificationShadeWindowController>(), + assistManagerLazy = { mock<AssistManager>() }, + ) + } + +val Kosmos.shadeControllerImpl by + Kosmos.Fixture { + ShadeControllerImpl( + mock<CommandQueue>(), + fakeExecutor, + mock<LogBuffer>(), + windowRootViewVisibilityInteractor, + mock<KeyguardStateController>(), + statusBarStateController, + statusBarKeyguardViewManager, + mock<StatusBarWindowController>(), + deviceProvisionedController, + mock<NotificationShadeWindowController>(), + mock<WindowManager>(), + { mock<ShadeViewController>() }, + { mock<AssistManager>() }, + { mock<NotificationGutsManager>() }, + ) + } +var Kosmos.shadeController: ShadeController by Kosmos.Fixture { shadeControllerImpl } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeViewControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeViewControllerKosmos.kt new file mode 100644 index 000000000000..1ceab68604f3 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeViewControllerKosmos.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shade + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.util.mockito.mock + +var Kosmos.shadeViewController by Kosmos.Fixture { mock<ShadeViewController>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/ShadeAnimationRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/ShadeAnimationRepositoryKosmos.kt new file mode 100644 index 000000000000..4dcd2208b152 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/ShadeAnimationRepositoryKosmos.kt @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shade.data.repository + +import com.android.systemui.kosmos.Kosmos + +val Kosmos.shadeAnimationRepository by Kosmos.Fixture { ShadeAnimationRepository() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractorKosmos.kt new file mode 100644 index 000000000000..57b272f10c67 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeAnimationInteractorKosmos.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shade.domain.interactor + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.shade.data.repository.shadeAnimationRepository + +var Kosmos.shadeAnimationInteractor: ShadeAnimationInteractor by + Kosmos.Fixture { ShadeAnimationInteractorEmptyImpl(shadeAnimationRepository) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepositoryKosmos.kt new file mode 100644 index 000000000000..a75d2bc4aaf6 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shared/notifications/data/repository/NotificationSettingsRepositoryKosmos.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shared.notifications.data.repository + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.shared.settings.data.repository.secureSettingsRepository + +val Kosmos.notificationSettingsRepository by + Kosmos.Fixture { + NotificationSettingsRepository( + scope = testScope.backgroundScope, + backgroundDispatcher = testDispatcher, + secureSettingsRepository = secureSettingsRepository, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shared/notifications/domain/interactor/NotificationSettingsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shared/notifications/domain/interactor/NotificationSettingsInteractorKosmos.kt new file mode 100644 index 000000000000..17b4603e17b9 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shared/notifications/domain/interactor/NotificationSettingsInteractorKosmos.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shared.notifications.domain.interactor + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.shared.notifications.data.repository.notificationSettingsRepository + +val Kosmos.notificationSettingsInteractor by + Kosmos.Fixture { NotificationSettingsInteractor(notificationSettingsRepository) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shared/settings/data/repository/SecureSettingsRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shared/settings/data/repository/SecureSettingsRepositoryKosmos.kt new file mode 100644 index 000000000000..552b09e933de --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shared/settings/data/repository/SecureSettingsRepositoryKosmos.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shared.settings.data.repository + +import com.android.systemui.kosmos.Kosmos + +var Kosmos.secureSettingsRepository: SecureSettingsRepository by + Kosmos.Fixture { fakeSecureSettingsRepository } +val Kosmos.fakeSecureSettingsRepository by Kosmos.Fixture { FakeSecureSettingsRepository() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationClickNotifierKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationClickNotifierKosmos.kt new file mode 100644 index 000000000000..7b912aef3011 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationClickNotifierKosmos.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.util.mockito.mock + +var Kosmos.notificationClickNotifier by Kosmos.Fixture { mock<NotificationClickNotifier>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationPresenterKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationPresenterKosmos.kt new file mode 100644 index 000000000000..8d30049bfb09 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationPresenterKosmos.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.util.mockito.mock + +var Kosmos.notificationPresenter by Kosmos.Fixture { mock<NotificationPresenter>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationRemoteInputManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationRemoteInputManagerKosmos.kt new file mode 100644 index 000000000000..554bdbe0c382 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationRemoteInputManagerKosmos.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.util.mockito.mock + +var Kosmos.notificationRemoteInputManager by + Kosmos.Fixture { mock<NotificationRemoteInputManager>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationShadeWindowControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationShadeWindowControllerKosmos.kt new file mode 100644 index 000000000000..e8ca3b8234e8 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/NotificationShadeWindowControllerKosmos.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.util.mockito.mock + +var Kosmos.notificationShadeWindowController by + Kosmos.Fixture { mock<NotificationShadeWindowController>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/NotificationActivityStarterKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/NotificationActivityStarterKosmos.kt new file mode 100644 index 000000000000..c337ac201b3d --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/NotificationActivityStarterKosmos.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.statusbar.phone.statusBarNotificationActivityStarter + +var Kosmos.notificationActivityStarter: NotificationActivityStarter by + Kosmos.Fixture { statusBarNotificationActivityStarter } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorControllerProviderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorControllerProviderKosmos.kt new file mode 100644 index 000000000000..c3db34bdddb7 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/NotificationLaunchAnimatorControllerProviderKosmos.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.util.mockito.mock + +var Kosmos.notificationLaunchAnimatorControllerProvider by + Kosmos.Fixture { mock<NotificationLaunchAnimatorControllerProvider>() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/EntryUtil.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/EntryUtil.kt index da956ec67696..da956ec67696 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/EntryUtil.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/EntryUtil.kt diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/GroupEntryBuilder.java b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/GroupEntryBuilder.java index dda7fadde2d7..4efcada96a14 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/GroupEntryBuilder.java +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/GroupEntryBuilder.java @@ -52,32 +52,38 @@ public class GroupEntryBuilder { return ge; } + /** Sets the group key. */ public GroupEntryBuilder setKey(String key) { mKey = key; return this; } + /** Sets the creation time. */ public GroupEntryBuilder setCreationTime(long creationTime) { mCreationTime = creationTime; return this; } + /** Sets the parent entry of the group. */ public GroupEntryBuilder setParent(@Nullable GroupEntry entry) { mParent = entry; return this; } + /** Sets the section the group belongs to. */ public GroupEntryBuilder setSection(@Nullable NotifSection section) { mNotifSection = section; return this; } + /** Sets the group summary. */ public GroupEntryBuilder setSummary( NotificationEntry summary) { mSummary = summary; return this; } + /** Sets the group children. */ public GroupEntryBuilder setChildren(List<NotificationEntry> children) { mChildren.clear(); mChildren.addAll(children); @@ -90,6 +96,7 @@ public class GroupEntryBuilder { return this; } + /** Get the group's internal children list. */ public static List<NotificationEntry> getRawChildren(GroupEntry groupEntry) { return groupEntry.getRawChildren(); } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/NotifLiveDataStoreKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/NotifLiveDataStoreKosmos.kt new file mode 100644 index 000000000000..1f45fbbcf927 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/NotifLiveDataStoreKosmos.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.collection + +import com.android.systemui.concurrency.fakeExecutor +import com.android.systemui.kosmos.Kosmos + +var Kosmos.notifLiveDataStore: NotifLiveDataStore by + Kosmos.Fixture { NotifLiveDataStoreImpl(fakeExecutor) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorKosmos.kt new file mode 100644 index 000000000000..358d2519556b --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/coordinator/VisualStabilityCoordinatorKosmos.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.collection.coordinator + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.util.mockito.mock + +var Kosmos.visualStabilityCoordinator by Kosmos.Fixture { mock<VisualStabilityCoordinator>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/provider/LaunchFullScreenIntentProviderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/provider/LaunchFullScreenIntentProviderKosmos.kt new file mode 100644 index 000000000000..a5c956155351 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/provider/LaunchFullScreenIntentProviderKosmos.kt @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.collection.provider + +import com.android.systemui.kosmos.Kosmos + +val Kosmos.launchFullScreenIntentProvider by Kosmos.Fixture { LaunchFullScreenIntentProvider() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/render/NotificationVisibilityProviderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/render/NotificationVisibilityProviderKosmos.kt new file mode 100644 index 000000000000..edce5d58b351 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/collection/render/NotificationVisibilityProviderKosmos.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.collection.render + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.statusbar.notification.collection.notifLiveDataStore +import com.android.systemui.statusbar.notification.collection.notifPipeline +import com.android.systemui.statusbar.notification.collection.provider.NotificationVisibilityProviderImpl +import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor + +var Kosmos.notificationVisibilityProvider: NotificationVisibilityProvider by + Kosmos.Fixture { + NotificationVisibilityProviderImpl( + activeNotificationsInteractor, + notifLiveDataStore, + notifPipeline, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/OnUserInteractionCallbackKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/OnUserInteractionCallbackKosmos.kt new file mode 100644 index 000000000000..1e3897ba46c6 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/row/OnUserInteractionCallbackKosmos.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.notification.row + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.plugins.statusbar.statusBarStateController +import com.android.systemui.statusbar.notification.collection.coordinator.visualStabilityCoordinator +import com.android.systemui.statusbar.notification.collection.inflation.OnUserInteractionCallbackImpl +import com.android.systemui.statusbar.notification.collection.notifCollection +import com.android.systemui.statusbar.notification.collection.render.notificationVisibilityProvider +import com.android.systemui.statusbar.policy.headsUpManager + +var Kosmos.onUserInteractionCallback: OnUserInteractionCallback by + Kosmos.Fixture { + OnUserInteractionCallbackImpl( + notificationVisibilityProvider, + notifCollection, + headsUpManager, + statusBarStateController, + visualStabilityCoordinator, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt index 5ef9a8e8edd8..db4050905200 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt @@ -16,8 +16,11 @@ package com.android.systemui.statusbar.notification.stack.ui.viewmodel +import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor +import com.android.systemui.keyguard.ui.viewmodel.glanceableHubToLockscreenTransitionViewModel +import com.android.systemui.keyguard.ui.viewmodel.lockscreenToGlanceableHubTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.lockscreenToOccludedTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.occludedToLockscreenTransitionViewModel import com.android.systemui.kosmos.Kosmos @@ -33,7 +36,10 @@ val Kosmos.sharedNotificationContainerViewModel by Fixture { keyguardInteractor = keyguardInteractor, keyguardTransitionInteractor = keyguardTransitionInteractor, shadeInteractor = shadeInteractor, + communalInteractor = communalInteractor, occludedToLockscreenTransitionViewModel = occludedToLockscreenTransitionViewModel, lockscreenToOccludedTransitionViewModel = lockscreenToOccludedTransitionViewModel, + glanceableHubToLockscreenTransitionViewModel = glanceableHubToLockscreenTransitionViewModel, + lockscreenToGlanceableHubTransitionViewModel = lockscreenToGlanceableHubTransitionViewModel ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/WindowRootViewVisibilityInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/WindowRootViewVisibilityInteractorKosmos.kt index d80ee758269f..cf800d04f816 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/WindowRootViewVisibilityInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/WindowRootViewVisibilityInteractorKosmos.kt @@ -23,6 +23,8 @@ import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.scene.data.repository.windowRootViewVisibilityRepository import com.android.systemui.scene.domain.interactor.WindowRootViewVisibilityInteractor +import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.scene.shared.flag.sceneContainerFlags import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor import com.android.systemui.statusbar.policy.headsUpManager @@ -34,5 +36,7 @@ val Kosmos.windowRootViewVisibilityInteractor by Fixture { headsUpManager = headsUpManager, powerInteractor = powerInteractor, activeNotificationsInteractor = activeNotificationsInteractor, + sceneInteractorProvider = { sceneInteractor }, + sceneContainerFlags = sceneContainerFlags, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/DozeServiceHostKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/DozeServiceHostKosmos.kt new file mode 100644 index 000000000000..370b1773e8f7 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/DozeServiceHostKosmos.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.phone + +import com.android.systemui.kosmos.Kosmos +import kotlinx.coroutines.ExperimentalCoroutinesApi +import org.mockito.Mockito.mock + +@OptIn(ExperimentalCoroutinesApi::class) +val Kosmos.dozeServiceHost: DozeServiceHost by Kosmos.Fixture { mock(DozeServiceHost::class.java) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterKosmos.kt new file mode 100644 index 000000000000..6ddc9df930f3 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterKosmos.kt @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.phone + +import android.app.keyguardManager +import android.content.applicationContext +import android.os.fakeExecutorHandler +import android.service.dream.dreamManager +import com.android.internal.logging.metricsLogger +import com.android.internal.widget.lockPatternUtils +import com.android.systemui.activityIntentHelper +import com.android.systemui.animation.activityLaunchAnimator +import com.android.systemui.assist.assistManager +import com.android.systemui.concurrency.fakeExecutor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.plugins.activityStarter +import com.android.systemui.power.domain.interactor.powerInteractor +import com.android.systemui.settings.userTracker +import com.android.systemui.shade.domain.interactor.shadeAnimationInteractor +import com.android.systemui.shade.shadeController +import com.android.systemui.shade.shadeViewController +import com.android.systemui.statusbar.notification.collection.provider.launchFullScreenIntentProvider +import com.android.systemui.statusbar.notification.collection.render.notificationVisibilityProvider +import com.android.systemui.statusbar.notification.notificationLaunchAnimatorControllerProvider +import com.android.systemui.statusbar.notification.row.onUserInteractionCallback +import com.android.systemui.statusbar.notificationClickNotifier +import com.android.systemui.statusbar.notificationLockscreenUserManager +import com.android.systemui.statusbar.notificationPresenter +import com.android.systemui.statusbar.notificationRemoteInputManager +import com.android.systemui.statusbar.notificationShadeWindowController +import com.android.systemui.statusbar.policy.headsUpManager +import com.android.systemui.statusbar.policy.keyguardStateController +import com.android.systemui.wmshell.bubblesManager +import java.util.Optional +import kotlinx.coroutines.ExperimentalCoroutinesApi + +@OptIn(ExperimentalCoroutinesApi::class) +val Kosmos.statusBarNotificationActivityStarter by + Kosmos.Fixture { + StatusBarNotificationActivityStarter( + applicationContext, + applicationContext.displayId, + fakeExecutorHandler, + fakeExecutor, + notificationVisibilityProvider, + headsUpManager, + activityStarter, + notificationClickNotifier, + statusBarKeyguardViewManager, + keyguardManager, + dreamManager, + Optional.of(bubblesManager), + { assistManager }, + notificationRemoteInputManager, + notificationLockscreenUserManager, + shadeController, + keyguardStateController, + lockPatternUtils, + statusBarRemoteInputCallback, + activityIntentHelper, + metricsLogger, + statusBarNotificationActivityStarterLogger, + onUserInteractionCallback, + notificationPresenter, + shadeViewController, + notificationShadeWindowController, + activityLaunchAnimator, + shadeAnimationInteractor, + notificationLaunchAnimatorControllerProvider, + launchFullScreenIntentProvider, + powerInteractor, + userTracker, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLoggerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLoggerKosmos.kt new file mode 100644 index 000000000000..31cfc979a11a --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarNotificationActivityStarterLoggerKosmos.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.phone + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.log.logcatLogBuffer + +val Kosmos.statusBarNotificationActivityStarterLogger by + Kosmos.Fixture { StatusBarNotificationActivityStarterLogger(logcatLogBuffer()) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackKosmos.kt new file mode 100644 index 000000000000..475d7fa6ef4b --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarRemoteInputCallbackKosmos.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.phone + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.util.mockito.mock + +var Kosmos.statusBarRemoteInputCallback by Kosmos.Fixture { mock<StatusBarRemoteInputCallback>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt index 5f4d7bf6f371..c01032757bb0 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/pipeline/mobile/domain/interactor/FakeMobileIconInteractor.kt @@ -60,6 +60,8 @@ class FakeMobileIconInteractor( override val isInService = MutableStateFlow(true) + override val isNonTerrestrial = MutableStateFlow(false) + private val _isDataEnabled = MutableStateFlow(true) override val isDataEnabled = _isDataEnabled @@ -69,7 +71,7 @@ class FakeMobileIconInteractor( override val signalLevelIcon: MutableStateFlow<SignalIconModel> = MutableStateFlow( - SignalIconModel( + SignalIconModel.Cellular( level = 0, numberOfLevels = 4, showExclamationMark = false, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/KeyguardStateControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/KeyguardStateControllerKosmos.kt new file mode 100644 index 000000000000..0e909c498a2b --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/KeyguardStateControllerKosmos.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.policy + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.util.mockito.mock + +var Kosmos.keyguardStateController by Kosmos.Fixture { mock<KeyguardStateController>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/wmshell/BubblesManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/wmshell/BubblesManagerKosmos.kt new file mode 100644 index 000000000000..1d05d62a02e4 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/wmshell/BubblesManagerKosmos.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.wmshell + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.util.mockito.mock + +var Kosmos.bubblesManager by Kosmos.Fixture { mock<BubblesManager>() } diff --git a/packages/overlays/NavigationBarModeGesturalOverlay/res/values-sw600dp/config.xml b/packages/overlays/NavigationBarModeGesturalOverlay/res/values-sw600dp/config.xml new file mode 100644 index 000000000000..be1f081d5b8f --- /dev/null +++ b/packages/overlays/NavigationBarModeGesturalOverlay/res/values-sw600dp/config.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/** + * Copyright (c) 2023, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +--> +<resources> + <!-- If true, attach the navigation bar to the app during app transition --> + <bool name="config_attachNavBarToAppDuringTransition">false</bool> +</resources> diff --git a/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/res/values-sw600dp/config.xml b/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/res/values-sw600dp/config.xml new file mode 100644 index 000000000000..be1f081d5b8f --- /dev/null +++ b/packages/overlays/NavigationBarModeGesturalOverlayExtraWideBack/res/values-sw600dp/config.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/** + * Copyright (c) 2023, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +--> +<resources> + <!-- If true, attach the navigation bar to the app during app transition --> + <bool name="config_attachNavBarToAppDuringTransition">false</bool> +</resources> diff --git a/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/res/values-sw600dp/config.xml b/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/res/values-sw600dp/config.xml new file mode 100644 index 000000000000..be1f081d5b8f --- /dev/null +++ b/packages/overlays/NavigationBarModeGesturalOverlayNarrowBack/res/values-sw600dp/config.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/** + * Copyright (c) 2023, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +--> +<resources> + <!-- If true, attach the navigation bar to the app during app transition --> + <bool name="config_attachNavBarToAppDuringTransition">false</bool> +</resources> diff --git a/packages/overlays/NavigationBarModeGesturalOverlayWideBack/res/values-sw600dp/config.xml b/packages/overlays/NavigationBarModeGesturalOverlayWideBack/res/values-sw600dp/config.xml new file mode 100644 index 000000000000..be1f081d5b8f --- /dev/null +++ b/packages/overlays/NavigationBarModeGesturalOverlayWideBack/res/values-sw600dp/config.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- +/** + * Copyright (c) 2023, The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +--> +<resources> + <!-- If true, attach the navigation bar to the app during app transition --> + <bool name="config_attachNavBarToAppDuringTransition">false</bool> +</resources> diff --git a/ravenwood/framework-minus-apex-ravenwood-policies.txt b/ravenwood/framework-minus-apex-ravenwood-policies.txt index f5e4af50fc29..e33fff117d41 100644 --- a/ravenwood/framework-minus-apex-ravenwood-policies.txt +++ b/ravenwood/framework-minus-apex-ravenwood-policies.txt @@ -6,6 +6,9 @@ class :aidl stubclass # Keep all feature flag implementations class :feature_flags stubclass +# Keep all sysprops generated code implementations +class :sysprops stubclass + # Collections class android.util.ArrayMap stubclass class android.util.ArraySet stubclass @@ -112,6 +115,12 @@ class android.os.HandlerExecutor stubclass class android.os.PatternMatcher stubclass class android.os.ParcelUuid stubclass +# Logging related interfaces from modules-utils +class com.android.internal.logging.InstanceId stubclass +class com.android.internal.logging.InstanceIdSequence stubclass +class com.android.internal.logging.UiEvent stubclass +class com.android.internal.logging.UiEventLogger stubclass + # XML class com.android.internal.util.XmlPullParserWrapper stubclass class com.android.internal.util.XmlSerializerWrapper stubclass diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java index eacdc2f79254..91c522e82cce 100644 --- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java +++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java @@ -19,8 +19,6 @@ package android.platform.test.ravenwood; import android.os.HandlerThread; import android.os.Looper; -import java.util.Objects; - public class RavenwoodRuleImpl { private static final String MAIN_THREAD_NAME = "RavenwoodMain"; @@ -31,6 +29,10 @@ public class RavenwoodRuleImpl { public static void init(RavenwoodRule rule) { android.os.Process.init$ravenwood(rule.mUid, rule.mPid); android.os.Binder.init$ravenwood(); + android.os.SystemProperties.init$ravenwood( + rule.mSystemProperties.getValues(), + rule.mSystemProperties.getKeyReadablePredicate(), + rule.mSystemProperties.getKeyWritablePredicate()); com.android.server.LocalServices.removeAllServicesForTest(); @@ -49,7 +51,8 @@ public class RavenwoodRuleImpl { com.android.server.LocalServices.removeAllServicesForTest(); - android.os.Process.reset$ravenwood(); + android.os.SystemProperties.reset$ravenwood(); android.os.Binder.reset$ravenwood(); + android.os.Process.reset$ravenwood(); } } diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java index 53da8ba14a2c..dd442f08321f 100644 --- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java +++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java @@ -62,6 +62,8 @@ public class RavenwoodRule implements TestRule { boolean mProvideMainThread = false; + final RavenwoodSystemProperties mSystemProperties = new RavenwoodSystemProperties(); + public RavenwoodRule() { } @@ -98,6 +100,40 @@ public class RavenwoodRule implements TestRule { return this; } + /** + * Configure the given system property as immutable for the duration of the test. + * Read access to the key is allowed, and write access will fail. When {@code value} is + * {@code null}, the value is left as undefined. + * + * All properties in the {@code debug.*} namespace are automatically mutable, with no + * developer action required. + * + * Has no effect under non-Ravenwood environments. + */ + public Builder setSystemPropertyImmutable(/* @NonNull */ String key, + /* @Nullable */ Object value) { + mRule.mSystemProperties.setValue(key, value); + mRule.mSystemProperties.setAccessReadOnly(key); + return this; + } + + /** + * Configure the given system property as mutable for the duration of the test. + * Both read and write access to the key is allowed, and its value will be reset between + * each test. When {@code value} is {@code null}, the value is left as undefined. + * + * All properties in the {@code debug.*} namespace are automatically mutable, with no + * developer action required. + * + * Has no effect under non-Ravenwood environments. + */ + public Builder setSystemPropertyMutable(/* @NonNull */ String key, + /* @Nullable */ Object value) { + mRule.mSystemProperties.setValue(key, value); + mRule.mSystemProperties.setAccessReadWrite(key); + return this; + } + public RavenwoodRule build() { return mRule; } diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java new file mode 100644 index 000000000000..85ad4e444f24 --- /dev/null +++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodSystemProperties.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 android.platform.test.ravenwood; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.function.Predicate; + +class RavenwoodSystemProperties { + private final Map<String, String> mValues = new HashMap<>(); + + /** Set of additional keys that should be considered readable */ + private final Set<String> mKeyReadable = new HashSet<>(); + private final Predicate<String> mKeyReadablePredicate = (key) -> { + final String root = getKeyRoot(key); + + if (root.startsWith("debug.")) return true; + + // This set is carefully curated to help identify situations where a test may + // accidentally depend on a default value of an obscure property whose owner hasn't + // decided how Ravenwood should behave. + if (root.startsWith("boot.")) return true; + if (root.startsWith("build.")) return true; + if (root.startsWith("product.")) return true; + if (root.startsWith("soc.")) return true; + if (root.startsWith("system.")) return true; + + switch (key) { + case "gsm.version.baseband": + case "no.such.thing": + case "ro.bootloader": + case "ro.debuggable": + case "ro.hardware": + case "ro.hw_timeout_multiplier": + case "ro.odm.build.media_performance_class": + case "ro.treble.enabled": + case "ro.vndk.version": + return true; + } + + return mKeyReadable.contains(key); + }; + + /** Set of additional keys that should be considered writable */ + private final Set<String> mKeyWritable = new HashSet<>(); + private final Predicate<String> mKeyWritablePredicate = (key) -> { + final String root = getKeyRoot(key); + + if (root.startsWith("debug.")) return true; + + return mKeyWritable.contains(key); + }; + + public RavenwoodSystemProperties() { + // TODO: load these values from build.prop generated files + setValueForPartitions("product.brand", "Android"); + setValueForPartitions("product.device", "Ravenwood"); + setValueForPartitions("product.manufacturer", "Android"); + setValueForPartitions("product.model", "Ravenwood"); + setValueForPartitions("product.name", "Ravenwood"); + + setValueForPartitions("product.cpu.abilist", "x86_64"); + setValueForPartitions("product.cpu.abilist32", ""); + setValueForPartitions("product.cpu.abilist64", "x86_64"); + + setValueForPartitions("build.date", "Thu Jan 01 00:00:00 GMT 2024"); + setValueForPartitions("build.date.utc", "1704092400"); + setValueForPartitions("build.id", "MAIN"); + setValueForPartitions("build.tags", "dev-keys"); + setValueForPartitions("build.type", "userdebug"); + setValueForPartitions("build.version.all_codenames", "REL"); + setValueForPartitions("build.version.codename", "REL"); + setValueForPartitions("build.version.incremental", "userdebug.ravenwood.20240101"); + setValueForPartitions("build.version.known_codenames", "REL"); + setValueForPartitions("build.version.release", "14"); + setValueForPartitions("build.version.release_or_codename", "VanillaIceCream"); + setValueForPartitions("build.version.sdk", "34"); + + setValue("ro.board.first_api_level", "1"); + setValue("ro.product.first_api_level", "1"); + + setValue("ro.soc.manufacturer", "Android"); + setValue("ro.soc.model", "Ravenwood"); + + setValue("ro.debuggable", "1"); + } + + Map<String, String> getValues() { + return new HashMap<>(mValues); + } + + Predicate<String> getKeyReadablePredicate() { + return mKeyReadablePredicate; + } + + Predicate<String> getKeyWritablePredicate() { + return mKeyWritablePredicate; + } + + private static final String[] PARTITIONS = { + "bootimage", + "odm", + "product", + "system", + "system_ext", + "vendor", + "vendor_dlkm", + }; + + /** + * Set the given property for all possible partitions where it could be defined. For + * example, the value of {@code ro.build.type} is typically also mirrored under + * {@code ro.system.build.type}, etc. + */ + private void setValueForPartitions(String key, String value) { + setValue("ro." + key, value); + for (String partition : PARTITIONS) { + setValue("ro." + partition + "." + key, value); + } + } + + public void setValue(String key, Object value) { + final String valueString = (value == null) ? null : String.valueOf(value); + if ((valueString == null) || valueString.isEmpty()) { + mValues.remove(key); + } else { + mValues.put(key, valueString); + } + } + + public void setAccessNone(String key) { + mKeyReadable.remove(key); + mKeyWritable.remove(key); + } + + public void setAccessReadOnly(String key) { + mKeyReadable.add(key); + mKeyWritable.remove(key); + } + + public void setAccessReadWrite(String key) { + mKeyReadable.add(key); + mKeyWritable.add(key); + } + + /** + * Return the "root" of the given property key, stripping away any modifier prefix such as + * {@code ro.} or {@code persist.}. + */ + private static String getKeyRoot(String key) { + if (key.startsWith("ro.")) { + return key.substring(3); + } else if (key.startsWith("persist.")) { + return key.substring(8); + } else { + return key; + } + } +} diff --git a/ravenwood/ravenwood-annotation-allowed-classes.txt b/ravenwood/ravenwood-annotation-allowed-classes.txt index ab2546bab246..5700f000cbc5 100644 --- a/ravenwood/ravenwood-annotation-allowed-classes.txt +++ b/ravenwood/ravenwood-annotation-allowed-classes.txt @@ -1,6 +1,9 @@ # Only classes listed here can use the Ravenwood annotations. com.android.internal.util.ArrayUtils +com.android.internal.logging.MetricsLogger +com.android.internal.logging.testing.FakeMetricsLogger +com.android.internal.logging.testing.UiEventLoggerFake com.android.internal.os.BatteryStatsHistory com.android.internal.os.BatteryStatsHistory$TraceDelegate com.android.internal.os.BatteryStatsHistory$VarintParceler @@ -47,6 +50,7 @@ android.os.BatteryUsageStatsQuery android.os.Binder android.os.Binder$IdentitySupplier android.os.Broadcaster +android.os.Build android.os.BundleMerger android.os.ConditionVariable android.os.FileUtils @@ -65,8 +69,10 @@ android.os.PowerComponents android.os.Process android.os.ServiceSpecificException android.os.SystemClock +android.os.SystemProperties android.os.ThreadLocalWorkSource android.os.TimestampedValue +android.os.Trace android.os.UidBatteryConsumer android.os.UidBatteryConsumer$Builder android.os.UserHandle @@ -126,6 +132,8 @@ android.graphics.RectF android.content.ContentProvider +android.metrics.LogMaker + com.android.server.LocalServices com.android.server.power.stats.BatteryStatsImpl diff --git a/services/accessibility/accessibility.aconfig b/services/accessibility/accessibility.aconfig index 993b2544f110..44682e2088f4 100644 --- a/services/accessibility/accessibility.aconfig +++ b/services/accessibility/accessibility.aconfig @@ -45,6 +45,13 @@ flag { } flag { + name: "fix_drag_pointer_when_ending_drag" + namespace: "accessibility" + description: "Send the correct pointer id when transitioning from dragging to delegating states." + bug: "300002193" +} + +flag { name: "pinch_zoom_zero_min_span" namespace: "accessibility" description: "Whether to set min span of ScaleGestureDetector to zero." diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 3d8d7b738233..d656892062d1 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -4413,25 +4413,50 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub @Override public boolean isAccessibilityServiceWarningRequired(AccessibilityServiceInfo info) { mSecurityPolicy.enforceCallingOrSelfPermission(Manifest.permission.MANAGE_ACCESSIBILITY); + final ComponentName componentName = info.getComponentName(); // Warning is not required if the service is already enabled. synchronized (mLock) { final AccessibilityUserState userState = getCurrentUserStateLocked(); - if (userState.getEnabledServicesLocked().contains(info.getComponentName())) { + if (userState.getEnabledServicesLocked().contains(componentName)) { return false; } } // Warning is not required if the service is already assigned to a shortcut. for (int shortcutType : AccessibilityManager.SHORTCUT_TYPES) { if (getAccessibilityShortcutTargets(shortcutType).contains( - info.getComponentName().flattenToString())) { + componentName.flattenToString())) { return false; } } + // Warning is not required if the service is preinstalled and in the + // trustedAccessibilityServices allowlist. + if (android.view.accessibility.Flags.skipAccessibilityWarningDialogForTrustedServices() + && isAccessibilityServicePreinstalledAndTrusted(info)) { + return false; + } + // Warning is required by default. return true; } + private boolean isAccessibilityServicePreinstalledAndTrusted(AccessibilityServiceInfo info) { + final ComponentName componentName = info.getComponentName(); + final boolean isPreinstalled = + info.getResolveInfo().serviceInfo.applicationInfo.isSystemApp(); + if (isPreinstalled) { + final String[] trustedAccessibilityServices = + mContext.getResources().getStringArray( + R.array.config_trustedAccessibilityServices); + if (Arrays.stream(trustedAccessibilityServices) + .map(ComponentName::unflattenFromString) + .anyMatch(componentName::equals)) { + return true; + } + } + return false; + } + @Override public void dump(FileDescriptor fd, final PrintWriter pw, String[] args) { if (!DumpUtils.checkDumpPermission(mContext, LOG_TAG, pw)) return; diff --git a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java index c4184854e690..3086ce1ceb40 100644 --- a/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java +++ b/services/accessibility/java/com/android/server/accessibility/gestures/TouchExplorer.java @@ -1466,8 +1466,11 @@ public class TouchExplorer extends BaseEventStreamTransformation int policyFlags = mState.getLastReceivedPolicyFlags(); if (mState.isDragging()) { // Send an event to the end of the drag gesture. - mDispatcher.sendMotionEvent( - event, ACTION_UP, rawEvent, ALL_POINTER_ID_BITS, policyFlags); + int pointerIdBits = ALL_POINTER_ID_BITS; + if (Flags.fixDragPointerWhenEndingDrag()) { + pointerIdBits = 1 << mDraggingPointerId; + } + mDispatcher.sendMotionEvent(event, ACTION_UP, rawEvent, pointerIdBits, policyFlags); } mState.startDelegating(); // Deliver all pointers to the view hierarchy. diff --git a/services/autofill/java/com/android/server/autofill/ui/SaveUi.java b/services/autofill/java/com/android/server/autofill/ui/SaveUi.java index 42ab05fdd231..4d42f154a392 100644 --- a/services/autofill/java/com/android/server/autofill/ui/SaveUi.java +++ b/services/autofill/java/com/android/server/autofill/ui/SaveUi.java @@ -58,11 +58,13 @@ import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; +import android.view.ViewTreeObserver; import android.view.Window; import android.view.WindowManager; import android.view.autofill.AutofillManager; import android.widget.ImageView; import android.widget.RemoteViews; +import android.widget.ScrollView; import android.widget.TextView; import com.android.internal.R; @@ -370,9 +372,23 @@ final class SaveUi { params.windowAnimations = R.style.AutofillSaveAnimation; params.setTrustedOverlay(); + ScrollView scrollView = view.findViewById(R.id.autofill_sheet_scroll_view); + + View divider = view.findViewById(R.id.autofill_sheet_divider); + + ViewTreeObserver observer = scrollView.getViewTreeObserver(); + observer.addOnGlobalLayoutListener(() -> adjustDividerVisibility(scrollView, divider)); + + scrollView.getViewTreeObserver() + .addOnScrollChangedListener(() -> adjustDividerVisibility(scrollView, divider)); show(); } + private void adjustDividerVisibility(ScrollView scrollView, View divider) { + boolean canScrollDown = scrollView.canScrollVertically(1); // 1 to check scrolling down + divider.setVisibility(canScrollDown ? View.VISIBLE : View.INVISIBLE); + } + private boolean applyCustomDescription(@NonNull Context context, @NonNull View saveUiView, @NonNull ValueFinder valueFinder, @NonNull SaveInfo info) { final CustomDescription customDescription = info.getCustomDescription(); diff --git a/services/backup/flags.aconfig b/services/backup/flags.aconfig index 549fa36597b7..4022e3378954 100644 --- a/services/backup/flags.aconfig +++ b/services/backup/flags.aconfig @@ -10,6 +10,15 @@ flag { } flag { + name: "enable_metrics_system_backup_agents" + namespace: "backup" + description: "Enable SystemBackupAgent to collect B&R agent metrics by passing an instance of " + "the logger to each BackupHelper." + bug: "296844513" + is_fixed_read_only: true +} + +flag { name: "enable_max_size_writes_to_pipes" namespace: "onboarding" description: "Enables the write buffer to pipes to be of maximum size." diff --git a/services/companion/java/com/android/server/companion/CompanionApplicationController.java b/services/companion/java/com/android/server/companion/CompanionApplicationController.java index c2d2468bbe44..586aa8aaae98 100644 --- a/services/companion/java/com/android/server/companion/CompanionApplicationController.java +++ b/services/companion/java/com/android/server/companion/CompanionApplicationController.java @@ -22,9 +22,11 @@ import android.annotation.SuppressLint; import android.annotation.UserIdInt; import android.companion.AssociationInfo; import android.companion.CompanionDeviceService; +import android.companion.DevicePresenceEvent; import android.content.ComponentName; import android.content.Context; import android.os.Handler; +import android.os.ParcelUuid; import android.util.Log; import android.util.Slog; import android.util.SparseArray; @@ -46,7 +48,8 @@ import java.util.Map; * the services, maintaining the connection (the binding), and invoking callback methods such as * {@link CompanionDeviceService#onDeviceAppeared(AssociationInfo)}, * {@link CompanionDeviceService#onDeviceDisappeared(AssociationInfo)} and - * {@link CompanionDeviceService#onDeviceEvent(AssociationInfo, int)} in the application process. + * {@link CompanionDeviceService#onDevicePresenceEvent(DevicePresenceEvent)} in the + * application process. * * <p> * The following is the list of the APIs provided by {@link CompanionApplicationController} (to be @@ -54,7 +57,7 @@ import java.util.Map; * <ul> * <li> {@link #bindCompanionApplication(int, String, boolean)} * <li> {@link #unbindCompanionApplication(int, String)} - * <li> {@link #notifyCompanionApplicationDeviceEvent(AssociationInfo, int)} (AssociationInfo, int)} + * <li> {@link #notifyCompanionApplicationDevicePresenceEvent(AssociationInfo, int)} * <li> {@link #isCompanionApplicationBound(int, String)} * <li> {@link #isRebindingCompanionApplicationScheduled(int, String)} * </ul> @@ -72,6 +75,7 @@ public class CompanionApplicationController { private final @NonNull Context mContext; private final @NonNull AssociationStore mAssociationStore; + private final @NonNull ObservableUuidStore mObservableUuidStore; private final @NonNull CompanionDevicePresenceMonitor mDevicePresenceMonitor; private final @NonNull CompanionServicesRegister mCompanionServicesRegister; @@ -82,9 +86,11 @@ public class CompanionApplicationController { private final @NonNull AndroidPackageMap<Boolean> mScheduledForRebindingCompanionApplications; CompanionApplicationController(Context context, AssociationStore associationStore, + ObservableUuidStore observableUuidStore, CompanionDevicePresenceMonitor companionDevicePresenceMonitor) { mContext = context; mAssociationStore = associationStore; + mObservableUuidStore = observableUuidStore; mDevicePresenceMonitor = companionDevicePresenceMonitor; mCompanionServicesRegister = new CompanionServicesRegister(); mBoundCompanionApplications = new AndroidPackageMap<>(); @@ -281,25 +287,50 @@ public class CompanionApplicationController { primaryServiceConnector.postOnDeviceDisappeared(association); } - void notifyCompanionApplicationDeviceEvent(AssociationInfo association, int event) { + void notifyCompanionApplicationDevicePresenceEvent(AssociationInfo association, int event) { final int userId = association.getUserId(); final String packageName = association.getPackageName(); final CompanionDeviceServiceConnector primaryServiceConnector = getPrimaryServiceConnector(userId, packageName); + final DevicePresenceEvent devicePresenceEvent = + new DevicePresenceEvent(association.getId(), event, null); if (primaryServiceConnector == null) { - Slog.e(TAG, "notifyCompanionApplicationDeviceEvent(): " + Slog.e(TAG, "notifyCompanionApplicationDevicePresenceEvent(): " + "u" + userId + "/" + packageName + " event=[ " + event + " ] is NOT bound."); Slog.e(TAG, "Stacktrace", new Throwable()); return; } - Slog.i(TAG, "Calling onDeviceEvent() to userId=[" + userId + "] package=[" + Slog.i(TAG, "Calling onDevicePresenceEvent() to userId=[" + userId + "] package=[" + packageName + "] associationId=[" + association.getId() - + "] state=[" + event + "]"); + + "] event=[" + event + "]"); - primaryServiceConnector.postOnDeviceEvent(association, event); + primaryServiceConnector.postOnDevicePresenceEvent(devicePresenceEvent); + } + + void notifyApplicationDevicePresenceEvent(ObservableUuid uuid, int event) { + final int userId = uuid.getUserId(); + final ParcelUuid parcelUuid = uuid.getUuid(); + final String packageName = uuid.getPackageName(); + final CompanionDeviceServiceConnector primaryServiceConnector = + getPrimaryServiceConnector(userId, packageName); + final DevicePresenceEvent devicePresenceEvent = + new DevicePresenceEvent(DevicePresenceEvent.NO_ASSOCIATION, event, parcelUuid); + + if (primaryServiceConnector == null) { + Slog.e(TAG, "notifyApplicationDevicePresenceChanged(): " + + "u" + userId + "/" + packageName + + " event=[ " + event + " ] is NOT bound."); + Slog.e(TAG, "Stacktrace", new Throwable()); + return; + } + + Slog.i(TAG, "Calling onDevicePresenceEvent() to userId=[" + userId + "] package=[" + + packageName + "]" + "event= [" + event + "]"); + + primaryServiceConnector.postOnDevicePresenceEvent(devicePresenceEvent); } void dump(@NonNull PrintWriter out) { @@ -364,6 +395,9 @@ public class CompanionApplicationController { // Make sure to clean up the state for all the associations // that associate with this package. boolean shouldScheduleRebind = false; + boolean shouldScheduleRebindForUuid = false; + final List<ObservableUuid> uuids = + mObservableUuidStore.getObservableUuidsForPackage(userId, packageName); for (AssociationInfo ai : mAssociationStore.getAssociationsForPackage(userId, packageName)) { @@ -385,7 +419,14 @@ public class CompanionApplicationController { } } - return stillAssociated && shouldScheduleRebind; + for (ObservableUuid uuid : uuids) { + if (mDevicePresenceMonitor.isDeviceUuidPresent(uuid.getUuid())) { + shouldScheduleRebindForUuid = true; + break; + } + } + + return (stillAssociated && shouldScheduleRebind) || shouldScheduleRebindForUuid; } private class CompanionServicesRegister extends PerUser<Map<String, List<ComponentName>>> { diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java index 84e1d9062fd5..2e01ced2022b 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java @@ -20,17 +20,17 @@ package com.android.server.companion; import static android.Manifest.permission.ASSOCIATE_COMPANION_DEVICES; import static android.Manifest.permission.DELIVER_COMPANION_MESSAGES; import static android.Manifest.permission.MANAGE_COMPANION_DEVICES; -import static android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE; import static android.Manifest.permission.REQUEST_COMPANION_PROFILE_AUTOMOTIVE_PROJECTION; +import static android.Manifest.permission.REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE; import static android.Manifest.permission.USE_COMPANION_TRANSPORTS; import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE; import static android.companion.AssociationRequest.DEVICE_PROFILE_AUTOMOTIVE_PROJECTION; -import static android.companion.CompanionDeviceService.DEVICE_EVENT_BLE_APPEARED; -import static android.companion.CompanionDeviceService.DEVICE_EVENT_BLE_DISAPPEARED; -import static android.companion.CompanionDeviceService.DEVICE_EVENT_BT_CONNECTED; -import static android.companion.CompanionDeviceService.DEVICE_EVENT_BT_DISCONNECTED; -import static android.companion.CompanionDeviceService.DEVICE_EVENT_SELF_MANAGED_APPEARED; -import static android.companion.CompanionDeviceService.DEVICE_EVENT_SELF_MANAGED_DISAPPEARED; +import static android.companion.DevicePresenceEvent.EVENT_BLE_APPEARED; +import static android.companion.DevicePresenceEvent.EVENT_BLE_DISAPPEARED; +import static android.companion.DevicePresenceEvent.EVENT_BT_CONNECTED; +import static android.companion.DevicePresenceEvent.EVENT_BT_DISCONNECTED; +import static android.companion.DevicePresenceEvent.EVENT_SELF_MANAGED_APPEARED; +import static android.companion.DevicePresenceEvent.EVENT_SELF_MANAGED_DISAPPEARED; import static android.content.pm.PackageManager.CERT_INPUT_SHA256; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.os.Process.SYSTEM_UID; @@ -45,6 +45,7 @@ import static com.android.server.companion.PackageUtils.enforceUsesCompanionDevi import static com.android.server.companion.PackageUtils.getPackageInfo; import static com.android.server.companion.PermissionsUtils.checkCallerCanManageCompanionDevice; import static com.android.server.companion.PermissionsUtils.enforceCallerCanManageAssociationsForPackage; +import static com.android.server.companion.PermissionsUtils.enforceCallerCanObservingDevicePresenceByUuid; import static com.android.server.companion.PermissionsUtils.enforceCallerIsSystemOr; import static com.android.server.companion.PermissionsUtils.enforceCallerIsSystemOrCanInteractWithUserId; import static com.android.server.companion.PermissionsUtils.sanitizeWithCallerChecks; @@ -75,6 +76,7 @@ import android.companion.IOnAssociationsChangedListener; import android.companion.IOnMessageReceivedListener; import android.companion.IOnTransportsChangedListener; import android.companion.ISystemDataTransferCallback; +import android.companion.ObservingDevicePresenceRequest; import android.companion.datatransfer.PermissionSyncRequest; import android.content.ComponentName; import android.content.Context; @@ -92,6 +94,7 @@ import android.os.Handler; import android.os.Message; import android.os.Parcel; import android.os.ParcelFileDescriptor; +import android.os.ParcelUuid; import android.os.PowerManagerInternal; import android.os.PowerWhitelistManager; import android.os.RemoteCallbackList; @@ -132,6 +135,7 @@ import java.io.File; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; @@ -221,6 +225,8 @@ public class CompanionDeviceManagerService extends SystemService { private CrossDeviceSyncController mCrossDeviceSyncController; + private ObservableUuidStore mObservableUuidStore; + public CompanionDeviceManagerService(Context context) { super(context); @@ -240,6 +246,7 @@ public class CompanionDeviceManagerService extends SystemService { mOnPackageVisibilityChangeListener = new OnPackageVisibilityChangeListener(mActivityManager); mPowerManagerInternal = LocalServices.getService(PowerManagerInternal.class); + mObservableUuidStore = new ObservableUuidStore(); } @Override @@ -254,13 +261,16 @@ public class CompanionDeviceManagerService extends SystemService { mSystemDataTransferRequestStore, mAssociationRequestsProcessor); loadAssociationsFromDisk(); + + mObservableUuidStore.getObservableUuidsForUser(getContext().getUserId()); + mAssociationStore.registerListener(mAssociationStoreChangeListener); mDevicePresenceMonitor = new CompanionDevicePresenceMonitor(mUserManager, - mAssociationStore, mDevicePresenceCallback); + mAssociationStore, mObservableUuidStore, mDevicePresenceCallback); mCompanionAppController = new CompanionApplicationController( - context, mAssociationStore, mDevicePresenceMonitor); + context, mAssociationStore, mObservableUuidStore, mDevicePresenceMonitor); mTransportManager = new CompanionTransportManager(context, mAssociationStore); mSystemDataTransferProcessor = new SystemDataTransferProcessor(this, mPackageManagerInternal, mAssociationStore, @@ -352,13 +362,29 @@ public class CompanionDeviceManagerService extends SystemService { final int userId = user.getUserIdentifier(); final Set<BluetoothDevice> blueToothDevices = mDevicePresenceMonitor.getPendingConnectedDevices().get(userId); + + final List<ObservableUuid> observableUuids = + mObservableUuidStore.getObservableUuidsForUser(userId); + if (blueToothDevices != null) { for (BluetoothDevice bluetoothDevice : blueToothDevices) { + final List<ParcelUuid> deviceUuids = bluetoothDevice.getUuids() == null + ? Collections.emptyList() : Arrays.asList(bluetoothDevice.getUuids()); + for (AssociationInfo ai: mAssociationStore.getAssociationsByAddress(bluetoothDevice.getAddress())) { Slog.i(TAG, "onUserUnlocked, device id( " + ai.getId() + " ) is connected"); mDevicePresenceMonitor.onBluetoothCompanionDeviceConnected(ai.getId()); } + + for (ObservableUuid observableUuid : observableUuids) { + if (deviceUuids.contains(observableUuid.getUuid())) { + Slog.i(TAG, "onUserUnlocked, UUID( " + + observableUuid.getUuid() + " ) is connected"); + mDevicePresenceMonitor.onDevicePresenceEventByUuid( + observableUuid, EVENT_BT_CONNECTED); + } + } } } } @@ -423,31 +449,31 @@ public class CompanionDeviceManagerService extends SystemService { } } - private void onDeviceEventInternal(int associationId, int event) { - Slog.i(TAG, "onDeviceEventInternal() id=" + associationId + " event= " + event); + private void onDevicePresenceEventInternal(int associationId, int event) { + Slog.i(TAG, "onDevicePresenceEventInternal() id=" + associationId + " event= " + event); final AssociationInfo association = mAssociationStore.getAssociationById(associationId); final String packageName = association.getPackageName(); final int userId = association.getUserId(); switch (event) { - case DEVICE_EVENT_BLE_APPEARED: - case DEVICE_EVENT_BT_CONNECTED: - case DEVICE_EVENT_SELF_MANAGED_APPEARED: + case EVENT_BLE_APPEARED: + case EVENT_BT_CONNECTED: + case EVENT_SELF_MANAGED_APPEARED: if (!association.shouldBindWhenPresent()) return; bindApplicationIfNeeded(association); - mCompanionAppController.notifyCompanionApplicationDeviceEvent( + mCompanionAppController.notifyCompanionApplicationDevicePresenceEvent( association, event); break; - case DEVICE_EVENT_BLE_DISAPPEARED: - case DEVICE_EVENT_BT_DISCONNECTED: - case DEVICE_EVENT_SELF_MANAGED_DISAPPEARED: + case EVENT_BLE_DISAPPEARED: + case EVENT_BT_DISCONNECTED: + case EVENT_SELF_MANAGED_DISAPPEARED: if (!mCompanionAppController.isCompanionApplicationBound(userId, packageName)) { if (DEBUG) Log.w(TAG, "u" + userId + "\\" + packageName + " is NOT bound"); return; } if (association.shouldBindWhenPresent()) { - mCompanionAppController.notifyCompanionApplicationDeviceEvent( + mCompanionAppController.notifyCompanionApplicationDevicePresenceEvent( association, event); } // Check if there are other devices associated to the app that are present. @@ -460,6 +486,45 @@ public class CompanionDeviceManagerService extends SystemService { } } + private void onDevicePresenceEventByUuidInternal(ObservableUuid uuid, int event) { + Slog.i(TAG, "onDevicePresenceEventByUuidInternal() id=" + uuid.getUuid() + + "for package=" + uuid.getPackageName() + " event=" + event); + final String packageName = uuid.getPackageName(); + final int userId = uuid.getUserId(); + + switch(event) { + case EVENT_BT_CONNECTED: + if (!mCompanionAppController.isCompanionApplicationBound(userId, packageName)) { + mCompanionAppController.bindCompanionApplication( + userId, packageName, /*bindImportant*/ false); + + } else if (DEBUG) { + Log.i(TAG, "u" + userId + "\\" + packageName + " is already bound"); + } + + mCompanionAppController.notifyApplicationDevicePresenceEvent(uuid, event); + + break; + case EVENT_BT_DISCONNECTED: + if (!mCompanionAppController.isCompanionApplicationBound(userId, packageName)) { + if (DEBUG) Log.w(TAG, "u" + userId + "\\" + packageName + " is NOT bound"); + return; + } + + mCompanionAppController.notifyApplicationDevicePresenceEvent(uuid, event); + // Check if there are other devices associated to the app or the UUID to be + // observed are present. + if (shouldBindPackage(userId, packageName)) return; + + mCompanionAppController.unbindCompanionApplication(userId, packageName); + + break; + default: + Slog.e(TAG, "Event: " + event + "is not supported"); + break; + } + } + private void bindApplicationIfNeeded(AssociationInfo association) { final String packageName = association.getPackageName(); final int userId = association.getUserId(); @@ -476,15 +541,26 @@ public class CompanionDeviceManagerService extends SystemService { /** * @return whether the package should be bound (i.e. at least one of the devices associated with - * the package is currently present). + * the package is currently present OR the UUID to be observed by this package is + * currently present). */ private boolean shouldBindPackage(@UserIdInt int userId, @NonNull String packageName) { final List<AssociationInfo> packageAssociations = mAssociationStore.getAssociationsForPackage(userId, packageName); + final List<ObservableUuid> observableUuids = + mObservableUuidStore.getObservableUuidsForPackage(userId, packageName); + for (AssociationInfo association : packageAssociations) { if (!association.shouldBindWhenPresent()) continue; if (mDevicePresenceMonitor.isDevicePresent(association.getId())) return true; } + + for (ObservableUuid uuid : observableUuids) { + if (mDevicePresenceMonitor.isDeviceUuidPresent(uuid.getUuid())) { + return true; + } + } + return false; } @@ -568,6 +644,8 @@ public class CompanionDeviceManagerService extends SystemService { // Clear associations. final List<AssociationInfo> associationsForPackage = mAssociationStore.getAssociationsForPackage(userId, packageName); + final List<ObservableUuid> uuidsTobeObserved = + mObservableUuidStore.getObservableUuidsForPackage(userId, packageName); for (AssociationInfo association : associationsForPackage) { mAssociationStore.removeAssociation(association.getId()); } @@ -575,6 +653,10 @@ public class CompanionDeviceManagerService extends SystemService { for (AssociationInfo association : associationsForPackage) { maybeRemoveRoleHolderForAssociation(association); } + // Clear the uuids to be observed. + for (ObservableUuid uuid : uuidsTobeObserved) { + mObservableUuidStore.removeObservableUuid(userId, uuid.getUuid(), packageName); + } mCompanionAppController.onPackagesChanged(userId); } @@ -855,6 +937,95 @@ public class CompanionDeviceManagerService extends SystemService { } @Override + @EnforcePermission(REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE) + public void startObservingDevicePresence(ObservingDevicePresenceRequest request, + String packageName, int userId) { + startObservingDevicePresence_enforcePermission(); + registerDevicePresenceListener(request, packageName, userId, /* active */ true); + } + + @Override + @EnforcePermission(REQUEST_OBSERVE_COMPANION_DEVICE_PRESENCE) + public void stopObservingDevicePresence(ObservingDevicePresenceRequest request, + String packageName, int userId) { + stopObservingDevicePresence_enforcePermission(); + registerDevicePresenceListener(request, packageName, userId, /* active */ false); + } + + private void registerDevicePresenceListener(ObservingDevicePresenceRequest request, + String packageName, int userId, boolean active) { + enforceUsesCompanionDeviceFeature(getContext(), userId, packageName); + enforceCallerIsSystemOr(userId, packageName); + + final int associationId = request.getAssociationId(); + final AssociationInfo associationInfo = mAssociationStore.getAssociationById( + associationId); + final ParcelUuid uuid = request.getUuid(); + + if (uuid != null) { + enforceCallerCanObservingDevicePresenceByUuid(getContext()); + if (active) { + startObservingDevicePresenceByUuid(uuid, packageName, userId); + } else { + stopObservingDevicePresenceByUuid(uuid, packageName, userId); + } + } else if (associationInfo == null) { + throw new IllegalArgumentException("App " + packageName + + " is not associated with device " + request.getAssociationId() + + " for user " + userId); + } else { + processDevicePresenceListener( + associationInfo, userId, packageName, active); + } + } + + private void startObservingDevicePresenceByUuid(ParcelUuid uuid, String packageName, + int userId) { + final List<ObservableUuid> observableUuids = + mObservableUuidStore.getObservableUuidsForPackage(userId, packageName); + + for (ObservableUuid observableUuid : observableUuids) { + if (observableUuid.getUuid().equals(uuid)) { + Slog.i(TAG, "The uuid: " + uuid + " for package:" + packageName + + "has been already scheduled for observing"); + return; + } + } + + final ObservableUuid observableUuid = new ObservableUuid(userId, uuid, + packageName, System.currentTimeMillis()); + + mObservableUuidStore.writeObservableUuid(userId, observableUuid); + } + + private void stopObservingDevicePresenceByUuid(ParcelUuid uuid, String packageName, + int userId) { + final List<ObservableUuid> uuidsTobeObserved = + mObservableUuidStore.getObservableUuidsForPackage(userId, packageName); + boolean isScheduledObserving = false; + + for (ObservableUuid observableUuid : uuidsTobeObserved) { + if (observableUuid.getUuid().equals(uuid)) { + isScheduledObserving = true; + break; + } + } + + if (!isScheduledObserving) { + Slog.i(TAG, "The uuid: " + uuid.toString() + " for package:" + packageName + + "has NOT been scheduled for observing yet"); + return; + } + + mObservableUuidStore.removeObservableUuid(userId, uuid, packageName); + mDevicePresenceMonitor.removeCurrentConnectedUuidDevice(uuid); + + if (!shouldBindPackage(userId, packageName)) { + mCompanionAppController.unbindCompanionApplication(userId, packageName); + } + } + + @Override public PendingIntent buildPermissionTransferUserConsentIntent(String packageName, int userId, int associationId) { return mSystemDataTransferProcessor.buildPermissionTransferUserConsentIntent( @@ -1002,6 +1173,11 @@ public class CompanionDeviceManagerService extends SystemService { + " for user " + userId)); } + processDevicePresenceListener(association, userId, packageName, active); + } + + private void processDevicePresenceListener(AssociationInfo association, + int userId, String packageName, boolean active) { // If already at specified state, then no-op. if (active == association.isNotifyOnDeviceNearby()) { if (DEBUG) Log.d(TAG, "Device presence listener is already at desired state."); @@ -1025,9 +1201,9 @@ public class CompanionDeviceManagerService extends SystemService { if (mDevicePresenceMonitor.isBlePresent(associationId) || mDevicePresenceMonitor.isSimulatePresent(associationId)) { onDeviceAppearedInternal(associationId); - onDeviceEventInternal(associationId, DEVICE_EVENT_BLE_APPEARED); + onDevicePresenceEventInternal(associationId, EVENT_BLE_APPEARED); } else if (mDevicePresenceMonitor.isBtConnected(associationId)) { - onDeviceEventInternal(associationId, DEVICE_EVENT_BT_CONNECTED); + onDevicePresenceEventInternal(associationId, EVENT_BT_CONNECTED); } } @@ -1518,20 +1694,25 @@ public class CompanionDeviceManagerService extends SystemService { private final CompanionDevicePresenceMonitor.Callback mDevicePresenceCallback = new CompanionDevicePresenceMonitor.Callback() { - @Override - public void onDeviceAppeared(int associationId) { - onDeviceAppearedInternal(associationId); - } + @Override + public void onDeviceAppeared(int associationId) { + onDeviceAppearedInternal(associationId); + } - @Override - public void onDeviceDisappeared(int associationId) { - onDeviceDisappearedInternal(associationId); - } + @Override + public void onDeviceDisappeared(int associationId) { + onDeviceDisappearedInternal(associationId); + } - @Override - public void onDeviceEvent(int associationId, int event) { - onDeviceEventInternal(associationId, event); - } + @Override + public void onDevicePresenceEvent(int associationId, int event) { + onDevicePresenceEventInternal(associationId, event); + } + + @Override + public void onDevicePresenceEventByUuid(ObservableUuid uuid, int event) { + onDevicePresenceEventByUuidInternal(uuid, event); + } }; private final PackageMonitor mPackageMonitor = new PackageMonitor() { diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceServiceConnector.java b/services/companion/java/com/android/server/companion/CompanionDeviceServiceConnector.java index 928842c79190..5abdb42b34fc 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceServiceConnector.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceServiceConnector.java @@ -26,6 +26,7 @@ import android.annotation.SuppressLint; import android.annotation.UserIdInt; import android.companion.AssociationInfo; import android.companion.CompanionDeviceService; +import android.companion.DevicePresenceEvent; import android.companion.ICompanionDeviceService; import android.content.ComponentName; import android.content.Context; @@ -106,11 +107,10 @@ class CompanionDeviceServiceConnector extends ServiceConnector.Impl<ICompanionDe void postOnDeviceDisappeared(@NonNull AssociationInfo associationInfo) { post(companionService -> companionService.onDeviceDisappeared(associationInfo)); } - void postOnDeviceEvent(@NonNull AssociationInfo associationInfo, int event) { - post(companionService -> companionService.onDeviceEvent(associationInfo, event)); - } - + void postOnDevicePresenceEvent(@NonNull DevicePresenceEvent event) { + post(companionService -> companionService.onDevicePresenceEvent(event)); + } /** * Post "unbind" job, which will run *after* all previously posted jobs complete. diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java index e5a8c4fa22b7..5663434e2b6d 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceShellCommand.java @@ -27,6 +27,7 @@ import android.companion.Telecom; import android.companion.datatransfer.PermissionSyncRequest; import android.net.MacAddress; import android.os.Binder; +import android.os.ParcelUuid; import android.os.ShellCommand; import android.util.Base64; import android.util.proto.ProtoOutputStream; @@ -80,6 +81,19 @@ class CompanionDeviceShellCommand extends ShellCommand { mDevicePresenceMonitor.simulateDeviceEvent(associationId, event); return 0; } + + if ("simulate-device-uuid-event".equals(cmd) && Flags.devicePresence()) { + String uuid = getNextArgRequired(); + String packageName = getNextArgRequired(); + int userId = getNextIntArgRequired(); + int event = getNextIntArgRequired(); + ObservableUuid observableUuid = new ObservableUuid( + userId, ParcelUuid.fromString(uuid), packageName, + System.currentTimeMillis()); + mDevicePresenceMonitor.simulateDeviceEventByUuid(observableUuid, event); + return 0; + } + switch (cmd) { case "list": { final int userId = getNextIntArgRequired(); @@ -447,6 +461,16 @@ class CompanionDeviceShellCommand extends ShellCommand { pw.println(" Case(3): "); pw.println(" Make CDM act as if the given companion device is BT disconnected "); pw.println(" USE FOR DEBUGGING AND/OR TESTING PURPOSES ONLY."); + + pw.println(" simulate-device-uuid-event UUID PACKAGE USERID EVENT"); + pw.println(" Simulate the companion device event changes:"); + pw.println(" Case(2): "); + pw.println(" Make CDM act as if the given DEVICE is BT connected base" + + "on the UUID"); + pw.println(" Case(3): "); + pw.println(" Make CDM act as if the given DEVICE is BT disconnected base" + + "on the UUID"); + pw.println(" USE FOR DEBUGGING AND/OR TESTING PURPOSES ONLY."); } pw.println(" remove-inactive-associations"); diff --git a/services/companion/java/com/android/server/companion/ObservableUuid.java b/services/companion/java/com/android/server/companion/ObservableUuid.java new file mode 100644 index 000000000000..6ab3188c8fd2 --- /dev/null +++ b/services/companion/java/com/android/server/companion/ObservableUuid.java @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.companion; + +import android.annotation.NonNull; +import android.annotation.UserIdInt; +import android.os.ParcelUuid; + +public class ObservableUuid { + private final int mUserId; + private final String mPackageName; + + private final ParcelUuid mUuid; + + private final long mTimeApprovedMs; + + public ObservableUuid(@UserIdInt int userId, @NonNull ParcelUuid uuid, + @NonNull String packageName, Long timeApprovedMs) { + mUserId = userId; + mUuid = uuid; + mPackageName = packageName; + mTimeApprovedMs = timeApprovedMs; + } + + public int getUserId() { + return mUserId; + } + + public ParcelUuid getUuid() { + return mUuid; + } + + public String getPackageName() { + return mPackageName; + } + + public long getTimeApprovedMs() { + return mTimeApprovedMs; + } +} diff --git a/services/companion/java/com/android/server/companion/ObservableUuidStore.java b/services/companion/java/com/android/server/companion/ObservableUuidStore.java new file mode 100644 index 000000000000..94be22afd9e2 --- /dev/null +++ b/services/companion/java/com/android/server/companion/ObservableUuidStore.java @@ -0,0 +1,295 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.companion; + +import static com.android.internal.util.XmlUtils.readIntAttribute; +import static com.android.internal.util.XmlUtils.readLongAttribute; +import static com.android.internal.util.XmlUtils.readStringAttribute; +import static com.android.internal.util.XmlUtils.writeIntAttribute; +import static com.android.internal.util.XmlUtils.writeLongAttribute; +import static com.android.internal.util.XmlUtils.writeStringAttribute; +import static com.android.server.companion.DataStoreUtils.createStorageFileForUser; +import static com.android.server.companion.DataStoreUtils.isEndOfTag; +import static com.android.server.companion.DataStoreUtils.isStartOfTag; +import static com.android.server.companion.DataStoreUtils.writeToFileSafely; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.UserIdInt; +import android.os.ParcelUuid; +import android.util.AtomicFile; +import android.util.Slog; +import android.util.SparseArray; +import android.util.Xml; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.util.XmlUtils; +import com.android.modules.utils.TypedXmlPullParser; +import com.android.modules.utils.TypedXmlSerializer; + +import org.xmlpull.v1.XmlPullParserException; + +import java.io.FileInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +public class ObservableUuidStore { + private static final String TAG = "CDM_ObservableUuidStore"; + private static final String FILE_NAME = "observing_uuids_presence.xml"; + private static final String XML_TAG_UUIDS = "uuids"; + private static final String XML_TAG_UUID = "uuid"; + private static final String XML_ATTR_UUID = "uuid"; + private static final String XML_ATTR_TIME_APPROVED = "time_approved"; + private static final String XML_ATTR_USER_ID = "user_id"; + private static final String XML_ATTR_PACKAGE = "package_name"; + private static final int READ_FROM_DISK_TIMEOUT = 5; // in seconds + + + private final ExecutorService mExecutor; + private final ConcurrentMap<Integer, AtomicFile> mUserIdToStorageFile = + new ConcurrentHashMap<>(); + + private final Object mLock = new Object(); + + @GuardedBy("mLock") + private final SparseArray<List<ObservableUuid>> mCachedPerUser = + new SparseArray<>(); + + public ObservableUuidStore() { + mExecutor = Executors.newSingleThreadExecutor(); + } + + /** + * Remove the observable uuid from the disk. + */ + void removeObservableUuid(@UserIdInt int userId, ParcelUuid uuid, String packageName) { + List<ObservableUuid> cachedObservableUuids; + + synchronized (mLock) { + // Remove requests from cache + cachedObservableUuids = readObservableUuidsFromCache(userId); + cachedObservableUuids.removeIf( + uuid1 -> uuid1.getPackageName().equals(packageName) + && uuid1.getUuid().equals(uuid)); + mCachedPerUser.set(userId, cachedObservableUuids); + } + // Remove requests from store + mExecutor.execute(() -> writeObservableUuidToStore(userId, cachedObservableUuids)); + } + + void writeObservableUuid(@UserIdInt int userId, ObservableUuid uuid) { + Slog.i(TAG, "Writing uuid=" + uuid.getUuid() + " to store."); + + List<ObservableUuid> cachedObservableUuids; + synchronized (mLock) { + // Write to cache + cachedObservableUuids = readObservableUuidsFromCache(userId); + cachedObservableUuids.removeIf(uuid1 -> uuid1.getUuid().equals( + uuid.getUuid()) && uuid1.getPackageName().equals(uuid.getPackageName())); + cachedObservableUuids.add(uuid); + mCachedPerUser.set(userId, cachedObservableUuids); + } + // Write to store + mExecutor.execute(() -> writeObservableUuidToStore(userId, cachedObservableUuids)); + } + + private void writeObservableUuidToStore(@UserIdInt int userId, + @NonNull List<ObservableUuid> cachedObservableUuids) { + final AtomicFile file = getStorageFileForUser(userId); + Slog.i(TAG, "Writing ObservableUuid for user " + userId + " to file=" + + file.getBaseFile().getPath()); + + // getStorageFileForUser() ALWAYS returns the SAME OBJECT, which allows us to synchronize + // accesses to the file on the file system using this AtomicFile object. + synchronized (file) { + writeToFileSafely(file, out -> { + final TypedXmlSerializer serializer = Xml.resolveSerializer(out); + serializer.setFeature( + "http://xmlpull.org/v1/doc/features.html#indent-output", true); + serializer.startDocument(null, true); + writeObservableUuidToXml(serializer, cachedObservableUuids); + serializer.endDocument(); + }); + } + } + + private void writeObservableUuidToXml(@NonNull TypedXmlSerializer serializer, + @Nullable Collection<ObservableUuid> uuids) throws IOException { + serializer.startTag(null, XML_TAG_UUIDS); + + for (ObservableUuid uuid : uuids) { + writeUuidToXml(serializer, uuid); + } + + serializer.endTag(null, XML_TAG_UUIDS); + } + + private void writeUuidToXml(@NonNull TypedXmlSerializer serializer, + @NonNull ObservableUuid uuid) throws IOException { + serializer.startTag(null, XML_TAG_UUID); + + writeIntAttribute(serializer, XML_ATTR_USER_ID, uuid.getUserId()); + writeStringAttribute(serializer, XML_ATTR_UUID, uuid.getUuid().toString()); + writeStringAttribute(serializer, XML_ATTR_PACKAGE, uuid.getPackageName()); + writeLongAttribute(serializer, XML_ATTR_TIME_APPROVED, uuid.getTimeApprovedMs()); + + serializer.endTag(null, XML_TAG_UUID); + } + + /** + * Read the observable UUIDs from the cache. + */ + @GuardedBy("mLock") + private List<ObservableUuid> readObservableUuidsFromCache(@UserIdInt int userId) { + List<ObservableUuid> cachedObservableUuids = mCachedPerUser.get(userId); + if (cachedObservableUuids == null) { + Future<List<ObservableUuid>> future = + mExecutor.submit(() -> readObservableUuidFromStore(userId)); + try { + cachedObservableUuids = future.get(READ_FROM_DISK_TIMEOUT, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Slog.e(TAG, "Thread reading ObservableUuid from disk is " + + "interrupted."); + } catch (ExecutionException e) { + Slog.e(TAG, "Error occurred while reading ObservableUuid " + + "from disk."); + } catch (TimeoutException e) { + Slog.e(TAG, "Reading ObservableUuid from disk timed out."); + } + mCachedPerUser.set(userId, cachedObservableUuids); + } + return cachedObservableUuids; + } + + /** + * Reads previously persisted data for the given user + * + * @param userId Android UserID + * @return a list of ObservableUuid + */ + @NonNull + public List<ObservableUuid> readObservableUuidFromStore(@UserIdInt int userId) { + final AtomicFile file = getStorageFileForUser(userId); + Slog.i(TAG, "Reading ObservableUuid for user " + userId + " from " + + "file=" + file.getBaseFile().getPath()); + + // getStorageFileForUser() ALWAYS returns the SAME OBJECT, which allows us to synchronize + // accesses to the file on the file system using this AtomicFile object. + synchronized (file) { + if (!file.getBaseFile().exists()) { + Slog.d(TAG, "File does not exist -> Abort"); + return new ArrayList<>(); + } + try (FileInputStream in = file.openRead()) { + final TypedXmlPullParser parser = Xml.resolvePullParser(in); + XmlUtils.beginDocument(parser, XML_TAG_UUIDS); + + return readObservableUuidFromXml(parser); + } catch (XmlPullParserException | IOException e) { + Slog.e(TAG, "Error while reading requests file", e); + return new ArrayList<>(); + } + } + } + + @NonNull + private List<ObservableUuid> readObservableUuidFromXml( + @NonNull TypedXmlPullParser parser) throws XmlPullParserException, IOException { + if (!isStartOfTag(parser, XML_TAG_UUIDS)) { + throw new XmlPullParserException("The XML doesn't have start tag: " + XML_TAG_UUIDS); + } + + List<ObservableUuid> observableUuids = new ArrayList<>(); + + while (true) { + parser.nextTag(); + if (isEndOfTag(parser, XML_TAG_UUIDS)) { + break; + } + if (isStartOfTag(parser, XML_TAG_UUID)) { + observableUuids.add(readUuidFromXml(parser)); + } + } + + return observableUuids; + } + + private ObservableUuid readUuidFromXml(@NonNull TypedXmlPullParser parser) + throws XmlPullParserException, IOException { + if (!isStartOfTag(parser, XML_TAG_UUID)) { + throw new XmlPullParserException("XML doesn't have start tag: " + XML_TAG_UUID); + } + + final int userId = readIntAttribute(parser, XML_ATTR_USER_ID); + final ParcelUuid uuid = ParcelUuid.fromString(readStringAttribute(parser, XML_ATTR_UUID)); + final String packageName = readStringAttribute(parser, XML_ATTR_PACKAGE); + final Long timeApproved = readLongAttribute(parser, XML_ATTR_TIME_APPROVED); + + return new ObservableUuid(userId, uuid, packageName, timeApproved); + } + + /** + * Creates and caches {@link AtomicFile} object that represents the back-up file for the given + * user. + * <p> + * IMPORTANT: the method will ALWAYS return the same {@link AtomicFile} object, which makes it + * possible to synchronize reads and writes to the file using the returned object. + */ + @NonNull + private AtomicFile getStorageFileForUser(@UserIdInt int userId) { + return mUserIdToStorageFile.computeIfAbsent(userId, + u -> createStorageFileForUser(userId, FILE_NAME)); + } + + /** + * @return A list of ObservableUuids per package. + */ + public List<ObservableUuid> getObservableUuidsForPackage( + @UserIdInt int userId, @NonNull String packageName) { + final List<ObservableUuid> uuidsTobeObservedPerPackage = new ArrayList<>(); + synchronized (mLock) { + final List<ObservableUuid> uuids = readObservableUuidsFromCache(userId); + + for (ObservableUuid uuid : uuids) { + if (uuid.getPackageName().equals(packageName)) { + uuidsTobeObservedPerPackage.add(uuid); + } + } + } + + return uuidsTobeObservedPerPackage; + } + + /** + * @return A list of ObservableUuids per user. + */ + public List<ObservableUuid> getObservableUuidsForUser(@UserIdInt int userId) { + synchronized (mLock) { + return readObservableUuidsFromCache(userId); + } + } +} diff --git a/services/companion/java/com/android/server/companion/PermissionsUtils.java b/services/companion/java/com/android/server/companion/PermissionsUtils.java index f4e14df4de99..15bebbae05b1 100644 --- a/services/companion/java/com/android/server/companion/PermissionsUtils.java +++ b/services/companion/java/com/android/server/companion/PermissionsUtils.java @@ -19,6 +19,7 @@ package com.android.server.companion; import static android.Manifest.permission.INTERACT_ACROSS_USERS; import static android.Manifest.permission.MANAGE_COMPANION_DEVICES; import static android.Manifest.permission.REQUEST_COMPANION_SELF_MANAGED; +import static android.Manifest.permission.REQUEST_OBSERVE_DEVICE_UUID_PRESENCE; import static android.app.AppOpsManager.MODE_ALLOWED; import static android.companion.AssociationRequest.DEVICE_PROFILE_APP_STREAMING; import static android.companion.AssociationRequest.DEVICE_PROFILE_AUTOMOTIVE_PROJECTION; @@ -174,6 +175,14 @@ public final class PermissionsUtils { + " for u" + userId + "/" + packageName); } + static void enforceCallerCanObservingDevicePresenceByUuid(@NonNull Context context) { + if (context.checkCallingPermission(REQUEST_OBSERVE_DEVICE_UUID_PRESENCE) + != PERMISSION_GRANTED) { + throw new SecurityException("Caller (uid=" + getCallingUid() + ") does not have " + + "permissions to request observing device presence base on the UUID"); + } + } + /** * Check if the caller is either: * <ul> diff --git a/services/companion/java/com/android/server/companion/presence/BluetoothCompanionDeviceConnectionListener.java b/services/companion/java/com/android/server/companion/presence/BluetoothCompanionDeviceConnectionListener.java index 6ba85bdda9e4..7eca1193ca12 100644 --- a/services/companion/java/com/android/server/companion/presence/BluetoothCompanionDeviceConnectionListener.java +++ b/services/companion/java/com/android/server/companion/presence/BluetoothCompanionDeviceConnectionListener.java @@ -16,6 +16,9 @@ package com.android.server.companion.presence; +import static android.companion.DevicePresenceEvent.EVENT_BT_CONNECTED; +import static android.companion.DevicePresenceEvent.EVENT_BT_DISCONNECTED; + import static com.android.server.companion.presence.CompanionDevicePresenceMonitor.DEBUG; import static com.android.server.companion.presence.Utils.btDeviceToString; @@ -27,6 +30,7 @@ import android.companion.AssociationInfo; import android.net.MacAddress; import android.os.Handler; import android.os.HandlerExecutor; +import android.os.ParcelUuid; import android.os.UserHandle; import android.os.UserManager; import android.util.Log; @@ -35,8 +39,11 @@ import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; import com.android.server.companion.AssociationStore; +import com.android.server.companion.ObservableUuid; +import com.android.server.companion.ObservableUuidStore; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -53,6 +60,8 @@ public class BluetoothCompanionDeviceConnectionListener void onBluetoothCompanionDeviceConnected(int associationId); void onBluetoothCompanionDeviceDisconnected(int associationId); + + void onDevicePresenceEventByUuid(ObservableUuid uuid, int event); } private final UserManager mUserManager; @@ -61,6 +70,8 @@ public class BluetoothCompanionDeviceConnectionListener /** A set of ALL connected BT device (not only companion.) */ private final @NonNull Map<MacAddress, BluetoothDevice> mAllConnectedDevices = new HashMap<>(); + private final @NonNull ObservableUuidStore mObservableUuidStore; + /** * A structure hold the connected BT devices that are pending to be reported to the companion * app when the user unlocks the local device per userId. @@ -70,8 +81,10 @@ public class BluetoothCompanionDeviceConnectionListener final SparseArray<Set<BluetoothDevice>> mPendingConnectedDevices = new SparseArray<>(); BluetoothCompanionDeviceConnectionListener(UserManager userManager, - @NonNull AssociationStore associationStore, @NonNull Callback callback) { + @NonNull AssociationStore associationStore, + @NonNull ObservableUuidStore observableUuidStore, @NonNull Callback callback) { mAssociationStore = associationStore; + mObservableUuidStore = observableUuidStore; mCallback = callback; mUserManager = userManager; } @@ -109,7 +122,6 @@ public class BluetoothCompanionDeviceConnectionListener bluetoothDevices.add(device); mPendingConnectedDevices.put(userId, bluetoothDevices); } - } else { onDeviceConnectivityChanged(device, true); } @@ -155,8 +167,13 @@ public class BluetoothCompanionDeviceConnectionListener } private void onDeviceConnectivityChanged(@NonNull BluetoothDevice device, boolean connected) { + int userId = UserHandle.myUserId(); final List<AssociationInfo> associations = mAssociationStore.getAssociationsByAddress(device.getAddress()); + final List<ObservableUuid> observableUuids = + mObservableUuidStore.getObservableUuidsForUser(userId); + final List<ParcelUuid> deviceUuids = device.getUuids() == null + ? Collections.emptyList() : Arrays.asList(device.getUuids()); if (DEBUG) { Log.d(TAG, "onDevice_ConnectivityChanged() " + btDeviceToString(device) @@ -177,6 +194,14 @@ public class BluetoothCompanionDeviceConnectionListener mCallback.onBluetoothCompanionDeviceDisconnected(id); } } + + for (ObservableUuid uuid : observableUuids) { + if (deviceUuids.contains(uuid.getUuid())) { + mCallback.onDevicePresenceEventByUuid( + uuid, connected ? EVENT_BT_CONNECTED + : EVENT_BT_DISCONNECTED); + } + } } @Override diff --git a/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java b/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java index e42b9356cca3..54a4692d964d 100644 --- a/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java +++ b/services/companion/java/com/android/server/companion/presence/CompanionDevicePresenceMonitor.java @@ -16,12 +16,12 @@ package com.android.server.companion.presence; -import static android.companion.CompanionDeviceService.DEVICE_EVENT_BLE_APPEARED; -import static android.companion.CompanionDeviceService.DEVICE_EVENT_BLE_DISAPPEARED; -import static android.companion.CompanionDeviceService.DEVICE_EVENT_BT_CONNECTED; -import static android.companion.CompanionDeviceService.DEVICE_EVENT_BT_DISCONNECTED; -import static android.companion.CompanionDeviceService.DEVICE_EVENT_SELF_MANAGED_APPEARED; -import static android.companion.CompanionDeviceService.DEVICE_EVENT_SELF_MANAGED_DISAPPEARED; +import static android.companion.DevicePresenceEvent.EVENT_BLE_APPEARED; +import static android.companion.DevicePresenceEvent.EVENT_BLE_DISAPPEARED; +import static android.companion.DevicePresenceEvent.EVENT_BT_CONNECTED; +import static android.companion.DevicePresenceEvent.EVENT_BT_DISCONNECTED; +import static android.companion.DevicePresenceEvent.EVENT_SELF_MANAGED_APPEARED; +import static android.companion.DevicePresenceEvent.EVENT_SELF_MANAGED_DISAPPEARED; import static android.os.Process.ROOT_UID; import static android.os.Process.SHELL_UID; @@ -36,12 +36,15 @@ import android.os.Binder; import android.os.Handler; import android.os.Looper; import android.os.Message; +import android.os.ParcelUuid; import android.os.UserManager; import android.util.Log; import android.util.Slog; import android.util.SparseArray; import com.android.server.companion.AssociationStore; +import com.android.server.companion.ObservableUuid; +import com.android.server.companion.ObservableUuidStore; import java.io.PrintWriter; import java.util.HashSet; @@ -61,7 +64,7 @@ import java.util.Set; * <li> {@link #isDevicePresent(int)} * <li> {@link Callback#onDeviceAppeared(int) Callback.onDeviceAppeared(int)} * <li> {@link Callback#onDeviceDisappeared(int) Callback.onDeviceDisappeared(int)} - * <li> {@link Callback#onDeviceStateChanged(int, int)}} + * <li> {@link Callback#onDevicePresenceEvent(int, int)}} * </ul> */ @SuppressLint("LongLogTag") @@ -78,11 +81,15 @@ public class CompanionDevicePresenceMonitor implements AssociationStore.OnChange /** Invoked when a companion device no longer seen nearby or disconnects. */ void onDeviceDisappeared(int associationId); - /**Invoked when device has corresponding event changes. */ - void onDeviceEvent(int associationId, int event); + /** Invoked when device has corresponding event changes. */ + void onDevicePresenceEvent(int associationId, int event); + + /** Invoked when device has corresponding event changes base on the UUID */ + void onDevicePresenceEventByUuid(ObservableUuid uuid, int event); } private final @NonNull AssociationStore mAssociationStore; + private final @NonNull ObservableUuidStore mObservableUuidStore; private final @NonNull Callback mCallback; private final @NonNull BluetoothCompanionDeviceConnectionListener mBtConnectionListener; private final @NonNull BleCompanionDeviceScanner mBleScanner; @@ -94,6 +101,7 @@ public class CompanionDevicePresenceMonitor implements AssociationStore.OnChange private final @NonNull Set<Integer> mConnectedBtDevices = new HashSet<>(); private final @NonNull Set<Integer> mNearbyBleDevices = new HashSet<>(); private final @NonNull Set<Integer> mReportedSelfManagedDevices = new HashSet<>(); + private final @NonNull Set<ParcelUuid> mConnectedUuidDevices = new HashSet<>(); // Tracking "simulated" presence. Used for debugging and testing only. private final @NonNull Set<Integer> mSimulated = new HashSet<>(); @@ -101,11 +109,14 @@ public class CompanionDevicePresenceMonitor implements AssociationStore.OnChange new SimulatedDevicePresenceSchedulerHelper(); public CompanionDevicePresenceMonitor(UserManager userManager, - @NonNull AssociationStore associationStore, @NonNull Callback callback) { + @NonNull AssociationStore associationStore, + @NonNull ObservableUuidStore observableUuidStore, @NonNull Callback callback) { mAssociationStore = associationStore; + mObservableUuidStore = observableUuidStore; mCallback = callback; mBtConnectionListener = new BluetoothCompanionDeviceConnectionListener(userManager, - associationStore, /* BluetoothCompanionDeviceConnectionListener.Callback */ this); + associationStore, mObservableUuidStore, + /* BluetoothCompanionDeviceConnectionListener.Callback */ this); mBleScanner = new BleCompanionDeviceScanner(associationStore, /* BleCompanionDeviceScanner.Callback */ this); } @@ -126,6 +137,20 @@ public class CompanionDevicePresenceMonitor implements AssociationStore.OnChange } /** + * @return current connected UUID devices. + */ + public Set<ParcelUuid> getCurrentConnectedUuidDevices() { + return mConnectedUuidDevices; + } + + /** + * Remove current connected UUID device. + */ + public void removeCurrentConnectedUuidDevice(ParcelUuid uuid) { + mConnectedUuidDevices.remove(uuid); + } + + /** * @return whether the associated companion devices is present. I.e. device is nearby (for BLE); * or devices is connected (for Bluetooth); or reported (by the application) to be * nearby (for "self-managed" associations). @@ -138,6 +163,13 @@ public class CompanionDevicePresenceMonitor implements AssociationStore.OnChange } /** + * @return whether the current uuid to be observed is present. + */ + public boolean isDeviceUuidPresent(ParcelUuid uuid) { + return mConnectedUuidDevices.contains(uuid); + } + + /** * @return whether the current device is BT connected and had already reported to the app. */ @@ -169,8 +201,8 @@ public class CompanionDevicePresenceMonitor implements AssociationStore.OnChange * {@link android.companion.CompanionDeviceManager#notifyDeviceAppeared(int) notifyDeviceAppeared()} */ public void onSelfManagedDeviceConnected(int associationId) { - onDeviceEvent(mReportedSelfManagedDevices, - associationId, DEVICE_EVENT_SELF_MANAGED_APPEARED); + onDevicePresenceEvent(mReportedSelfManagedDevices, + associationId, EVENT_SELF_MANAGED_APPEARED); } /** @@ -183,23 +215,23 @@ public class CompanionDevicePresenceMonitor implements AssociationStore.OnChange * {@link android.companion.CompanionDeviceManager#notifyDeviceDisappeared(int) notifyDeviceDisappeared()} */ public void onSelfManagedDeviceDisconnected(int associationId) { - onDeviceEvent(mReportedSelfManagedDevices, - associationId, DEVICE_EVENT_SELF_MANAGED_DISAPPEARED); + onDevicePresenceEvent(mReportedSelfManagedDevices, + associationId, EVENT_SELF_MANAGED_DISAPPEARED); } /** * Marks a "self-managed" device as disconnected when binderDied. */ public void onSelfManagedDeviceReporterBinderDied(int associationId) { - onDeviceEvent(mReportedSelfManagedDevices, - associationId, DEVICE_EVENT_SELF_MANAGED_DISAPPEARED); + onDevicePresenceEvent(mReportedSelfManagedDevices, + associationId, EVENT_SELF_MANAGED_DISAPPEARED); } @Override public void onBluetoothCompanionDeviceConnected(int associationId) { Slog.i(TAG, "onBluetoothCompanionDeviceConnected: " + "associationId( " + associationId + " )"); - onDeviceEvent(mConnectedBtDevices, associationId, DEVICE_EVENT_BT_CONNECTED); + onDevicePresenceEvent(mConnectedBtDevices, associationId, EVENT_BT_CONNECTED); // Stop scanning for BLE devices when this device is connected // and there are no other devices to connect to. if (canStopBleScan()) { @@ -214,22 +246,53 @@ public class CompanionDevicePresenceMonitor implements AssociationStore.OnChange // Start BLE scanning when the device is disconnected. mBleScanner.startScan(); - onDeviceEvent(mConnectedBtDevices, associationId, DEVICE_EVENT_BT_DISCONNECTED); + onDevicePresenceEvent(mConnectedBtDevices, associationId, EVENT_BT_DISCONNECTED); } @Override + public void onDevicePresenceEventByUuid(ObservableUuid uuid, int event) { + final ParcelUuid parcelUuid = uuid.getUuid(); + + switch(event) { + case EVENT_BT_CONNECTED: + boolean added = mConnectedUuidDevices.add(parcelUuid); + + if (!added) { + Slog.w(TAG, "Uuid= " + parcelUuid + "is ALREADY reported as " + + "present by this event=" + event); + } + + break; + case EVENT_BT_DISCONNECTED: + final boolean removed = mConnectedUuidDevices.remove(parcelUuid); + + if (!removed) { + Slog.w(TAG, "UUID= " + parcelUuid + " was NOT reported " + + "as present by this event= " + event); + + return; + } + + break; + } + + mCallback.onDevicePresenceEventByUuid(uuid, event); + } + + + @Override public void onBleCompanionDeviceFound(int associationId) { - onDeviceEvent(mNearbyBleDevices, associationId, DEVICE_EVENT_BLE_APPEARED); + onDevicePresenceEvent(mNearbyBleDevices, associationId, EVENT_BLE_APPEARED); } @Override public void onBleCompanionDeviceLost(int associationId) { - onDeviceEvent(mNearbyBleDevices, associationId, DEVICE_EVENT_BLE_DISAPPEARED); + onDevicePresenceEvent(mNearbyBleDevices, associationId, EVENT_BLE_DISAPPEARED); } /** FOR DEBUGGING AND/OR TESTING PURPOSES ONLY. */ @TestApi - public void simulateDeviceEvent(int associationId, int state) { + public void simulateDeviceEvent(int associationId, int event) { // IMPORTANT: this API should only be invoked via the // 'companiondevice simulate-device-appeared' Shell command, so the only uid-s allowed to // make this call are SHELL and ROOT. @@ -238,32 +301,43 @@ public class CompanionDevicePresenceMonitor implements AssociationStore.OnChange // Make sure the association exists. enforceAssociationExists(associationId); - switch (state) { - case DEVICE_EVENT_BLE_APPEARED: - simulateDeviceAppeared(associationId, state); + switch (event) { + case EVENT_BLE_APPEARED: + simulateDeviceAppeared(associationId, event); break; - case DEVICE_EVENT_BT_CONNECTED: + case EVENT_BT_CONNECTED: onBluetoothCompanionDeviceConnected(associationId); break; - case DEVICE_EVENT_BLE_DISAPPEARED: - simulateDeviceDisappeared(associationId, state); + case EVENT_BLE_DISAPPEARED: + simulateDeviceDisappeared(associationId, event); break; - case DEVICE_EVENT_BT_DISCONNECTED: + case EVENT_BT_DISCONNECTED: onBluetoothCompanionDeviceDisconnected(associationId); break; default: - throw new IllegalArgumentException("State: " + state + "is not supported"); + throw new IllegalArgumentException("Event: " + event + "is not supported"); } } + /** FOR DEBUGGING AND/OR TESTING PURPOSES ONLY. */ + @TestApi + public void simulateDeviceEventByUuid(ObservableUuid uuid, int event) { + // IMPORTANT: this API should only be invoked via the + // 'companiondevice simulate-device-uuid-events' Shell command, so the only uid-s allowed to + // make this call are SHELL and ROOT. + // No other caller (including SYSTEM!) should be allowed. + enforceCallerShellOrRoot(); + onDevicePresenceEventByUuid(uuid, event); + } + private void simulateDeviceAppeared(int associationId, int state) { - onDeviceEvent(mSimulated, associationId, state); + onDevicePresenceEvent(mSimulated, associationId, state); mSchedulerHelper.scheduleOnDeviceGoneCallForSimulatedDevicePresence(associationId); } private void simulateDeviceDisappeared(int associationId, int state) { mSchedulerHelper.unscheduleOnDeviceGoneCallForSimulatedDevicePresence(associationId); - onDeviceEvent(mSimulated, associationId, state); + onDevicePresenceEvent(mSimulated, associationId, state); } private void enforceAssociationExists(int associationId) { @@ -273,14 +347,14 @@ public class CompanionDevicePresenceMonitor implements AssociationStore.OnChange } } - private void onDeviceEvent(@NonNull Set<Integer> presentDevicesForSource, + private void onDevicePresenceEvent(@NonNull Set<Integer> presentDevicesForSource, int associationId, int event) { - Slog.i(TAG, "onDeviceEvent() id=" + associationId + ", state=" + event); + Slog.i(TAG, "onDevicePresenceEvent() id=" + associationId + ", event=" + event); switch (event) { - case DEVICE_EVENT_BLE_APPEARED: - case DEVICE_EVENT_BT_CONNECTED: - case DEVICE_EVENT_SELF_MANAGED_APPEARED: + case EVENT_BLE_APPEARED: + case EVENT_BT_CONNECTED: + case EVENT_SELF_MANAGED_APPEARED: final boolean added = presentDevicesForSource.add(associationId); if (!added) { @@ -292,9 +366,9 @@ public class CompanionDevicePresenceMonitor implements AssociationStore.OnChange mCallback.onDeviceAppeared(associationId); break; - case DEVICE_EVENT_BLE_DISAPPEARED: - case DEVICE_EVENT_BT_DISCONNECTED: - case DEVICE_EVENT_SELF_MANAGED_DISAPPEARED: + case EVENT_BLE_DISAPPEARED: + case EVENT_BT_DISCONNECTED: + case EVENT_SELF_MANAGED_DISAPPEARED: final boolean removed = presentDevicesForSource.remove(associationId); if (!removed) { @@ -312,7 +386,7 @@ public class CompanionDevicePresenceMonitor implements AssociationStore.OnChange return; } - mCallback.onDeviceEvent(associationId, event); + mCallback.onDevicePresenceEvent(associationId, event); } /** @@ -436,7 +510,7 @@ public class CompanionDevicePresenceMonitor implements AssociationStore.OnChange public void handleMessage(@NonNull Message msg) { final int associationId = msg.what; if (mSimulated.contains(associationId)) { - onDeviceEvent(mSimulated, associationId, DEVICE_EVENT_BLE_DISAPPEARED); + onDevicePresenceEvent(mSimulated, associationId, EVENT_BLE_DISAPPEARED); } } } diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java index b6e114087f30..2168cb2043ed 100644 --- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java @@ -583,6 +583,11 @@ public class VirtualDeviceManagerService extends SystemService { return associationInfo == null ? null : associationInfo.getDisplayName(); } + @Override // Binder call + public @NonNull List<String> getAllPersistentDeviceIds() { + return new ArrayList<>(mLocalService.getAllPersistentDeviceIds()); + } + // Binder call @Override public boolean isValidVirtualDeviceId(int deviceId) { diff --git a/services/core/Android.bp b/services/core/Android.bp index fdcd27da5bdc..a54a48a7e84e 100644 --- a/services/core/Android.bp +++ b/services/core/Android.bp @@ -211,7 +211,10 @@ java_library_static { "com_android_wm_shell_flags_lib", "com.android.server.utils_aconfig-java", "service-jobscheduler-deviceidle.flags-aconfig-java", + "backup_flags_lib", "policy_flags_lib", + "net_flags_lib", + "stats_flags_lib", ], javac_shard_size: 50, javacflags: [ diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index 9b1fade198fc..afb8345249b1 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -4491,6 +4491,12 @@ public final class ActiveServices { } } if (userId > 0) { + if (mAm.isSystemUserOnly(sInfo.flags)) { + Slog.w(TAG_SERVICE, service + " is only available for the SYSTEM user," + + " calling userId is: " + userId); + return null; + } + if (mAm.isSingleton(sInfo.processName, sInfo.applicationInfo, sInfo.name, sInfo.flags) && mAm.isValidSingletonCall(callingUid, sInfo.applicationInfo.uid)) { diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 86894fd9b405..ca04e41769ee 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -472,6 +472,8 @@ import com.android.server.pm.pkg.SELinuxUtil; import com.android.server.pm.snapshot.PackageDataSnapshot; import com.android.server.power.stats.BatteryStatsImpl; import com.android.server.sdksandbox.SdkSandboxManagerLocal; +import com.android.server.stats.pull.StatsPullAtomService; +import com.android.server.stats.pull.StatsPullAtomServiceInternal; import com.android.server.uri.GrantUri; import com.android.server.uri.NeededUriGrants; import com.android.server.uri.UriGrantsManagerInternal; @@ -1308,6 +1310,8 @@ public class ActivityManagerService extends IActivityManager.Stub */ final BatteryStatsService mBatteryStatsService; + StatsPullAtomServiceInternal mStatsPullAtomServiceInternal; + /** * Information about component usage */ @@ -5087,13 +5091,11 @@ public class ActivityManagerService extends IActivityManager.Stub intent.addFlags(Intent.FLAG_RECEIVER_NO_ABORT | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND | Intent.FLAG_INCLUDE_STOPPED_PACKAGES); - final BroadcastOptions bOptions = mUserController.getTemporaryAppAllowlistBroadcastOptions( - reason); broadcastIntentLocked(null, null, null, intent, null, null, 0, null, null, new String[]{android.Manifest.permission.RECEIVE_BOOT_COMPLETED}, null, null, AppOpsManager.OP_NONE, - bOptions.toBundle(), true, + null, true, false, MY_PID, SYSTEM_UID, SYSTEM_UID, MY_PID, app.userId); } @@ -13763,6 +13765,11 @@ public class ActivityManagerService extends IActivityManager.Stub return result; } + boolean isSystemUserOnly(int flags) { + return android.multiuser.Flags.enableSystemUserOnlyForServicesAndProviders() + && (flags & ServiceInfo.FLAG_SYSTEM_USER_ONLY) != 0; + } + /** * Checks to see if the caller is in the same app as the singleton * component, or the component is in a special app. It allows special apps @@ -16551,6 +16558,21 @@ public class ActivityManagerService extends IActivityManager.Stub final @ProcessCapability int capability) { mBatteryStatsService.noteUidProcessState(uid, state); mAppOpsService.updateUidProcState(uid, state, capability); + if (StatsPullAtomService.ENABLE_MOBILE_DATA_STATS_AGGREGATED_PULLER) { + try { + if (mStatsPullAtomServiceInternal == null) { + mStatsPullAtomServiceInternal = LocalServices.getService( + StatsPullAtomServiceInternal.class); + } + if (mStatsPullAtomServiceInternal != null) { + mStatsPullAtomServiceInternal.noteUidProcessState(uid, state); + } else { + Slog.d(TAG, "StatsPullAtomService not ready yet"); + } + } catch (Exception e) { + Slog.e(TAG, "Exception during logging uid proc state change event", e); + } + } if (mTrackingAssociations) { for (int i1=0, N1=mAssociations.size(); i1<N1; i1++) { ArrayMap<ComponentName, SparseArray<ArrayMap<String, Association>>> targetComponents diff --git a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java index 57c52c2cf408..45f657d713ad 100644 --- a/services/core/java/com/android/server/am/ActivityManagerShellCommand.java +++ b/services/core/java/com/android/server/am/ActivityManagerShellCommand.java @@ -3754,6 +3754,11 @@ final class ActivityManagerShellCommand extends ShellCommand { } @Override + public void onProcessStarted(int pid, int processUid, int packageUid, String packageName, + String processName) { + } + + @Override public void onForegroundServicesChanged(int pid, int uid, int serviceTypes) { } diff --git a/services/core/java/com/android/server/am/AppFGSTracker.java b/services/core/java/com/android/server/am/AppFGSTracker.java index 1f98aba5bbd7..fb89b8e4f3b4 100644 --- a/services/core/java/com/android/server/am/AppFGSTracker.java +++ b/services/core/java/com/android/server/am/AppFGSTracker.java @@ -102,6 +102,11 @@ final class AppFGSTracker extends BaseAppStateDurationsTracker<AppFGSPolicy, Pac } @Override + public void onProcessStarted(int pid, int processUid, int packageUid, String packageName, + String processName) { + } + + @Override public void onProcessDied(int pid, int uid) { } }; diff --git a/services/core/java/com/android/server/am/ContentProviderHelper.java b/services/core/java/com/android/server/am/ContentProviderHelper.java index 095d907d7df6..30f21a65b5b1 100644 --- a/services/core/java/com/android/server/am/ContentProviderHelper.java +++ b/services/core/java/com/android/server/am/ContentProviderHelper.java @@ -1249,9 +1249,9 @@ public class ContentProviderHelper { ProviderInfo cpi = providers.get(i); boolean singleton = mService.isSingleton(cpi.processName, cpi.applicationInfo, cpi.name, cpi.flags); - if (singleton && app.userId != UserHandle.USER_SYSTEM) { - // This is a singleton provider, but a user besides the - // default user is asking to initialize a process it runs + if (isSingletonOrSystemUserOnly(cpi) && app.userId != UserHandle.USER_SYSTEM) { + // This is a singleton or a SYSTEM user only provider, but a user besides the + // SYSTEM user is asking to initialize a process it runs // in... well, no, it doesn't actually run in this process, // it runs in the process of the default user. Get rid of it. providers.remove(i); @@ -1398,8 +1398,7 @@ public class ContentProviderHelper { final boolean processMatch = Objects.equals(pi.processName, app.processName) || pi.multiprocess; - final boolean userMatch = !mService.isSingleton( - pi.processName, pi.applicationInfo, pi.name, pi.flags) + final boolean userMatch = !isSingletonOrSystemUserOnly(pi) || app.userId == UserHandle.USER_SYSTEM; final boolean isInstantApp = pi.applicationInfo.isInstantApp(); final boolean splitInstalled = pi.splitName == null @@ -1985,4 +1984,13 @@ public class ContentProviderHelper { return isAuthRedirected; } } + + /** + * Returns true if Provider is either singleUser or systemUserOnly provider. + */ + private boolean isSingletonOrSystemUserOnly(ProviderInfo pi) { + return (android.multiuser.Flags.enableSystemUserOnlyForServicesAndProviders() + && mService.isSystemUserOnly(pi.flags)) + || mService.isSingleton(pi.processName, pi.applicationInfo, pi.name, pi.flags); + } } diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java index fa5dbd2543d3..f5c34a5da1c1 100644 --- a/services/core/java/com/android/server/am/ProcessList.java +++ b/services/core/java/com/android/server/am/ProcessList.java @@ -2852,6 +2852,7 @@ public final class ProcessList { ? PROC_START_TIMEOUT_WITH_WRAPPER : PROC_START_TIMEOUT); } } + dispatchProcessStarted(app, pid); checkSlow(app.getStartTime(), "startProcess: done updating pids map"); return true; } @@ -4977,6 +4978,22 @@ public final class ProcessList { } } + void dispatchProcessStarted(ProcessRecord app, int pid) { + int i = mProcessObservers.beginBroadcast(); + while (i > 0) { + i--; + final IProcessObserver observer = mProcessObservers.getBroadcastItem(i); + if (observer != null) { + try { + observer.onProcessStarted(pid, app.uid, app.info.uid, + app.info.packageName, app.processName); + } catch (RemoteException e) { + } + } + } + mProcessObservers.finishBroadcast(); + } + void dispatchProcessDied(int pid, int uid) { int i = mProcessObservers.beginBroadcast(); while (i > 0) { diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java index 9db5d0a99480..7aafda59298c 100644 --- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java +++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java @@ -153,6 +153,7 @@ public class SettingsToPropertiesMapper { "machine_learning", "mainline_modularization", "mainline_sdk", + "make_pixel_haptics", "media_audio", "media_drm", "media_reliability", @@ -162,6 +163,7 @@ public class SettingsToPropertiesMapper { "pdf_viewer", "pixel_audio_android", "pixel_bluetooth", + "pixel_connectivity_gps", "pixel_system_sw_video", "pixel_watch", "platform_security", diff --git a/services/core/java/com/android/server/app/GameServiceProviderInstanceImpl.java b/services/core/java/com/android/server/app/GameServiceProviderInstanceImpl.java index 684d6a0fc596..cdd147a0ec37 100644 --- a/services/core/java/com/android/server/app/GameServiceProviderInstanceImpl.java +++ b/services/core/java/com/android/server/app/GameServiceProviderInstanceImpl.java @@ -177,6 +177,11 @@ final class GameServiceProviderInstanceImpl implements GameServiceProviderInstan } @Override + public void onProcessStarted(int pid, int processUid, int packageUid, String packageName, + String processName) { + } + + @Override public void onForegroundServicesChanged(int pid, int uid, int serviceTypes) { } }; diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java index 99b45ec79571..cd295b521e89 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java +++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java @@ -1047,11 +1047,9 @@ public class AudioDeviceBroker { private void initAudioHalBluetoothState() { synchronized (mBluetoothAudioStateLock) { mBluetoothScoOnApplied = false; - AudioSystem.setParameters("BT_SCO=off"); mBluetoothA2dpSuspendedApplied = false; - AudioSystem.setParameters("A2dpSuspended=false"); mBluetoothLeSuspendedApplied = false; - AudioSystem.setParameters("LeAudioSuspended=false"); + reapplyAudioHalBluetoothState(); } } @@ -1114,6 +1112,34 @@ public class AudioDeviceBroker { } } + @GuardedBy("mBluetoothAudioStateLock") + private void reapplyAudioHalBluetoothState() { + if (AudioService.DEBUG_COMM_RTE) { + Log.v(TAG, "reapplyAudioHalBluetoothState() mBluetoothScoOnApplied: " + + mBluetoothScoOnApplied + ", mBluetoothA2dpSuspendedApplied: " + + mBluetoothA2dpSuspendedApplied + ", mBluetoothLeSuspendedApplied: " + + mBluetoothLeSuspendedApplied); + } + // Note: the order of parameters is important. + if (mBluetoothScoOnApplied) { + AudioSystem.setParameters("A2dpSuspended=true"); + AudioSystem.setParameters("LeAudioSuspended=true"); + AudioSystem.setParameters("BT_SCO=on"); + } else { + AudioSystem.setParameters("BT_SCO=off"); + if (mBluetoothA2dpSuspendedApplied) { + AudioSystem.setParameters("A2dpSuspended=true"); + } else { + AudioSystem.setParameters("A2dpSuspended=false"); + } + if (mBluetoothLeSuspendedApplied) { + AudioSystem.setParameters("LeAudioSuspended=true"); + } else { + AudioSystem.setParameters("LeAudioSuspended=false"); + } + } + } + /*package*/ void setBluetoothScoOn(boolean on, String eventSource) { if (AudioService.DEBUG_COMM_RTE) { Log.v(TAG, "setBluetoothScoOn: " + on + " " + eventSource); @@ -1775,6 +1801,9 @@ public class AudioDeviceBroker { initRoutingStrategyIds(); updateActiveCommunicationDevice(); mDeviceInventory.onRestoreDevices(); + synchronized (mBluetoothAudioStateLock) { + reapplyAudioHalBluetoothState(); + } mBtHelper.onAudioServerDiedRestoreA2dp(); updateCommunicationRoute("MSG_RESTORE_DEVICES"); } diff --git a/services/core/java/com/android/server/backup/SystemBackupAgent.java b/services/core/java/com/android/server/backup/SystemBackupAgent.java index de4979a0c826..5b9469bc5610 100644 --- a/services/core/java/com/android/server/backup/SystemBackupAgent.java +++ b/services/core/java/com/android/server/backup/SystemBackupAgent.java @@ -20,7 +20,8 @@ import android.app.IWallpaperManager; import android.app.backup.BackupAgentHelper; import android.app.backup.BackupAnnotations.BackupDestination; import android.app.backup.BackupDataInput; -import android.app.backup.BackupHelper; +import android.app.backup.BackupHelperWithLogger; +import android.app.backup.BackupRestoreEventLogger; import android.app.backup.FullBackup; import android.app.backup.FullBackupDataOutput; import android.app.backup.WallpaperBackupHelper; @@ -33,9 +34,10 @@ import android.os.ServiceManager; import android.os.UserHandle; import android.os.UserManager; import android.util.Slog; - import com.google.android.collect.Sets; +import com.android.server.backup.Flags; + import java.io.File; import java.io.IOException; import java.util.Set; @@ -107,10 +109,12 @@ public class SystemBackupAgent extends BackupAgentHelper { private int mUserId = UserHandle.USER_SYSTEM; private boolean mIsProfileUser = false; + private BackupRestoreEventLogger mLogger; @Override public void onCreate(UserHandle user, @BackupDestination int backupDestination) { super.onCreate(user, backupDestination); + mLogger = this.getBackupRestoreEventLogger(); mUserId = user.getIdentifier(); if (mUserId != UserHandle.USER_SYSTEM) { @@ -209,9 +213,12 @@ public class SystemBackupAgent extends BackupAgentHelper { } } - private void addHelperIfEligibleForUser(String keyPrefix, BackupHelper helper) { + private void addHelperIfEligibleForUser(String keyPrefix, BackupHelperWithLogger helper) { if (isHelperEligibleForUser(keyPrefix)) { addHelper(keyPrefix, helper); + if (Flags.enableMetricsSystemBackupAgents()) { + helper.setLogger(mLogger); + } } } diff --git a/services/core/java/com/android/server/biometrics/AuthService.java b/services/core/java/com/android/server/biometrics/AuthService.java index 8fd2ee2bdc33..21e6bac53cde 100644 --- a/services/core/java/com/android/server/biometrics/AuthService.java +++ b/services/core/java/com/android/server/biometrics/AuthService.java @@ -439,6 +439,10 @@ public class AuthService extends SystemService { if (fingerprintService != null) { fingerprintService.registerAuthenticationStateListener(listener); } + final IFaceService faceService = mInjector.getFaceService(); + if (faceService != null) { + faceService.registerAuthenticationStateListener(listener); + } } @Override @@ -449,6 +453,10 @@ public class AuthService extends SystemService { if (fingerprintService != null) { fingerprintService.unregisterAuthenticationStateListener(listener); } + final IFaceService faceService = mInjector.getFaceService(); + if (faceService != null) { + faceService.unregisterAuthenticationStateListener(listener); + } } @Override diff --git a/services/core/java/com/android/server/biometrics/sensors/AuthenticationStateListeners.java b/services/core/java/com/android/server/biometrics/sensors/AuthenticationStateListeners.java index 58635353c780..1ae4d6465c57 100644 --- a/services/core/java/com/android/server/biometrics/sensors/AuthenticationStateListeners.java +++ b/services/core/java/com/android/server/biometrics/sensors/AuthenticationStateListeners.java @@ -91,6 +91,40 @@ public class AuthenticationStateListeners implements IBinder.DeathRecipient { } } + /** + * Defines behavior in response to a successful authentication + * @param requestReason Reason from [BiometricRequestConstants.RequestReason] for the requested + * authentication + * @param userId The user Id for the requested authentication + */ + public void onAuthenticationSucceeded(int requestReason, int userId) { + for (AuthenticationStateListener listener: mAuthenticationStateListeners) { + try { + listener.onAuthenticationSucceeded(requestReason, userId); + } catch (RemoteException e) { + Slog.e(TAG, "Remote exception in notifying listener that authentication " + + "succeeded", e); + } + } + } + + /** + * Defines behavior in response to a failed authentication + * @param requestReason Reason from [BiometricRequestConstants.RequestReason] for the requested + * authentication + * @param userId The user Id for the requested authentication + */ + public void onAuthenticationFailed(int requestReason, int userId) { + for (AuthenticationStateListener listener: mAuthenticationStateListeners) { + try { + listener.onAuthenticationFailed(requestReason, userId); + } catch (RemoteException e) { + Slog.e(TAG, "Remote exception in notifying listener that authentication " + + "failed", e); + } + } + } + @Override public void binderDied() { // Do nothing, handled below diff --git a/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java b/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java index 73f3999f40ce..321e951ec09b 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/FaceService.java @@ -24,6 +24,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; import android.content.Context; +import android.hardware.biometrics.AuthenticationStateListener; import android.hardware.biometrics.BiometricsProtoEnums; import android.hardware.biometrics.IBiometricSensorReceiver; import android.hardware.biometrics.IBiometricService; @@ -63,6 +64,7 @@ import com.android.server.SystemService; import com.android.server.biometrics.Flags; import com.android.server.biometrics.Utils; import com.android.server.biometrics.log.BiometricContext; +import com.android.server.biometrics.sensors.AuthenticationStateListeners; import com.android.server.biometrics.sensors.BiometricStateCallback; import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter; import com.android.server.biometrics.sensors.LockoutResetDispatcher; @@ -99,6 +101,8 @@ public class FaceService extends SystemService { private final BiometricStateCallback<ServiceProvider, FaceSensorPropertiesInternal> mBiometricStateCallback; @NonNull + private final AuthenticationStateListeners mAuthenticationStateListeners; + @NonNull private final FaceProviderFunction mFaceProviderFunction; @NonNull private final Function<String, FaceProvider> mFaceProvider; @NonNull @@ -695,7 +699,8 @@ public class FaceService extends SystemService { for (FaceSensorPropertiesInternal hidlSensor : hidlSensors) { providers.add( Face10.newInstance(getContext(), mBiometricStateCallback, - hidlSensor, mLockoutResetDispatcher)); + mAuthenticationStateListeners, hidlSensor, + mLockoutResetDispatcher)); } return providers; @@ -830,6 +835,24 @@ public class FaceService extends SystemService { public void registerBiometricStateListener(@NonNull IBiometricStateListener listener) { mBiometricStateCallback.registerBiometricStateListener(listener); } + + @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL) + @Override + public void registerAuthenticationStateListener( + @NonNull AuthenticationStateListener listener) { + super.registerAuthenticationStateListener_enforcePermission(); + + mAuthenticationStateListeners.registerAuthenticationStateListener(listener); + } + + @android.annotation.EnforcePermission(android.Manifest.permission.USE_BIOMETRIC_INTERNAL) + @Override + public void unregisterAuthenticationStateListener( + @NonNull AuthenticationStateListener listener) { + super.unregisterAuthenticationStateListener_enforcePermission(); + + mAuthenticationStateListeners.unregisterAuthenticationStateListener(listener); + } } public FaceService(Context context) { @@ -848,6 +871,7 @@ public class FaceService extends SystemService { mLockoutResetDispatcher = new LockoutResetDispatcher(context); mLockPatternUtils = new LockPatternUtils(context); mBiometricStateCallback = new BiometricStateCallback<>(UserManager.get(context)); + mAuthenticationStateListeners = new AuthenticationStateListeners(); mRegistry = new FaceServiceRegistry(mServiceWrapper, biometricServiceSupplier); mRegistry.addAllRegisteredCallback(new IFaceAuthenticatorsRegisteredCallback.Stub() { @Override @@ -868,8 +892,8 @@ public class FaceService extends SystemService { try { final SensorProps[] props = face.getSensorProps(); return new FaceProvider(getContext(), - mBiometricStateCallback, props, name, mLockoutResetDispatcher, - BiometricContext.getInstance(getContext()), + mBiometricStateCallback, mAuthenticationStateListeners, props, name, + mLockoutResetDispatcher, BiometricContext.getInstance(getContext()), false /* resetLockoutRequiresChallenge */); } catch (RemoteException e) { Slog.e(TAG, "Remote exception in getSensorProps: " + fqName); @@ -881,7 +905,7 @@ public class FaceService extends SystemService { if (Flags.deHidl()) { mFaceProviderFunction = faceProviderFunction != null ? faceProviderFunction : ((filteredSensorProps, resetLockoutRequiresChallenge) -> new FaceProvider( - getContext(), mBiometricStateCallback, + getContext(), mBiometricStateCallback, mAuthenticationStateListeners, filteredSensorProps.second, filteredSensorProps.first, mLockoutResetDispatcher, BiometricContext.getInstance(getContext()), diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java index 22e399c6747b..f35de93af625 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java @@ -16,6 +16,8 @@ package com.android.server.biometrics.sensors.face.aidl; +import static android.adaptiveauth.Flags.reportBiometricAuthAttempts; + import android.annotation.NonNull; import android.annotation.Nullable; import android.app.NotificationManager; @@ -44,6 +46,7 @@ import com.android.server.biometrics.log.BiometricLogger; import com.android.server.biometrics.log.OperationContextExt; import com.android.server.biometrics.sensors.AuthSessionCoordinator; import com.android.server.biometrics.sensors.AuthenticationClient; +import com.android.server.biometrics.sensors.AuthenticationStateListeners; import com.android.server.biometrics.sensors.ClientMonitorCallback; import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter; import com.android.server.biometrics.sensors.ClientMonitorCompositeCallback; @@ -77,6 +80,8 @@ public class FaceAuthenticationClient private ICancellationSignal mCancellationSignal; @Nullable private final SensorPrivacyManager mSensorPrivacyManager; + @NonNull + private final AuthenticationStateListeners mAuthenticationStateListeners; @FaceManager.FaceAcquired private int mLastAcquire = FaceManager.FACE_ACQUIRED_UNKNOWN; @@ -89,11 +94,13 @@ public class FaceAuthenticationClient @NonNull BiometricLogger logger, @NonNull BiometricContext biometricContext, boolean isStrongBiometric, @NonNull UsageStats usageStats, @NonNull LockoutTracker lockoutCache, boolean allowBackgroundAuthentication, - @Authenticators.Types int sensorStrength) { + @Authenticators.Types int sensorStrength, + @NonNull AuthenticationStateListeners authenticationStateListeners) { this(context, lazyDaemon, token, requestId, listener, operationId, restricted, options, cookie, requireConfirmation, logger, biometricContext, isStrongBiometric, usageStats, lockoutCache, allowBackgroundAuthentication, - context.getSystemService(SensorPrivacyManager.class), sensorStrength); + context.getSystemService(SensorPrivacyManager.class), sensorStrength, + authenticationStateListeners); } @VisibleForTesting @@ -107,7 +114,8 @@ public class FaceAuthenticationClient boolean isStrongBiometric, @NonNull UsageStats usageStats, @NonNull LockoutTracker lockoutTracker, boolean allowBackgroundAuthentication, SensorPrivacyManager sensorPrivacyManager, - @Authenticators.Types int biometricStrength) { + @Authenticators.Types int biometricStrength, + @NonNull AuthenticationStateListeners authenticationStateListeners) { super(context, lazyDaemon, token, listener, operationId, restricted, options, cookie, requireConfirmation, logger, biometricContext, isStrongBiometric, null /* taskStackListener */, lockoutTracker, @@ -118,6 +126,7 @@ public class FaceAuthenticationClient mNotificationManager = context.getSystemService(NotificationManager.class); mSensorPrivacyManager = sensorPrivacyManager; mAuthSessionCoordinator = biometricContext.getAuthSessionCoordinator(); + mAuthenticationStateListeners = authenticationStateListeners; final Resources resources = getContext().getResources(); mBiometricPromptIgnoreList = resources.getIntArray( @@ -262,6 +271,16 @@ public class FaceAuthenticationClient 0 /* error */, 0 /* vendorError */, getTargetUserId())); + + if (reportBiometricAuthAttempts()) { + if (authenticated) { + mAuthenticationStateListeners.onAuthenticationSucceeded(getRequestReason(), + getTargetUserId()); + } else { + mAuthenticationStateListeners.onAuthenticationFailed(getRequestReason(), + getTargetUserId()); + } + } } @Override diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java index e4ecf1a61155..d01c2687b1ff 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceProvider.java @@ -59,6 +59,7 @@ import com.android.server.biometrics.log.BiometricContext; import com.android.server.biometrics.log.BiometricLogger; import com.android.server.biometrics.sensors.AuthSessionCoordinator; import com.android.server.biometrics.sensors.AuthenticationClient; +import com.android.server.biometrics.sensors.AuthenticationStateListeners; import com.android.server.biometrics.sensors.BaseClientMonitor; import com.android.server.biometrics.sensors.BiometricScheduler; import com.android.server.biometrics.sensors.BiometricStateCallback; @@ -103,6 +104,8 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { @NonNull private final BiometricStateCallback mBiometricStateCallback; @NonNull + private final AuthenticationStateListeners mAuthenticationStateListeners; + @NonNull private final String mHalInstanceName; @NonNull private final Handler mHandler; @@ -156,18 +159,20 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { public FaceProvider(@NonNull Context context, @NonNull BiometricStateCallback biometricStateCallback, + @NonNull AuthenticationStateListeners authenticationStateListeners, @NonNull SensorProps[] props, @NonNull String halInstanceName, @NonNull LockoutResetDispatcher lockoutResetDispatcher, @NonNull BiometricContext biometricContext, boolean resetLockoutRequiresChallenge) { - this(context, biometricStateCallback, props, halInstanceName, lockoutResetDispatcher, - biometricContext, null /* daemon */, getHandler(), resetLockoutRequiresChallenge, - false /* testHalEnabled */); + this(context, biometricStateCallback, authenticationStateListeners, props, halInstanceName, + lockoutResetDispatcher, biometricContext, null /* daemon */, getHandler(), + resetLockoutRequiresChallenge, false /* testHalEnabled */); } @VisibleForTesting FaceProvider(@NonNull Context context, @NonNull BiometricStateCallback biometricStateCallback, + @NonNull AuthenticationStateListeners authenticationStateListeners, @NonNull SensorProps[] props, @NonNull String halInstanceName, @NonNull LockoutResetDispatcher lockoutResetDispatcher, @@ -178,6 +183,7 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { boolean testHalEnabled) { mContext = context; mBiometricStateCallback = biometricStateCallback; + mAuthenticationStateListeners = authenticationStateListeners; mHalInstanceName = halInstanceName; mFaceSensors = new SensorList<>(ActivityManager.getService()); if (Flags.deHidl()) { @@ -610,7 +616,8 @@ public class FaceProvider implements IBinder.DeathRecipient, ServiceProvider { mAuthenticationStatsCollector), mBiometricContext, isStrongBiometric, mUsageStats, lockoutTracker, - allowBackgroundAuthentication, Utils.getCurrentStrength(sensorId)); + allowBackgroundAuthentication, Utils.getCurrentStrength(sensorId), + mAuthenticationStateListeners); scheduleForSensor(sensorId, client, new ClientMonitorCallback() { @Override public void onClientStarted( diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java index 53376669b387..48a676ce4937 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/Face10.java @@ -64,6 +64,7 @@ import com.android.server.biometrics.log.BiometricLogger; import com.android.server.biometrics.sensors.AcquisitionClient; import com.android.server.biometrics.sensors.AuthSessionCoordinator; import com.android.server.biometrics.sensors.AuthenticationConsumer; +import com.android.server.biometrics.sensors.AuthenticationStateListeners; import com.android.server.biometrics.sensors.BaseClientMonitor; import com.android.server.biometrics.sensors.BiometricScheduler; import com.android.server.biometrics.sensors.BiometricStateCallback; @@ -119,6 +120,8 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider { @NonNull private final FaceSensorPropertiesInternal mSensorProperties; @NonNull private final BiometricStateCallback mBiometricStateCallback; + @NonNull + private final AuthenticationStateListeners mAuthenticationStateListeners; @NonNull private final Context mContext; @NonNull private final BiometricScheduler<IBiometricsFace, AidlSession> mScheduler; @NonNull private final Handler mHandler; @@ -350,6 +353,7 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider { @VisibleForTesting Face10(@NonNull Context context, @NonNull BiometricStateCallback biometricStateCallback, + @NonNull AuthenticationStateListeners authenticationStateListeners, @NonNull FaceSensorPropertiesInternal sensorProps, @NonNull LockoutResetDispatcher lockoutResetDispatcher, @NonNull Handler handler, @@ -358,6 +362,7 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider { mSensorProperties = sensorProps; mContext = context; mBiometricStateCallback = biometricStateCallback; + mAuthenticationStateListeners = authenticationStateListeners; mSensorId = sensorProps.sensorId; mScheduler = scheduler; mHandler = handler; @@ -392,11 +397,12 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider { public static Face10 newInstance(@NonNull Context context, @NonNull BiometricStateCallback biometricStateCallback, + @NonNull AuthenticationStateListeners authenticationStateListeners, @NonNull FaceSensorPropertiesInternal sensorProps, @NonNull LockoutResetDispatcher lockoutResetDispatcher) { final Handler handler = new Handler(Looper.getMainLooper()); - return new Face10(context, biometricStateCallback, sensorProps, lockoutResetDispatcher, - handler, new BiometricScheduler<>( + return new Face10(context, biometricStateCallback, authenticationStateListeners, + sensorProps, lockoutResetDispatcher, handler, new BiometricScheduler<>( BiometricScheduler.SENSOR_TYPE_FACE, null /* gestureAvailabilityTracker */), BiometricContext.getInstance(context)); @@ -846,7 +852,8 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider { createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient, mAuthenticationStatsCollector), mBiometricContext, isStrongBiometric, mUsageStats, mLockoutTracker, - allowBackgroundAuthentication, Utils.getCurrentStrength(mSensorId)); + allowBackgroundAuthentication, Utils.getCurrentStrength(mSensorId), + mAuthenticationStateListeners); mScheduler.scheduleClientMonitor(client); } @@ -860,7 +867,8 @@ public class Face10 implements IHwBinder.DeathRecipient, ServiceProvider { createLogger(BiometricsProtoEnums.ACTION_AUTHENTICATE, statsClient, mAuthenticationStatsCollector), mBiometricContext, isStrongBiometric, mLockoutTracker, mUsageStats, - allowBackgroundAuthentication, Utils.getCurrentStrength(mSensorId)); + allowBackgroundAuthentication, Utils.getCurrentStrength(mSensorId), + mAuthenticationStateListeners); mScheduler.scheduleClientMonitor(client); } diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceAuthenticationClient.java index 8ab88923d01e..e44b26399549 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceAuthenticationClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceAuthenticationClient.java @@ -16,6 +16,8 @@ package com.android.server.biometrics.sensors.face.hidl; +import static android.adaptiveauth.Flags.reportBiometricAuthAttempts; + import android.annotation.NonNull; import android.content.Context; import android.content.res.Resources; @@ -36,6 +38,7 @@ import com.android.server.biometrics.Utils; import com.android.server.biometrics.log.BiometricContext; import com.android.server.biometrics.log.BiometricLogger; import com.android.server.biometrics.sensors.AuthenticationClient; +import com.android.server.biometrics.sensors.AuthenticationStateListeners; import com.android.server.biometrics.sensors.BiometricNotificationUtils; import com.android.server.biometrics.sensors.ClientMonitorCallback; import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter; @@ -65,6 +68,8 @@ class FaceAuthenticationClient private int mLastAcquire; private SensorPrivacyManager mSensorPrivacyManager; + @NonNull + private final AuthenticationStateListeners mAuthenticationStateListeners; FaceAuthenticationClient(@NonNull Context context, @NonNull Supplier<IBiometricsFace> lazyDaemon, @@ -75,7 +80,8 @@ class FaceAuthenticationClient @NonNull BiometricLogger logger, @NonNull BiometricContext biometricContext, boolean isStrongBiometric, @NonNull LockoutTracker lockoutTracker, @NonNull UsageStats usageStats, boolean allowBackgroundAuthentication, - @Authenticators.Types int sensorStrength) { + @Authenticators.Types int sensorStrength, + @NonNull AuthenticationStateListeners authenticationStateListeners) { super(context, lazyDaemon, token, listener, operationId, restricted, options, cookie, requireConfirmation, logger, biometricContext, isStrongBiometric, null /* taskStackListener */, @@ -84,6 +90,7 @@ class FaceAuthenticationClient setRequestId(requestId); mUsageStats = usageStats; mSensorPrivacyManager = context.getSystemService(SensorPrivacyManager.class); + mAuthenticationStateListeners = authenticationStateListeners; final Resources resources = getContext().getResources(); mBiometricPromptIgnoreList = resources.getIntArray( @@ -186,6 +193,16 @@ class FaceAuthenticationClient 0 /* error */, 0 /* vendorError */, getTargetUserId())); + + if (reportBiometricAuthAttempts()) { + if (authenticated) { + mAuthenticationStateListeners.onAuthenticationSucceeded(getRequestReason(), + getTargetUserId()); + } else { + mAuthenticationStateListeners.onAuthenticationFailed(getRequestReason(), + getTargetUserId()); + } + } } @Override diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java index f7e812330ece..6912961ab94b 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java @@ -16,6 +16,8 @@ package com.android.server.biometrics.sensors.fingerprint.aidl; +import static android.adaptiveauth.Flags.reportBiometricAuthAttempts; + import static com.android.systemui.shared.Flags.sidefpsControllerRefactor; import android.annotation.NonNull; @@ -232,8 +234,16 @@ public class FingerprintAuthenticationClient if (sidefpsControllerRefactor()) { mAuthenticationStateListeners.onAuthenticationStopped(); } + if (reportBiometricAuthAttempts()) { + mAuthenticationStateListeners.onAuthenticationSucceeded(getRequestReason(), + getTargetUserId()); + } } else { mState = STATE_STARTED_PAUSED_ATTEMPTED; + if (reportBiometricAuthAttempts()) { + mAuthenticationStateListeners.onAuthenticationFailed(getRequestReason(), + getTargetUserId()); + } } } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java index 4c1d4d6d6d12..7a329e9d69e9 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java @@ -16,6 +16,8 @@ package com.android.server.biometrics.sensors.fingerprint.hidl; +import static android.adaptiveauth.Flags.reportBiometricAuthAttempts; + import static com.android.systemui.shared.Flags.sidefpsControllerRefactor; import android.annotation.NonNull; @@ -142,6 +144,10 @@ class FingerprintAuthenticationClient if (sidefpsControllerRefactor()) { mAuthenticationStateListeners.onAuthenticationStopped(); } + if (reportBiometricAuthAttempts()) { + mAuthenticationStateListeners.onAuthenticationSucceeded(getRequestReason(), + getTargetUserId()); + } } else { mState = STATE_STARTED_PAUSED_ATTEMPTED; final @LockoutTracker.LockoutMode int lockoutMode = @@ -161,6 +167,10 @@ class FingerprintAuthenticationClient onErrorInternal(errorCode, 0 /* vendorCode */, false /* finish */); cancel(); } + if (reportBiometricAuthAttempts()) { + mAuthenticationStateListeners.onAuthenticationFailed(getRequestReason(), + getTargetUserId()); + } } } diff --git a/services/core/java/com/android/server/compat/CompatConfig.java b/services/core/java/com/android/server/compat/CompatConfig.java index 9dd7daf1a1cc..9102cfd0d426 100644 --- a/services/core/java/com/android/server/compat/CompatConfig.java +++ b/services/core/java/com/android/server/compat/CompatConfig.java @@ -286,6 +286,9 @@ final class CompatConfig { return new CompatChange(changeId); }); c.addPackageOverride(packageName, overrides, allowedState, versionCode); + Slog.d(TAG, (overrides.isEnabled() ? "Enabled" : "Disabled") + + " change " + changeId + (c.getName() != null ? " [" + c.getName() + "]" : "") + + " for " + packageName); invalidateCache(); return alreadyKnown.get(); } @@ -372,7 +375,14 @@ final class CompatConfig { long changeId = change.getId(); OverrideAllowedState allowedState = mOverrideValidator.getOverrideAllowedState(changeId, packageName); - return change.removePackageOverride(packageName, allowedState, versionCode); + boolean overrideExists = change.removePackageOverride(packageName, allowedState, + versionCode); + if (overrideExists) { + Slog.d(TAG, "Reset change " + changeId + + (change.getName() != null ? " [" + change.getName() + "]" : "") + + " for " + packageName + " to default value."); + } + return overrideExists; } /** diff --git a/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java b/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java index 6ec6a123a4e7..77cb08bc02bd 100644 --- a/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java +++ b/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java @@ -204,6 +204,10 @@ public final class DeviceStateManagerService extends SystemService { } @Override + public void onProcessStarted(int pid, int processUid, int packageUid, String packageName, + String processName) {} + + @Override public void onProcessDied(int pid, int uid) {} @Override diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java index d34661d4d6ac..34e75c087864 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceTv.java @@ -47,6 +47,7 @@ import android.media.AudioDeviceInfo; import android.media.AudioProfile; import android.media.tv.TvInputInfo; import android.media.tv.TvInputManager.TvInputCallback; +import android.os.Handler; import android.util.Slog; import android.util.SparseBooleanArray; @@ -97,9 +98,15 @@ public final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice { private boolean mSystemAudioMute = false; // If true, do not do routing control/send active source for internal source. - // Set to true when the device was woken up by <Text/Image View On>. + // Set to true for a short duration when the device is woken up by <Text/Image View On>. private boolean mSkipRoutingControl; + // Handler for posting a runnable to set `mSkipRoutingControl` to false after a delay + private final Handler mSkipRoutingControlHandler; + + // Runnable that sets `mSkipRoutingControl` to false + private final Runnable mResetSkipRoutingControlRunnable = () -> mSkipRoutingControl = false; + // Message buffer used to buffer selected messages to process later. <Active Source> // from a source device, for instance, needs to be buffered if the device is not // discovered yet. The buffered commands are taken out and when they are ready to @@ -162,6 +169,7 @@ public final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice { HdmiControlManager.CEC_SETTING_NAME_SYSTEM_AUDIO_CONTROL) == HdmiControlManager.SYSTEM_AUDIO_CONTROL_ENABLED; mStandbyHandler = new HdmiCecStandbyModeHandler(service, this); + mSkipRoutingControlHandler = new Handler(service.getServiceLooper()); } @Override @@ -184,7 +192,14 @@ public final class HdmiCecLocalDeviceTv extends HdmiCecLocalDevice { mService.getHdmiCecNetwork().addCecSwitch( mService.getHdmiCecNetwork().getPhysicalAddress()); // TV is a CEC switch too. mTvInputs.clear(); + mSkipRoutingControl = (reason == HdmiControlService.INITIATED_BY_WAKE_UP_MESSAGE); + mSkipRoutingControlHandler.removeCallbacks(mResetSkipRoutingControlRunnable); + if (mSkipRoutingControl) { + mSkipRoutingControlHandler.postDelayed(mResetSkipRoutingControlRunnable, + HdmiConfig.TIMEOUT_MS); + } + launchRoutingControl(reason != HdmiControlService.INITIATED_BY_ENABLE_CEC && reason != HdmiControlService.INITIATED_BY_BOOT_UP); resetSelectRequestBuffer(); diff --git a/services/core/java/com/android/server/input/InputSettingsObserver.java b/services/core/java/com/android/server/input/InputSettingsObserver.java index bc55b24ce1eb..572d844d752d 100644 --- a/services/core/java/com/android/server/input/InputSettingsObserver.java +++ b/services/core/java/com/android/server/input/InputSettingsObserver.java @@ -16,6 +16,8 @@ package com.android.server.input; +import static com.android.input.flags.Flags.rateLimitUserActivityPokeInDispatcher; + import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -113,6 +115,8 @@ class InputSettingsObserver extends ContentObserver { for (Consumer<String> observer : mObservers.values()) { observer.accept("just booted"); } + + configureUserActivityPokeInterval(); } @Override @@ -228,4 +232,13 @@ class InputSettingsObserver extends ContentObserver { mService.setAccessibilityStickyKeysEnabled( InputSettings.isAccessibilityStickyKeysEnabled(mContext)); } + + private void configureUserActivityPokeInterval() { + if (rateLimitUserActivityPokeInDispatcher()) { + final int intervalMillis = mContext.getResources().getInteger( + com.android.internal.R.integer.config_minMillisBetweenInputUserActivityEvents); + Log.i(TAG, "Setting user activity interval (ms) of " + intervalMillis); + mNative.setMinTimeBetweenUserActivityPokes(intervalMillis); + } + } } diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java index 6f5202029998..8aec8ca7a23f 100644 --- a/services/core/java/com/android/server/input/NativeInputManagerService.java +++ b/services/core/java/com/android/server/input/NativeInputManagerService.java @@ -108,6 +108,8 @@ interface NativeInputManagerService { void setFocusedDisplay(int displayId); + void setMinTimeBetweenUserActivityPokes(long millis); + boolean transferTouchFocus(IBinder fromChannelToken, IBinder toChannelToken, boolean isDragDrop); @@ -344,6 +346,9 @@ interface NativeInputManagerService { public native void setFocusedDisplay(int displayId); @Override + public native void setMinTimeBetweenUserActivityPokes(long millis); + + @Override public native boolean transferTouchFocus(IBinder fromChannelToken, IBinder toChannelToken, boolean isDragDrop); diff --git a/services/core/java/com/android/server/inputmethod/AdditionalSubtypeUtils.java b/services/core/java/com/android/server/inputmethod/AdditionalSubtypeUtils.java index c76ca2beda96..fba71fd0ff9e 100644 --- a/services/core/java/com/android/server/inputmethod/AdditionalSubtypeUtils.java +++ b/services/core/java/com/android/server/inputmethod/AdditionalSubtypeUtils.java @@ -114,7 +114,7 @@ final class AdditionalSubtypeUtils { * @param userId The user ID to be associated with. */ static void save(ArrayMap<String, List<InputMethodSubtype>> allSubtypes, - ArrayMap<String, InputMethodInfo> methodMap, @UserIdInt int userId) { + InputMethodMap methodMap, @UserIdInt int userId) { final File inputMethodDir = getInputMethodDir(userId); if (allSubtypes.isEmpty()) { @@ -143,7 +143,7 @@ final class AdditionalSubtypeUtils { @VisibleForTesting static void saveToFile(ArrayMap<String, List<InputMethodSubtype>> allSubtypes, - ArrayMap<String, InputMethodInfo> methodMap, AtomicFile subtypesFile) { + InputMethodMap methodMap, AtomicFile subtypesFile) { // Safety net for the case that this function is called before methodMap is set. final boolean isSetMethodMap = methodMap != null && methodMap.size() > 0; FileOutputStream fos = null; diff --git a/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java b/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java index 4b85d099b600..62adb25954b4 100644 --- a/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java +++ b/services/core/java/com/android/server/inputmethod/HardwareKeyboardShortcutController.java @@ -20,7 +20,6 @@ import android.annotation.AnyThread; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; -import android.util.ArrayMap; import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodSubtype; @@ -44,24 +43,23 @@ final class HardwareKeyboardShortcutController { return mUserId; } - HardwareKeyboardShortcutController( - @NonNull ArrayMap<String, InputMethodInfo> methodMap, @UserIdInt int userId) { + HardwareKeyboardShortcutController(@NonNull InputMethodMap methodMap, @UserIdInt int userId) { mUserId = userId; reset(methodMap); } @GuardedBy("ImfLock.class") - void reset(@NonNull ArrayMap<String, InputMethodInfo> methodMap) { + void reset(@NonNull InputMethodMap methodMap) { mSubtypeHandles.clear(); - final InputMethodSettings settings = new InputMethodSettings(methodMap, mUserId); - final List<InputMethodInfo> inputMethods = settings.getEnabledInputMethodListLocked(); + final InputMethodSettings settings = InputMethodSettings.create(methodMap, mUserId); + final List<InputMethodInfo> inputMethods = settings.getEnabledInputMethodList(); for (int i = 0; i < inputMethods.size(); ++i) { final InputMethodInfo imi = inputMethods.get(i); if (!imi.shouldShowInInputMethodPicker()) { continue; } final List<InputMethodSubtype> subtypes = - settings.getEnabledInputMethodSubtypeListLocked(imi, true); + settings.getEnabledInputMethodSubtypeList(imi, true); if (subtypes.isEmpty()) { mSubtypeHandles.add(InputMethodSubtypeHandle.of(imi, null)); } else { diff --git a/services/core/java/com/android/server/inputmethod/InputMethodInfoUtils.java b/services/core/java/com/android/server/inputmethod/InputMethodInfoUtils.java index 542165d06a92..6339686629f5 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodInfoUtils.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodInfoUtils.java @@ -23,7 +23,6 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.text.TextUtils; -import android.util.ArrayMap; import android.util.Slog; import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodSubtype; @@ -204,7 +203,7 @@ final class InputMethodInfoUtils { */ @Nullable static InputMethodInfo chooseSystemVoiceIme( - @NonNull ArrayMap<String, InputMethodInfo> methodMap, + @NonNull InputMethodMap methodMap, @Nullable String systemSpeechRecognizerPackageName, @Nullable String currentDefaultVoiceImeId) { if (TextUtils.isEmpty(systemSpeechRecognizerPackageName)) { diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 8448fc233ae2..50340d241347 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -314,10 +314,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @Nullable private VirtualDeviceManagerInternal mVdmInternal = null; - // All known input methods. - final ArrayList<InputMethodInfo> mMethodList = new ArrayList<>(); - private final ArrayMap<String, InputMethodInfo> mMethodMap = new ArrayMap<>(); - // Mapping from deviceId to the device-specific imeId for that device. @GuardedBy("ImfLock.class") private final SparseArray<String> mVirtualDeviceMethodMap = new SparseArray<>(); @@ -330,7 +326,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub private HardwareKeyboardShortcutController mHardwareKeyboardShortcutController; /** - * Tracks how many times {@link #mMethodMap} was updated. + * Tracks how many times {@link #mSettings} was updated. */ @GuardedBy("ImfLock.class") private int mMethodMapUpdateCount = 0; @@ -472,7 +468,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") @Nullable InputMethodInfo queryInputMethodForCurrentUserLocked(@NonNull String imeId) { - return mMethodMap.get(imeId); + return mSettings.getMethodMap().get(imeId); } /** @@ -1171,7 +1167,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // sender userId can be a real user ID or USER_ALL. final int senderUserId = pendingResult.getSendingUserId(); if (senderUserId != UserHandle.USER_ALL) { - if (senderUserId != mSettings.getCurrentUserId()) { + if (senderUserId != mSettings.getUserId()) { // A background user is trying to hide the dialog. Ignore. return; } @@ -1249,7 +1245,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") private boolean isChangingPackagesOfCurrentUserLocked() { final int userId = getChangingUserId(); - final boolean retval = userId == mSettings.getCurrentUserId(); + final boolean retval = userId == mSettings.getUserId(); if (DEBUG) { if (!retval) { Slog.d(TAG, "--- ignore this call back from a background user: " + userId); @@ -1265,10 +1261,11 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return false; } String curInputMethodId = mSettings.getSelectedInputMethod(); - final int numImes = mMethodList.size(); + final List<InputMethodInfo> methodList = mSettings.getMethodList(); + final int numImes = methodList.size(); if (curInputMethodId != null) { for (int i = 0; i < numImes; i++) { - InputMethodInfo imi = mMethodList.get(i); + InputMethodInfo imi = methodList.get(i); if (imi.getId().equals(curInputMethodId)) { for (String pkg : packages) { if (imi.getPackageName().equals(pkg)) { @@ -1339,7 +1336,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @Override public void onPackageDataCleared(String packageName, int uid) { boolean changed = false; - for (InputMethodInfo imi : mMethodList) { + for (InputMethodInfo imi : mSettings.getMethodList()) { if (imi.getPackageName().equals(packageName)) { mAdditionalSubtypeMap.remove(imi.getId()); changed = true; @@ -1347,7 +1344,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } if (changed) { AdditionalSubtypeUtils.save( - mAdditionalSubtypeMap, mMethodMap, mSettings.getCurrentUserId()); + mAdditionalSubtypeMap, mSettings.getMethodMap(), mSettings.getUserId()); mChangedPackages.add(packageName); } } @@ -1405,10 +1402,11 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub InputMethodInfo curIm = null; String curInputMethodId = mSettings.getSelectedInputMethod(); - final int numImes = mMethodList.size(); + final List<InputMethodInfo> methodList = mSettings.getMethodList(); + final int numImes = methodList.size(); if (curInputMethodId != null) { for (int i = 0; i < numImes; i++) { - InputMethodInfo imi = mMethodList.get(i); + InputMethodInfo imi = methodList.get(i); final String imiId = imi.getId(); if (imiId.equals(curInputMethodId)) { curIm = imi; @@ -1426,8 +1424,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub + imi.getComponent()); mAdditionalSubtypeMap.remove(imi.getId()); AdditionalSubtypeUtils.save(mAdditionalSubtypeMap, - mMethodMap, - mSettings.getCurrentUserId()); + mSettings.getMethodMap(), mSettings.getUserId()); } } } @@ -1441,7 +1438,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (change == PACKAGE_TEMPORARY_CHANGE || change == PACKAGE_PERMANENT_CHANGE) { final PackageManager userAwarePackageManager = - getPackageManagerForUser(mContext, mSettings.getCurrentUserId()); + getPackageManagerForUser(mContext, mSettings.getUserId()); ServiceInfo si = null; try { si = userAwarePackageManager.getServiceInfo(curIm.getComponent(), @@ -1577,14 +1574,14 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub void onUnlockUser(@UserIdInt int userId) { synchronized (ImfLock.class) { - final int currentUserId = mSettings.getCurrentUserId(); + final int currentUserId = mSettings.getUserId(); if (DEBUG) { Slog.d(TAG, "onUnlockUser: userId=" + userId + " curUserId=" + currentUserId); } if (userId != currentUserId) { return; } - mSettings = new InputMethodSettings(mMethodMap, userId); + mSettings = InputMethodSettings.createEmptyMap(userId); if (mSystemReady) { // We need to rebuild IMEs. buildInputMethodListLocked(false /* resetDefaultEnabledIme */); @@ -1658,14 +1655,15 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mLastSwitchUserId = userId; // mSettings should be created before buildInputMethodListLocked - mSettings = new InputMethodSettings(mMethodMap, userId); + mSettings = InputMethodSettings.createEmptyMap(userId); AdditionalSubtypeUtils.load(mAdditionalSubtypeMap, userId); mSwitchingController = - InputMethodSubtypeSwitchingController.createInstanceLocked(context, mMethodMap, - userId); + InputMethodSubtypeSwitchingController.createInstanceLocked(context, + mSettings.getMethodMap(), userId); mHardwareKeyboardShortcutController = - new HardwareKeyboardShortcutController(mMethodMap, userId); + new HardwareKeyboardShortcutController(mSettings.getMethodMap(), + mSettings.getUserId()); mMenuController = new InputMethodMenuController(this); mBindingController = bindingControllerForTesting != null @@ -1696,7 +1694,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") @UserIdInt int getCurrentImeUserIdLocked() { - return mSettings.getCurrentUserId(); + return mSettings.getUserId(); } private final class InkWindowInitializer implements Runnable { @@ -1723,11 +1721,12 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub private void resetDefaultImeLocked(Context context) { // Do not reset the default (current) IME when it is a 3rd-party IME String selectedMethodId = getSelectedMethodIdLocked(); - if (selectedMethodId != null && !mMethodMap.get(selectedMethodId).isSystem()) { + if (selectedMethodId != null + && !mSettings.getMethodMap().get(selectedMethodId).isSystem()) { return; } final List<InputMethodInfo> suitableImes = InputMethodInfoUtils.getDefaultEnabledImes( - context, mSettings.getEnabledInputMethodListLocked()); + context, mSettings.getEnabledInputMethodList()); if (suitableImes.isEmpty()) { Slog.i(TAG, "No default found"); return; @@ -1783,7 +1782,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub IInputMethodClientInvoker clientToBeReset) { if (DEBUG) { Slog.d(TAG, "Switching user stage 1/3. newUserId=" + newUserId - + " currentUserId=" + mSettings.getCurrentUserId()); + + " currentUserId=" + mSettings.getUserId()); } maybeInitImeNavbarConfigLocked(newUserId); @@ -1791,7 +1790,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // ContentObserver should be registered again when the user is changed mSettingsObserver.registerContentObserverLocked(newUserId); - mSettings = new InputMethodSettings(mMethodMap, newUserId); + mSettings = InputMethodSettings.createEmptyMap(newUserId); // Additional subtypes should be reset when the user is changed AdditionalSubtypeUtils.load(mAdditionalSubtypeMap, newUserId); final String defaultImiId = mSettings.getSelectedInputMethod(); @@ -1822,7 +1821,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (initialUserSwitch) { InputMethodUtils.setNonSelectedSystemImesDisabledUntilUsed( getPackageManagerForUser(mContext, newUserId), - mSettings.getEnabledInputMethodListLocked()); + mSettings.getEnabledInputMethodList()); } if (DEBUG) { @@ -1853,7 +1852,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } if (!mSystemReady) { mSystemReady = true; - final int currentUserId = mSettings.getCurrentUserId(); + final int currentUserId = mSettings.getUserId(); mStatusBarManagerInternal = LocalServices.getService(StatusBarManagerInternal.class); hideStatusBarIconLocked(); @@ -1874,7 +1873,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // the "mImeDrawsImeNavBarResLazyInitFuture" field. synchronized (ImfLock.class) { mImeDrawsImeNavBarResLazyInitFuture = null; - if (currentUserId != mSettings.getCurrentUserId()) { + if (currentUserId != mSettings.getUserId()) { // This means that the current user is already switched to other user // before the background task is executed. In this scenario the relevant // field should already be initialized. @@ -1899,7 +1898,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub updateFromSettingsLocked(true); InputMethodUtils.setNonSelectedSystemImesDisabledUntilUsed( getPackageManagerForUser(mContext, currentUserId), - mSettings.getEnabledInputMethodListLocked()); + mSettings.getEnabledInputMethodList()); } } } @@ -1946,7 +1945,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } synchronized (ImfLock.class) { final int[] resolvedUserIds = InputMethodUtils.resolveUserId(userId, - mSettings.getCurrentUserId(), null); + mSettings.getUserId(), null); if (resolvedUserIds.length != 1) { return Collections.emptyList(); } @@ -1969,7 +1968,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } synchronized (ImfLock.class) { final int[] resolvedUserIds = InputMethodUtils.resolveUserId(userId, - mSettings.getCurrentUserId(), null); + mSettings.getUserId(), null); if (resolvedUserIds.length != 1) { return Collections.emptyList(); } @@ -1996,14 +1995,14 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } // Check if selected IME of current user supports handwriting. - if (userId == mSettings.getCurrentUserId()) { + if (userId == mSettings.getUserId()) { return mBindingController.supportsStylusHandwriting(); } //TODO(b/197848765): This can be optimized by caching multi-user methodMaps/methodList. //TODO(b/210039666): use cache. - final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId); - final InputMethodSettings settings = new InputMethodSettings(methodMap, userId); - final InputMethodInfo imi = methodMap.get(settings.getSelectedInputMethod()); + final InputMethodSettings settings = queryMethodMapForUser(userId); + final InputMethodInfo imi = settings.getMethodMap().get( + settings.getSelectedInputMethod()); return imi != null && imi.supportsStylusHandwriting(); } } @@ -2023,23 +2022,19 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") private List<InputMethodInfo> getInputMethodListLocked(@UserIdInt int userId, @DirectBootAwareness int directBootAwareness, int callingUid) { - final ArrayList<InputMethodInfo> methodList; final InputMethodSettings settings; - if (userId == mSettings.getCurrentUserId() + if (userId == mSettings.getUserId() && directBootAwareness == DirectBootAwareness.AUTO) { - // Create a copy. - methodList = new ArrayList<>(mMethodList); settings = mSettings; } else { - final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); - methodList = new ArrayList<>(); final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap = new ArrayMap<>(); AdditionalSubtypeUtils.load(additionalSubtypeMap, userId); - queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, methodMap, - methodList, directBootAwareness); - settings = new InputMethodSettings(methodMap, userId); + settings = queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, + directBootAwareness); } + // Create a copy. + final ArrayList<InputMethodInfo> methodList = new ArrayList<>(settings.getMethodList()); // filter caller's access to input methods methodList.removeIf(imi -> !canCallerAccessInputMethod(imi.getPackageName(), callingUid, userId, settings)); @@ -2051,13 +2046,12 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub int callingUid) { final ArrayList<InputMethodInfo> methodList; final InputMethodSettings settings; - if (userId == mSettings.getCurrentUserId()) { - methodList = mSettings.getEnabledInputMethodListLocked(); + if (userId == mSettings.getUserId()) { + methodList = mSettings.getEnabledInputMethodList(); settings = mSettings; } else { - final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId); - settings = new InputMethodSettings(methodMap, userId); - methodList = settings.getEnabledInputMethodListLocked(); + settings = queryMethodMapForUser(userId); + methodList = settings.getEnabledInputMethodList(); } // filter caller's access to input methods methodList.removeIf(imi -> @@ -2116,31 +2110,30 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") private List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked(String imiId, boolean allowsImplicitlyEnabledSubtypes, @UserIdInt int userId, int callingUid) { - if (userId == mSettings.getCurrentUserId()) { + if (userId == mSettings.getUserId()) { final InputMethodInfo imi; String selectedMethodId = getSelectedMethodIdLocked(); if (imiId == null && selectedMethodId != null) { - imi = mMethodMap.get(selectedMethodId); + imi = mSettings.getMethodMap().get(selectedMethodId); } else { - imi = mMethodMap.get(imiId); + imi = mSettings.getMethodMap().get(imiId); } if (imi == null || !canCallerAccessInputMethod( imi.getPackageName(), callingUid, userId, mSettings)) { return Collections.emptyList(); } - return mSettings.getEnabledInputMethodSubtypeListLocked( + return mSettings.getEnabledInputMethodSubtypeList( imi, allowsImplicitlyEnabledSubtypes); } - final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId); - final InputMethodInfo imi = methodMap.get(imiId); + final InputMethodSettings settings = queryMethodMapForUser(userId); + final InputMethodInfo imi = settings.getMethodMap().get(imiId); if (imi == null) { return Collections.emptyList(); } - final InputMethodSettings settings = new InputMethodSettings(methodMap, userId); if (!canCallerAccessInputMethod(imi.getPackageName(), callingUid, userId, settings)) { return Collections.emptyList(); } - return settings.getEnabledInputMethodSubtypeListLocked( + return settings.getEnabledInputMethodSubtypeList( imi, allowsImplicitlyEnabledSubtypes); } @@ -2305,7 +2298,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub final boolean restarting = !initial; final Binder startInputToken = new Binder(); - final StartInputInfo info = new StartInputInfo(mSettings.getCurrentUserId(), + final StartInputInfo info = new StartInputInfo(mSettings.getUserId(), getCurTokenLocked(), mCurTokenDisplayId, getCurIdLocked(), startInputReason, restarting, UserHandle.getUserId(mCurClient.mUid), @@ -2320,9 +2313,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // same-user scenarios. // That said ignoring cross-user scenario will never affect IMEs that do not have // INTERACT_ACROSS_USERS(_FULL) permissions, which is actually almost always the case. - if (mSettings.getCurrentUserId() == UserHandle.getUserId( + if (mSettings.getUserId() == UserHandle.getUserId( mCurClient.mUid)) { - mPackageManagerInternal.grantImplicitAccess(mSettings.getCurrentUserId(), + mPackageManagerInternal.grantImplicitAccess(mSettings.getUserId(), null /* intent */, UserHandle.getAppId(getCurMethodUidLocked()), mCurClient.mUid, true /* direct */); } @@ -2343,7 +2336,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } String curId = getCurIdLocked(); - final InputMethodInfo curInputMethodInfo = mMethodMap.get(curId); + final InputMethodInfo curInputMethodInfo = mSettings.getMethodMap().get(curId); final boolean suppressesSpellChecker = curInputMethodInfo != null && curInputMethodInfo.suppressesSpellChecker(); final SparseArray<IAccessibilityInputMethodSession> accessibilityInputMethodSessions = @@ -2559,7 +2552,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mVirtualDeviceMethodMap.get(mDeviceIdToShowIme, currentMethodId); if (Objects.equals(deviceMethodId, currentMethodId)) { return currentMethodId; - } else if (!mMethodMap.containsKey(deviceMethodId)) { + } else if (!mSettings.getMethodMap().containsKey(deviceMethodId)) { if (DEBUG) { Slog.v(TAG, "Disabling IME on virtual device with id " + mDeviceIdToShowIme + " because its custom input method is not available: " + deviceMethodId); @@ -2601,7 +2594,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (isSoftInputModeStateVisibleAllowed(unverifiedTargetSdkVersion, startInputFlags)) { return false; } - final InputMethodInfo imi = mMethodMap.get(selectedMethodId); + final InputMethodInfo imi = mSettings.getMethodMap().get(selectedMethodId); if (imi == null) { return false; } @@ -2944,7 +2937,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } else if (packageName != null) { if (DEBUG) Slog.d(TAG, "show a small icon for the input method"); final PackageManager userAwarePackageManager = - getPackageManagerForUser(mContext, mSettings.getCurrentUserId()); + getPackageManagerForUser(mContext, mSettings.getUserId()); ApplicationInfo applicationInfo = null; try { applicationInfo = userAwarePackageManager.getApplicationInfo(packageName, @@ -3006,7 +2999,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return false; } if (mWindowManagerInternal.isKeyguardShowingAndNotOccluded() - && mWindowManagerInternal.isKeyguardSecure(mSettings.getCurrentUserId())) { + && mWindowManagerInternal.isKeyguardSecure(mSettings.getUserId())) { return false; } if ((visibility & InputMethodService.IME_ACTIVE) == 0 @@ -3023,7 +3016,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return false; } - List<InputMethodInfo> imes = mSettings.getEnabledInputMethodListWithFilterLocked( + List<InputMethodInfo> imes = mSettings.getEnabledInputMethodListWithFilter( InputMethodInfo::shouldShowInInputMethodPicker); final int numImes = imes.size(); if (numImes > 2) return true; @@ -3035,7 +3028,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub for (int i = 0; i < numImes; ++i) { final InputMethodInfo imi = imes.get(i); final List<InputMethodSubtype> subtypes = - mSettings.getEnabledInputMethodSubtypeListLocked(imi, true); + mSettings.getEnabledInputMethodSubtypeList(imi, true); final int subtypeCount = subtypes.size(); if (subtypeCount == 0) { ++nonAuxCount; @@ -3185,9 +3178,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub void updateInputMethodsFromSettingsLocked(boolean enabledMayChange) { if (enabledMayChange) { final PackageManager userAwarePackageManager = getPackageManagerForUser(mContext, - mSettings.getCurrentUserId()); + mSettings.getUserId()); - List<InputMethodInfo> enabled = mSettings.getEnabledInputMethodListLocked(); + List<InputMethodInfo> enabled = mSettings.getEnabledInputMethodList(); for (int i = 0; i < enabled.size(); i++) { // We allow the user to select "disabled until used" apps, so if they // are enabling one of those here we now need to make it enabled. @@ -3233,18 +3226,18 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } // TODO: Instantiate mSwitchingController for each user. - if (mSettings.getCurrentUserId() == mSwitchingController.getUserId()) { - mSwitchingController.resetCircularListLocked(mMethodMap); + if (mSettings.getUserId() == mSwitchingController.getUserId()) { + mSwitchingController.resetCircularListLocked(mSettings.getMethodMap()); } else { mSwitchingController = InputMethodSubtypeSwitchingController.createInstanceLocked( - mContext, mMethodMap, mSettings.getCurrentUserId()); + mContext, mSettings.getMethodMap(), mSettings.getUserId()); } // TODO: Instantiate mHardwareKeyboardShortcutController for each user. - if (mSettings.getCurrentUserId() == mHardwareKeyboardShortcutController.getUserId()) { - mHardwareKeyboardShortcutController.reset(mMethodMap); + if (mSettings.getUserId() == mHardwareKeyboardShortcutController.getUserId()) { + mHardwareKeyboardShortcutController.reset(mSettings.getMethodMap()); } else { mHardwareKeyboardShortcutController = new HardwareKeyboardShortcutController( - mMethodMap, mSettings.getCurrentUserId()); + mSettings.getMethodMap(), mSettings.getUserId()); } sendOnNavButtonFlagsChangedLocked(); } @@ -3268,14 +3261,14 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") void setInputMethodLocked(String id, int subtypeId, int deviceId) { - InputMethodInfo info = mMethodMap.get(id); + InputMethodInfo info = mSettings.getMethodMap().get(id); if (info == null) { throw getExceptionForUnknownImeId(id); } // See if we need to notify a subtype change within the same IME. if (id.equals(getSelectedMethodIdLocked())) { - final int userId = mSettings.getCurrentUserId(); + final int userId = mSettings.getUserId(); final int subtypeCount = info.getSubtypeCount(); if (subtypeCount <= 0) { notifyInputMethodSubtypeChangedLocked(userId, info, null); @@ -3786,7 +3779,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return InputBindResult.USER_SWITCHING; } final int[] profileIdsWithDisabled = mUserManagerInternal.getProfileIds( - mSettings.getCurrentUserId(), false /* enabledOnly */); + mSettings.getUserId(), false /* enabledOnly */); for (int profileId : profileIdsWithDisabled) { if (profileId == userId) { scheduleSwitchUserTaskLocked(userId, cs.mClient); @@ -3805,7 +3798,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mVisibilityStateComputer.mShowForced = false; } - final int currentUserId = mSettings.getCurrentUserId(); + final int currentUserId = mSettings.getUserId(); if (userId != currentUserId) { if (ArrayUtils.contains( mUserManagerInternal.getProfileIds(currentUserId, false), userId)) { @@ -3949,7 +3942,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub && mCurFocusedWindowClient.mClient.asBinder() == client.asBinder()) { return true; } - if (mSettings.getCurrentUserId() != UserHandle.getUserId(uid)) { + if (mSettings.getUserId() != UserHandle.getUserId(uid)) { return false; } if (getCurIntentLocked() != null && InputMethodUtils.checkIfPackageBelongsToUid( @@ -4017,7 +4010,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (!calledWithValidTokenLocked(token)) { return; } - final InputMethodInfo imi = mMethodMap.get(id); + final InputMethodInfo imi = mSettings.getMethodMap().get(id); if (imi == null || !canCallerAccessInputMethod( imi.getPackageName(), callingUid, userId, mSettings)) { throw getExceptionForUnknownImeId(id); @@ -4035,7 +4028,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (!calledWithValidTokenLocked(token)) { return; } - final InputMethodInfo imi = mMethodMap.get(id); + final InputMethodInfo imi = mSettings.getMethodMap().get(id); if (imi == null || !canCallerAccessInputMethod( imi.getPackageName(), callingUid, userId, mSettings)) { throw getExceptionForUnknownImeId(id); @@ -4055,10 +4048,10 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (!calledWithValidTokenLocked(token)) { return false; } - final Pair<String, String> lastIme = mSettings.getLastInputMethodAndSubtypeLocked(); + final Pair<String, String> lastIme = mSettings.getLastInputMethodAndSubtype(); final InputMethodInfo lastImi; if (lastIme != null) { - lastImi = mMethodMap.get(lastIme.first); + lastImi = mSettings.getMethodMap().get(lastIme.first); } else { lastImi = null; } @@ -4082,7 +4075,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // This is a safety net. If the currentSubtype can't be added to the history // and the framework couldn't find the last ime, we will make the last ime be // the most applicable enabled keyboard subtype of the system imes. - final List<InputMethodInfo> enabled = mSettings.getEnabledInputMethodListLocked(); + final List<InputMethodInfo> enabled = mSettings.getEnabledInputMethodList(); if (enabled != null) { final int enabledCount = enabled.size(); final String locale; @@ -4090,14 +4083,13 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub && !TextUtils.isEmpty(mCurrentSubtype.getLocale())) { locale = mCurrentSubtype.getLocale(); } else { - locale = SystemLocaleWrapper.get(mSettings.getCurrentUserId()).get(0) - .toString(); + locale = SystemLocaleWrapper.get(mSettings.getUserId()).get(0).toString(); } for (int i = 0; i < enabledCount; ++i) { final InputMethodInfo imi = enabled.get(i); if (imi.getSubtypeCount() > 0 && imi.isSystem()) { InputMethodSubtype keyboardSubtype = - SubtypeUtils.findLastResortApplicableSubtypeLocked( + SubtypeUtils.findLastResortApplicableSubtype( SubtypeUtils.getSubtypes(imi), SubtypeUtils.SUBTYPE_MODE_KEYBOARD, locale, true); if (keyboardSubtype != null) { @@ -4139,7 +4131,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") private boolean switchToNextInputMethodLocked(@Nullable IBinder token, boolean onlyCurrentIme) { final ImeSubtypeListItem nextSubtype = mSwitchingController.getNextInputMethodLocked( - onlyCurrentIme, mMethodMap.get(getSelectedMethodIdLocked()), mCurrentSubtype); + onlyCurrentIme, mSettings.getMethodMap().get(getSelectedMethodIdLocked()), + mCurrentSubtype); if (nextSubtype == null) { return false; } @@ -4155,8 +4148,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return false; } final ImeSubtypeListItem nextSubtype = mSwitchingController.getNextInputMethodLocked( - false /* onlyCurrentIme */, mMethodMap.get(getSelectedMethodIdLocked()), - mCurrentSubtype); + false /* onlyCurrentIme */, + mSettings.getMethodMap().get(getSelectedMethodIdLocked()), mCurrentSubtype); return nextSubtype != null; } } @@ -4168,13 +4161,12 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub Manifest.permission.INTERACT_ACROSS_USERS_FULL, null); } synchronized (ImfLock.class) { - if (mSettings.getCurrentUserId() == userId) { - return mSettings.getLastInputMethodSubtypeLocked(); + if (mSettings.getUserId() == userId) { + return mSettings.getLastInputMethodSubtype(); } - final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId); - final InputMethodSettings settings = new InputMethodSettings(methodMap, userId); - return settings.getLastInputMethodSubtypeLocked(); + final InputMethodSettings settings = queryMethodMapForUser(userId); + return settings.getLastInputMethodSubtype(); } } @@ -4204,7 +4196,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return; } - if (mSettings.getCurrentUserId() == userId) { + if (mSettings.getUserId() == userId) { if (!mSettings.setAdditionalInputMethodSubtypes(imiId, toBeAdded, mAdditionalSubtypeMap, mPackageManagerInternal, callingUid)) { return; @@ -4218,14 +4210,11 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return; } - final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); - final ArrayList<InputMethodInfo> methodList = new ArrayList<>(); final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap = new ArrayMap<>(); AdditionalSubtypeUtils.load(additionalSubtypeMap, userId); - queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, methodMap, - methodList, DirectBootAwareness.AUTO); - final InputMethodSettings settings = new InputMethodSettings(methodMap, userId); + final InputMethodSettings settings = queryInputMethodServicesInternal(mContext, userId, + additionalSubtypeMap, DirectBootAwareness.AUTO); settings.setAdditionalInputMethodSubtypes(imiId, toBeAdded, additionalSubtypeMap, mPackageManagerInternal, callingUid); } @@ -4251,10 +4240,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub final long ident = Binder.clearCallingIdentity(); try { synchronized (ImfLock.class) { - final boolean currentUser = (mSettings.getCurrentUserId() == userId); + final boolean currentUser = (mSettings.getUserId() == userId); final InputMethodSettings settings = currentUser - ? mSettings - : new InputMethodSettings(queryMethodMapForUser(userId), userId); + ? mSettings : queryMethodMapForUser(userId); if (!settings.setEnabledInputMethodSubtypes(imeId, subtypeHashCodes)) { return; } @@ -4623,10 +4611,11 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } return; } - if (mSettings.getCurrentUserId() != mSwitchingController.getUserId()) { + if (mSettings.getUserId() != mSwitchingController.getUserId()) { return; } - final InputMethodInfo imi = mMethodMap.get(getSelectedMethodIdLocked()); + final InputMethodInfo imi = + mSettings.getMethodMap().get(getSelectedMethodIdLocked()); if (imi != null) { mSwitchingController.onUserActionLocked(imi, mCurrentSubtype); } @@ -4684,8 +4673,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return; } else { // Called with current IME's token. - if (mMethodMap.get(id) != null - && mSettings.getEnabledInputMethodListWithFilterLocked( + if (mSettings.getMethodMap().get(id) != null + && mSettings.getEnabledInputMethodListWithFilter( (info) -> info.getId().equals(id)).isEmpty()) { throw new IllegalStateException("Requested IME is not enabled: " + id); } @@ -4857,8 +4846,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } synchronized (ImfLock.class) { final boolean isScreenLocked = mWindowManagerInternal.isKeyguardLocked() - && mWindowManagerInternal.isKeyguardSecure( - mSettings.getCurrentUserId()); + && mWindowManagerInternal.isKeyguardSecure(mSettings.getUserId()); final String lastInputMethodId = mSettings.getSelectedInputMethod(); int lastInputMethodSubtypeId = mSettings.getSelectedInputMethodSubtypeId(lastInputMethodId); @@ -4866,7 +4854,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub final List<ImeSubtypeListItem> imList = InputMethodSubtypeSwitchingController .getSortedInputMethodAndSubtypeList( showAuxSubtypes, isScreenLocked, true /* forImeMenu */, - mContext, mMethodMap, mSettings.getCurrentUserId()); + mContext, mSettings.getMethodMap(), mSettings.getUserId()); mMenuController.showInputMethodMenuLocked(showAuxSubtypes, displayId, lastInputMethodId, lastInputMethodSubtypeId, imList); } @@ -5050,7 +5038,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") private boolean chooseNewDefaultIMELocked() { final InputMethodInfo imi = InputMethodInfoUtils.getMostApplicableDefaultIME( - mSettings.getEnabledInputMethodListLocked()); + mSettings.getEnabledInputMethodList()); if (imi != null) { if (DEBUG) { Slog.d(TAG, "New default IME was selected: " + imi.getId()); @@ -5062,17 +5050,14 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return false; } - static void queryInputMethodServicesInternal(Context context, + @NonNull + static InputMethodSettings queryInputMethodServicesInternal(Context context, @UserIdInt int userId, ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap, - ArrayMap<String, InputMethodInfo> methodMap, ArrayList<InputMethodInfo> methodList, @DirectBootAwareness int directBootAwareness) { final Context userAwareContext = context.getUserId() == userId ? context : context.createContextAsUser(UserHandle.of(userId), 0 /* flags */); - methodList.clear(); - methodMap.clear(); - final int directBootAwarenessFlags; switch (directBootAwareness) { case DirectBootAwareness.ANY: @@ -5095,24 +5080,23 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub new Intent(InputMethod.SERVICE_INTERFACE), PackageManager.ResolveInfoFlags.of(flags)); - methodList.ensureCapacity(services.size()); - methodMap.ensureCapacity(services.size()); - // Note: This is a temporary solution for Bug 261723412. If there is any better solution, // we should remove this data dependency. final List<String> enabledInputMethodList = InputMethodUtils.getEnabledInputMethodIdsForFiltering(context, userId); - filterInputMethodServices(additionalSubtypeMap, methodMap, methodList, - enabledInputMethodList, userAwareContext, services); + final InputMethodMap methodMap = filterInputMethodServices( + additionalSubtypeMap, enabledInputMethodList, userAwareContext, services); + return InputMethodSettings.create(methodMap, userId); } - static void filterInputMethodServices( + @NonNull + static InputMethodMap filterInputMethodServices( ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap, - ArrayMap<String, InputMethodInfo> methodMap, ArrayList<InputMethodInfo> methodList, List<String> enabledInputMethodList, Context userAwareContext, List<ResolveInfo> services) { final ArrayMap<String, Integer> imiPackageCount = new ArrayMap<>(); + final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(services.size()); for (int i = 0; i < services.size(); ++i) { ResolveInfo ri = services.get(i); @@ -5141,7 +5125,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub imiPackageCount.put(packageName, 1 + imiPackageCount.getOrDefault(packageName, 0)); - methodList.add(imi); methodMap.put(imi.getId(), imi); if (DEBUG) { Slog.d(TAG, "Found an input method " + imi); @@ -5153,6 +5136,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub Slog.wtf(TAG, "Unable to load input method " + imeId, e); } } + return InputMethodMap.of(methodMap); } @GuardedBy("ImfLock.class") @@ -5168,8 +5152,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mMethodMapUpdateCount++; mMyPackageMonitor.clearKnownImePackageNamesLocked(); - queryInputMethodServicesInternal(mContext, mSettings.getCurrentUserId(), - mAdditionalSubtypeMap, mMethodMap, mMethodList, DirectBootAwareness.AUTO); + mSettings = queryInputMethodServicesInternal(mContext, mSettings.getUserId(), + mAdditionalSubtypeMap, DirectBootAwareness.AUTO); // Construct the set of possible IME packages for onPackageChanged() to avoid false // negatives when the package state remains to be the same but only the component state is @@ -5181,7 +5165,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub final List<ResolveInfo> allInputMethodServices = mContext.getPackageManager().queryIntentServicesAsUser( new Intent(InputMethod.SERVICE_INTERFACE), - PackageManager.MATCH_DISABLED_COMPONENTS, mSettings.getCurrentUserId()); + PackageManager.MATCH_DISABLED_COMPONENTS, mSettings.getUserId()); final int numImes = allInputMethodServices.size(); for (int i = 0; i < numImes; ++i) { final ServiceInfo si = allInputMethodServices.get(i).serviceInfo; @@ -5196,11 +5180,11 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (!resetDefaultEnabledIme) { boolean enabledImeFound = false; boolean enabledNonAuxImeFound = false; - final List<InputMethodInfo> enabledImes = mSettings.getEnabledInputMethodListLocked(); + final List<InputMethodInfo> enabledImes = mSettings.getEnabledInputMethodList(); final int numImes = enabledImes.size(); for (int i = 0; i < numImes; ++i) { final InputMethodInfo imi = enabledImes.get(i); - if (mMethodList.contains(imi)) { + if (mSettings.getMethodMap().containsKey(imi.getId())) { enabledImeFound = true; if (!imi.isAuxiliaryIme()) { enabledNonAuxImeFound = true; @@ -5224,7 +5208,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (resetDefaultEnabledIme || reenableMinimumNonAuxSystemImes) { final ArrayList<InputMethodInfo> defaultEnabledIme = - InputMethodInfoUtils.getDefaultEnabledImes(mContext, mMethodList, + InputMethodInfoUtils.getDefaultEnabledImes(mContext, mSettings.getMethodList(), reenableMinimumNonAuxSystemImes); final int numImes = defaultEnabledIme.size(); for (int i = 0; i < numImes; ++i) { @@ -5238,7 +5222,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub final String defaultImiId = mSettings.getSelectedInputMethod(); if (!TextUtils.isEmpty(defaultImiId)) { - if (!mMethodMap.containsKey(defaultImiId)) { + if (!mSettings.getMethodMap().containsKey(defaultImiId)) { Slog.w(TAG, "Default IME is uninstalled. Choose new default IME."); if (chooseNewDefaultIMELocked()) { updateInputMethodsFromSettingsLocked(true); @@ -5252,26 +5236,26 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub updateDefaultVoiceImeIfNeededLocked(); // TODO: Instantiate mSwitchingController for each user. - if (mSettings.getCurrentUserId() == mSwitchingController.getUserId()) { - mSwitchingController.resetCircularListLocked(mMethodMap); + if (mSettings.getUserId() == mSwitchingController.getUserId()) { + mSwitchingController.resetCircularListLocked(mSettings.getMethodMap()); } else { mSwitchingController = InputMethodSubtypeSwitchingController.createInstanceLocked( - mContext, mMethodMap, mSettings.getCurrentUserId()); + mContext, mSettings.getMethodMap(), mSettings.getUserId()); } // TODO: Instantiate mHardwareKeyboardShortcutController for each user. - if (mSettings.getCurrentUserId() == mHardwareKeyboardShortcutController.getUserId()) { - mHardwareKeyboardShortcutController.reset(mMethodMap); + if (mSettings.getUserId() == mHardwareKeyboardShortcutController.getUserId()) { + mHardwareKeyboardShortcutController.reset(mSettings.getMethodMap()); } else { mHardwareKeyboardShortcutController = new HardwareKeyboardShortcutController( - mMethodMap, mSettings.getCurrentUserId()); + mSettings.getMethodMap(), mSettings.getUserId()); } sendOnNavButtonFlagsChangedLocked(); // Notify InputMethodListListeners of the new installed InputMethods. - final List<InputMethodInfo> inputMethodList = new ArrayList<>(mMethodList); + final List<InputMethodInfo> inputMethodList = mSettings.getMethodList(); mHandler.obtainMessage(MSG_DISPATCH_ON_INPUT_METHOD_LIST_UPDATED, - mSettings.getCurrentUserId(), 0 /* unused */, inputMethodList).sendToTarget(); + mSettings.getUserId(), 0 /* unused */, inputMethodList).sendToTarget(); } @GuardedBy("ImfLock.class") @@ -5290,7 +5274,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mContext.getString(com.android.internal.R.string.config_systemSpeechRecognizer); final String currentDefaultVoiceImeId = mSettings.getDefaultVoiceInputMethod(); final InputMethodInfo newSystemVoiceIme = InputMethodInfoUtils.chooseSystemVoiceIme( - mMethodMap, systemSpeechRecognizer, currentDefaultVoiceImeId); + mSettings.getMethodMap(), systemSpeechRecognizer, currentDefaultVoiceImeId); if (newSystemVoiceIme == null) { if (DEBUG) { Slog.i(TAG, "Found no valid default Voice IME. If the user is still locked," @@ -5341,9 +5325,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return false; } else { final List<Pair<String, ArrayList<String>>> enabledInputMethodsList = mSettings - .getEnabledInputMethodsAndSubtypeListLocked(); + .getEnabledInputMethodsAndSubtypeList(); StringBuilder builder = new StringBuilder(); - if (mSettings.buildAndPutEnabledInputMethodsStrRemovingIdLocked( + if (mSettings.buildAndPutEnabledInputMethodsStrRemovingId( builder, enabledInputMethodsList, id)) { if (mDeviceIdToShowIme == DEVICE_ID_DEFAULT) { // Disabled input method is currently selected, switch to another one. @@ -5357,7 +5341,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // new default one but only update the settings. InputMethodInfo newDefaultIme = InputMethodInfoUtils.getMostApplicableDefaultIME( - mSettings.getEnabledInputMethodListLocked()); + mSettings.getEnabledInputMethodList()); mSettings.putSelectedDefaultDeviceInputMethod( newDefaultIme == null ? "" : newDefaultIme.getId()); } @@ -5392,7 +5376,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub mCurrentSubtype = getCurrentInputMethodSubtypeLocked(); } } - notifyInputMethodSubtypeChangedLocked(mSettings.getCurrentUserId(), imi, mCurrentSubtype); + notifyInputMethodSubtypeChangedLocked(mSettings.getUserId(), imi, mCurrentSubtype); if (!setSubtypeOnly) { // Set InputMethod here @@ -5402,11 +5386,11 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") private void resetSelectedInputMethodAndSubtypeLocked(String newDefaultIme) { - InputMethodInfo imi = mMethodMap.get(newDefaultIme); + InputMethodInfo imi = mSettings.getMethodMap().get(newDefaultIme); int lastSubtypeId = NOT_A_SUBTYPE_ID; // newDefaultIme is empty when there is no candidate for the selected IME. if (imi != null && !TextUtils.isEmpty(newDefaultIme)) { - String subtypeHashCode = mSettings.getLastSubtypeForInputMethodLocked(newDefaultIme); + String subtypeHashCode = mSettings.getLastSubtypeForInputMethod(newDefaultIme); if (subtypeHashCode != null) { try { lastSubtypeId = SubtypeUtils.getSubtypeIdFromHashCode(imi, @@ -5433,12 +5417,11 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub Manifest.permission.INTERACT_ACROSS_USERS_FULL, null); } synchronized (ImfLock.class) { - if (mSettings.getCurrentUserId() == userId) { + if (mSettings.getUserId() == userId) { return getCurrentInputMethodSubtypeLocked(); } - final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId); - final InputMethodSettings settings = new InputMethodSettings(methodMap, userId); + final InputMethodSettings settings = queryMethodMapForUser(userId); return settings.getCurrentInputMethodSubtypeForNonCurrentUsers(); } } @@ -5460,7 +5443,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return null; } final boolean subtypeIsSelected = mSettings.isSubtypeSelected(); - final InputMethodInfo imi = mMethodMap.get(selectedMethodId); + final InputMethodInfo imi = mSettings.getMethodMap().get(selectedMethodId); if (imi == null || imi.getSubtypeCount() == 0) { return null; } @@ -5472,19 +5455,19 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // the most applicable subtype from explicitly or implicitly enabled // subtypes. List<InputMethodSubtype> explicitlyOrImplicitlyEnabledSubtypes = - mSettings.getEnabledInputMethodSubtypeListLocked(imi, true); + mSettings.getEnabledInputMethodSubtypeList(imi, true); // If there is only one explicitly or implicitly enabled subtype, // just returns it. if (explicitlyOrImplicitlyEnabledSubtypes.size() == 1) { mCurrentSubtype = explicitlyOrImplicitlyEnabledSubtypes.get(0); } else if (explicitlyOrImplicitlyEnabledSubtypes.size() > 1) { - final String locale = SystemLocaleWrapper.get(mSettings.getCurrentUserId()) + final String locale = SystemLocaleWrapper.get(mSettings.getUserId()) .get(0).toString(); - mCurrentSubtype = SubtypeUtils.findLastResortApplicableSubtypeLocked( + mCurrentSubtype = SubtypeUtils.findLastResortApplicableSubtype( explicitlyOrImplicitlyEnabledSubtypes, SubtypeUtils.SUBTYPE_MODE_KEYBOARD, locale, true); if (mCurrentSubtype == null) { - mCurrentSubtype = SubtypeUtils.findLastResortApplicableSubtypeLocked( + mCurrentSubtype = SubtypeUtils.findLastResortApplicableSubtype( explicitlyOrImplicitlyEnabledSubtypes, null, locale, true); } } @@ -5501,46 +5484,42 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub */ @GuardedBy("ImfLock.class") private InputMethodInfo queryDefaultInputMethodForUserIdLocked(@UserIdInt int userId) { - if (userId == mSettings.getCurrentUserId()) { - return mMethodMap.get(mSettings.getSelectedInputMethod()); + final InputMethodSettings settings; + if (userId == mSettings.getUserId()) { + settings = mSettings; + } else { + final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap = + new ArrayMap<>(); + AdditionalSubtypeUtils.load(additionalSubtypeMap, userId); + settings = queryInputMethodServicesInternal(mContext, userId, + additionalSubtypeMap, DirectBootAwareness.AUTO); } - - final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); - final ArrayList<InputMethodInfo> methodList = new ArrayList<>(); - final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap = new ArrayMap<>(); - AdditionalSubtypeUtils.load(additionalSubtypeMap, userId); - queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, methodMap, - methodList, DirectBootAwareness.AUTO); - InputMethodSettings settings = new InputMethodSettings(methodMap, userId); - return methodMap.get(settings.getSelectedInputMethod()); + return settings.getMethodMap().get(settings.getSelectedInputMethod()); } - private ArrayMap<String, InputMethodInfo> queryMethodMapForUser(@UserIdInt int userId) { - final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); - final ArrayList<InputMethodInfo> methodList = new ArrayList<>(); + private InputMethodSettings queryMethodMapForUser(@UserIdInt int userId) { final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap = new ArrayMap<>(); AdditionalSubtypeUtils.load(additionalSubtypeMap, userId); - queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, - methodMap, methodList, DirectBootAwareness.AUTO); - return methodMap; + return queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, + DirectBootAwareness.AUTO); } @GuardedBy("ImfLock.class") private boolean switchToInputMethodLocked(String imeId, @UserIdInt int userId) { - if (userId == mSettings.getCurrentUserId()) { - if (!mMethodMap.containsKey(imeId) - || !mSettings.getEnabledInputMethodListLocked() - .contains(mMethodMap.get(imeId))) { + if (userId == mSettings.getUserId()) { + if (!mSettings.getMethodMap().containsKey(imeId) + || !mSettings.getEnabledInputMethodList() + .contains(mSettings.getMethodMap().get(imeId))) { return false; // IME is not found or not enabled. } setInputMethodLocked(imeId, NOT_A_SUBTYPE_ID); return true; } - final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId); - final InputMethodSettings settings = new InputMethodSettings(methodMap, userId); - if (!methodMap.containsKey(imeId) - || !settings.getEnabledInputMethodListLocked().contains(methodMap.get(imeId))) { + final InputMethodSettings settings = queryMethodMapForUser(userId); + if (!settings.getMethodMap().containsKey(imeId) + || !settings.getEnabledInputMethodList().contains( + settings.getMethodMap().get(imeId))) { return false; // IME is not found or not enabled. } settings.putSelectedInputMethod(imeId); @@ -5578,7 +5557,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") private void switchKeyboardLayoutLocked(int direction) { - final InputMethodInfo currentImi = mMethodMap.get(getSelectedMethodIdLocked()); + final InputMethodInfo currentImi = mSettings.getMethodMap().get( + getSelectedMethodIdLocked()); if (currentImi == null) { return; } @@ -5590,7 +5570,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub if (nextSubtypeHandle == null) { return; } - final InputMethodInfo nextImi = mMethodMap.get(nextSubtypeHandle.getImeId()); + final InputMethodInfo nextImi = mSettings.getMethodMap().get(nextSubtypeHandle.getImeId()); if (nextImi == null) { return; } @@ -5669,16 +5649,15 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @Override public boolean setInputMethodEnabled(String imeId, boolean enabled, @UserIdInt int userId) { synchronized (ImfLock.class) { - if (userId == mSettings.getCurrentUserId()) { - if (!mMethodMap.containsKey(imeId)) { + if (userId == mSettings.getUserId()) { + if (!mSettings.getMethodMap().containsKey(imeId)) { return false; // IME is not found. } setInputMethodEnabledLocked(imeId, enabled); return true; } - final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId); - final InputMethodSettings settings = new InputMethodSettings(methodMap, userId); - if (!methodMap.containsKey(imeId)) { + final InputMethodSettings settings = queryMethodMapForUser(userId); + if (!settings.getMethodMap().containsKey(imeId)) { return false; // IME is not found. } if (enabled) { @@ -5689,9 +5668,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub settings.putEnabledInputMethodsStr(newEnabledImeIdsStr); } } else { - settings.buildAndPutEnabledInputMethodsStrRemovingIdLocked( + settings.buildAndPutEnabledInputMethodsStrRemovingId( new StringBuilder(), - settings.getEnabledInputMethodsAndSubtypeListLocked(), imeId); + settings.getEnabledInputMethodsAndSubtypeList(), imeId); } return true; } @@ -5997,10 +5976,11 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub synchronized (ImfLock.class) { p.println("Current Input Method Manager state:"); - int numImes = mMethodList.size(); + final List<InputMethodInfo> methodList = mSettings.getMethodList(); + int numImes = methodList.size(); p.println(" Input Methods: mMethodMapUpdateCount=" + mMethodMapUpdateCount); for (int i = 0; i < numImes; i++) { - InputMethodInfo info = mMethodList.get(i); + InputMethodInfo info = methodList.get(i); p.println(" InputMethod #" + i + ":"); info.dump(p, " "); } @@ -6047,7 +6027,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub p.println(" mSwitchingController:"); mSwitchingController.dump(p); p.println(" mSettings:"); - mSettings.dumpLocked(p, " "); + mSettings.dump(p, " "); p.println(" mStartInputHistory:"); mStartInputHistory.dump(pw, " "); @@ -6311,7 +6291,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } synchronized (ImfLock.class) { final int[] userIds = InputMethodUtils.resolveUserId(userIdToBeResolved, - mSettings.getCurrentUserId(), shellCommand.getErrPrintWriter()); + mSettings.getUserId(), shellCommand.getErrPrintWriter()); try (PrintWriter pr = shellCommand.getOutPrintWriter()) { for (int userId : userIds) { final List<InputMethodInfo> methods = all @@ -6356,7 +6336,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub PrintWriter error = shellCommand.getErrPrintWriter()) { synchronized (ImfLock.class) { final int[] userIds = InputMethodUtils.resolveUserId(userIdToBeResolved, - mSettings.getCurrentUserId(), shellCommand.getErrPrintWriter()); + mSettings.getUserId(), shellCommand.getErrPrintWriter()); for (int userId : userIds) { if (!userHasDebugPriv(userId, shellCommand)) { continue; @@ -6415,17 +6395,16 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub PrintWriter error) { boolean failedToEnableUnknownIme = false; boolean previouslyEnabled = false; - if (userId == mSettings.getCurrentUserId()) { - if (enabled && !mMethodMap.containsKey(imeId)) { + if (userId == mSettings.getUserId()) { + if (enabled && !mSettings.getMethodMap().containsKey(imeId)) { failedToEnableUnknownIme = true; } else { previouslyEnabled = setInputMethodEnabledLocked(imeId, enabled); } } else { - final ArrayMap<String, InputMethodInfo> methodMap = queryMethodMapForUser(userId); - final InputMethodSettings settings = new InputMethodSettings(methodMap, userId); + final InputMethodSettings settings = queryMethodMapForUser(userId); if (enabled) { - if (!methodMap.containsKey(imeId)) { + if (!settings.getMethodMap().containsKey(imeId)) { failedToEnableUnknownIme = true; } else { final String enabledImeIdsStr = settings.getEnabledInputMethodsStr(); @@ -6438,9 +6417,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } } else { previouslyEnabled = - settings.buildAndPutEnabledInputMethodsStrRemovingIdLocked( + settings.buildAndPutEnabledInputMethodsStrRemovingId( new StringBuilder(), - settings.getEnabledInputMethodsAndSubtypeListLocked(), imeId); + settings.getEnabledInputMethodsAndSubtypeList(), imeId); } } if (failedToEnableUnknownIme) { @@ -6478,7 +6457,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub PrintWriter error = shellCommand.getErrPrintWriter()) { synchronized (ImfLock.class) { final int[] userIds = InputMethodUtils.resolveUserId(userIdToBeResolved, - mSettings.getCurrentUserId(), shellCommand.getErrPrintWriter()); + mSettings.getUserId(), shellCommand.getErrPrintWriter()); for (int userId : userIds) { if (!userHasDebugPriv(userId, shellCommand)) { continue; @@ -6518,7 +6497,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub synchronized (ImfLock.class) { try (PrintWriter out = shellCommand.getOutPrintWriter()) { final int[] userIds = InputMethodUtils.resolveUserId(userIdToBeResolved, - mSettings.getCurrentUserId(), shellCommand.getErrPrintWriter()); + mSettings.getUserId(), shellCommand.getErrPrintWriter()); for (int userId : userIds) { if (!userHasDebugPriv(userId, shellCommand)) { continue; @@ -6530,16 +6509,16 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } final String nextIme; final List<InputMethodInfo> nextEnabledImes; - if (userId == mSettings.getCurrentUserId()) { + if (userId == mSettings.getUserId()) { hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */, null /* resultReceiver */, SoftInputShowHideReason.HIDE_RESET_SHELL_COMMAND); mBindingController.unbindCurrentMethod(); // Enable default IMEs, disable others - var toDisable = mSettings.getEnabledInputMethodListLocked(); + var toDisable = mSettings.getEnabledInputMethodList(); var defaultEnabled = InputMethodInfoUtils.getDefaultEnabledImes( - mContext, mMethodList); + mContext, mSettings.getMethodList()); toDisable.removeAll(defaultEnabled); for (InputMethodInfo info : toDisable) { setInputMethodEnabledLocked(info.getId(), false); @@ -6553,23 +6532,19 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } updateInputMethodsFromSettingsLocked(true /* enabledMayChange */); InputMethodUtils.setNonSelectedSystemImesDisabledUntilUsed( - getPackageManagerForUser(mContext, mSettings.getCurrentUserId()), - mSettings.getEnabledInputMethodListLocked()); + getPackageManagerForUser(mContext, mSettings.getUserId()), + mSettings.getEnabledInputMethodList()); nextIme = mSettings.getSelectedInputMethod(); - nextEnabledImes = mSettings.getEnabledInputMethodListLocked(); + nextEnabledImes = mSettings.getEnabledInputMethodList(); } else { - final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); - final ArrayList<InputMethodInfo> methodList = new ArrayList<>(); final ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap = new ArrayMap<>(); AdditionalSubtypeUtils.load(additionalSubtypeMap, userId); - queryInputMethodServicesInternal(mContext, userId, additionalSubtypeMap, - methodMap, methodList, DirectBootAwareness.AUTO); - final InputMethodSettings settings = new InputMethodSettings( - methodMap, userId); + final InputMethodSettings settings = queryInputMethodServicesInternal( + mContext, userId, additionalSubtypeMap, DirectBootAwareness.AUTO); nextEnabledImes = InputMethodInfoUtils.getDefaultEnabledImes(mContext, - methodList); + settings.getMethodList()); nextIme = InputMethodInfoUtils.getMostApplicableDefaultIME( nextEnabledImes).getId(); diff --git a/services/core/java/com/android/server/inputmethod/InputMethodMap.java b/services/core/java/com/android/server/inputmethod/InputMethodMap.java new file mode 100644 index 000000000000..a8e5e2ef4f72 --- /dev/null +++ b/services/core/java/com/android/server/inputmethod/InputMethodMap.java @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.inputmethod; + +import android.annotation.AnyThread; +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.util.ArrayMap; +import android.view.inputmethod.InputMethodInfo; + +import java.util.List; + +/** + * A map from IME ID to {@link InputMethodInfo}, which is guaranteed to be immutable thus + * thread-safe. + */ +final class InputMethodMap { + private static final ArrayMap<String, InputMethodInfo> EMPTY_MAP = + new ArrayMap<>(); + + private final ArrayMap<String, InputMethodInfo> mMap; + + static InputMethodMap emptyMap() { + return new InputMethodMap(EMPTY_MAP); + } + + static InputMethodMap of(@NonNull ArrayMap<String, InputMethodInfo> map) { + return new InputMethodMap(map); + } + + private InputMethodMap(@NonNull ArrayMap<String, InputMethodInfo> map) { + mMap = map.isEmpty() ? EMPTY_MAP : new ArrayMap<>(map); + } + + @AnyThread + @Nullable + InputMethodInfo get(@Nullable String imeId) { + return mMap.get(imeId); + } + + @AnyThread + @NonNull + List<InputMethodInfo> values() { + return List.copyOf(mMap.values()); + } + + @AnyThread + @Nullable + InputMethodInfo valueAt(int index) { + return mMap.valueAt(index); + } + + @AnyThread + boolean containsKey(@Nullable String imeId) { + return mMap.containsKey(imeId); + } + + @AnyThread + @IntRange(from = 0) + int size() { + return mMap.size(); + } +} diff --git a/services/core/java/com/android/server/inputmethod/InputMethodSettings.java b/services/core/java/com/android/server/inputmethod/InputMethodSettings.java index 9ddb428018c0..a51002be344f 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodSettings.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodSettings.java @@ -16,6 +16,7 @@ package com.android.server.inputmethod; +import android.annotation.AnyThread; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; @@ -58,9 +59,11 @@ final class InputMethodSettings { private static final char INPUT_METHOD_SUBTYPE_SEPARATOR = InputMethodUtils.INPUT_METHOD_SUBTYPE_SEPARATOR; - private final ArrayMap<String, InputMethodInfo> mMethodMap; + private final InputMethodMap mMethodMap; + private final List<InputMethodInfo> mMethodList; + @UserIdInt - private final int mCurrentUserId; + private final int mUserId; private static void buildEnabledInputMethodsSettingString( StringBuilder builder, Pair<String, ArrayList<String>> ime) { @@ -73,9 +76,18 @@ final class InputMethodSettings { } } - InputMethodSettings(ArrayMap<String, InputMethodInfo> methodMap, @UserIdInt int userId) { + static InputMethodSettings createEmptyMap(@UserIdInt int userId) { + return new InputMethodSettings(InputMethodMap.emptyMap(), userId); + } + + static InputMethodSettings create(InputMethodMap methodMap, @UserIdInt int userId) { + return new InputMethodSettings(methodMap, userId); + } + + private InputMethodSettings(InputMethodMap methodMap, @UserIdInt int userId) { mMethodMap = methodMap; - mCurrentUserId = userId; + mMethodList = methodMap.values(); + mUserId = userId; String ime = getSelectedInputMethod(); String defaultDeviceIme = getSelectedDefaultDeviceInputMethod(); if (defaultDeviceIme != null && !Objects.equals(ime, defaultDeviceIme)) { @@ -84,49 +96,61 @@ final class InputMethodSettings { } } + @AnyThread + @NonNull + InputMethodMap getMethodMap() { + return mMethodMap; + } + + @AnyThread + @NonNull + List<InputMethodInfo> getMethodList() { + return mMethodList; + } + private void putString(@NonNull String key, @Nullable String str) { - SecureSettingsWrapper.putString(key, str, mCurrentUserId); + SecureSettingsWrapper.putString(key, str, mUserId); } @Nullable private String getString(@NonNull String key, @Nullable String defaultValue) { - return SecureSettingsWrapper.getString(key, defaultValue, mCurrentUserId); + return SecureSettingsWrapper.getString(key, defaultValue, mUserId); } private void putInt(String key, int value) { - SecureSettingsWrapper.putInt(key, value, mCurrentUserId); + SecureSettingsWrapper.putInt(key, value, mUserId); } private int getInt(String key, int defaultValue) { - return SecureSettingsWrapper.getInt(key, defaultValue, mCurrentUserId); + return SecureSettingsWrapper.getInt(key, defaultValue, mUserId); } - ArrayList<InputMethodInfo> getEnabledInputMethodListLocked() { - return getEnabledInputMethodListWithFilterLocked(null /* matchingCondition */); + ArrayList<InputMethodInfo> getEnabledInputMethodList() { + return getEnabledInputMethodListWithFilter(null /* matchingCondition */); } @NonNull - ArrayList<InputMethodInfo> getEnabledInputMethodListWithFilterLocked( + ArrayList<InputMethodInfo> getEnabledInputMethodListWithFilter( @Nullable Predicate<InputMethodInfo> matchingCondition) { - return createEnabledInputMethodListLocked( - getEnabledInputMethodsAndSubtypeListLocked(), matchingCondition); + return createEnabledInputMethodList( + getEnabledInputMethodsAndSubtypeList(), matchingCondition); } - List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked( + List<InputMethodSubtype> getEnabledInputMethodSubtypeList( InputMethodInfo imi, boolean allowsImplicitlyEnabledSubtypes) { List<InputMethodSubtype> enabledSubtypes = - getEnabledInputMethodSubtypeListLocked(imi); + getEnabledInputMethodSubtypeList(imi); if (allowsImplicitlyEnabledSubtypes && enabledSubtypes.isEmpty()) { - enabledSubtypes = SubtypeUtils.getImplicitlyApplicableSubtypesLocked( - SystemLocaleWrapper.get(mCurrentUserId), imi); + enabledSubtypes = SubtypeUtils.getImplicitlyApplicableSubtypes( + SystemLocaleWrapper.get(mUserId), imi); } return InputMethodSubtype.sort(imi, enabledSubtypes); } - List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked(InputMethodInfo imi) { - final List<Pair<String, ArrayList<String>>> imsList = - getEnabledInputMethodsAndSubtypeListLocked(); - final List<InputMethodSubtype> enabledSubtypes = new ArrayList<>(); + List<InputMethodSubtype> getEnabledInputMethodSubtypeList(InputMethodInfo imi) { + List<Pair<String, ArrayList<String>>> imsList = + getEnabledInputMethodsAndSubtypeList(); + ArrayList<InputMethodSubtype> enabledSubtypes = new ArrayList<>(); if (imi != null) { for (int i = 0; i < imsList.size(); ++i) { final Pair<String, ArrayList<String>> imsPair = imsList.get(i); @@ -149,7 +173,7 @@ final class InputMethodSettings { return enabledSubtypes; } - List<Pair<String, ArrayList<String>>> getEnabledInputMethodsAndSubtypeListLocked() { + List<Pair<String, ArrayList<String>>> getEnabledInputMethodsAndSubtypeList() { final String enabledInputMethodsStr = getEnabledInputMethodsStr(); final TextUtils.SimpleStringSplitter inputMethodSplitter = new TextUtils.SimpleStringSplitter(INPUT_METHOD_SEPARATOR); @@ -181,7 +205,7 @@ final class InputMethodSettings { * * @return the specified id was removed or not. */ - boolean buildAndPutEnabledInputMethodsStrRemovingIdLocked( + boolean buildAndPutEnabledInputMethodsStrRemovingId( StringBuilder builder, List<Pair<String, ArrayList<String>>> imsList, String id) { boolean isRemoved = false; boolean needsAppendSeparator = false; @@ -209,7 +233,7 @@ final class InputMethodSettings { return isRemoved; } - private ArrayList<InputMethodInfo> createEnabledInputMethodListLocked( + private ArrayList<InputMethodInfo> createEnabledInputMethodList( List<Pair<String, ArrayList<String>>> imsList, Predicate<InputMethodInfo> matchingCondition) { final ArrayList<InputMethodInfo> res = new ArrayList<>(); @@ -271,7 +295,7 @@ final class InputMethodSettings { } private void addSubtypeToHistory(String imeId, String subtypeId) { - final List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked(); + final List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistory(); for (int i = 0; i < subtypeHistory.size(); ++i) { final Pair<String, String> ime = subtypeHistory.get(i); if (ime.first.equals(imeId)) { @@ -303,14 +327,14 @@ final class InputMethodSettings { } } - Pair<String, String> getLastInputMethodAndSubtypeLocked() { + Pair<String, String> getLastInputMethodAndSubtype() { // Gets the first one from the history - return getLastSubtypeForInputMethodLockedInternal(null); + return getLastSubtypeForInputMethodInternal(null); } @Nullable - InputMethodSubtype getLastInputMethodSubtypeLocked() { - final Pair<String, String> lastIme = getLastInputMethodAndSubtypeLocked(); + InputMethodSubtype getLastInputMethodSubtype() { + final Pair<String, String> lastIme = getLastInputMethodAndSubtype(); // TODO: Handle the case of the last IME with no subtypes if (lastIme == null || TextUtils.isEmpty(lastIme.first) || TextUtils.isEmpty(lastIme.second)) { @@ -331,8 +355,8 @@ final class InputMethodSettings { } } - String getLastSubtypeForInputMethodLocked(String imeId) { - Pair<String, String> ime = getLastSubtypeForInputMethodLockedInternal(imeId); + String getLastSubtypeForInputMethod(String imeId) { + Pair<String, String> ime = getLastSubtypeForInputMethodInternal(imeId); if (ime != null) { return ime.second; } else { @@ -340,10 +364,10 @@ final class InputMethodSettings { } } - private Pair<String, String> getLastSubtypeForInputMethodLockedInternal(String imeId) { + private Pair<String, String> getLastSubtypeForInputMethodInternal(String imeId) { final List<Pair<String, ArrayList<String>>> enabledImes = - getEnabledInputMethodsAndSubtypeListLocked(); - final List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked(); + getEnabledInputMethodsAndSubtypeList(); + final List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistory(); for (int i = 0; i < subtypeHistory.size(); ++i) { final Pair<String, String> imeAndSubtype = subtypeHistory.get(i); final String imeInTheHistory = imeAndSubtype.first; @@ -351,7 +375,7 @@ final class InputMethodSettings { if (TextUtils.isEmpty(imeId) || imeInTheHistory.equals(imeId)) { final String subtypeInTheHistory = imeAndSubtype.second; final String subtypeHashCode = - getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked( + getEnabledSubtypeHashCodeForInputMethodAndSubtype( enabledImes, imeInTheHistory, subtypeInTheHistory); if (!TextUtils.isEmpty(subtypeHashCode)) { if (DEBUG) { @@ -368,9 +392,9 @@ final class InputMethodSettings { return null; } - private String getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked(List<Pair<String, + private String getEnabledSubtypeHashCodeForInputMethodAndSubtype(List<Pair<String, ArrayList<String>>> enabledImes, String imeId, String subtypeHashCode) { - final LocaleList localeList = SystemLocaleWrapper.get(mCurrentUserId); + final LocaleList localeList = SystemLocaleWrapper.get(mUserId); for (int i = 0; i < enabledImes.size(); ++i) { final Pair<String, ArrayList<String>> enabledIme = enabledImes.get(i); if (enabledIme.first.equals(imeId)) { @@ -383,7 +407,7 @@ final class InputMethodSettings { // are enabled implicitly, so needs to treat them to be enabled. if (imi != null && imi.getSubtypeCount() > 0) { List<InputMethodSubtype> implicitlyEnabledSubtypes = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked(localeList, + SubtypeUtils.getImplicitlyApplicableSubtypes(localeList, imi); final int numSubtypes = implicitlyEnabledSubtypes.size(); for (int j = 0; j < numSubtypes; ++j) { @@ -420,7 +444,7 @@ final class InputMethodSettings { return null; } - private List<Pair<String, String>> loadInputMethodAndSubtypeHistoryLocked() { + private List<Pair<String, String>> loadInputMethodAndSubtypeHistory() { ArrayList<Pair<String, String>> imsList = new ArrayList<>(); final String subtypeHistoryStr = getSubtypeHistoryStr(); if (TextUtils.isEmpty(subtypeHistoryStr)) { @@ -459,16 +483,14 @@ final class InputMethodSettings { void putSelectedInputMethod(String imeId) { if (DEBUG) { - Slog.d(TAG, "putSelectedInputMethodStr: " + imeId + ", " - + mCurrentUserId); + Slog.d(TAG, "putSelectedInputMethodStr: " + imeId + ", " + mUserId); } putString(Settings.Secure.DEFAULT_INPUT_METHOD, imeId); } void putSelectedSubtype(int subtypeId) { if (DEBUG) { - Slog.d(TAG, "putSelectedInputMethodSubtypeStr: " + subtypeId + ", " - + mCurrentUserId); + Slog.d(TAG, "putSelectedInputMethodSubtypeStr: " + subtypeId + ", " + mUserId); } putInt(Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, subtypeId); } @@ -486,24 +508,21 @@ final class InputMethodSettings { String getSelectedDefaultDeviceInputMethod() { final String imi = getString(Settings.Secure.DEFAULT_DEVICE_INPUT_METHOD, null); if (DEBUG) { - Slog.d(TAG, "getSelectedDefaultDeviceInputMethodStr: " + imi + ", " - + mCurrentUserId); + Slog.d(TAG, "getSelectedDefaultDeviceInputMethodStr: " + imi + ", " + mUserId); } return imi; } void putSelectedDefaultDeviceInputMethod(String imeId) { if (DEBUG) { - Slog.d(TAG, "putSelectedDefaultDeviceInputMethodStr: " + imeId + ", " - + mCurrentUserId); + Slog.d(TAG, "putSelectedDefaultDeviceInputMethodStr: " + imeId + ", " + mUserId); } putString(Settings.Secure.DEFAULT_DEVICE_INPUT_METHOD, imeId); } void putDefaultVoiceInputMethod(String imeId) { if (DEBUG) { - Slog.d(TAG, - "putDefaultVoiceInputMethodStr: " + imeId + ", " + mCurrentUserId); + Slog.d(TAG, "putDefaultVoiceInputMethodStr: " + imeId + ", " + mUserId); } putString(Settings.Secure.DEFAULT_VOICE_INPUT_METHOD, imeId); } @@ -527,8 +546,8 @@ final class InputMethodSettings { } @UserIdInt - public int getCurrentUserId() { - return mCurrentUserId; + public int getUserId() { + return mUserId; } int getSelectedInputMethodSubtypeId(String selectedImiId) { @@ -583,7 +602,7 @@ final class InputMethodSettings { // If there are no selected subtypes, the framework will try to find the most applicable // subtype from explicitly or implicitly enabled subtypes. final List<InputMethodSubtype> explicitlyOrImplicitlyEnabledSubtypes = - getEnabledInputMethodSubtypeListLocked(imi, true); + getEnabledInputMethodSubtypeList(imi, true); // If there is only one explicitly or implicitly enabled subtype, just returns it. if (explicitlyOrImplicitlyEnabledSubtypes.isEmpty()) { return null; @@ -591,14 +610,14 @@ final class InputMethodSettings { if (explicitlyOrImplicitlyEnabledSubtypes.size() == 1) { return explicitlyOrImplicitlyEnabledSubtypes.get(0); } - final String locale = SystemLocaleWrapper.get(mCurrentUserId).get(0).toString(); - final InputMethodSubtype subtype = SubtypeUtils.findLastResortApplicableSubtypeLocked( + final String locale = SystemLocaleWrapper.get(mUserId).get(0).toString(); + final InputMethodSubtype subtype = SubtypeUtils.findLastResortApplicableSubtype( explicitlyOrImplicitlyEnabledSubtypes, SubtypeUtils.SUBTYPE_MODE_KEYBOARD, locale, true); if (subtype != null) { return subtype; } - return SubtypeUtils.findLastResortApplicableSubtypeLocked( + return SubtypeUtils.findLastResortApplicableSubtype( explicitlyOrImplicitlyEnabledSubtypes, null, locale, true); } @@ -620,7 +639,7 @@ final class InputMethodSettings { } else { additionalSubtypeMap.put(imi.getId(), subtypes); } - AdditionalSubtypeUtils.save(additionalSubtypeMap, mMethodMap, getCurrentUserId()); + AdditionalSubtypeUtils.save(additionalSubtypeMap, mMethodMap, getUserId()); return true; } @@ -690,7 +709,7 @@ final class InputMethodSettings { return sb.toString(); } - void dumpLocked(final Printer pw, final String prefix) { - pw.println(prefix + "mCurrentUserId=" + mCurrentUserId); + void dump(final Printer pw, final String prefix) { + pw.println(prefix + "mUserId=" + mUserId); } } diff --git a/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java b/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java index 834ba20b84fd..1379d166e805 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java @@ -23,7 +23,6 @@ import android.annotation.UserIdInt; import android.content.Context; import android.os.UserHandle; import android.text.TextUtils; -import android.util.ArrayMap; import android.util.ArraySet; import android.util.Printer; import android.util.Slog; @@ -158,15 +157,15 @@ final class InputMethodSubtypeSwitchingController { static List<ImeSubtypeListItem> getSortedInputMethodAndSubtypeList( boolean includeAuxiliarySubtypes, boolean isScreenLocked, boolean forImeMenu, - @NonNull Context context, @NonNull ArrayMap<String, InputMethodInfo> methodMap, + @NonNull Context context, @NonNull InputMethodMap methodMap, @UserIdInt int userId) { final Context userAwareContext = context.getUserId() == userId ? context : context.createContextAsUser(UserHandle.of(userId), 0 /* flags */); final String mSystemLocaleStr = SystemLocaleWrapper.get(userId).get(0).toLanguageTag(); - final InputMethodSettings settings = new InputMethodSettings(methodMap, userId); + final InputMethodSettings settings = InputMethodSettings.create(methodMap, userId); - final ArrayList<InputMethodInfo> imis = settings.getEnabledInputMethodListLocked(); + final ArrayList<InputMethodInfo> imis = settings.getEnabledInputMethodList(); if (imis.isEmpty()) { return new ArrayList<>(); } @@ -184,7 +183,7 @@ final class InputMethodSubtypeSwitchingController { continue; } final List<InputMethodSubtype> explicitlyOrImplicitlyEnabledSubtypeList = - settings.getEnabledInputMethodSubtypeListLocked(imi, true); + settings.getEnabledInputMethodSubtypeList(imi, true); final ArraySet<String> enabledSubtypeSet = new ArraySet<>(); for (InputMethodSubtype subtype : explicitlyOrImplicitlyEnabledSubtypeList) { enabledSubtypeSet.add(String.valueOf(subtype.hashCode())); @@ -479,7 +478,7 @@ final class InputMethodSubtypeSwitchingController { private ControllerImpl mController; private InputMethodSubtypeSwitchingController(@NonNull Context context, - @NonNull ArrayMap<String, InputMethodInfo> methodMap, @UserIdInt int userId) { + @NonNull InputMethodMap methodMap, @UserIdInt int userId) { mContext = context; mUserId = userId; mController = ControllerImpl.createFrom(null, @@ -491,7 +490,7 @@ final class InputMethodSubtypeSwitchingController { @NonNull public static InputMethodSubtypeSwitchingController createInstanceLocked( @NonNull Context context, - @NonNull ArrayMap<String, InputMethodInfo> methodMap, @UserIdInt int userId) { + @NonNull InputMethodMap methodMap, @UserIdInt int userId) { return new InputMethodSubtypeSwitchingController(context, methodMap, userId); } @@ -511,8 +510,7 @@ final class InputMethodSubtypeSwitchingController { mController.onUserActionLocked(imi, subtype); } - public void resetCircularListLocked( - @NonNull ArrayMap<String, InputMethodInfo> methodMap) { + public void resetCircularListLocked(@NonNull InputMethodMap methodMap) { mController = ControllerImpl.createFrom(mController, getSortedInputMethodAndSubtypeList( false /* includeAuxiliarySubtypes */, false /* isScreenLocked */, diff --git a/services/core/java/com/android/server/inputmethod/SubtypeUtils.java b/services/core/java/com/android/server/inputmethod/SubtypeUtils.java index 95df99855dcf..3d5c867768ac 100644 --- a/services/core/java/com/android/server/inputmethod/SubtypeUtils.java +++ b/services/core/java/com/android/server/inputmethod/SubtypeUtils.java @@ -26,7 +26,6 @@ import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodSubtype; import com.android.internal.annotations.GuardedBy; -import com.android.internal.annotations.VisibleForTesting; import java.util.ArrayList; import java.util.List; @@ -52,7 +51,7 @@ final class SubtypeUtils { "EnabledWhenDefaultIsNotAsciiCapable"; // A temporary workaround for the performance concerns in - // #getImplicitlyApplicableSubtypesLocked(Resources, InputMethodInfo). + // #getImplicitlyApplicableSubtypes(Resources, InputMethodInfo). // TODO: Optimize all the critical paths including this one. // TODO(b/235661780): Make the cache supports multi-users. private static final Object sCacheLock = new Object(); @@ -121,9 +120,8 @@ final class SubtypeUtils { private static final LocaleUtils.LocaleExtractor<InputMethodSubtype> sSubtypeToLocale = source -> source != null ? source.getLocaleObject() : null; - @VisibleForTesting @NonNull - static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesLocked( + static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypes( @NonNull LocaleList systemLocales, InputMethodInfo imi) { synchronized (sCacheLock) { // We intentionally do not use InputMethodInfo#equals(InputMethodInfo) here because @@ -133,11 +131,11 @@ final class SubtypeUtils { } } - // Note: Only resource info in "res" is used in getImplicitlyApplicableSubtypesLockedImpl(). - // TODO: Refactor getImplicitlyApplicableSubtypesLockedImpl() so that it can receive + // Note: Only resource info in "res" is used in getImplicitlyApplicableSubtypesImpl(). + // TODO: Refactor getImplicitlyApplicableSubtypesImpl() so that it can receive // LocaleList rather than Resource. final ArrayList<InputMethodSubtype> result = - getImplicitlyApplicableSubtypesLockedImpl(systemLocales, imi); + getImplicitlyApplicableSubtypesImpl(systemLocales, imi); synchronized (sCacheLock) { // Both LocaleList and InputMethodInfo are immutable. No need to copy them here. sCachedSystemLocales = systemLocales; @@ -147,7 +145,7 @@ final class SubtypeUtils { return result; } - private static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesLockedImpl( + private static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesImpl( @NonNull LocaleList systemLocales, InputMethodInfo imi) { final List<InputMethodSubtype> subtypes = getSubtypes(imi); final String systemLocale = systemLocales.get(0).toString(); @@ -215,7 +213,7 @@ final class SubtypeUtils { } if (applicableSubtypes.isEmpty()) { - InputMethodSubtype lastResortKeyboardSubtype = findLastResortApplicableSubtypeLocked( + InputMethodSubtype lastResortKeyboardSubtype = findLastResortApplicableSubtype( subtypes, SUBTYPE_MODE_KEYBOARD, systemLocale, true); if (lastResortKeyboardSubtype != null) { applicableSubtypes.add(lastResortKeyboardSubtype); @@ -244,7 +242,7 @@ final class SubtypeUtils { * * @return the most applicable subtypeId */ - static InputMethodSubtype findLastResortApplicableSubtypeLocked( + static InputMethodSubtype findLastResortApplicableSubtype( List<InputMethodSubtype> subtypes, String mode, @NonNull String locale, boolean canIgnoreLocaleAsLastResort) { if (subtypes == null || subtypes.isEmpty()) { diff --git a/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java b/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java index 403b421639cb..71a9f54c0b1c 100644 --- a/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java +++ b/services/core/java/com/android/server/location/gnss/GnssNetworkConnectivityHandler.java @@ -248,6 +248,7 @@ class GnssNetworkConnectivityHandler { SubscriptionManager subManager = mContext.getSystemService(SubscriptionManager.class); TelephonyManager telManager = mContext.getSystemService(TelephonyManager.class); if (subManager != null && telManager != null) { + subManager = subManager.createForAllUserProfiles(); List<SubscriptionInfo> subscriptionInfoList = subManager.getActiveSubscriptionInfoList(); HashSet<Integer> activeSubIds = new HashSet<Integer>(); diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java index 542b3b06184a..a06607b68fa9 100644 --- a/services/core/java/com/android/server/locksettings/LockSettingsService.java +++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java @@ -16,6 +16,7 @@ package com.android.server.locksettings; +import static android.security.Flags.reportPrimaryAuthAttempts; import static android.Manifest.permission.ACCESS_KEYGUARD_SECURE_STORAGE; import static android.Manifest.permission.MANAGE_BIOMETRIC; import static android.Manifest.permission.SET_AND_VERIFY_LOCKSCREEN_CREDENTIALS; @@ -92,6 +93,7 @@ import android.os.Handler; import android.os.IBinder; import android.os.IProgressListener; import android.os.Process; +import android.os.RemoteCallbackList; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.ServiceManager; @@ -137,6 +139,7 @@ import com.android.internal.util.IndentingPrintWriter; import com.android.internal.util.Preconditions; import com.android.internal.widget.ICheckCredentialProgressCallback; import com.android.internal.widget.ILockSettings; +import com.android.internal.widget.ILockSettingsStateListener; import com.android.internal.widget.IWeakEscrowTokenActivatedListener; import com.android.internal.widget.IWeakEscrowTokenRemovedListener; import com.android.internal.widget.LockPatternUtils; @@ -329,6 +332,9 @@ public class LockSettingsService extends ILockSettings.Stub { private HashMap<UserHandle, UserManager> mUserManagerCache = new HashMap<>(); + private final RemoteCallbackList<ILockSettingsStateListener> mLockSettingsStateListeners = + new RemoteCallbackList<>(); + // This class manages life cycle events for encrypted users on File Based Encryption (FBE) // devices. The most basic of these is to show/hide notifications about missing features until // the user unlocks the account and credential-encrypted storage is available. @@ -2364,9 +2370,37 @@ public class LockSettingsService extends ILockSettings.Stub { requireStrongAuth(STRONG_AUTH_REQUIRED_AFTER_LOCKOUT, userId); } } + if (reportPrimaryAuthAttempts()) { + final boolean success = + response.getResponseCode() == VerifyCredentialResponse.RESPONSE_OK; + notifyLockSettingsStateListeners(success, userId); + } return response; } + private void notifyLockSettingsStateListeners(boolean success, int userId) { + int i = mLockSettingsStateListeners.beginBroadcast(); + try { + while (i > 0) { + i--; + try { + if (success) { + mLockSettingsStateListeners.getBroadcastItem(i) + .onAuthenticationSucceeded(userId); + } else { + mLockSettingsStateListeners.getBroadcastItem(i) + .onAuthenticationFailed(userId); + } + } catch (RemoteException e) { + Slog.e(TAG, "Exception while notifying LockSettingsStateListener:" + + " success = " + success + ", userId = " + userId, e); + } + } + } finally { + mLockSettingsStateListeners.finishBroadcast(); + } + } + @Override public VerifyCredentialResponse verifyTiedProfileChallenge(LockscreenCredential credential, int userId, @LockPatternUtils.VerifyFlag int flags) { @@ -3684,6 +3718,18 @@ public class LockSettingsService extends ILockSettings.Stub { public void refreshStrongAuthTimeout(int userId) { mStrongAuth.refreshStrongAuthTimeout(userId); } + + @Override + public void registerLockSettingsStateListener( + @NonNull ILockSettingsStateListener listener) { + mLockSettingsStateListeners.register(listener); + } + + @Override + public void unregisterLockSettingsStateListener( + @NonNull ILockSettingsStateListener listener) { + mLockSettingsStateListeners.unregister(listener); + } } private class RebootEscrowCallbacks implements RebootEscrowManager.Callbacks { diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java index 550aed51c8e2..978f46808e3b 100644 --- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java +++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java @@ -214,6 +214,11 @@ public final class MediaProjectionManagerService extends SystemService } @Override + public void onProcessStarted(int pid, int processUid, int packageUid, + String packageName, String processName) { + } + + @Override public void onForegroundServicesChanged(int pid, int uid, int serviceTypes) { MediaProjectionManagerService.this.handleForegroundServicesChanged(pid, uid, serviceTypes); diff --git a/services/core/java/com/android/server/net/Android.bp b/services/core/java/com/android/server/net/Android.bp new file mode 100644 index 000000000000..71d8e6ba367e --- /dev/null +++ b/services/core/java/com/android/server/net/Android.bp @@ -0,0 +1,10 @@ +aconfig_declarations { + name: "net_flags", + package: "com.android.server.net", + srcs: ["*.aconfig"], +} + +java_aconfig_library { + name: "net_flags_lib", + aconfig_declarations: "net_flags", +} diff --git a/services/core/java/com/android/server/net/NetworkManagementService.java b/services/core/java/com/android/server/net/NetworkManagementService.java index 681d1a0ee10a..d25f52973085 100644 --- a/services/core/java/com/android/server/net/NetworkManagementService.java +++ b/services/core/java/com/android/server/net/NetworkManagementService.java @@ -17,6 +17,7 @@ package com.android.server.net; import static android.Manifest.permission.CONNECTIVITY_INTERNAL; +import static android.net.ConnectivityManager.FIREWALL_CHAIN_BACKGROUND; import static android.net.ConnectivityManager.FIREWALL_CHAIN_DOZABLE; import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY; import static android.net.ConnectivityManager.FIREWALL_CHAIN_POWERSAVE; @@ -27,6 +28,7 @@ import static android.net.INetd.FIREWALL_CHAIN_NONE; import static android.net.INetd.FIREWALL_DENYLIST; import static android.net.INetd.FIREWALL_RULE_ALLOW; import static android.net.INetd.FIREWALL_RULE_DENY; +import static android.net.NetworkPolicyManager.FIREWALL_CHAIN_NAME_BACKGROUND; import static android.net.NetworkPolicyManager.FIREWALL_CHAIN_NAME_DOZABLE; import static android.net.NetworkPolicyManager.FIREWALL_CHAIN_NAME_LOW_POWER_STANDBY; import static android.net.NetworkPolicyManager.FIREWALL_CHAIN_NAME_POWERSAVE; @@ -187,6 +189,13 @@ public class NetworkManagementService extends INetworkManagementService.Stub { */ @GuardedBy("mRulesLock") private final SparseIntArray mUidFirewallLowPowerStandbyRules = new SparseIntArray(); + + /** + * Contains the per-UID firewall rules that are used when Background chain is enabled. + */ + @GuardedBy("mRulesLock") + private final SparseIntArray mUidFirewallBackgroundRules = new SparseIntArray(); + /** Set of states for the child firewall chains. True if the chain is active. */ @GuardedBy("mRulesLock") final SparseBooleanArray mFirewallChainStates = new SparseBooleanArray(); @@ -449,13 +458,15 @@ public class NetworkManagementService extends INetworkManagementService.Stub { syncFirewallChainLocked(FIREWALL_CHAIN_POWERSAVE, "powersave "); syncFirewallChainLocked(FIREWALL_CHAIN_RESTRICTED, "restricted "); syncFirewallChainLocked(FIREWALL_CHAIN_LOW_POWER_STANDBY, "low power standby "); + syncFirewallChainLocked(FIREWALL_CHAIN_BACKGROUND, FIREWALL_CHAIN_NAME_BACKGROUND); final int[] chains = { FIREWALL_CHAIN_STANDBY, FIREWALL_CHAIN_DOZABLE, FIREWALL_CHAIN_POWERSAVE, FIREWALL_CHAIN_RESTRICTED, - FIREWALL_CHAIN_LOW_POWER_STANDBY + FIREWALL_CHAIN_LOW_POWER_STANDBY, + FIREWALL_CHAIN_BACKGROUND, }; for (int chain : chains) { @@ -1206,6 +1217,8 @@ public class NetworkManagementService extends INetworkManagementService.Stub { return FIREWALL_CHAIN_NAME_RESTRICTED; case FIREWALL_CHAIN_LOW_POWER_STANDBY: return FIREWALL_CHAIN_NAME_LOW_POWER_STANDBY; + case FIREWALL_CHAIN_BACKGROUND: + return FIREWALL_CHAIN_NAME_BACKGROUND; default: throw new IllegalArgumentException("Bad child chain: " + chain); } @@ -1223,6 +1236,8 @@ public class NetworkManagementService extends INetworkManagementService.Stub { return FIREWALL_ALLOWLIST; case FIREWALL_CHAIN_LOW_POWER_STANDBY: return FIREWALL_ALLOWLIST; + case FIREWALL_CHAIN_BACKGROUND: + return FIREWALL_ALLOWLIST; default: return isFirewallEnabled() ? FIREWALL_ALLOWLIST : FIREWALL_DENYLIST; } @@ -1343,6 +1358,8 @@ public class NetworkManagementService extends INetworkManagementService.Stub { return mUidFirewallRestrictedRules; case FIREWALL_CHAIN_LOW_POWER_STANDBY: return mUidFirewallLowPowerStandbyRules; + case FIREWALL_CHAIN_BACKGROUND: + return mUidFirewallBackgroundRules; case FIREWALL_CHAIN_NONE: return mUidFirewallRules; default: @@ -1395,6 +1412,10 @@ public class NetworkManagementService extends INetworkManagementService.Stub { pw.println(getFirewallChainState(FIREWALL_CHAIN_LOW_POWER_STANDBY)); dumpUidFirewallRule(pw, FIREWALL_CHAIN_NAME_LOW_POWER_STANDBY, mUidFirewallLowPowerStandbyRules); + + pw.print("UID firewall background chain enabled: "); + pw.println(getFirewallChainState(FIREWALL_CHAIN_BACKGROUND)); + dumpUidFirewallRule(pw, FIREWALL_CHAIN_NAME_BACKGROUND, mUidFirewallBackgroundRules); } pw.print("Firewall enabled: "); pw.println(mFirewallEnabled); @@ -1494,6 +1515,11 @@ public class NetworkManagementService extends INetworkManagementService.Stub { if (DBG) Slog.d(TAG, "Uid " + uid + " restricted because of low power standby"); return true; } + if (getFirewallChainState(FIREWALL_CHAIN_BACKGROUND) + && mUidFirewallBackgroundRules.get(uid) != FIREWALL_RULE_ALLOW) { + if (DBG) Slog.d(TAG, "Uid " + uid + " restricted because it is in background"); + return true; + } if (mUidRejectOnMetered.get(uid)) { if (DBG) Slog.d(TAG, "Uid " + uid + " restricted because of no metered data" + " in the background"); diff --git a/services/core/java/com/android/server/net/NetworkPolicyLogger.java b/services/core/java/com/android/server/net/NetworkPolicyLogger.java index d7188c7f10c6..8e2d7780204a 100644 --- a/services/core/java/com/android/server/net/NetworkPolicyLogger.java +++ b/services/core/java/com/android/server/net/NetworkPolicyLogger.java @@ -16,6 +16,7 @@ package com.android.server.net; import static android.net.ConnectivityManager.BLOCKED_REASON_NONE; +import static android.net.ConnectivityManager.FIREWALL_CHAIN_BACKGROUND; import static android.net.ConnectivityManager.FIREWALL_CHAIN_DOZABLE; import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY; import static android.net.ConnectivityManager.FIREWALL_CHAIN_POWERSAVE; @@ -24,6 +25,7 @@ import static android.net.ConnectivityManager.FIREWALL_CHAIN_STANDBY; import static android.net.INetd.FIREWALL_RULE_ALLOW; import static android.net.INetd.FIREWALL_RULE_DENY; import static android.net.NetworkPolicyManager.ALLOWED_REASON_NONE; +import static android.net.NetworkPolicyManager.FIREWALL_CHAIN_NAME_BACKGROUND; import static android.net.NetworkPolicyManager.FIREWALL_CHAIN_NAME_DOZABLE; import static android.net.NetworkPolicyManager.FIREWALL_CHAIN_NAME_LOW_POWER_STANDBY; import static android.net.NetworkPolicyManager.FIREWALL_CHAIN_NAME_POWERSAVE; @@ -389,6 +391,8 @@ public class NetworkPolicyLogger { return FIREWALL_CHAIN_NAME_RESTRICTED; case FIREWALL_CHAIN_LOW_POWER_STANDBY: return FIREWALL_CHAIN_NAME_LOW_POWER_STANDBY; + case FIREWALL_CHAIN_BACKGROUND: + return FIREWALL_CHAIN_NAME_BACKGROUND; default: return String.valueOf(chain); } diff --git a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java index e4e48bdd35d1..f9ffb1cebac4 100644 --- a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java +++ b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java @@ -47,6 +47,7 @@ import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_ADMIN_DISAB import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_DATA_SAVER; import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_MASK; import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_USER_RESTRICTED; +import static android.net.ConnectivityManager.BLOCKED_REASON_APP_BACKGROUND; import static android.net.ConnectivityManager.BLOCKED_REASON_APP_STANDBY; import static android.net.ConnectivityManager.BLOCKED_REASON_BATTERY_SAVER; import static android.net.ConnectivityManager.BLOCKED_REASON_DOZE; @@ -54,6 +55,7 @@ import static android.net.ConnectivityManager.BLOCKED_REASON_LOW_POWER_STANDBY; import static android.net.ConnectivityManager.BLOCKED_REASON_NONE; import static android.net.ConnectivityManager.BLOCKED_REASON_RESTRICTED_MODE; import static android.net.ConnectivityManager.CONNECTIVITY_ACTION; +import static android.net.ConnectivityManager.FIREWALL_CHAIN_BACKGROUND; import static android.net.ConnectivityManager.FIREWALL_CHAIN_DOZABLE; import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY; import static android.net.ConnectivityManager.FIREWALL_CHAIN_POWERSAVE; @@ -77,6 +79,7 @@ import static android.net.NetworkPolicyManager.ALLOWED_METERED_REASON_USER_EXEMP import static android.net.NetworkPolicyManager.ALLOWED_REASON_FOREGROUND; import static android.net.NetworkPolicyManager.ALLOWED_REASON_LOW_POWER_STANDBY_ALLOWLIST; import static android.net.NetworkPolicyManager.ALLOWED_REASON_NONE; +import static android.net.NetworkPolicyManager.ALLOWED_REASON_NOT_IN_BACKGROUND; import static android.net.NetworkPolicyManager.ALLOWED_REASON_POWER_SAVE_ALLOWLIST; import static android.net.NetworkPolicyManager.ALLOWED_REASON_POWER_SAVE_EXCEPT_IDLE_ALLOWLIST; import static android.net.NetworkPolicyManager.ALLOWED_REASON_RESTRICTED_MODE_PERMISSIONS; @@ -96,6 +99,7 @@ import static android.net.NetworkPolicyManager.RULE_TEMPORARY_ALLOW_METERED; import static android.net.NetworkPolicyManager.SUBSCRIPTION_OVERRIDE_UNMETERED; import static android.net.NetworkPolicyManager.allowedReasonsToString; import static android.net.NetworkPolicyManager.blockedReasonsToString; +import static android.net.NetworkPolicyManager.isProcStateAllowedNetworkWhileBackground; import static android.net.NetworkPolicyManager.isProcStateAllowedWhileIdleOrPowerSaveMode; import static android.net.NetworkPolicyManager.isProcStateAllowedWhileInLowPowerStandby; import static android.net.NetworkPolicyManager.isProcStateAllowedWhileOnRestrictBackground; @@ -202,12 +206,12 @@ import android.os.Message; import android.os.MessageQueue.IdleHandler; import android.os.ParcelFileDescriptor; import android.os.PersistableBundle; +import android.os.PowerExemptionManager; import android.os.PowerExemptionManager.ReasonCode; import android.os.PowerManager; import android.os.PowerManager.ServiceType; import android.os.PowerManagerInternal; import android.os.PowerSaveState; -import android.os.PowerWhitelistManager; import android.os.Process; import android.os.RemoteCallbackList; import android.os.RemoteException; @@ -243,6 +247,7 @@ import android.util.SparseBooleanArray; import android.util.SparseIntArray; import android.util.SparseLongArray; import android.util.SparseSetArray; +import android.util.TimeUtils; import android.util.Xml; import com.android.internal.R; @@ -458,6 +463,12 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { */ private static final int MSG_UIDS_BLOCKED_REASONS_CHANGED = 23; + /** + * Message to update background restriction rules for uids that should lose network access + * due to being in the background. + */ + private static final int MSG_PROCESS_BACKGROUND_TRANSITIONING_UIDS = 24; + private static final int UID_MSG_STATE_CHANGED = 100; private static final int UID_MSG_GONE = 101; @@ -476,7 +487,7 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { private ConnectivityManager mConnManager; private PowerManagerInternal mPowerManagerInternal; - private PowerWhitelistManager mPowerWhitelistManager; + private PowerExemptionManager mPowerExemptionManager; @NonNull private final Dependencies mDeps; @@ -491,6 +502,12 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { // Denotes the status of restrict background read from disk. private boolean mLoadedRestrictBackground; + /** + * Whether or not network for apps in proc-states greater than + * {@link NetworkPolicyManager#BACKGROUND_THRESHOLD_STATE} is always blocked. + */ + private boolean mBackgroundNetworkRestricted; + // See main javadoc for instructions on how to use these locks. final Object mUidRulesFirstLock = new Object(); final Object mNetworkPoliciesSecondLock = new Object(); @@ -515,6 +532,15 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { private volatile boolean mNetworkManagerReady; + /** + * Delay after which a uid going into a process state greater than or equal to + * {@link NetworkPolicyManager#BACKGROUND_THRESHOLD_STATE} will lose network access. + * The delay is meant to prevent churn due to quick process-state changes. + * Note that there is no delay while granting network access. + */ + @VisibleForTesting + long mBackgroundRestrictionDelayMs = TimeUnit.SECONDS.toMillis(5); + /** Defined network policies. */ @GuardedBy("mNetworkPoliciesSecondLock") final ArrayMap<NetworkTemplate, NetworkPolicy> mNetworkPolicy = new ArrayMap<>(); @@ -546,6 +572,8 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { @GuardedBy("mUidRulesFirstLock") final SparseIntArray mUidFirewallPowerSaveRules = new SparseIntArray(); @GuardedBy("mUidRulesFirstLock") + final SparseIntArray mUidFirewallBackgroundRules = new SparseIntArray(); + @GuardedBy("mUidRulesFirstLock") final SparseIntArray mUidFirewallRestrictedModeRules = new SparseIntArray(); @GuardedBy("mUidRulesFirstLock") final SparseIntArray mUidFirewallLowPowerStandbyModeRules = new SparseIntArray(); @@ -625,6 +653,14 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { @GuardedBy("mUidRulesFirstLock") private final SparseArray<UidBlockedState> mTmpUidBlockedState = new SparseArray<>(); + /** + * Stores a map of uids to the time their transition to background is considered complete. They + * will lose network access after this time. This is used to prevent churn in rules due to quick + * process-state transitions. + */ + @GuardedBy("mUidRulesFirstLock") + private final SparseLongArray mBackgroundTransitioningUids = new SparseLongArray(); + /** Map from network ID to last observed meteredness state */ @GuardedBy("mNetworkPoliciesSecondLock") private final SparseBooleanArray mNetworkMetered = new SparseBooleanArray(); @@ -824,7 +860,7 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { mContext = Objects.requireNonNull(context, "missing context"); mActivityManager = Objects.requireNonNull(activityManager, "missing activityManager"); mNetworkManager = Objects.requireNonNull(networkManagement, "missing networkManagement"); - mPowerWhitelistManager = mContext.getSystemService(PowerWhitelistManager.class); + mPowerExemptionManager = mContext.getSystemService(PowerExemptionManager.class); mClock = Objects.requireNonNull(clock, "missing Clock"); mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE); mCarrierConfigManager = mContext.getSystemService(CarrierConfigManager.class); @@ -860,15 +896,15 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { @GuardedBy("mUidRulesFirstLock") private void updatePowerSaveAllowlistUL() { - int[] whitelist = mPowerWhitelistManager.getWhitelistedAppIds(/* includingIdle */ false); + int[] allowlist = mPowerExemptionManager.getAllowListedAppIds(/* includingIdle */ false); mPowerSaveWhitelistExceptIdleAppIds.clear(); - for (int uid : whitelist) { + for (int uid : allowlist) { mPowerSaveWhitelistExceptIdleAppIds.put(uid, true); } - whitelist = mPowerWhitelistManager.getWhitelistedAppIds(/* includingIdle */ true); + allowlist = mPowerExemptionManager.getAllowListedAppIds(/* includingIdle */ true); mPowerSaveWhitelistAppIds.clear(); - for (int uid : whitelist) { + for (int uid : allowlist) { mPowerSaveWhitelistAppIds.put(uid, true); } } @@ -1018,6 +1054,14 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { writePolicyAL(); } + // The flag is boot-stable. + mBackgroundNetworkRestricted = Flags.networkBlockedForTopSleepingAndAbove(); + if (mBackgroundNetworkRestricted) { + // Firewall rules and UidBlockedState will get updated in + // updateRulesForGlobalChangeAL below. + enableFirewallChainUL(FIREWALL_CHAIN_BACKGROUND, true); + } + setRestrictBackgroundUL(mLoadedRestrictBackground, "init_service"); updateRulesForGlobalChangeAL(false); updateNotificationsNL(); @@ -1028,17 +1072,22 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { final int changes = ActivityManager.UID_OBSERVER_PROCSTATE | ActivityManager.UID_OBSERVER_GONE | ActivityManager.UID_OBSERVER_CAPABILITY; + + final int cutpoint = mBackgroundNetworkRestricted ? PROCESS_STATE_UNKNOWN + : NetworkPolicyManager.FOREGROUND_THRESHOLD_STATE; + // TODO (b/319728914): Filter out the unnecessary changes when using no cutpoint. + mActivityManagerInternal.registerNetworkPolicyUidObserver(mUidObserver, changes, - NetworkPolicyManager.FOREGROUND_THRESHOLD_STATE, "android"); + cutpoint, "android"); mNetworkManager.registerObserver(mAlertObserver); } catch (RemoteException e) { // ignored; both services live in system_server } // listen for changes to power save allowlist - final IntentFilter whitelistFilter = new IntentFilter( + final IntentFilter allowlistFilter = new IntentFilter( PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED); - mContext.registerReceiver(mPowerSaveWhitelistReceiver, whitelistFilter, null, mHandler); + mContext.registerReceiver(mPowerSaveAllowlistReceiver, allowlistFilter, null, mHandler); // watch for network interfaces to be claimed final IntentFilter connFilter = new IntentFilter(CONNECTIVITY_ACTION); @@ -1189,12 +1238,15 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { } } - final private BroadcastReceiver mPowerSaveWhitelistReceiver = new BroadcastReceiver() { + private final BroadcastReceiver mPowerSaveAllowlistReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { // on background handler thread, and POWER_SAVE_WHITELIST_CHANGED is protected synchronized (mUidRulesFirstLock) { updatePowerSaveAllowlistUL(); + if (mBackgroundNetworkRestricted) { + updateRulesForBackgroundChainUL(); + } updateRulesForRestrictPowerUL(); updateRulesForAppIdleUL(); } @@ -3914,6 +3966,11 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { } fout.println(); + fout.println("Flags:"); + fout.println("Network blocked for TOP_SLEEPING and above: " + + mBackgroundNetworkRestricted); + + fout.println(); fout.println("mRestrictBackgroundLowPowerMode: " + mRestrictBackgroundLowPowerMode); fout.println("mRestrictBackgroundBeforeBsm: " + mRestrictBackgroundBeforeBsm); fout.println("mLoadedRestrictBackground: " + mLoadedRestrictBackground); @@ -4055,6 +4112,22 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { fout.decreaseIndent(); } + size = mBackgroundTransitioningUids.size(); + if (size > 0) { + final long nowUptime = SystemClock.uptimeMillis(); + fout.println("Uids transitioning to background:"); + fout.increaseIndent(); + for (int i = 0; i < size; i++) { + fout.print("UID="); + fout.print(mBackgroundTransitioningUids.keyAt(i)); + fout.print(", "); + TimeUtils.formatDuration(mBackgroundTransitioningUids.valueAt(i), nowUptime, + fout); + fout.println(); + } + fout.decreaseIndent(); + } + final SparseBooleanArray knownUids = new SparseBooleanArray(); collectKeys(mUidState, knownUids); synchronized (mUidBlockedState) { @@ -4182,6 +4255,12 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { return isProcStateAllowedWhileInLowPowerStandby(uidState); } + @GuardedBy("mUidRulesFirstLock") + private boolean isUidExemptFromBackgroundRestrictions(int uid) { + return mBackgroundTransitioningUids.indexOfKey(uid) >= 0 + || isProcStateAllowedNetworkWhileBackground(mUidState.get(uid)); + } + /** * Process state of UID changed; if needed, will trigger * {@link #updateRulesForDataUsageRestrictionsUL(int)} and @@ -4207,6 +4286,8 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { // state changed, push updated rules mUidState.put(uid, newUidState); updateRestrictBackgroundRulesOnUidStatusChangedUL(uid, oldUidState, newUidState); + + boolean updatePowerRestrictionRules = false; boolean allowedWhileIdleOrPowerSaveModeChanged = isProcStateAllowedWhileIdleOrPowerSaveMode(oldUidState) != isProcStateAllowedWhileIdleOrPowerSaveMode(newUidState); @@ -4218,19 +4299,44 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { if (mRestrictPower) { updateRuleForRestrictPowerUL(uid); } - updateRulesForPowerRestrictionsUL(uid, procState); + updatePowerRestrictionRules = true; + } + if (mBackgroundNetworkRestricted) { + final boolean wasAllowed = isProcStateAllowedNetworkWhileBackground( + oldUidState); + final boolean isAllowed = isProcStateAllowedNetworkWhileBackground(newUidState); + if (!wasAllowed && isAllowed) { + mBackgroundTransitioningUids.delete(uid); + updateRuleForBackgroundUL(uid); + updatePowerRestrictionRules = true; + } else if (wasAllowed && !isAllowed) { + final long completionTimeMs = SystemClock.uptimeMillis() + + mBackgroundRestrictionDelayMs; + if (mBackgroundTransitioningUids.indexOfKey(uid) < 0) { + // This is just a defensive check in case the upstream code ever makes + // multiple calls for the same process state change. + mBackgroundTransitioningUids.put(uid, completionTimeMs); + } + if (!mHandler.hasMessages(MSG_PROCESS_BACKGROUND_TRANSITIONING_UIDS)) { + // Many uids may be in this "transitioning" state at the same time, so + // using one message at a time to avoid congestion in the MessageQueue. + mHandler.sendEmptyMessageAtTime( + MSG_PROCESS_BACKGROUND_TRANSITIONING_UIDS, completionTimeMs); + } + } } if (mLowPowerStandbyActive) { boolean allowedInLpsChanged = isProcStateAllowedWhileInLowPowerStandby(oldUidState) != isProcStateAllowedWhileInLowPowerStandby(newUidState); if (allowedInLpsChanged) { - if (!allowedWhileIdleOrPowerSaveModeChanged) { - updateRulesForPowerRestrictionsUL(uid, procState); - } updateRuleForLowPowerStandbyUL(uid); + updatePowerRestrictionRules = true; } } + if (updatePowerRestrictionRules) { + updateRulesForPowerRestrictionsUL(uid, procState); + } return true; } } finally { @@ -4253,6 +4359,12 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { if (mRestrictPower) { updateRuleForRestrictPowerUL(uid); } + if (mBackgroundNetworkRestricted) { + // Uid is no longer running, there is no point in any grace period of network + // access during transitions to lower importance proc-states. + mBackgroundTransitioningUids.delete(uid); + updateRuleForBackgroundUL(uid); + } updateRulesForPowerRestrictionsUL(uid); if (mLowPowerStandbyActive) { updateRuleForLowPowerStandbyUL(uid); @@ -4460,11 +4572,41 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { } } + /** + * Updates the rules for apps allowlisted to use network while in the background. + */ + @GuardedBy("mUidRulesFirstLock") + private void updateRulesForBackgroundChainUL() { + Trace.traceBegin(Trace.TRACE_TAG_NETWORK, "updateRulesForBackgroundChainUL"); + try { + final SparseIntArray uidRules = mUidFirewallBackgroundRules; + uidRules.clear(); + + final List<UserInfo> users = mUserManager.getUsers(); + for (int ui = users.size() - 1; ui >= 0; ui--) { + final UserInfo user = users.get(ui); + updateRulesForAllowlistedAppIds(uidRules, mPowerSaveTempWhitelistAppIds, user.id); + updateRulesForAllowlistedAppIds(uidRules, mPowerSaveWhitelistAppIds, user.id); + updateRulesForAllowlistedAppIds(uidRules, mPowerSaveWhitelistExceptIdleAppIds, + user.id); + } + for (int i = mUidState.size() - 1; i >= 0; i--) { + if (mBackgroundTransitioningUids.indexOfKey(mUidState.keyAt(i)) >= 0 + || isProcStateAllowedNetworkWhileBackground(mUidState.valueAt(i))) { + uidRules.put(mUidState.keyAt(i), FIREWALL_RULE_ALLOW); + } + } + setUidFirewallRulesUL(FIREWALL_CHAIN_BACKGROUND, uidRules); + } finally { + Trace.traceEnd(TRACE_TAG_NETWORK); + } + } + private void updateRulesForAllowlistedAppIds(final SparseIntArray uidRules, - final SparseBooleanArray whitelistedAppIds, int userId) { - for (int i = whitelistedAppIds.size() - 1; i >= 0; --i) { - if (whitelistedAppIds.valueAt(i)) { - final int appId = whitelistedAppIds.keyAt(i); + final SparseBooleanArray allowlistedAppIds, int userId) { + for (int i = allowlistedAppIds.size() - 1; i >= 0; --i) { + if (allowlistedAppIds.valueAt(i)) { + final int appId = allowlistedAppIds.keyAt(i); final int uid = UserHandle.getUid(userId, appId); uidRules.put(uid, FIREWALL_RULE_ALLOW); } @@ -4523,12 +4665,12 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { @GuardedBy("mUidRulesFirstLock") private boolean isAllowlistedFromPowerSaveUL(int uid, boolean deviceIdleMode) { final int appId = UserHandle.getAppId(uid); - boolean isWhitelisted = mPowerSaveTempWhitelistAppIds.get(appId) + boolean allowlisted = mPowerSaveTempWhitelistAppIds.get(appId) || mPowerSaveWhitelistAppIds.get(appId); if (!deviceIdleMode) { - isWhitelisted = isWhitelisted || isAllowlistedFromPowerSaveExceptIdleUL(uid); + allowlisted = allowlisted || isAllowlistedFromPowerSaveExceptIdleUL(uid); } - return isWhitelisted; + return allowlisted; } /** @@ -4617,6 +4759,38 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { } /** + * Update firewall rule for a single uid whenever there are any interesting changes in the uid. + * Currently, it is called when: + * - The uid is added to or removed from power allowlists + * - The uid undergoes a process-state change + * - A package belonging to this uid is added + * - The uid is evicted from memory + */ + @GuardedBy("mUidRulesFirstLock") + void updateRuleForBackgroundUL(int uid) { + if (!isUidValidForAllowlistRulesUL(uid)) { + return; + } + + Trace.traceBegin(Trace.TRACE_TAG_NETWORK, "updateRuleForBackgroundUL: " + uid); + try { + // The uid should be absent from mUidState and mBackgroundTransitioningUids if it is + // not running when this method is called. Then, the firewall state will depend on the + // allowlist alone. This is the desired behavior. + if (isAllowlistedFromPowerSaveUL(uid, false) + || isUidExemptFromBackgroundRestrictions(uid)) { + setUidFirewallRuleUL(FIREWALL_CHAIN_BACKGROUND, uid, FIREWALL_RULE_ALLOW); + if (LOGD) Log.d(TAG, "updateRuleForBackgroundUL ALLOW " + uid); + } else { + setUidFirewallRuleUL(FIREWALL_CHAIN_BACKGROUND, uid, FIREWALL_RULE_DEFAULT); + if (LOGD) Log.d(TAG, "updateRuleForBackgroundUL " + uid + " to DEFAULT"); + } + } finally { + Trace.traceEnd(Trace.TRACE_TAG_NETWORK); + } + } + + /** * Toggle the firewall standby chain and inform listeners if the uid rules have effectively * changed. */ @@ -4663,6 +4837,9 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { "updateRulesForGlobalChangeAL: " + (restrictedNetworksChanged ? "R" : "-")); } try { + if (mBackgroundNetworkRestricted) { + updateRulesForBackgroundChainUL(); + } updateRulesForAppIdleUL(); updateRulesForRestrictPowerUL(); updateRulesForRestrictBackgroundUL(); @@ -4822,6 +4999,9 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { updateRuleForAppIdleUL(uid, PROCESS_STATE_UNKNOWN); updateRuleForDeviceIdleUL(uid); updateRuleForRestrictPowerUL(uid); + if (mBackgroundNetworkRestricted) { + updateRuleForBackgroundUL(uid); + } // Update internal rules. updateRulesForPowerRestrictionsUL(uid); } @@ -4959,6 +5139,8 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { mUidFirewallStandbyRules.delete(uid); mUidFirewallDozableRules.delete(uid); mUidFirewallPowerSaveRules.delete(uid); + mUidFirewallBackgroundRules.delete(uid); + mBackgroundTransitioningUids.delete(uid); mPowerSaveWhitelistExceptIdleAppIds.delete(uid); mPowerSaveWhitelistAppIds.delete(uid); mPowerSaveTempWhitelistAppIds.delete(uid); @@ -4992,6 +5174,9 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { updateRuleForDeviceIdleUL(uid); updateRuleForAppIdleUL(uid, PROCESS_STATE_UNKNOWN); updateRuleForRestrictPowerUL(uid); + if (mBackgroundNetworkRestricted) { + updateRuleForBackgroundUL(uid); + } // If the uid has the necessary permissions, then it should be added to the restricted mode // firewall allowlist. @@ -5176,7 +5361,6 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { * Similar to above but ignores idle state if app standby is currently disabled by parole. * * @param uid the uid of the app to update rules for - * @param oldUidRules the current rules for the uid, in order to determine if there's a change * @param isUidIdle whether uid is idle or not */ @GuardedBy("mUidRulesFirstLock") @@ -5222,6 +5406,7 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { newBlockedReasons |= (mLowPowerStandbyActive ? BLOCKED_REASON_LOW_POWER_STANDBY : 0); newBlockedReasons |= (isUidIdle ? BLOCKED_REASON_APP_STANDBY : 0); newBlockedReasons |= (uidBlockedState.blockedReasons & BLOCKED_REASON_RESTRICTED_MODE); + newBlockedReasons |= mBackgroundNetworkRestricted ? BLOCKED_REASON_APP_BACKGROUND : 0; newAllowedReasons |= (isSystem(uid) ? ALLOWED_REASON_SYSTEM : 0); newAllowedReasons |= (isForeground ? ALLOWED_REASON_FOREGROUND : 0); @@ -5234,6 +5419,9 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { & ALLOWED_REASON_RESTRICTED_MODE_PERMISSIONS); newAllowedReasons |= (isAllowlistedFromLowPowerStandbyUL(uid)) ? ALLOWED_REASON_LOW_POWER_STANDBY_ALLOWLIST : 0; + newAllowedReasons |= (mBackgroundNetworkRestricted + && isUidExemptFromBackgroundRestrictions(uid)) + ? ALLOWED_REASON_NOT_IN_BACKGROUND : 0; uidBlockedState.blockedReasons = (uidBlockedState.blockedReasons & BLOCKED_METERED_REASON_MASK) | newBlockedReasons; @@ -5255,7 +5443,7 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { oldEffectiveBlockedReasons = previousUidBlockedState.effectiveBlockedReasons; newEffectiveBlockedReasons = uidBlockedState.effectiveBlockedReasons; - uidRules = oldEffectiveBlockedReasons == newEffectiveBlockedReasons + uidRules = (oldEffectiveBlockedReasons == newEffectiveBlockedReasons) ? RULE_NONE : uidBlockedState.deriveUidRules(); } @@ -5448,6 +5636,28 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { mContext.sendBroadcastAsUser(intent, UserHandle.ALL); return true; } + case MSG_PROCESS_BACKGROUND_TRANSITIONING_UIDS: { + final long now = SystemClock.uptimeMillis(); + long nextCheckTime = Long.MAX_VALUE; + synchronized (mUidRulesFirstLock) { + for (int i = mBackgroundTransitioningUids.size() - 1; i >= 0; i--) { + final long completionTimeMs = mBackgroundTransitioningUids.valueAt(i); + if (completionTimeMs > now) { + nextCheckTime = Math.min(nextCheckTime, completionTimeMs); + continue; + } + final int uid = mBackgroundTransitioningUids.keyAt(i); + mBackgroundTransitioningUids.removeAt(i); + updateRuleForBackgroundUL(uid); + updateRulesForPowerRestrictionsUL(uid, false); + } + } + if (nextCheckTime < Long.MAX_VALUE) { + mHandler.sendEmptyMessageAtTime(MSG_PROCESS_BACKGROUND_TRANSITIONING_UIDS, + nextCheckTime); + } + return true; + } case MSG_POLICIES_CHANGED: { final int uid = msg.arg1; final int policy = msg.arg2; @@ -5859,6 +6069,8 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { mUidFirewallRestrictedModeRules.put(uid, rule); } else if (chain == FIREWALL_CHAIN_LOW_POWER_STANDBY) { mUidFirewallLowPowerStandbyModeRules.put(uid, rule); + } else if (chain == FIREWALL_CHAIN_BACKGROUND) { + mUidFirewallBackgroundRules.put(uid, rule); } try { @@ -5915,6 +6127,8 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { FIREWALL_RULE_DEFAULT); mNetworkManager.setFirewallUidRule(FIREWALL_CHAIN_LOW_POWER_STANDBY, uid, FIREWALL_RULE_DEFAULT); + mNetworkManager.setFirewallUidRule(FIREWALL_CHAIN_BACKGROUND, uid, + FIREWALL_RULE_DEFAULT); mNetworkManager.setUidOnMeteredNetworkAllowlist(uid, false); mLogger.meteredAllowlistChanged(uid, false); mNetworkManager.setUidOnMeteredNetworkDenylist(uid, false); @@ -6441,10 +6655,12 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { effectiveBlockedReasons &= ~BLOCKED_REASON_BATTERY_SAVER; effectiveBlockedReasons &= ~BLOCKED_REASON_DOZE; effectiveBlockedReasons &= ~BLOCKED_REASON_APP_STANDBY; + effectiveBlockedReasons &= ~BLOCKED_REASON_APP_BACKGROUND; } if ((allowedReasons & ALLOWED_REASON_POWER_SAVE_EXCEPT_IDLE_ALLOWLIST) != 0) { effectiveBlockedReasons &= ~BLOCKED_REASON_BATTERY_SAVER; effectiveBlockedReasons &= ~BLOCKED_REASON_APP_STANDBY; + effectiveBlockedReasons &= ~BLOCKED_REASON_APP_BACKGROUND; } if ((allowedReasons & ALLOWED_REASON_RESTRICTED_MODE_PERMISSIONS) != 0) { effectiveBlockedReasons &= ~BLOCKED_REASON_RESTRICTED_MODE; @@ -6455,19 +6671,24 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { if ((allowedReasons & ALLOWED_REASON_LOW_POWER_STANDBY_ALLOWLIST) != 0) { effectiveBlockedReasons &= ~BLOCKED_REASON_LOW_POWER_STANDBY; } + if ((allowedReasons & ALLOWED_REASON_NOT_IN_BACKGROUND) != 0) { + effectiveBlockedReasons &= ~BLOCKED_REASON_APP_BACKGROUND; + } return effectiveBlockedReasons; } static int getAllowedReasonsForProcState(int procState) { - if (procState > NetworkPolicyManager.FOREGROUND_THRESHOLD_STATE) { - return ALLOWED_REASON_NONE; - } else if (procState <= NetworkPolicyManager.TOP_THRESHOLD_STATE) { + if (procState <= NetworkPolicyManager.TOP_THRESHOLD_STATE) { return ALLOWED_REASON_TOP | ALLOWED_REASON_FOREGROUND - | ALLOWED_METERED_REASON_FOREGROUND; - } else { - return ALLOWED_REASON_FOREGROUND | ALLOWED_METERED_REASON_FOREGROUND; + | ALLOWED_METERED_REASON_FOREGROUND | ALLOWED_REASON_NOT_IN_BACKGROUND; + } else if (procState <= NetworkPolicyManager.FOREGROUND_THRESHOLD_STATE) { + return ALLOWED_REASON_FOREGROUND | ALLOWED_METERED_REASON_FOREGROUND + | ALLOWED_REASON_NOT_IN_BACKGROUND; + } else if (procState < NetworkPolicyManager.BACKGROUND_THRESHOLD_STATE) { + return ALLOWED_REASON_NOT_IN_BACKGROUND; } + return ALLOWED_REASON_NONE; } @Override @@ -6492,6 +6713,7 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { BLOCKED_REASON_APP_STANDBY, BLOCKED_REASON_RESTRICTED_MODE, BLOCKED_REASON_LOW_POWER_STANDBY, + BLOCKED_REASON_APP_BACKGROUND, BLOCKED_METERED_REASON_DATA_SAVER, BLOCKED_METERED_REASON_USER_RESTRICTED, BLOCKED_METERED_REASON_ADMIN_DISABLED, @@ -6505,6 +6727,7 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { ALLOWED_REASON_POWER_SAVE_EXCEPT_IDLE_ALLOWLIST, ALLOWED_REASON_RESTRICTED_MODE_PERMISSIONS, ALLOWED_REASON_LOW_POWER_STANDBY_ALLOWLIST, + ALLOWED_REASON_NOT_IN_BACKGROUND, ALLOWED_METERED_REASON_USER_EXEMPTED, ALLOWED_METERED_REASON_SYSTEM, ALLOWED_METERED_REASON_FOREGROUND, @@ -6524,6 +6747,8 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { return "RESTRICTED_MODE"; case BLOCKED_REASON_LOW_POWER_STANDBY: return "LOW_POWER_STANDBY"; + case BLOCKED_REASON_APP_BACKGROUND: + return "APP_BACKGROUND"; case BLOCKED_METERED_REASON_DATA_SAVER: return "DATA_SAVER"; case BLOCKED_METERED_REASON_USER_RESTRICTED: @@ -6554,6 +6779,8 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { return "RESTRICTED_MODE_PERMISSIONS"; case ALLOWED_REASON_LOW_POWER_STANDBY_ALLOWLIST: return "LOW_POWER_STANDBY_ALLOWLIST"; + case ALLOWED_REASON_NOT_IN_BACKGROUND: + return "NOT_IN_BACKGROUND"; case ALLOWED_METERED_REASON_USER_EXEMPTED: return "METERED_USER_EXEMPTED"; case ALLOWED_METERED_REASON_SYSTEM: @@ -6621,7 +6848,8 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { int powerBlockedReasons = BLOCKED_REASON_APP_STANDBY | BLOCKED_REASON_DOZE | BLOCKED_REASON_BATTERY_SAVER - | BLOCKED_REASON_LOW_POWER_STANDBY; + | BLOCKED_REASON_LOW_POWER_STANDBY + | BLOCKED_REASON_APP_BACKGROUND; if ((effectiveBlockedReasons & powerBlockedReasons) != 0) { uidRule |= RULE_REJECT_ALL; } else if ((blockedReasons & powerBlockedReasons) != 0) { diff --git a/services/core/java/com/android/server/net/flags.aconfig b/services/core/java/com/android/server/net/flags.aconfig new file mode 100644 index 000000000000..419665a0a5ab --- /dev/null +++ b/services/core/java/com/android/server/net/flags.aconfig @@ -0,0 +1,8 @@ +package: "com.android.server.net" + +flag { + name: "network_blocked_for_top_sleeping_and_above" + namespace: "backstage_power" + description: "Block network access for apps in a low importance background state" + bug: "304347838" +} diff --git a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java index 85c4ffe6ac67..f852b8173f30 100644 --- a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java +++ b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java @@ -57,6 +57,7 @@ import android.telephony.PhoneStateListener; import android.telephony.TelephonyManager; import android.text.TextUtils; import android.util.Log; +import android.util.Pair; import android.util.Slog; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; @@ -71,7 +72,6 @@ import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.server.EventLogTags; import com.android.server.lights.LightsManager; import com.android.server.lights.LogicalLight; -import com.android.server.notification.Flags; import java.io.PrintWriter; import java.lang.annotation.Retention; @@ -81,6 +81,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Set; /** * NotificationManagerService helper for handling notification attention effects: @@ -100,6 +101,20 @@ public final class NotificationAttentionHelper { private static final int DEFAULT_NOTIFICATION_COOLDOWN_ALL = 1; private static final int DEFAULT_NOTIFICATION_COOLDOWN_VIBRATE_UNLOCKED = 0; + @VisibleForTesting + static final Set<String> NOTIFICATION_AVALANCHE_TRIGGER_INTENTS = Set.of( + Intent.ACTION_AIRPLANE_MODE_CHANGED, + Intent.ACTION_BOOT_COMPLETED, + Intent.ACTION_USER_SWITCHED, + Intent.ACTION_MANAGED_PROFILE_AVAILABLE + ); + + @VisibleForTesting + static final Map<String, Pair<String, Boolean>> NOTIFICATION_AVALANCHE_TRIGGER_EXTRAS = Map.of( + Intent.ACTION_AIRPLANE_MODE_CHANGED, new Pair<>("state", false), + Intent.ACTION_MANAGED_PROFILE_AVAILABLE, new Pair<>(Intent.EXTRA_QUIET_MODE, false) + ); + private final Context mContext; private final PackageManager mPackageManager; private final TelephonyManager mTelephonyManager; @@ -191,7 +206,7 @@ public final class NotificationAttentionHelper { mInCallNotificationVolume = resources.getFloat(R.dimen.config_inCallNotificationVolume); if (Flags.politeNotifications()) { - mStrategy = getPolitenessStrategy(); + mStrategy = createPolitenessStrategy(); } else { mStrategy = null; } @@ -200,7 +215,7 @@ public final class NotificationAttentionHelper { loadUserSettings(); } - private PolitenessStrategy getPolitenessStrategy() { + private PolitenessStrategy createPolitenessStrategy() { if (Flags.crossAppPoliteNotifications()) { PolitenessStrategy appStrategy = new StrategyPerApp( mFlagResolver.getIntValue(NotificationFlags.NOTIF_COOLDOWN_T1), @@ -209,11 +224,12 @@ public final class NotificationAttentionHelper { mFlagResolver.getIntValue(NotificationFlags.NOTIF_VOLUME2), mFlagResolver.getIntValue(NotificationFlags.NOTIF_COOLDOWN_COUNTER_RESET)); - return new StrategyGlobal( + return new StrategyAvalanche( mFlagResolver.getIntValue(NotificationFlags.NOTIF_COOLDOWN_T1), mFlagResolver.getIntValue(NotificationFlags.NOTIF_COOLDOWN_T2), mFlagResolver.getIntValue(NotificationFlags.NOTIF_VOLUME1), mFlagResolver.getIntValue(NotificationFlags.NOTIF_VOLUME2), + mFlagResolver.getIntValue(NotificationFlags.NOTIF_AVALANCHE_TIMEOUT), appStrategy); } else { return new StrategyPerApp( @@ -225,6 +241,11 @@ public final class NotificationAttentionHelper { } } + @VisibleForTesting + PolitenessStrategy getPolitenessStrategy() { + return mStrategy; + } + public void onSystemReady() { mSystemReady = true; @@ -259,6 +280,11 @@ public final class NotificationAttentionHelper { filter.addAction(Intent.ACTION_USER_REMOVED); filter.addAction(Intent.ACTION_USER_SWITCHED); filter.addAction(Intent.ACTION_USER_UNLOCKED); + if (Flags.crossAppPoliteNotifications()) { + for (String avalancheIntent : NOTIFICATION_AVALANCHE_TRIGGER_INTENTS) { + filter.addAction(avalancheIntent); + } + } mContext.registerReceiverAsUser(mIntentReceiver, UserHandle.ALL, filter, null, null); mContext.getContentResolver().registerContentObserver( @@ -1052,7 +1078,8 @@ public final class NotificationAttentionHelper { } } - abstract private static class PolitenessStrategy { + @VisibleForTesting + abstract static class PolitenessStrategy { static final int POLITE_STATE_DEFAULT = 0; static final int POLITE_STATE_POLITE = 1; static final int POLITE_STATE_MUTED = 2; @@ -1079,6 +1106,8 @@ public final class NotificationAttentionHelper { protected boolean mApplyPerPackage; protected final Map<String, Long> mLastUpdatedTimestampByPackage; + protected boolean mIsActive = true; + public PolitenessStrategy(int timeoutPolite, int timeoutMuted, int volumePolite, int volumeMuted) { mVolumeStates = new HashMap<>(); @@ -1218,6 +1247,10 @@ public final class NotificationAttentionHelper { } return nextState; } + + boolean isActive() { + return mIsActive; + } } // TODO b/270456865: Only one of the two strategies will be released. @@ -1289,55 +1322,60 @@ public final class NotificationAttentionHelper { } /** - * Global (cross-app) strategy. + * Avalanche (cross-app) strategy. */ - private static class StrategyGlobal extends PolitenessStrategy { + private static class StrategyAvalanche extends PolitenessStrategy { private static final String COMMON_KEY = "cross_app_common_key"; private final PolitenessStrategy mAppStrategy; private long mLastNotificationTimestamp = 0; - public StrategyGlobal(int timeoutPolite, int timeoutMuted, int volumePolite, - int volumeMuted, PolitenessStrategy appStrategy) { + private final int mTimeoutAvalanche; + private long mLastAvalancheTriggerTimestamp = 0; + + StrategyAvalanche(int timeoutPolite, int timeoutMuted, int volumePolite, + int volumeMuted, int timeoutAvalanche, PolitenessStrategy appStrategy) { super(timeoutPolite, timeoutMuted, volumePolite, volumeMuted); + mTimeoutAvalanche = timeoutAvalanche; mAppStrategy = appStrategy; if (DEBUG) { - Log.i(TAG, "StrategyGlobal: " + timeoutPolite + " " + timeoutMuted); + Log.i(TAG, "StrategyAvalanche: " + timeoutPolite + " " + timeoutMuted + " " + + timeoutAvalanche); } } @Override void onNotificationPosted(NotificationRecord record) { - if (shouldIgnoreNotification(record)) { - return; - } + if (isAvalancheActive()) { + if (shouldIgnoreNotification(record)) { + return; + } - long timeSinceLastNotif = + long timeSinceLastNotif = System.currentTimeMillis() - getLastNotificationUpdateTimeMs(record); - final String key = getChannelKey(record); - @PolitenessState final int currState = getPolitenessState(record); - @PolitenessState int nextState = getNextState(currState, timeSinceLastNotif); + final String key = getChannelKey(record); + @PolitenessState final int currState = getPolitenessState(record); + @PolitenessState int nextState = getNextState(currState, timeSinceLastNotif); - if (DEBUG) { - Log.i(TAG, "StrategyGlobal onNotificationPosted time delta: " + timeSinceLastNotif - + " vol state: " + nextState + " key: " + key); - } + if (DEBUG) { + Log.i(TAG, + "StrategyAvalanche onNotificationPosted time delta: " + + timeSinceLastNotif + + " vol state: " + nextState + " key: " + key); + } - mVolumeStates.put(key, nextState); + mVolumeStates.put(key, nextState); + } mAppStrategy.onNotificationPosted(record); } @Override public float getSoundVolume(final NotificationRecord record) { - final @PolitenessState int globalVolState = getPolitenessState(record); - final @PolitenessState int appVolState = mAppStrategy.getPolitenessState(record); - - // Prioritize the most polite outcome - if (globalVolState > appVolState) { + if (isAvalancheActive()) { return super.getSoundVolume(record); } else { return mAppStrategy.getSoundVolume(record); @@ -1382,6 +1420,24 @@ public final class NotificationAttentionHelper { super.setApplyCooldownPerPackage(applyPerPackage); mAppStrategy.setApplyCooldownPerPackage(applyPerPackage); } + + boolean isAvalancheActive() { + mIsActive = (System.currentTimeMillis() - mLastAvalancheTriggerTimestamp + < mTimeoutAvalanche); + if (DEBUG) { + Log.i(TAG, "StrategyAvalanche: active " + mIsActive); + } + return mIsActive; + } + + @Override + boolean isActive() { + return isAvalancheActive(); + } + + void setTriggerTimeMs(long timestamp) { + mLastAvalancheTriggerTimestamp = timestamp; + } } //====================== Observers ============================= @@ -1415,6 +1471,30 @@ public final class NotificationAttentionHelper { || action.equals(Intent.ACTION_USER_UNLOCKED)) { loadUserSettings(); } + + if (Flags.crossAppPoliteNotifications()) { + if (NOTIFICATION_AVALANCHE_TRIGGER_INTENTS.contains(action)) { + boolean enableAvalancheStrategy = true; + // Some actions must also match extras, ie. airplane mode => disabled + Pair<String, Boolean> expectedExtras = + NOTIFICATION_AVALANCHE_TRIGGER_EXTRAS.get(action); + if (expectedExtras != null) { + enableAvalancheStrategy = + intent.getBooleanExtra(expectedExtras.first, false) + == expectedExtras.second; + } + + if (DEBUG) { + Log.i(TAG, "Avalanche trigger intent received: " + action + + ". Enabling avalanche strategy: " + enableAvalancheStrategy); + } + + if (enableAvalancheStrategy && mStrategy instanceof StrategyAvalanche) { + ((StrategyAvalanche) mStrategy) + .setTriggerTimeMs(System.currentTimeMillis()); + } + } + } } }; diff --git a/services/core/java/com/android/server/om/IdmapManager.java b/services/core/java/com/android/server/om/IdmapManager.java index 25a39cc8456f..86d05d92c95b 100644 --- a/services/core/java/com/android/server/om/IdmapManager.java +++ b/services/core/java/com/android/server/om/IdmapManager.java @@ -257,7 +257,7 @@ final class IdmapManager { private boolean matchesActorSignature(@NonNull AndroidPackage targetPackage, @NonNull AndroidPackage overlayPackage, int userId) { String targetOverlayableName = overlayPackage.getOverlayTargetOverlayableName(); - if (targetOverlayableName != null) { + if (targetOverlayableName != null && !mPackageManager.getNamedActors().isEmpty()) { try { OverlayableInfo overlayableInfo = mPackageManager.getOverlayableForTarget( targetPackage.getPackageName(), targetOverlayableName, userId); diff --git a/services/core/java/com/android/server/om/OverlayManagerService.java b/services/core/java/com/android/server/om/OverlayManagerService.java index b9464d96a019..a61b03fdbb39 100644 --- a/services/core/java/com/android/server/om/OverlayManagerService.java +++ b/services/core/java/com/android/server/om/OverlayManagerService.java @@ -32,6 +32,7 @@ import static android.os.Process.INVALID_UID; import static android.os.Trace.TRACE_TAG_RRO; import static android.os.Trace.traceBegin; import static android.os.Trace.traceEnd; + import static com.android.server.om.OverlayManagerServiceImpl.OperationFailedException; import android.annotation.NonNull; @@ -362,7 +363,7 @@ public final class OverlayManagerService extends SystemService { defaultPackages.add(packageName); } } - return defaultPackages.toArray(new String[defaultPackages.size()]); + return defaultPackages.toArray(new String[0]); } private final class OverlayManagerPackageMonitor extends PackageMonitor { @@ -1143,9 +1144,10 @@ public final class OverlayManagerService extends SystemService { }; private static final class PackageManagerHelperImpl implements PackageManagerHelper { - private static class PackageStateUsers { + private static final class PackageStateUsers { private PackageState mPackageState; - private final Set<Integer> mInstalledUsers = new ArraySet<>(); + private Boolean mDefinesOverlayable = null; + private final ArraySet<Integer> mInstalledUsers = new ArraySet<>(); private PackageStateUsers(@NonNull PackageState packageState) { this.mPackageState = packageState; } @@ -1160,7 +1162,7 @@ public final class OverlayManagerService extends SystemService { // state may lead to contradictions within OMS. Better then to lag // behind until all pending intents have been processed. private final ArrayMap<String, PackageStateUsers> mCache = new ArrayMap<>(); - private final Set<Integer> mInitializedUsers = new ArraySet<>(); + private final ArraySet<Integer> mInitializedUsers = new ArraySet<>(); PackageManagerHelperImpl(Context context) { mContext = context; @@ -1176,8 +1178,7 @@ public final class OverlayManagerService extends SystemService { */ @NonNull public ArrayMap<String, PackageState> initializeForUser(final int userId) { - if (!mInitializedUsers.contains(userId)) { - mInitializedUsers.add(userId); + if (mInitializedUsers.add(userId)) { mPackageManagerInternal.forEachPackageState((packageState -> { if (packageState.getPkg() != null && packageState.getUserStateOrDefault(userId).isInstalled()) { @@ -1196,13 +1197,11 @@ public final class OverlayManagerService extends SystemService { return userPackages; } - @Override - @Nullable - public PackageState getPackageStateForUser(@NonNull final String packageName, + private PackageStateUsers getRawPackageStateForUser(@NonNull final String packageName, final int userId) { final PackageStateUsers pkg = mCache.get(packageName); if (pkg != null && pkg.mInstalledUsers.contains(userId)) { - return pkg.mPackageState; + return pkg; } try { if (!mPackageManager.isPackageAvailable(packageName, userId)) { @@ -1216,8 +1215,14 @@ public final class OverlayManagerService extends SystemService { return addPackageUser(packageName, userId); } - @NonNull - private PackageState addPackageUser(@NonNull final String packageName, + @Override + public PackageState getPackageStateForUser(@NonNull final String packageName, + final int userId) { + final PackageStateUsers pkg = getRawPackageStateForUser(packageName, userId); + return pkg != null ? pkg.mPackageState : null; + } + + private PackageStateUsers addPackageUser(@NonNull final String packageName, final int user) { final PackageState pkg = mPackageManagerInternal.getPackageStateInternal(packageName); if (pkg == null) { @@ -1229,20 +1234,20 @@ public final class OverlayManagerService extends SystemService { } @NonNull - private PackageState addPackageUser(@NonNull final PackageState pkg, + private PackageStateUsers addPackageUser(@NonNull final PackageState pkg, final int user) { PackageStateUsers pkgUsers = mCache.get(pkg.getPackageName()); if (pkgUsers == null) { pkgUsers = new PackageStateUsers(pkg); mCache.put(pkg.getPackageName(), pkgUsers); - } else { + } else if (pkgUsers.mPackageState != pkg) { pkgUsers.mPackageState = pkg; + pkgUsers.mDefinesOverlayable = null; } pkgUsers.mInstalledUsers.add(user); - return pkgUsers.mPackageState; + return pkgUsers; } - @NonNull private void removePackageUser(@NonNull final String packageName, final int user) { final PackageStateUsers pkgUsers = mCache.get(packageName); @@ -1260,15 +1265,15 @@ public final class OverlayManagerService extends SystemService { } } - @Nullable public PackageState onPackageAdded(@NonNull final String packageName, final int userId) { - return addPackageUser(packageName, userId); + final var pu = addPackageUser(packageName, userId); + return pu != null ? pu.mPackageState : null; } - @Nullable public PackageState onPackageUpdated(@NonNull final String packageName, final int userId) { - return addPackageUser(packageName, userId); + final var pu = addPackageUser(packageName, userId); + return pu != null ? pu.mPackageState : null; } public void onPackageRemoved(@NonNull final String packageName, final int userId) { @@ -1308,22 +1313,30 @@ public final class OverlayManagerService extends SystemService { return (pkgs.length == 0) ? null : pkgs[0]; } - @Nullable @Override public OverlayableInfo getOverlayableForTarget(@NonNull String packageName, @NonNull String targetOverlayableName, int userId) throws IOException { - var packageState = getPackageStateForUser(packageName, userId); - var pkg = packageState == null ? null : packageState.getAndroidPackage(); + final var psu = getRawPackageStateForUser(packageName, userId); + final var pkg = (psu == null || psu.mPackageState == null) + ? null : psu.mPackageState.getAndroidPackage(); if (pkg == null) { throw new IOException("Unable to get target package"); } + if (Boolean.FALSE.equals(psu.mDefinesOverlayable)) { + return null; + } + ApkAssets apkAssets = null; try { apkAssets = ApkAssets.loadFromPath(pkg.getSplits().get(0).getPath(), ApkAssets.PROPERTY_ONLY_OVERLAYABLES); - return apkAssets.getOverlayableInfo(targetOverlayableName); + if (psu.mDefinesOverlayable == null) { + psu.mDefinesOverlayable = apkAssets.definesOverlayable(); + } + return Boolean.FALSE.equals(psu.mDefinesOverlayable) + ? null : apkAssets.getOverlayableInfo(targetOverlayableName); } finally { if (apkAssets != null) { try { @@ -1337,24 +1350,29 @@ public final class OverlayManagerService extends SystemService { @Override public boolean doesTargetDefineOverlayable(String targetPackageName, int userId) throws IOException { - var packageState = getPackageStateForUser(targetPackageName, userId); - var pkg = packageState == null ? null : packageState.getAndroidPackage(); + final var psu = getRawPackageStateForUser(targetPackageName, userId); + var pkg = (psu == null || psu.mPackageState == null) + ? null : psu.mPackageState.getAndroidPackage(); if (pkg == null) { throw new IOException("Unable to get target package"); } - ApkAssets apkAssets = null; - try { - apkAssets = ApkAssets.loadFromPath(pkg.getSplits().get(0).getPath()); - return apkAssets.definesOverlayable(); - } finally { - if (apkAssets != null) { - try { - apkAssets.close(); - } catch (Throwable ignored) { + if (psu.mDefinesOverlayable == null) { + ApkAssets apkAssets = null; + try { + apkAssets = ApkAssets.loadFromPath(pkg.getSplits().get(0).getPath(), + ApkAssets.PROPERTY_ONLY_OVERLAYABLES); + psu.mDefinesOverlayable = apkAssets.definesOverlayable(); + } finally { + if (apkAssets != null) { + try { + apkAssets.close(); + } catch (Throwable ignored) { + } } } } + return psu.mDefinesOverlayable; } @Override @@ -1545,8 +1563,7 @@ public final class OverlayManagerService extends SystemService { final OverlayPaths frameworkOverlays = mImpl.getEnabledOverlayPaths("android", userId, false); for (final String targetPackageName : targetPackageNames) { - final OverlayPaths.Builder list = new OverlayPaths.Builder(); - list.addAll(frameworkOverlays); + final var list = new OverlayPaths.Builder(frameworkOverlays); if (!"android".equals(targetPackageName)) { list.addAll(mImpl.getEnabledOverlayPaths(targetPackageName, userId, true)); } @@ -1558,17 +1575,21 @@ public final class OverlayManagerService extends SystemService { final HashSet<String> invalidPackages = new HashSet<>(); pm.setEnabledOverlayPackages(userId, pendingChanges, updatedPackages, invalidPackages); - for (final String targetPackageName : targetPackageNames) { - if (DEBUG) { - Slog.d(TAG, "-> Updating overlay: target=" + targetPackageName + " overlays=[" - + pendingChanges.get(targetPackageName) - + "] userId=" + userId); - } + if (DEBUG || !invalidPackages.isEmpty()) { + for (final String targetPackageName : targetPackageNames) { + if (DEBUG) { + Slog.d(TAG, + "-> Updating overlay: target=" + targetPackageName + " overlays=[" + + pendingChanges.get(targetPackageName) + + "] userId=" + userId); + } - if (invalidPackages.contains(targetPackageName)) { - Slog.e(TAG, TextUtils.formatSimple( - "Failed to change enabled overlays for %s user %d", targetPackageName, - userId)); + if (invalidPackages.contains(targetPackageName)) { + Slog.e(TAG, TextUtils.formatSimple( + "Failed to change enabled overlays for %s user %d", + targetPackageName, + userId)); + } } } return new ArrayList<>(updatedPackages); diff --git a/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java b/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java index 972c78db9460..c1b6ccc7e25c 100644 --- a/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java +++ b/services/core/java/com/android/server/om/OverlayManagerServiceImpl.java @@ -772,24 +772,20 @@ final class OverlayManagerServiceImpl { OverlayPaths getEnabledOverlayPaths(@NonNull final String targetPackageName, final int userId, boolean includeImmutableOverlays) { - final List<OverlayInfo> overlays = mSettings.getOverlaysForTarget(targetPackageName, - userId); - final OverlayPaths.Builder paths = new OverlayPaths.Builder(); - final int n = overlays.size(); - for (int i = 0; i < n; i++) { - final OverlayInfo oi = overlays.get(i); + final var paths = new OverlayPaths.Builder(); + mSettings.forEachMatching(userId, null, targetPackageName, oi -> { if (!oi.isEnabled()) { - continue; + return; } if (!includeImmutableOverlays && !oi.isMutable) { - continue; + return; } if (oi.isFabricated()) { paths.addNonApkPath(oi.baseCodePath); } else { paths.addApkPath(oi.baseCodePath); } - } + }); return paths.build(); } diff --git a/services/core/java/com/android/server/om/OverlayManagerSettings.java b/services/core/java/com/android/server/om/OverlayManagerSettings.java index eae614ac9e77..b8b49f3eed2f 100644 --- a/services/core/java/com/android/server/om/OverlayManagerSettings.java +++ b/services/core/java/com/android/server/om/OverlayManagerSettings.java @@ -47,6 +47,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Set; +import java.util.function.Consumer; import java.util.function.Predicate; import java.util.stream.Stream; @@ -182,6 +183,23 @@ final class OverlayManagerSettings { return CollectionUtils.map(items, SettingsItem::getOverlayInfo); } + void forEachMatching(int userId, String overlayName, String targetPackageName, + @NonNull Consumer<OverlayInfo> consumer) { + for (int i = 0, n = mItems.size(); i < n; i++) { + final SettingsItem item = mItems.get(i); + if (item.getUserId() != userId) { + continue; + } + if (overlayName != null && !item.mOverlay.getPackageName().equals(overlayName)) { + continue; + } + if (targetPackageName != null && !item.mTargetPackageName.equals(targetPackageName)) { + continue; + } + consumer.accept(item.getOverlayInfo()); + } + } + ArrayMap<String, List<OverlayInfo>> getOverlaysForUser(final int userId) { final List<SettingsItem> items = selectWhereUser(userId); diff --git a/services/core/java/com/android/server/pm/AppsFilterImpl.java b/services/core/java/com/android/server/pm/AppsFilterImpl.java index f3df4244c47f..cc4c2b5bf893 100644 --- a/services/core/java/com/android/server/pm/AppsFilterImpl.java +++ b/services/core/java/com/android/server/pm/AppsFilterImpl.java @@ -1031,12 +1031,12 @@ public final class AppsFilterImpl extends AppsFilterLocked implements Watchable, private void recomputeComponentVisibility( ArrayMap<String, ? extends PackageStateInternal> existingSettings) { final WatchedArraySet<String> protectedBroadcasts; - final WatchedArraySet<Integer> forceQueryable; + final ArraySet<Integer> forceQueryable; synchronized (mProtectedBroadcastsLock) { - protectedBroadcasts = mProtectedBroadcasts.snapshot(); + protectedBroadcasts = new WatchedArraySet<String>(mProtectedBroadcasts); } synchronized (mForceQueryableLock) { - forceQueryable = mForceQueryable.snapshot(); + forceQueryable = new ArraySet<Integer>(mForceQueryable.untrackedStorage()); } final ParallelComputeComponentVisibility computer = new ParallelComputeComponentVisibility( existingSettings, forceQueryable, protectedBroadcasts); diff --git a/services/core/java/com/android/server/pm/AppsFilterUtils.java b/services/core/java/com/android/server/pm/AppsFilterUtils.java index 200734b37269..a02a1bc13b17 100644 --- a/services/core/java/com/android/server/pm/AppsFilterUtils.java +++ b/services/core/java/com/android/server/pm/AppsFilterUtils.java @@ -198,12 +198,12 @@ final class AppsFilterUtils { private static final int MAX_THREADS = 4; private final ArrayMap<String, ? extends PackageStateInternal> mExistingSettings; - private final WatchedArraySet<Integer> mForceQueryable; + private final ArraySet<Integer> mForceQueryable; private final WatchedArraySet<String> mProtectedBroadcasts; ParallelComputeComponentVisibility( @NonNull ArrayMap<String, ? extends PackageStateInternal> existingSettings, - @NonNull WatchedArraySet<Integer> forceQueryable, + @NonNull ArraySet<Integer> forceQueryable, @NonNull WatchedArraySet<String> protectedBroadcasts) { mExistingSettings = existingSettings; mForceQueryable = forceQueryable; diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index 4f9eab91f7a8..ada79aed9d16 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -2900,6 +2900,12 @@ final class InstallPackageHelper { // code is loaded by a new Activity before ApplicationInfo changes have // propagated to all application threads. mPm.scheduleDeferredNoKillPostDelete(args); + if (Flags.improveInstallDontKill()) { + synchronized (mPm.mInstallLock) { + PackageManagerServiceUtils.linkSplitsToOldDirs(mPm.mInstaller, + packageName, pkgSetting.getPath(), pkgSetting.getOldPaths()); + } + } } else { mRemovePackageHelper.cleanUpResources(packageName, args.getCodeFile(), args.getInstructionSets()); @@ -4211,8 +4217,10 @@ final class InstallPackageHelper { } } + final long firstInstallTime = Flags.fixSystemAppsFirstInstallTime() + ? System.currentTimeMillis() : 0; final ScanResult scanResult = scanPackageNewLI(parsedPackage, parseFlags, - scanFlags | SCAN_UPDATE_SIGNATURE, 0 /* currentTime */, user, null); + scanFlags | SCAN_UPDATE_SIGNATURE, firstInstallTime, user, null); return new Pair<>(scanResult, shouldHideSystemApp); } diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java index 68c95b16d318..b5346a351f38 100644 --- a/services/core/java/com/android/server/pm/LauncherAppsService.java +++ b/services/core/java/com/android/server/pm/LauncherAppsService.java @@ -18,10 +18,6 @@ package com.android.server.pm; import static android.Manifest.permission.READ_FRAME_BUFFER; import static android.app.ActivityOptions.KEY_SPLASH_SCREEN_THEME; -import static android.app.AppOpsManager.MODE_ALLOWED; -import static android.app.AppOpsManager.MODE_IGNORED; -import static android.app.AppOpsManager.OP_ARCHIVE_ICON_OVERLAY; -import static android.app.AppOpsManager.OP_UNARCHIVAL_CONFIRMATION; import static android.app.ComponentOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED; import static android.app.ComponentOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED; import static android.app.PendingIntent.FLAG_IMMUTABLE; @@ -47,7 +43,6 @@ import android.app.ActivityManager; import android.app.ActivityManagerInternal; import android.app.ActivityOptions; import android.app.AppGlobals; -import android.app.AppOpsManager; import android.app.IApplicationThread; import android.app.PendingIntent; import android.app.admin.DevicePolicyCache; @@ -218,7 +213,6 @@ public class LauncherAppsService extends SystemService { private final ActivityTaskManagerInternal mActivityTaskManagerInternal; private final ShortcutServiceInternal mShortcutServiceInternal; private final PackageManagerInternal mPackageManagerInternal; - private final AppOpsManager mAppOpsManager; private final PackageCallbackList<IOnAppsChangedListener> mListeners = new PackageCallbackList<IOnAppsChangedListener>(); private final DevicePolicyManager mDpm; @@ -259,7 +253,6 @@ public class LauncherAppsService extends SystemService { LocalServices.getService(ShortcutServiceInternal.class)); mPackageManagerInternal = Objects.requireNonNull( LocalServices.getService(PackageManagerInternal.class)); - mAppOpsManager = mContext.getSystemService(AppOpsManager.class); mShortcutServiceInternal.addListener(mPackageMonitor); mShortcutChangeHandler = new ShortcutChangeHandler(mUserManagerInternal); mShortcutServiceInternal.addShortcutChangeCallback(mShortcutChangeHandler); @@ -1687,9 +1680,8 @@ public class LauncherAppsService extends SystemService { mContext, /* requestCode */ 0, intent, - PendingIntent.FLAG_ONE_SHOT - | PendingIntent.FLAG_IMMUTABLE - | PendingIntent.FLAG_CANCEL_CURRENT, + PendingIntent.FLAG_IMMUTABLE + | FLAG_UPDATE_CURRENT, /* options */ null, user); return pi == null ? null : pi.getIntentSender(); @@ -2005,23 +1997,6 @@ public class LauncherAppsService extends SystemService { } } - @Override - public void setArchiveCompatibilityOptions(boolean enableIconOverlay, - boolean enableUnarchivalConfirmation) { - int callingUid = Binder.getCallingUid(); - Binder.withCleanCallingIdentity( - () -> { - mAppOpsManager.setUidMode( - OP_ARCHIVE_ICON_OVERLAY, - callingUid, - enableIconOverlay ? MODE_ALLOWED : MODE_IGNORED); - mAppOpsManager.setUidMode( - OP_UNARCHIVAL_CONFIRMATION, - callingUid, - enableUnarchivalConfirmation ? MODE_ALLOWED : MODE_IGNORED); - }); - } - /** Checks if user is a profile of or same as listeningUser. * and the user is enabled. */ private boolean isEnabledProfileOf(UserHandle listeningUser, UserHandle user, diff --git a/services/core/java/com/android/server/pm/PackageArchiver.java b/services/core/java/com/android/server/pm/PackageArchiver.java index c1b3673865dc..32f56463c8de 100644 --- a/services/core/java/com/android/server/pm/PackageArchiver.java +++ b/services/core/java/com/android/server/pm/PackageArchiver.java @@ -20,7 +20,6 @@ import static android.app.ActivityManager.START_ABORTED; import static android.app.ActivityManager.START_CLASS_NOT_FOUND; import static android.app.ActivityManager.START_PERMISSION_DENIED; import static android.app.ActivityManager.START_SUCCESS; -import static android.app.AppOpsManager.MODE_ALLOWED; import static android.app.AppOpsManager.MODE_IGNORED; import static android.app.ComponentOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED; import static android.content.pm.ArchivedActivityInfo.bytesFromBitmap; @@ -196,6 +195,7 @@ public class PackageArchiver { Computer snapshot = mPm.snapshotComputer(); int userId = userHandle.getIdentifier(); int binderUid = Binder.getCallingUid(); + int binderPid = Binder.getCallingPid(); if (!PackageManagerServiceUtils.isSystemOrRootOrShell(binderUid)) { verifyCaller(snapshot.getPackageUid(callerPackageName, 0, userId), binderUid); } @@ -230,7 +230,8 @@ public class PackageArchiver { DELETE_ARCHIVE | DELETE_KEEP_DATA, intentSender, userId, - binderUid); + binderUid, + binderPid); }) .exceptionally( e -> { @@ -274,12 +275,11 @@ public class PackageArchiver { Slog.i(TAG, TextUtils.formatSimple("Unarchival is starting for: %s", packageName)); try { + // TODO(b/311709794) Make showUnarchivalConfirmation dependent on the compat options. requestUnarchive(packageName, callerPackageName, getOrCreateLauncherListener(userId, packageName), UserHandle.of(userId), - getAppOpsManager().checkOp( - AppOpsManager.OP_UNARCHIVAL_CONFIRMATION, callingUid, callerPackageName) - == MODE_ALLOWED); + false /* showUnarchivalConfirmation= */); } catch (Throwable t) { Slog.e(TAG, TextUtils.formatSimple( "Unexpected error occurred while unarchiving package %s: %s.", packageName, @@ -796,8 +796,7 @@ public class PackageArchiver { * <p> The icon is returned without any treatment/overlay. In the rare case the app had multiple * launcher activities, only one of the icons is returned arbitrarily. */ - public Bitmap getArchivedAppIcon(@NonNull String packageName, @NonNull UserHandle user, - String callingPackageName) { + public Bitmap getArchivedAppIcon(@NonNull String packageName, @NonNull UserHandle user) { Objects.requireNonNull(packageName); Objects.requireNonNull(user); @@ -820,13 +819,7 @@ public class PackageArchiver { // TODO(b/298452477) Handle monochrome icons. // In the rare case the archived app defined more than two launcher activities, we choose // the first one arbitrarily. - Bitmap icon = decodeIcon(archiveState.getActivityInfos().get(0)); - if (getAppOpsManager().checkOp( - AppOpsManager.OP_ARCHIVE_ICON_OVERLAY, callingUid, callingPackageName) - == MODE_ALLOWED) { - icon = includeCloudOverlay(icon); - } - return icon; + return includeCloudOverlay(decodeIcon(archiveState.getActivityInfos().get(0))); } /** diff --git a/services/core/java/com/android/server/pm/PackageHandler.java b/services/core/java/com/android/server/pm/PackageHandler.java index ee5875ee9497..68f6ca1c019f 100644 --- a/services/core/java/com/android/server/pm/PackageHandler.java +++ b/services/core/java/com/android/server/pm/PackageHandler.java @@ -88,6 +88,13 @@ final class PackageHandler extends Handler { final boolean didRestore = (msg.arg2 != 0); mPm.mRunningInstalls.delete(msg.arg1); + if (request == null) { + if (DEBUG_INSTALL) { + Slog.i(TAG, "InstallRequest is null. Nothing to do for post-install " + + "token " + msg.arg1); + } + break; + } request.closeFreezer(); request.onInstallCompleted(); request.runPostInstallRunnable(); @@ -116,10 +123,19 @@ final class PackageHandler extends Handler { } } break; case WRITE_SETTINGS: { - mPm.writeSettings(/*sync=*/false); + if (!mPm.tryWriteSettings(/*sync=*/false)) { + // Failed to write. + this.removeMessages(WRITE_SETTINGS); + mPm.scheduleWriteSettings(); + } } break; case WRITE_PACKAGE_LIST: { - mPm.writePackageList(msg.arg1); + int userId = msg.arg1; + if (!mPm.tryWritePackageList(userId)) { + // Failed to write. + this.removeMessages(WRITE_PACKAGE_LIST); + mPm.scheduleWritePackageList(userId); + } } break; case CHECK_PENDING_VERIFICATION: { final int verificationId = msg.arg1; diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java index cfafe7cc00df..c6d448d97673 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerService.java +++ b/services/core/java/com/android/server/pm/PackageInstallerService.java @@ -1405,11 +1405,12 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements flags, statusReceiver, userId, - Binder.getCallingUid()); + Binder.getCallingUid(), + Binder.getCallingPid()); } void uninstall(VersionedPackage versionedPackage, String callerPackageName, int flags, - IntentSender statusReceiver, int userId, int callingUid) { + IntentSender statusReceiver, int userId, int callingUid, int callingPid) { final Computer snapshot = mPm.snapshotComputer(); snapshot.enforceCrossUserPermission(callingUid, userId, true, true, "uninstall"); if (!PackageManagerServiceUtils.isRootOrShell(callingUid)) { @@ -1426,7 +1427,7 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements final PackageDeleteObserverAdapter adapter = new PackageDeleteObserverAdapter(mContext, statusReceiver, versionedPackage.getPackageName(), canSilentlyInstallPackage, userId, mPackageArchiver, flags); - if (mContext.checkCallingOrSelfPermission(Manifest.permission.DELETE_PACKAGES) + if (mContext.checkPermission(Manifest.permission.DELETE_PACKAGES, callingPid, callingUid) == PackageManager.PERMISSION_GRANTED) { // Sweet, call straight through! mPm.deletePackageVersioned(versionedPackage, adapter.getBinder(), userId, flags); @@ -1446,8 +1447,8 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements } else { ApplicationInfo appInfo = snapshot.getApplicationInfo(callerPackageName, 0, userId); if (appInfo.targetSdkVersion >= Build.VERSION_CODES.P) { - mContext.enforceCallingOrSelfPermission(Manifest.permission.REQUEST_DELETE_PACKAGES, - null); + mContext.enforcePermission(Manifest.permission.REQUEST_DELETE_PACKAGES, callingPid, + callingUid, null); } // Take a short detour to confirm with user diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java index 1a0e8079996e..27c3dad23450 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerSession.java +++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java @@ -54,6 +54,7 @@ import static com.android.internal.util.XmlUtils.writeStringAttribute; import static com.android.internal.util.XmlUtils.writeUriAttribute; import static com.android.server.pm.PackageInstallerService.prepareStageDir; import static com.android.server.pm.PackageManagerService.APP_METADATA_FILE_NAME; +import static com.android.server.pm.PackageManagerService.DEFAULT_FILE_ACCESS_MODE; import static com.android.server.pm.PackageManagerServiceUtils.isInstalledByAdb; import static com.android.server.pm.PackageManagerShellCommandDataLoader.Metadata; @@ -1832,7 +1833,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { try { Os.link(path, sourcePath); // Grant READ access for APK to be read successfully - Os.chmod(sourcePath, 0644); + Os.chmod(sourcePath, DEFAULT_FILE_ACCESS_MODE); } catch (ErrnoException e) { e.rethrowAsIOException(); } @@ -1901,7 +1902,8 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { // If file is app metadata then set permission to 0640 to deny user read access since it // might contain sensitive information. - int mode = name.equals(APP_METADATA_FILE_NAME) ? APP_METADATA_FILE_ACCESS_MODE : 0644; + int mode = name.equals(APP_METADATA_FILE_NAME) + ? APP_METADATA_FILE_ACCESS_MODE : DEFAULT_FILE_ACCESS_MODE; ParcelFileDescriptor targetPfd = openTargetInternal(target.getAbsolutePath(), O_CREAT | O_WRONLY, mode); Os.chmod(target.getAbsolutePath(), mode); @@ -4246,7 +4248,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { throw new IOException("Failed to copy " + fromFile + " to " + tmpFile); } try { - Os.chmod(tmpFile.getAbsolutePath(), 0644); + Os.chmod(tmpFile.getAbsolutePath(), DEFAULT_FILE_ACCESS_MODE); } catch (ErrnoException e) { throw new IOException("Failed to chmod " + tmpFile); } diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index f09fa21792dd..135bd4f911f9 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -489,6 +489,9 @@ public class PackageManagerService implements PackageSender, TestUtilityService */ static final long WATCHDOG_TIMEOUT = 1000*60*10; // ten minutes + // How long to wait for Lock in async writeSettings and writePackageList. + private static final long WRITE_LOCK_TIMEOUT_MS = 1000 * 10; // 10 seconds + /** * Default IncFs timeouts. Maximum values in IncFs is 1hr. * @@ -593,6 +596,8 @@ public class PackageManagerService implements PackageSender, TestUtilityService static final String APP_METADATA_FILE_NAME = "app.metadata"; + static final int DEFAULT_FILE_ACCESS_MODE = 0644; + final Handler mHandler; final Handler mBackgroundHandler; @@ -1562,7 +1567,7 @@ public class PackageManagerService implements PackageSender, TestUtilityService } } - private void scheduleWritePackageListLocked(int userId) { + void scheduleWritePackageList(int userId) { invalidatePackageInfoCache(); if (!mHandler.hasMessages(WRITE_PACKAGE_LIST)) { Message msg = mHandler.obtainMessage(WRITE_PACKAGE_LIST); @@ -1614,22 +1619,41 @@ public class PackageManagerService implements PackageSender, TestUtilityService mSettings.writePackageRestrictions(dirtyUsers); } - void writeSettings(boolean sync) { - synchronized (mLock) { + private boolean tryUnderLock(boolean sync, long timeoutMs, Runnable runnable) { + try { + if (sync) { + mLock.lock(); + } else if (!mLock.tryLock(timeoutMs, TimeUnit.MILLISECONDS)) { + return false; + } + try { + runnable.run(); + return true; + } finally { + mLock.unlock(); + } + } catch (InterruptedException e) { + Slog.e(TAG, "Failed to obtain mLock", e); + } + return false; + } + + boolean tryWriteSettings(boolean sync) { + return tryUnderLock(sync, WRITE_LOCK_TIMEOUT_MS, () -> { mHandler.removeMessages(WRITE_SETTINGS); mBackgroundHandler.removeMessages(WRITE_DIRTY_PACKAGE_RESTRICTIONS); writeSettingsLPrTEMP(sync); synchronized (mDirtyUsers) { mDirtyUsers.clear(); } - } + }); } - void writePackageList(int userId) { - synchronized (mLock) { + boolean tryWritePackageList(int userId) { + return tryUnderLock(/*sync=*/false, WRITE_LOCK_TIMEOUT_MS, () -> { mHandler.removeMessages(WRITE_PACKAGE_LIST); mSettings.writePackageListLPr(userId); - } + }); } private static final Handler.Callback BACKGROUND_HANDLER_CALLBACK = new Handler.Callback() { @@ -3054,7 +3078,9 @@ public class PackageManagerService implements PackageSender, TestUtilityService if (mHandler.hasMessages(WRITE_SETTINGS) || mBackgroundHandler.hasMessages(WRITE_DIRTY_PACKAGE_RESTRICTIONS) || mHandler.hasMessages(WRITE_PACKAGE_LIST)) { - writeSettings(/*sync=*/true); + while (!tryWriteSettings(/*sync=*/true)) { + Slog.wtf(TAG, "Failed to write settings on shutdown"); + } } } } @@ -4334,11 +4360,11 @@ public class PackageManagerService implements PackageSender, TestUtilityService mDirtyUsers.remove(userId); } mUserNeedsBadging.delete(userId); - mPermissionManager.onUserRemoved(userId); + mDeletePackageHelper.removeUnusedPackagesLPw(userManager, userId); mSettings.removeUserLPw(userId); mPendingBroadcasts.remove(userId); - mDeletePackageHelper.removeUnusedPackagesLPw(userManager, userId); mAppsFilter.onUserDeleted(snapshotComputer(), userId); + mPermissionManager.onUserRemoved(userId); } mInstantAppRegistry.onUserRemoved(userId); mPackageMonitorCallbackHelper.onUserRemoved(userId); @@ -4361,7 +4387,7 @@ public class PackageManagerService implements PackageSender, TestUtilityService } synchronized (mLock) { scheduleWritePackageRestrictions(userId); - scheduleWritePackageListLocked(userId); + scheduleWritePackageList(userId); mAppsFilter.onUserCreated(snapshotComputer(), userId); } } @@ -6388,10 +6414,8 @@ public class PackageManagerService implements PackageSender, TestUtilityService } @Override - public Bitmap getArchivedAppIcon(@NonNull String packageName, @NonNull UserHandle user, - @NonNull String callingPackageName) { - return mInstallerService.mPackageArchiver.getArchivedAppIcon(packageName, user, - callingPackageName); + public Bitmap getArchivedAppIcon(@NonNull String packageName, @NonNull UserHandle user) { + return mInstallerService.mPackageArchiver.getArchivedAppIcon(packageName, user); } @Override diff --git a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java index cd3416348153..85316920f363 100644 --- a/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java +++ b/services/core/java/com/android/server/pm/PackageManagerServiceUtils.java @@ -30,6 +30,7 @@ import static com.android.server.pm.PackageManagerService.COMPRESSED_EXTENSION; import static com.android.server.pm.PackageManagerService.DEBUG_COMPRESSION; import static com.android.server.pm.PackageManagerService.DEBUG_INTENT_MATCHING; import static com.android.server.pm.PackageManagerService.DEBUG_PREFERRED; +import static com.android.server.pm.PackageManagerService.DEFAULT_FILE_ACCESS_MODE; import static com.android.server.pm.PackageManagerService.RANDOM_CODEPATH_PREFIX; import static com.android.server.pm.PackageManagerService.RANDOM_DIR_PREFIX; import static com.android.server.pm.PackageManagerService.SHELL_PACKAGE_NAME; @@ -69,6 +70,7 @@ import android.os.Debug; import android.os.Environment; import android.os.FileUtils; import android.os.Process; +import android.os.SELinux; import android.os.SystemProperties; import android.os.UserHandle; import android.os.incremental.IncrementalManager; @@ -129,10 +131,12 @@ import java.security.SecureRandom; import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; import java.text.SimpleDateFormat; +import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.Objects; +import java.util.Set; import java.util.function.Function; import java.util.function.Predicate; import java.util.zip.GZIPInputStream; @@ -853,7 +857,7 @@ public class PackageManagerServiceUtils { FileUtils.copy(fileIn, outputStream); // Flush anything in buffer before chmod, because any writes after chmod will fail. outputStream.flush(); - Os.fchmod(outputStream.getFD(), 0644); + Os.fchmod(outputStream.getFD(), DEFAULT_FILE_ACCESS_MODE); atomicFile.finishWrite(outputStream); return PackageManager.INSTALL_SUCCEEDED; } catch (IOException e) { @@ -1081,8 +1085,8 @@ public class PackageManagerServiceUtils { final File targetFile = new File(targetDir, targetName); final FileDescriptor targetFd = Os.open(targetFile.getAbsolutePath(), - O_RDWR | O_CREAT, 0644); - Os.chmod(targetFile.getAbsolutePath(), 0644); + O_RDWR | O_CREAT, DEFAULT_FILE_ACCESS_MODE); + Os.chmod(targetFile.getAbsolutePath(), DEFAULT_FILE_ACCESS_MODE); FileInputStream source = null; try { source = new FileInputStream(sourcePath); @@ -1552,4 +1556,72 @@ public class PackageManagerServiceUtils { public static boolean isInstalledByAdb(String initiatingPackageName) { return initiatingPackageName == null || SHELL_PACKAGE_NAME.equals(initiatingPackageName); } + + public static void linkSplitsToOldDirs(@NonNull Installer installer, + @NonNull String packageName, + @NonNull File newPath, + @Nullable Set<File> oldPaths) { + if (oldPaths == null || oldPaths.isEmpty()) { + return; + } + if (IncrementalManager.isIncrementalPath(newPath.getPath())) { + //TODO(b/291212866): handle incremental installs + return; + } + final File[] filesInNewPath = newPath.listFiles(); + if (filesInNewPath == null || filesInNewPath.length == 0) { + return; + } + final List<String> splitApkNames = new ArrayList<String>(); + for (int i = 0; i < filesInNewPath.length; i++) { + if (!filesInNewPath[i].isDirectory() && filesInNewPath[i].toString().endsWith(".apk")) { + splitApkNames.add(filesInNewPath[i].getName()); + } + } + final int numSplits = splitApkNames.size(); + if (numSplits == 0) { + return; + } + for (File oldPath : oldPaths) { + if (!oldPath.exists()) { + continue; + } + for (int i = 0; i < numSplits; i++) { + final String splitApkName = splitApkNames.get(i); + final File linkedSplit = new File(oldPath, splitApkName); + if (linkedSplit.exists()) { + if (DEBUG) { + Slog.d(PackageManagerService.TAG, "Skipping existing linked split <" + + linkedSplit + ">"); + } + continue; + } + final File sourceSplit = new File(newPath, splitApkName); + try { + installer.linkFile(packageName, splitApkName, + newPath.getAbsolutePath(), oldPath.getAbsolutePath()); + if (DEBUG) { + Slog.d(PackageManagerService.TAG, "Linked <" + + sourceSplit + "> to <" + linkedSplit + ">"); + } + } catch (Installer.InstallerException e) { + Slog.w(PackageManagerService.TAG, "Failed to link split <" + + sourceSplit + " > to <" + linkedSplit + ">", e); + continue; + } + try { + Os.chmod(linkedSplit.getAbsolutePath(), DEFAULT_FILE_ACCESS_MODE); + } catch (ErrnoException e) { + Slog.w(PackageManagerService.TAG, "Failed to set mode for linked split <" + + linkedSplit + ">", e); + continue; + } + if (!SELinux.restorecon(linkedSplit)) { + Slog.w(PackageManagerService.TAG, "Failed to restorecon for linked split <" + + linkedSplit + ">"); + } + } + } + //TODO(b/291212866): support native libs + } } diff --git a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java index bdde272cc46f..5c9c8c6d249a 100644 --- a/services/core/java/com/android/server/pm/PackageManagerShellCommand.java +++ b/services/core/java/com/android/server/pm/PackageManagerShellCommand.java @@ -28,6 +28,7 @@ import static android.content.pm.PackageManager.RESTRICTION_HIDE_NOTIFICATIONS; import static android.content.pm.PackageManager.RESTRICTION_NONE; import static com.android.server.LocalManagerRegistry.ManagerNotFoundException; +import static com.android.server.pm.PackageManagerService.DEFAULT_FILE_ACCESS_MODE; import static com.android.server.pm.PackageManagerService.PLATFORM_PACKAGE_NAME; import android.accounts.IAccountManager; @@ -2349,7 +2350,7 @@ class PackageManagerShellCommand extends ShellCommand { Streams.copy(inStream, outStream); } // Give read permissions to the other group. - Os.chmod(outputProfilePath, /*mode*/ 0644 ); + Os.chmod(outputProfilePath, /*mode*/ DEFAULT_FILE_ACCESS_MODE); } catch (IOException | ErrnoException e) { pw.println("Error when reading the profile fd: " + e.getMessage()); e.printStackTrace(pw); @@ -2678,7 +2679,7 @@ class PackageManagerShellCommand extends ShellCommand { } final int translatedUserId = translateUserId(userId, UserHandle.USER_NULL, "runSetStoppedState"); - mInterface.setPackageStoppedState(pkg, state, userId); + mInterface.setPackageStoppedState(pkg, state, translatedUserId); getOutPrintWriter().println("Package " + pkg + " new stopped state: " + mInterface.isPackageStoppedForUser(pkg, translatedUserId)); return 0; diff --git a/services/core/java/com/android/server/pm/PackageManagerTracedLock.java b/services/core/java/com/android/server/pm/PackageManagerTracedLock.java index e15e8a839a9a..75e1803f1695 100644 --- a/services/core/java/com/android/server/pm/PackageManagerTracedLock.java +++ b/services/core/java/com/android/server/pm/PackageManagerTracedLock.java @@ -16,9 +16,11 @@ package com.android.server.pm; +import java.util.concurrent.locks.ReentrantLock; + /** * This is a unique class that is used as the PackageManager lock. It can be targeted for lock * injection, similar to {@link ActivityManagerGlobalLock}. */ -public class PackageManagerTracedLock { +public class PackageManagerTracedLock extends ReentrantLock { } diff --git a/services/core/java/com/android/server/pm/pkg/PackageStateInternal.java b/services/core/java/com/android/server/pm/pkg/PackageStateInternal.java index c73728393016..f7603b5cfb57 100644 --- a/services/core/java/com/android/server/pm/pkg/PackageStateInternal.java +++ b/services/core/java/com/android/server/pm/pkg/PackageStateInternal.java @@ -27,6 +27,8 @@ import com.android.server.pm.InstallSource; import com.android.server.pm.PackageKeySetData; import com.android.server.pm.permission.LegacyPermissionState; +import java.io.File; +import java.util.Set; import java.util.UUID; /** @@ -111,4 +113,7 @@ public interface PackageStateInternal extends PackageState { */ @Nullable String getAppMetadataFilePath(); + + @Nullable + Set<File> getOldPaths(); } diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index bf669fba82ce..0abf304c34ee 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -5634,7 +5634,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { Trace.asyncTraceBegin(Trace.TRACE_TAG_WINDOW_MANAGER, "screenTurningOn", 0 /* cookie */); updateScreenOffSleepToken(false /* acquire */, false /* isSwappingDisplay */); - mDefaultDisplayPolicy.screenTurnedOn(screenOnListener); + mDefaultDisplayPolicy.screenTurningOn(screenOnListener); mBootAnimationDismissable = false; synchronized (mLock) { @@ -5676,6 +5676,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { mKeyguardDelegate.onScreenTurnedOn(); } } + mDefaultDisplayPolicy.screenTurnedOn(); reportScreenStateToVrManager(true); } diff --git a/services/core/java/com/android/server/power/ShutdownThread.java b/services/core/java/com/android/server/power/ShutdownThread.java index 871e98bf4ab3..4bf8a78a1f16 100644 --- a/services/core/java/com/android/server/power/ShutdownThread.java +++ b/services/core/java/com/android/server/power/ShutdownThread.java @@ -319,6 +319,11 @@ public final class ShutdownThread extends Thread { pd.setMax(100); pd.setProgress(0); pd.setIndeterminate(false); + boolean showPercent = context.getResources().getBoolean( + com.android.internal.R.bool.config_showPercentageTextDuringRebootToUpdate); + if (!showPercent) { + pd.setProgressPercentFormat(null); + } pd.setProgressNumberFormat(null); pd.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); pd.setMessage(context.getText( @@ -911,4 +916,4 @@ public final class ShutdownThread extends Thread { com.android.internal.R.string.config_defaultShutdownVibrationFile); } } -}
\ No newline at end of file +} diff --git a/services/core/java/com/android/server/power/feature/power_flags.aconfig b/services/core/java/com/android/server/power/feature/power_flags.aconfig index c8c16db15e0e..f5dfb5cc3afe 100644 --- a/services/core/java/com/android/server/power/feature/power_flags.aconfig +++ b/services/core/java/com/android/server/power/feature/power_flags.aconfig @@ -4,7 +4,7 @@ package: "com.android.server.power.feature.flags" flag { name: "enable_early_screen_timeout_detector" - namespace: "power_manager" + namespace: "power" description: "Feature flag for Early Screen Timeout detector" bug: "309861917" is_fixed_read_only: true diff --git a/services/core/java/com/android/server/stats/Android.bp b/services/core/java/com/android/server/stats/Android.bp new file mode 100644 index 000000000000..e597c3a6b6e6 --- /dev/null +++ b/services/core/java/com/android/server/stats/Android.bp @@ -0,0 +1,12 @@ +aconfig_declarations { + name: "stats_flags", + package: "com.android.server.stats", + srcs: [ + "stats_flags.aconfig", + ], +} + +java_aconfig_library { + name: "stats_flags_lib", + aconfig_declarations: "stats_flags", +} diff --git a/services/core/java/com/android/server/stats/pull/AggregatedMobileDataStatsPuller.java b/services/core/java/com/android/server/stats/pull/AggregatedMobileDataStatsPuller.java new file mode 100644 index 000000000000..0de73a5a30f6 --- /dev/null +++ b/services/core/java/com/android/server/stats/pull/AggregatedMobileDataStatsPuller.java @@ -0,0 +1,285 @@ +/* + * 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 + * + * 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.stats.pull; + +import android.app.ActivityManager; +import android.app.StatsManager; +import android.app.usage.NetworkStatsManager; +import android.net.NetworkStats; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Trace; +import android.util.ArrayMap; +import android.util.Slog; +import android.util.SparseIntArray; +import android.util.StatsEvent; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.util.FrameworkStatsLog; + +import java.util.List; +import java.util.Map; + +/** + * Aggregates Mobile Data Usage by process state per uid + */ +class AggregatedMobileDataStatsPuller { + private static final String TAG = "AggregatedMobileDataStatsPuller"; + + private static final boolean DEBUG = false; + + private static class UidProcState { + + private final int mUid; + private final int mState; + + UidProcState(int uid, int state) { + mUid = uid; + mState = state; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof UidProcState key)) return false; + return mUid == key.mUid && mState == key.mState; + } + + @Override + public int hashCode() { + int result = mUid; + result = 31 * result + mState; + return result; + } + + public int getUid() { + return mUid; + } + + public int getState() { + return mState; + } + + } + + private static class MobileDataStats { + private long mRxPackets = 0; + private long mTxPackets = 0; + private long mRxBytes = 0; + private long mTxBytes = 0; + + public long getRxPackets() { + return mRxPackets; + } + + public long getTxPackets() { + return mTxPackets; + } + + public long getRxBytes() { + return mRxBytes; + } + + public long getTxBytes() { + return mTxBytes; + } + + public void addRxPackets(long rxPackets) { + mRxPackets += rxPackets; + } + + public void addTxPackets(long txPackets) { + mTxPackets += txPackets; + } + + public void addRxBytes(long rxBytes) { + mRxBytes += rxBytes; + } + + public void addTxBytes(long txBytes) { + mTxBytes += txBytes; + } + + public boolean isEmpty() { + return mRxPackets == 0 && mTxPackets == 0 && mRxBytes == 0 && mTxBytes == 0; + } + } + + private final Object mLock = new Object(); + @GuardedBy("mLock") + private final Map<UidProcState, MobileDataStats> mUidStats; + + private final SparseIntArray mUidPreviousState; + + private NetworkStats mLastMobileUidStats = new NetworkStats(0, -1); + + private final NetworkStatsManager mNetworkStatsManager; + + private final Handler mMobileDataStatsHandler; + + AggregatedMobileDataStatsPuller(NetworkStatsManager networkStatsManager) { + mUidStats = new ArrayMap<>(); + mUidPreviousState = new SparseIntArray(); + + mNetworkStatsManager = networkStatsManager; + + if (mNetworkStatsManager != null) { + updateNetworkStats(mNetworkStatsManager); + } + + HandlerThread mMobileDataStatsHandlerThread = new HandlerThread("MobileDataStatsHandler"); + mMobileDataStatsHandlerThread.start(); + mMobileDataStatsHandler = new Handler(mMobileDataStatsHandlerThread.getLooper()); + } + + public void noteUidProcessState(int uid, int state, long unusedElapsedRealtime, + long unusedUptime) { + mMobileDataStatsHandler.post( + () -> { + noteUidProcessStateImpl(uid, state); + }); + } + + public int pullDataBytesTransfer(List<StatsEvent> data) { + synchronized (mLock) { + return pullDataBytesTransferLocked(data); + } + } + + @GuardedBy("mLock") + private MobileDataStats getUidStatsForPreviousStateLocked(int uid) { + final int previousState = mUidPreviousState.get(uid, ActivityManager.PROCESS_STATE_UNKNOWN); + if (DEBUG && previousState == ActivityManager.PROCESS_STATE_UNKNOWN) { + Slog.d(TAG, "getUidStatsForPreviousStateLocked() no prev state info for uid " + + uid + ". Tracking stats with ActivityManager.PROCESS_STATE_UNKNOWN"); + } + + final UidProcState statsKey = new UidProcState(uid, previousState); + MobileDataStats stats; + if (mUidStats.containsKey(statsKey)) { + stats = mUidStats.get(statsKey); + } else { + stats = new MobileDataStats(); + mUidStats.put(statsKey, stats); + } + return stats; + } + + private void noteUidProcessStateImpl(int uid, int state) { + // noteUidProcessStateLocked can be called back to back several times while + // the updateNetworkStatsLocked loops over several stats for multiple uids + // and during the first call in a batch of proc state change event it can + // contain info for uid with unknown previous state yet which can happen due to a few + // reasons: + // - app was just started + // - app was started before the ActivityManagerService + // as result stats would be created with state == ActivityManager.PROCESS_STATE_UNKNOWN + if (mNetworkStatsManager != null) { + updateNetworkStats(mNetworkStatsManager); + } else { + Slog.w(TAG, "noteUidProcessStateLocked() can not get mNetworkStatsManager"); + } + mUidPreviousState.put(uid, state); + } + + private void updateNetworkStats(NetworkStatsManager networkStatsManager) { + if (DEBUG) { + if (Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) { + Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, TAG + "-updateNetworkStats"); + } + } + + final NetworkStats latestStats = networkStatsManager.getMobileUidStats(); + if (isEmpty(latestStats)) { + if (DEBUG) { + Slog.w(TAG, "getMobileUidStats() failed"); + Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER); + } + return; + } + NetworkStats delta = latestStats.subtract(mLastMobileUidStats); + mLastMobileUidStats = latestStats; + + if (!isEmpty(delta)) { + updateNetworkStatsDelta(delta); + } else if (DEBUG) { + Slog.w(TAG, "updateNetworkStats() no delta"); + } + if (DEBUG) { + Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER); + } + } + + private void updateNetworkStatsDelta(NetworkStats delta) { + synchronized (mLock) { + for (NetworkStats.Entry entry : delta) { + if (entry.getRxPackets() == 0 && entry.getTxPackets() == 0) { + continue; + } + MobileDataStats stats = getUidStatsForPreviousStateLocked(entry.getUid()); + stats.addTxBytes(entry.getTxBytes()); + stats.addRxBytes(entry.getRxBytes()); + stats.addTxPackets(entry.getTxPackets()); + stats.addRxPackets(entry.getRxPackets()); + } + } + } + + @GuardedBy("mLock") + private int pullDataBytesTransferLocked(List<StatsEvent> pulledData) { + if (DEBUG) { + Slog.d(TAG, "pullDataBytesTransferLocked() start"); + } + for (Map.Entry<UidProcState, MobileDataStats> uidStats : mUidStats.entrySet()) { + if (!uidStats.getValue().isEmpty()) { + MobileDataStats stats = uidStats.getValue(); + pulledData.add(FrameworkStatsLog.buildStatsEvent( + FrameworkStatsLog.MOBILE_BYTES_TRANSFER_BY_PROC_STATE, + uidStats.getKey().getUid(), + ActivityManager.processStateAmToProto(uidStats.getKey().getState()), + stats.getRxBytes(), + stats.getRxPackets(), + stats.getTxBytes(), + stats.getTxPackets())); + } + } + if (DEBUG) { + Slog.d(TAG, + "pullDataBytesTransferLocked() done. results count " + pulledData.size()); + } + if (!pulledData.isEmpty()) { + return StatsManager.PULL_SUCCESS; + } + return StatsManager.PULL_SKIP; + } + + private static boolean isEmpty(NetworkStats stats) { + long totalRxPackets = 0; + long totalTxPackets = 0; + for (NetworkStats.Entry entry : stats) { + if (entry.getRxPackets() == 0 && entry.getTxPackets() == 0) { + continue; + } + totalRxPackets += entry.getRxPackets(); + totalTxPackets += entry.getTxPackets(); + // at least one non empty entry located + break; + } + final long totalPackets = totalRxPackets + totalTxPackets; + return totalPackets == 0; + } +} diff --git a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java index e876241f385e..285bcc328c0c 100644 --- a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java +++ b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java @@ -59,6 +59,7 @@ import static com.android.internal.util.FrameworkStatsLog.TIME_ZONE_DETECTOR_STA import static com.android.internal.util.FrameworkStatsLog.TIME_ZONE_DETECTOR_STATE__DETECTION_MODE__TELEPHONY; import static com.android.internal.util.FrameworkStatsLog.TIME_ZONE_DETECTOR_STATE__DETECTION_MODE__UNKNOWN; import static com.android.server.am.MemoryStatUtil.readMemoryStatFromFilesystem; +import static com.android.server.stats.Flags.addMobileBytesTransferByProcStatePuller; import static com.android.server.stats.pull.IonMemoryUtil.readProcessSystemIonHeapSizesFromDebugfs; import static com.android.server.stats.pull.IonMemoryUtil.readSystemIonHeapSizeFromDebugfs; import static com.android.server.stats.pull.ProcfsMemoryUtil.getProcessCmdlines; @@ -409,6 +410,15 @@ public class StatsPullAtomService extends SystemService { @GuardedBy("mKeystoreLock") private IKeystoreMetrics mIKeystoreMetrics; + private AggregatedMobileDataStatsPuller mAggregatedMobileDataStatsPuller = null; + + /** + * Whether or not to enable the new puller with aggregation by process state per uid on a + * system server side. + */ + public static final boolean ENABLE_MOBILE_DATA_STATS_AGGREGATED_PULLER = + addMobileBytesTransferByProcStatePuller(); + // Puller locks private final Object mDataBytesTransferLock = new Object(); private final Object mBluetoothBytesTransferLock = new Object(); @@ -469,6 +479,20 @@ public class StatsPullAtomService extends SystemService { mContext = context; } + private final class StatsPullAtomServiceInternalImpl extends StatsPullAtomServiceInternal { + + @Override + public void noteUidProcessState(int uid, int state) { + if (ENABLE_MOBILE_DATA_STATS_AGGREGATED_PULLER + && mAggregatedMobileDataStatsPuller != null) { + final long elapsedRealtime = SystemClock.elapsedRealtime(); + final long uptime = SystemClock.uptimeMillis(); + mAggregatedMobileDataStatsPuller.noteUidProcessState(uid, state, elapsedRealtime, + uptime); + } + } + } + private native void initializeNativePullers(); /** @@ -486,6 +510,11 @@ public class StatsPullAtomService extends SystemService { } try { switch (atomTag) { + case FrameworkStatsLog.MOBILE_BYTES_TRANSFER_BY_PROC_STATE: + if (ENABLE_MOBILE_DATA_STATS_AGGREGATED_PULLER + && mAggregatedMobileDataStatsPuller != null) { + return mAggregatedMobileDataStatsPuller.pullDataBytesTransfer(data); + } case FrameworkStatsLog.WIFI_BYTES_TRANSFER: case FrameworkStatsLog.WIFI_BYTES_TRANSFER_BY_FG_BG: case FrameworkStatsLog.MOBILE_BYTES_TRANSFER: @@ -776,7 +805,10 @@ public class StatsPullAtomService extends SystemService { @Override public void onStart() { - // no op + if (ENABLE_MOBILE_DATA_STATS_AGGREGATED_PULLER) { + LocalServices.addService(StatsPullAtomServiceInternal.class, + new StatsPullAtomServiceInternalImpl()); + } } @Override @@ -811,6 +843,9 @@ public class StatsPullAtomService extends SystemService { mStatsSubscriptionsListener = new StatsSubscriptionsListener(mSubscriptionManager); mStorageManager = (StorageManager) mContext.getSystemService(StorageManager.class); mNetworkStatsManager = mContext.getSystemService(NetworkStatsManager.class); + + initMobileDataStatsPuller(); + // Initialize DiskIO mStoragedUidIoStatsReader = new StoragedUidIoStatsReader(); @@ -972,6 +1007,18 @@ public class StatsPullAtomService extends SystemService { registerCachedAppsHighWatermarkPuller(); } + private void initMobileDataStatsPuller() { + if (DEBUG) { + Slog.d(TAG, + "ENABLE_MOBILE_DATA_STATS_AGGREGATED_PULLER = " + + ENABLE_MOBILE_DATA_STATS_AGGREGATED_PULLER); + } + if (ENABLE_MOBILE_DATA_STATS_AGGREGATED_PULLER) { + mAggregatedMobileDataStatsPuller = + new AggregatedMobileDataStatsPuller(mNetworkStatsManager); + } + } + private void initAndRegisterNetworkStatsPullers() { if (DEBUG) { Slog.d(TAG, "Registering NetworkStats pullers with statsd"); @@ -1013,6 +1060,9 @@ public class StatsPullAtomService extends SystemService { registerWifiBytesTransferBackground(); registerMobileBytesTransfer(); registerMobileBytesTransferBackground(); + if (ENABLE_MOBILE_DATA_STATS_AGGREGATED_PULLER) { + registerMobileBytesTransferByProcState(); + } registerBytesTransferByTagAndMetered(); registerDataUsageBytesTransfer(); registerOemManagedBytesTransfer(); @@ -1021,6 +1071,13 @@ public class StatsPullAtomService extends SystemService { } } + private void registerMobileBytesTransferByProcState() { + final int tagId = FrameworkStatsLog.MOBILE_BYTES_TRANSFER_BY_PROC_STATE; + PullAtomMetadata metadata = + new PullAtomMetadata.Builder().setAdditiveFields(new int[] {3, 4, 5, 6}).build(); + mStatsManager.setPullAtomCallback(tagId, metadata, DIRECT_EXECUTOR, mStatsCallbackImpl); + } + private void initAndRegisterDeferredPullers() { mUwbManager = mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_UWB) ? mContext.getSystemService(UwbManager.class) : null; diff --git a/services/core/java/com/android/server/stats/pull/StatsPullAtomServiceInternal.java b/services/core/java/com/android/server/stats/pull/StatsPullAtomServiceInternal.java new file mode 100644 index 000000000000..06adbfc65abc --- /dev/null +++ b/services/core/java/com/android/server/stats/pull/StatsPullAtomServiceInternal.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.stats.pull; + +/** + * System-server internal interface to the {@link StatsPullAtomService}. + * + * @hide Only for use within the system server. + */ +public abstract class StatsPullAtomServiceInternal { + + /** + * @param state Process state from ActivityManager.java. + */ + public abstract void noteUidProcessState(int uid, int state); + +} diff --git a/services/core/java/com/android/server/stats/stats_flags.aconfig b/services/core/java/com/android/server/stats/stats_flags.aconfig new file mode 100644 index 000000000000..5101a6982fe1 --- /dev/null +++ b/services/core/java/com/android/server/stats/stats_flags.aconfig @@ -0,0 +1,9 @@ +package: "com.android.server.stats" + +flag { + name: "add_mobile_bytes_transfer_by_proc_state_puller" + namespace: "statsd" + description: "Adds mobile_bytes_transfer_by_proc_state atom with system server side aggregation" + bug: "309512867" + is_fixed_read_only: true +}
\ No newline at end of file diff --git a/services/core/java/com/android/server/tv/TvInputHardwareManager.java b/services/core/java/com/android/server/tv/TvInputHardwareManager.java index b384725711c4..92b57645b9a3 100755 --- a/services/core/java/com/android/server/tv/TvInputHardwareManager.java +++ b/services/core/java/com/android/server/tv/TvInputHardwareManager.java @@ -309,6 +309,24 @@ class TvInputHardwareManager implements TvInputHal.Callback { } } + public SparseArray<String> getHardwareInputIdMap() { + synchronized (mLock) { + return mHardwareInputIdMap.clone(); + } + } + + public SparseArray<String> getHdmiInputIdMap() { + synchronized (mLock) { + return mHdmiInputIdMap.clone(); + } + } + + public Map<String, TvInputInfo> getInputMap() { + synchronized (mLock) { + return Collections.unmodifiableMap(mInputMap); + } + } + public Map<String, List<String>> getHdmiParentInputMap() { synchronized (mLock) { return Collections.unmodifiableMap(mHdmiParentInputMap); diff --git a/services/core/java/com/android/server/tv/TvInputManagerService.java b/services/core/java/com/android/server/tv/TvInputManagerService.java index ac1b4df930eb..e434df7836c4 100644 --- a/services/core/java/com/android/server/tv/TvInputManagerService.java +++ b/services/core/java/com/android/server/tv/TvInputManagerService.java @@ -135,6 +135,7 @@ public final class TvInputManagerService extends SystemService { private static final int APP_TAG_SELF = TunedInfo.APP_TAG_SELF; private static final String PERMISSION_ACCESS_WATCHED_PROGRAMS = "com.android.providers.tv.permission.ACCESS_WATCHED_PROGRAMS"; + private static final long UPDATE_HARDWARE_TIS_BINDING_DELAY_IN_MILLIS = 10 * 1000; // 10 seconds // There are two different formats of DVB frontend devices. One is /dev/dvb%d.frontend%d, // another one is /dev/dvb/adapter%d/frontend%d. Followings are the patterns for selecting the @@ -174,7 +175,7 @@ public final class TvInputManagerService extends SystemService { @GuardedBy("mLock") private final Map<String, SessionState> mSessionIdToSessionStateMap = new HashMap<>(); - private final WatchLogHandler mWatchLogHandler; + private final MessageHandler mMessageHandler; private final ActivityManager mActivityManager; @@ -187,8 +188,8 @@ public final class TvInputManagerService extends SystemService { super(context); mContext = context; - mWatchLogHandler = new WatchLogHandler(mContext.getContentResolver(), - IoThread.get().getLooper()); + mMessageHandler = + new MessageHandler(mContext.getContentResolver(), IoThread.get().getLooper()); mTvInputHardwareManager = new TvInputHardwareManager(context, new HardwareListener()); mActivityManager = @@ -372,10 +373,10 @@ public final class TvInputManagerService extends SystemService { // service to populate the hardware list. serviceState = new ServiceState(component, userId); userState.serviceStateMap.put(component, serviceState); + updateServiceConnectionLocked(component, userId); } else { inputList.addAll(serviceState.hardwareInputMap.values()); } - updateServiceConnectionLocked(component, userId); } else { try { TvInputInfo info = new TvInputInfo.Builder(mContext, ri).build(); @@ -510,6 +511,7 @@ public final class TvInputManagerService extends SystemService { } } + @GuardedBy("mLock") private void startProfileLocked(int userId) { mRunningProfiles.add(userId); buildTvInputListLocked(userId, null); @@ -538,8 +540,10 @@ public final class TvInputManagerService extends SystemService { mCurrentUserId = userId; buildTvInputListLocked(userId, null); buildTvContentRatingSystemListLocked(userId); - mWatchLogHandler.obtainMessage(WatchLogHandler.MSG_SWITCH_CONTENT_RESOLVER, - getContentResolverForUser(userId)).sendToTarget(); + mMessageHandler + .obtainMessage(MessageHandler.MSG_SWITCH_CONTENT_RESOLVER, + getContentResolverForUser(userId)) + .sendToTarget(); } } @@ -593,7 +597,7 @@ public final class TvInputManagerService extends SystemService { Slog.e(TAG, "error in unregisterCallback", e); } } - mContext.unbindService(serviceState.connection); + unbindService(serviceState); it.remove(); } } @@ -661,7 +665,7 @@ public final class TvInputManagerService extends SystemService { Slog.e(TAG, "error in unregisterCallback", e); } } - mContext.unbindService(serviceState.connection); + unbindService(serviceState); } } userState.serviceStateMap.clear(); @@ -774,7 +778,8 @@ public final class TvInputManagerService extends SystemService { boolean shouldBind; if (userId == mCurrentUserId || mRunningProfiles.contains(userId)) { - shouldBind = !serviceState.sessionTokens.isEmpty() || serviceState.isHardware; + shouldBind = !serviceState.sessionTokens.isEmpty() + || (serviceState.isHardware && serviceState.neverConnected); } else { // For a non-current user, // if sessionTokens is not empty, it contains recording sessions only @@ -783,31 +788,14 @@ public final class TvInputManagerService extends SystemService { shouldBind = !serviceState.sessionTokens.isEmpty(); } - if (serviceState.service == null && shouldBind) { - // This means that the service is not yet connected but its state indicates that we - // have pending requests. Then, connect the service. - if (serviceState.bound) { - // We have already bound to the service so we don't try to bind again until after we - // unbind later on. - return; - } - if (DEBUG) { - Slog.d(TAG, "bindServiceAsUser(service=" + component + ", userId=" + userId + ")"); - } - - Intent i = new Intent(TvInputService.SERVICE_INTERFACE).setComponent(component); - serviceState.bound = mContext.bindServiceAsUser( - i, serviceState.connection, - Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE, - new UserHandle(userId)); - } else if (serviceState.service != null && !shouldBind) { - // This means that the service is already connected but its state indicates that we have - // nothing to do with it. Then, disconnect the service. - if (DEBUG) { - Slog.d(TAG, "unbindService(service=" + component + ")"); + // only bind/unbind when necessary. + if (shouldBind && !serviceState.bound) { + bindService(serviceState, userId); + } else if (!shouldBind && serviceState.bound) { + unbindService(serviceState); + if (!serviceState.isHardware) { + userState.serviceStateMap.remove(component); } - mContext.unbindService(serviceState.connection); - userState.serviceStateMap.remove(component); } } @@ -829,7 +817,11 @@ public final class TvInputManagerService extends SystemService { sendSessionTokenToClientLocked(sessionState.client, sessionState.inputId, null, null, sessionState.seq); } - updateServiceConnectionLocked(serviceState.component, userId); + if (!serviceState.isHardware) { + updateServiceConnectionLocked(serviceState.component, userId); + } else { + updateHardwareServiceConnectionDelayed(userId); + } } @GuardedBy("mLock") @@ -948,13 +940,17 @@ public final class TvInputManagerService extends SystemService { if (serviceState != null) { serviceState.sessionTokens.remove(sessionToken); } - updateServiceConnectionLocked(sessionState.componentName, userId); + if (!serviceState.isHardware) { + updateServiceConnectionLocked(sessionState.componentName, userId); + } else { + updateHardwareServiceConnectionDelayed(userId); + } // Log the end of watch. SomeArgs args = SomeArgs.obtain(); args.arg1 = sessionToken; args.arg2 = System.currentTimeMillis(); - mWatchLogHandler.obtainMessage(WatchLogHandler.MSG_LOG_WATCH_END, args).sendToTarget(); + mMessageHandler.obtainMessage(MessageHandler.MSG_LOG_WATCH_END, args).sendToTarget(); } @GuardedBy("mLock") @@ -1153,8 +1149,7 @@ public final class TvInputManagerService extends SystemService { ServiceState serviceState = userState.serviceStateMap.get(inputState.info.getComponent()); int oldState = inputState.state; inputState.state = state; - if (serviceState != null && serviceState.service == null - && (!serviceState.sessionTokens.isEmpty() || serviceState.isHardware)) { + if (serviceState != null && serviceState.reconnecting) { // We don't notify state change while reconnecting. It should remain disconnected. return; } @@ -1881,7 +1876,7 @@ public final class TvInputManagerService extends SystemService { args.arg3 = ContentUris.parseId(channelUri); args.arg4 = params; args.arg5 = sessionToken; - mWatchLogHandler.obtainMessage(WatchLogHandler.MSG_LOG_WATCH_START, args) + mMessageHandler.obtainMessage(MessageHandler.MSG_LOG_WATCH_START, args) .sendToTarget(); } catch (RemoteException | SessionNotFoundException e) { Slog.e(TAG, "error in tune", e); @@ -3327,16 +3322,21 @@ public final class TvInputManagerService extends SystemService { private final ComponentName component; private final boolean isHardware; private final Map<String, TvInputInfo> hardwareInputMap = new HashMap<>(); + private final List<TvInputHardwareInfo> hardwareDeviceRemovedBuffer = new ArrayList<>(); + private final List<HdmiDeviceInfo> hdmiDeviceRemovedBuffer = new ArrayList<>(); + private final List<HdmiDeviceInfo> hdmiDeviceUpdatedBuffer = new ArrayList<>(); private ITvInputService service; private ServiceCallback callback; private boolean bound; private boolean reconnecting; + private boolean neverConnected; private ServiceState(ComponentName component, int userId) { this.component = component; this.connection = new InputServiceConnection(component, userId); this.isHardware = hasHardwarePermission(mContext.getPackageManager(), component); + this.neverConnected = true; } } @@ -3449,6 +3449,97 @@ public final class TvInputManagerService extends SystemService { } } + @GuardedBy("mLock") + private void bindService(ServiceState serviceState, int userId) { + if (serviceState.bound) { + // We have already bound to the service so we don't try to bind again until after we + // unbind later on. + // For hardware services, call updateHardwareServiceConnectionDelayed() to delay the + // possible unbinding. + if (serviceState.isHardware) { + updateHardwareServiceConnectionDelayed(userId); + } + return; + } + if (DEBUG) { + Slog.d(TAG, + "bindServiceAsUser(service=" + serviceState.component + ", userId=" + userId + + ")"); + } + Intent i = + new Intent(TvInputService.SERVICE_INTERFACE).setComponent(serviceState.component); + serviceState.bound = mContext.bindServiceAsUser(i, serviceState.connection, + Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE_WHILE_AWAKE, + new UserHandle(userId)); + if (!serviceState.bound) { + Slog.e(TAG, "failed to bind " + serviceState.component + " for userId " + userId); + mContext.unbindService(serviceState.connection); + } + } + + @GuardedBy("mLock") + private void unbindService(ServiceState serviceState) { + if (!serviceState.bound) { + return; + } + if (DEBUG) { + Slog.d(TAG, "unbindService(service=" + serviceState.component + ")"); + } + mContext.unbindService(serviceState.connection); + serviceState.bound = false; + serviceState.service = null; + serviceState.callback = null; + } + + @GuardedBy("mLock") + private void updateHardwareTvInputServiceBindingLocked(int userId) { + PackageManager pm = mContext.getPackageManager(); + List<ResolveInfo> services = + pm.queryIntentServicesAsUser(new Intent(TvInputService.SERVICE_INTERFACE), + PackageManager.GET_SERVICES | PackageManager.GET_META_DATA, userId); + for (ResolveInfo ri : services) { + ServiceInfo si = ri.serviceInfo; + if (!android.Manifest.permission.BIND_TV_INPUT.equals(si.permission)) { + continue; + } + ComponentName component = new ComponentName(si.packageName, si.name); + if (hasHardwarePermission(pm, component)) { + updateServiceConnectionLocked(component, userId); + } + } + } + + private void updateHardwareServiceConnectionDelayed(int userId) { + mMessageHandler.removeMessages(MessageHandler.MSG_UPDATE_HARDWARE_TIS_BINDING); + SomeArgs args = SomeArgs.obtain(); + args.arg1 = userId; + Message msg = + mMessageHandler.obtainMessage(MessageHandler.MSG_UPDATE_HARDWARE_TIS_BINDING, args); + mMessageHandler.sendMessageDelayed(msg, UPDATE_HARDWARE_TIS_BINDING_DELAY_IN_MILLIS); + } + + @GuardedBy("mLock") + private void addHardwareInputLocked( + TvInputInfo inputInfo, ComponentName component, int userId) { + ServiceState serviceState = getServiceStateLocked(component, userId); + serviceState.hardwareInputMap.put(inputInfo.getId(), inputInfo); + buildTvInputListLocked(userId, null); + } + + @GuardedBy("mLock") + private void removeHardwareInputLocked(String inputId, int userId) { + if (!mTvInputHardwareManager.getInputMap().containsKey(inputId)) { + return; + } + ComponentName component = mTvInputHardwareManager.getInputMap().get(inputId).getComponent(); + ServiceState serviceState = getServiceStateLocked(component, userId); + boolean removed = serviceState.hardwareInputMap.remove(inputId) != null; + if (removed) { + buildTvInputListLocked(userId, null); + mTvInputHardwareManager.removeHardwareInput(inputId); + } + } + private final class InputServiceConnection implements ServiceConnection { private final ComponentName mComponent; private final int mUserId; @@ -3472,6 +3563,7 @@ public final class TvInputManagerService extends SystemService { } ServiceState serviceState = userState.serviceStateMap.get(mComponent); serviceState.service = ITvInputService.Stub.asInterface(service); + serviceState.neverConnected = false; // Register a callback, if we need to. if (serviceState.isHardware && serviceState.callback == null) { @@ -3483,19 +3575,6 @@ public final class TvInputManagerService extends SystemService { } } - List<IBinder> tokensToBeRemoved = new ArrayList<>(); - - // And create sessions, if any. - for (IBinder sessionToken : serviceState.sessionTokens) { - if (!createSessionInternalLocked(serviceState.service, sessionToken, mUserId)) { - tokensToBeRemoved.add(sessionToken); - } - } - - for (IBinder sessionToken : tokensToBeRemoved) { - removeSessionStateLocked(sessionToken, mUserId); - } - for (TvInputState inputState : userState.inputMap.values()) { if (inputState.info.getComponent().equals(component) && inputState.state != INPUT_STATE_CONNECTED) { @@ -3505,7 +3584,24 @@ public final class TvInputManagerService extends SystemService { } if (serviceState.isHardware) { - serviceState.hardwareInputMap.clear(); + for (TvInputHardwareInfo hardwareToBeRemoved : + serviceState.hardwareDeviceRemovedBuffer) { + try { + serviceState.service.notifyHardwareRemoved(hardwareToBeRemoved); + } catch (RemoteException e) { + Slog.e(TAG, "error in hardwareDeviceRemovedBuffer", e); + } + } + serviceState.hardwareDeviceRemovedBuffer.clear(); + for (HdmiDeviceInfo hdmiDeviceToBeRemoved : + serviceState.hdmiDeviceRemovedBuffer) { + try { + serviceState.service.notifyHdmiDeviceRemoved(hdmiDeviceToBeRemoved); + } catch (RemoteException e) { + Slog.e(TAG, "error in hdmiDeviceRemovedBuffer", e); + } + } + serviceState.hdmiDeviceRemovedBuffer.clear(); for (TvInputHardwareInfo hardware : mTvInputHardwareManager.getHardwareList()) { try { serviceState.service.notifyHardwareAdded(hardware); @@ -3520,6 +3616,32 @@ public final class TvInputManagerService extends SystemService { Slog.e(TAG, "error in notifyHdmiDeviceAdded", e); } } + for (HdmiDeviceInfo hdmiDeviceToBeUpdated : + serviceState.hdmiDeviceUpdatedBuffer) { + try { + serviceState.service.notifyHdmiDeviceUpdated(hdmiDeviceToBeUpdated); + } catch (RemoteException e) { + Slog.e(TAG, "error in hdmiDeviceUpdatedBuffer", e); + } + } + serviceState.hdmiDeviceUpdatedBuffer.clear(); + } + + List<IBinder> tokensToBeRemoved = new ArrayList<>(); + + // And create sessions, if any. + for (IBinder sessionToken : serviceState.sessionTokens) { + if (!createSessionInternalLocked(serviceState.service, sessionToken, mUserId)) { + tokensToBeRemoved.add(sessionToken); + } + } + + for (IBinder sessionToken : tokensToBeRemoved) { + removeSessionStateLocked(sessionToken, mUserId); + } + + if (serviceState.isHardware) { + updateHardwareServiceConnectionDelayed(mUserId); } } } @@ -3570,13 +3692,6 @@ public final class TvInputManagerService extends SystemService { } } - @GuardedBy("mLock") - private void addHardwareInputLocked(TvInputInfo inputInfo) { - ServiceState serviceState = getServiceStateLocked(mComponent, mUserId); - serviceState.hardwareInputMap.put(inputInfo.getId(), inputInfo); - buildTvInputListLocked(mUserId, null); - } - public void addHardwareInput(int deviceId, TvInputInfo inputInfo) { ensureHardwarePermission(); ensureValidInput(inputInfo); @@ -3587,8 +3702,11 @@ public final class TvInputManagerService extends SystemService { if (serviceState.hardwareInputMap.containsKey(inputInfo.getId())) { return; } + Slog.d("ServiceCallback", + "addHardwareInput: device id " + deviceId + ", " + + inputInfo.toString()); mTvInputHardwareManager.addHardwareInput(deviceId, inputInfo); - addHardwareInputLocked(inputInfo); + addHardwareInputLocked(inputInfo, mComponent, mUserId); } } finally { Binder.restoreCallingIdentity(identity); @@ -3606,7 +3724,7 @@ public final class TvInputManagerService extends SystemService { return; } mTvInputHardwareManager.addHdmiInput(id, inputInfo); - addHardwareInputLocked(inputInfo); + addHardwareInputLocked(inputInfo, mComponent, mUserId); if (mOnScreenInputId != null && mOnScreenSessionState != null) { if (TextUtils.equals(mOnScreenInputId, inputInfo.getParentId())) { // catch the use case when a CEC device is plugged in an HDMI port, @@ -3635,14 +3753,9 @@ public final class TvInputManagerService extends SystemService { final long identity = Binder.clearCallingIdentity(); try { synchronized (mLock) { - ServiceState serviceState = getServiceStateLocked(mComponent, mUserId); - boolean removed = serviceState.hardwareInputMap.remove(inputId) != null; - if (removed) { - buildTvInputListLocked(mUserId, null); - mTvInputHardwareManager.removeHardwareInput(inputId); - } else { - Slog.e(TAG, "failed to remove input " + inputId); - } + Slog.d("ServiceCallback", + "removeHardwareInput " + inputId + " by " + mComponent); + removeHardwareInputLocked(inputId, mUserId); } } finally { Binder.restoreCallingIdentity(identity); @@ -4226,11 +4339,12 @@ public final class TvInputManagerService extends SystemService { return loggedReason; } + @GuardedBy("mLock") private UserState getUserStateLocked(int userId) { return mUserStates.get(userId); } - private static final class WatchLogHandler extends Handler { + private final class MessageHandler extends Handler { // There are only two kinds of watch events that can happen on the system: // 1. The current TV input session is tuned to a new channel. // 2. The session is released for some reason. @@ -4242,10 +4356,11 @@ public final class TvInputManagerService extends SystemService { static final int MSG_LOG_WATCH_START = 1; static final int MSG_LOG_WATCH_END = 2; static final int MSG_SWITCH_CONTENT_RESOLVER = 3; + static final int MSG_UPDATE_HARDWARE_TIS_BINDING = 4; private ContentResolver mContentResolver; - WatchLogHandler(ContentResolver contentResolver, Looper looper) { + MessageHandler(ContentResolver contentResolver, Looper looper) { super(looper); mContentResolver = contentResolver; } @@ -4304,6 +4419,14 @@ public final class TvInputManagerService extends SystemService { mContentResolver = (ContentResolver) msg.obj; break; } + case MSG_UPDATE_HARDWARE_TIS_BINDING: + SomeArgs args = (SomeArgs) msg.obj; + int userId = (int) args.arg1; + synchronized (mLock) { + updateHardwareTvInputServiceBindingLocked(userId); + } + args.recycle(); + break; default: { Slog.w(TAG, "unhandled message code: " + msg.what); break; @@ -4359,29 +4482,46 @@ public final class TvInputManagerService extends SystemService { UserState userState = getOrCreateUserStateLocked(mCurrentUserId); // Broadcast the event to all hardware inputs. for (ServiceState serviceState : userState.serviceStateMap.values()) { - if (!serviceState.isHardware || serviceState.service == null) continue; + if (!serviceState.isHardware) { + continue; + } try { - serviceState.service.notifyHardwareAdded(info); + bindService(serviceState, mCurrentUserId); + if (serviceState.service != null) { + serviceState.service.notifyHardwareAdded(info); + } } catch (RemoteException e) { Slog.e(TAG, "error in notifyHardwareAdded", e); } } + updateHardwareServiceConnectionDelayed(mCurrentUserId); } } @Override public void onHardwareDeviceRemoved(TvInputHardwareInfo info) { synchronized (mLock) { + String relatedInputId = + mTvInputHardwareManager.getHardwareInputIdMap().get(info.getDeviceId()); + removeHardwareInputLocked(relatedInputId, mCurrentUserId); UserState userState = getOrCreateUserStateLocked(mCurrentUserId); // Broadcast the event to all hardware inputs. for (ServiceState serviceState : userState.serviceStateMap.values()) { - if (!serviceState.isHardware || serviceState.service == null) continue; + if (!serviceState.isHardware) { + continue; + } try { - serviceState.service.notifyHardwareRemoved(info); + bindService(serviceState, mCurrentUserId); + if (serviceState.service != null) { + serviceState.service.notifyHardwareRemoved(info); + } else { + serviceState.hardwareDeviceRemovedBuffer.add(info); + } } catch (RemoteException e) { Slog.e(TAG, "error in notifyHardwareRemoved", e); } } + updateHardwareServiceConnectionDelayed(mCurrentUserId); } } @@ -4391,29 +4531,46 @@ public final class TvInputManagerService extends SystemService { UserState userState = getOrCreateUserStateLocked(mCurrentUserId); // Broadcast the event to all hardware inputs. for (ServiceState serviceState : userState.serviceStateMap.values()) { - if (!serviceState.isHardware || serviceState.service == null) continue; + if (!serviceState.isHardware) { + continue; + } try { - serviceState.service.notifyHdmiDeviceAdded(deviceInfo); + bindService(serviceState, mCurrentUserId); + if (serviceState.service != null) { + serviceState.service.notifyHdmiDeviceAdded(deviceInfo); + } } catch (RemoteException e) { Slog.e(TAG, "error in notifyHdmiDeviceAdded", e); } } + updateHardwareServiceConnectionDelayed(mCurrentUserId); } } @Override public void onHdmiDeviceRemoved(HdmiDeviceInfo deviceInfo) { synchronized (mLock) { + String relatedInputId = + mTvInputHardwareManager.getHdmiInputIdMap().get(deviceInfo.getId()); + removeHardwareInputLocked(relatedInputId, mCurrentUserId); UserState userState = getOrCreateUserStateLocked(mCurrentUserId); // Broadcast the event to all hardware inputs. for (ServiceState serviceState : userState.serviceStateMap.values()) { - if (!serviceState.isHardware || serviceState.service == null) continue; + if (!serviceState.isHardware) { + continue; + } try { - serviceState.service.notifyHdmiDeviceRemoved(deviceInfo); + bindService(serviceState, mCurrentUserId); + if (serviceState.service != null) { + serviceState.service.notifyHdmiDeviceRemoved(deviceInfo); + } else { + serviceState.hdmiDeviceRemovedBuffer.add(deviceInfo); + } } catch (RemoteException e) { Slog.e(TAG, "error in notifyHdmiDeviceRemoved", e); } } + updateHardwareServiceConnectionDelayed(mCurrentUserId); } } @@ -4441,13 +4598,21 @@ public final class TvInputManagerService extends SystemService { UserState userState = getOrCreateUserStateLocked(mCurrentUserId); // Broadcast the event to all hardware inputs. for (ServiceState serviceState : userState.serviceStateMap.values()) { - if (!serviceState.isHardware || serviceState.service == null) continue; + if (!serviceState.isHardware) { + continue; + } try { - serviceState.service.notifyHdmiDeviceUpdated(deviceInfo); + bindService(serviceState, mCurrentUserId); + if (serviceState.service != null) { + serviceState.service.notifyHdmiDeviceUpdated(deviceInfo); + } else { + serviceState.hdmiDeviceUpdatedBuffer.add(deviceInfo); + } } catch (RemoteException e) { Slog.e(TAG, "error in notifyHdmiDeviceUpdated", e); } } + updateHardwareServiceConnectionDelayed(mCurrentUserId); } } diff --git a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java index 9c60fbb6bb9a..d7b8495929a2 100644 --- a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java +++ b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java @@ -1038,7 +1038,7 @@ public class TvInteractiveAppManagerService extends SystemService { } } finally { if (surface != null) { - // surface is not used in TvInteractiveAppManagerService. + // surface is not used in TvAdManagerService. surface.release(); } Binder.restoreCallingIdentity(identity); @@ -1070,6 +1070,253 @@ public class TvInteractiveAppManagerService extends SystemService { @Override public void startAdService(IBinder sessionToken, int userId) { + if (DEBUG) { + Slogf.d(TAG, "startAdService(userId=%d)", userId); + } + final int callingUid = Binder.getCallingUid(); + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid, + userId, "startAdService"); + AdSessionState sessionState = null; + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + try { + sessionState = getAdSessionStateLocked(sessionToken, callingUid, + resolvedUserId); + getAdSessionLocked(sessionState).startAdService(); + } catch (RemoteException | SessionNotFoundException e) { + Slogf.e(TAG, "error in start", e); + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void stopAdService(IBinder sessionToken, int userId) { + if (DEBUG) { + Slogf.d(TAG, "stopAdService(userId=%d)", userId); + } + final int callingUid = Binder.getCallingUid(); + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid, + userId, "stopAdService"); + AdSessionState sessionState = null; + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + try { + sessionState = getAdSessionStateLocked(sessionToken, callingUid, + resolvedUserId); + getAdSessionLocked(sessionState).stopAdService(); + } catch (RemoteException | SessionNotFoundException e) { + Slogf.e(TAG, "error in stop", e); + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void resetAdService(IBinder sessionToken, int userId) { + if (DEBUG) { + Slogf.d(TAG, "resetAdService(userId=%d)", userId); + } + final int callingUid = Binder.getCallingUid(); + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid, + userId, "resetAdService"); + AdSessionState sessionState = null; + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + try { + sessionState = getAdSessionStateLocked(sessionToken, callingUid, + resolvedUserId); + getAdSessionLocked(sessionState).resetAdService(); + } catch (RemoteException | SessionNotFoundException e) { + Slogf.e(TAG, "error in reset", e); + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void sendCurrentVideoBounds(IBinder sessionToken, Rect bounds, int userId) { + if (DEBUG) { + Slogf.d(TAG, "sendCurrentVideoBounds(bounds=%s)", bounds.toString()); + } + final int callingUid = Binder.getCallingUid(); + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid, + userId, "sendCurrentVideoBounds"); + AdSessionState sessionState = null; + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + try { + sessionState = getAdSessionStateLocked(sessionToken, callingUid, + resolvedUserId); + getAdSessionLocked(sessionState).sendCurrentVideoBounds(bounds); + } catch (RemoteException | SessionNotFoundException e) { + Slogf.e(TAG, "error in sendCurrentVideoBounds", e); + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void sendCurrentChannelUri(IBinder sessionToken, Uri channelUri, int userId) { + if (DEBUG) { + Slogf.d(TAG, "sendCurrentChannelUri(channelUri=%s)", channelUri.toString()); + } + final int callingUid = Binder.getCallingUid(); + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid, + userId, "sendCurrentChannelUri"); + AdSessionState sessionState = null; + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + try { + sessionState = getAdSessionStateLocked(sessionToken, callingUid, + resolvedUserId); + getAdSessionLocked(sessionState).sendCurrentChannelUri(channelUri); + } catch (RemoteException | SessionNotFoundException e) { + Slogf.e(TAG, "error in sendCurrentChannelUri", e); + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void sendTrackInfoList(IBinder sessionToken, List<TvTrackInfo> tracks, int userId) { + if (DEBUG) { + Slogf.d(TAG, "sendTrackInfoList(tracks=%s)", tracks.toString()); + } + final int callingUid = Binder.getCallingUid(); + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid, + userId, "sendTrackInfoList"); + AdSessionState sessionState = null; + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + try { + sessionState = getAdSessionStateLocked(sessionToken, callingUid, + resolvedUserId); + getAdSessionLocked(sessionState).sendTrackInfoList(tracks); + } catch (RemoteException | SessionNotFoundException e) { + Slogf.e(TAG, "error in sendTrackInfoList", e); + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void sendCurrentTvInputId(IBinder sessionToken, String inputId, int userId) { + if (DEBUG) { + Slogf.d(TAG, "sendCurrentTvInputId(inputId=%s)", inputId); + } + final int callingUid = Binder.getCallingUid(); + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid, + userId, "sendCurrentTvInputId"); + AdSessionState sessionState = null; + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + try { + sessionState = getAdSessionStateLocked(sessionToken, callingUid, + resolvedUserId); + getAdSessionLocked(sessionState).sendCurrentTvInputId(inputId); + } catch (RemoteException | SessionNotFoundException e) { + Slogf.e(TAG, "error in sendCurrentTvInputId", e); + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void sendSigningResult( + IBinder sessionToken, String signingId, byte[] result, int userId) { + if (DEBUG) { + Slogf.d(TAG, "sendSigningResult(signingId=%s)", signingId); + } + final int callingUid = Binder.getCallingUid(); + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid, + userId, "sendSigningResult"); + AdSessionState sessionState = null; + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + try { + sessionState = getAdSessionStateLocked(sessionToken, callingUid, + resolvedUserId); + getAdSessionLocked(sessionState).sendSigningResult(signingId, result); + } catch (RemoteException | SessionNotFoundException e) { + Slogf.e(TAG, "error in sendSigningResult", e); + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void notifyError(IBinder sessionToken, String errMsg, Bundle params, int userId) { + if (DEBUG) { + Slogf.d(TAG, "notifyError(errMsg=%s)", errMsg); + } + final int callingUid = Binder.getCallingUid(); + final int resolvedUserId = + resolveCallingUserId(Binder.getCallingPid(), callingUid, userId, "notifyError"); + AdSessionState sessionState = null; + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + try { + sessionState = + getAdSessionStateLocked(sessionToken, callingUid, resolvedUserId); + getAdSessionLocked(sessionState).notifyError(errMsg, params); + } catch (RemoteException | SessionNotFoundException e) { + Slogf.e(TAG, "error in notifyError", e); + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void notifyTvMessage(IBinder sessionToken, int type, Bundle data, int userId) { + if (DEBUG) { + Slogf.d(TAG, "notifyTvMessage(type=%d)", type); + } + final int callingUid = Binder.getCallingUid(); + final int callingPid = Binder.getCallingPid(); + final int resolvedUserId = resolveCallingUserId(callingPid, callingUid, userId, + "notifyTvMessage"); + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + try { + AdSessionState sessionState = + getAdSessionStateLocked(sessionToken, callingUid, resolvedUserId); + getAdSessionLocked(sessionState).notifyTvMessage(type, data); + } catch (RemoteException | SessionNotFoundException e) { + Slogf.e(TAG, "error in notifyTvMessage", e); + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } } @Override @@ -1106,6 +1353,67 @@ public class TvInteractiveAppManagerService extends SystemService { } } + @Override + public void createMediaView(IBinder sessionToken, IBinder windowToken, Rect frame, + int userId) { + final int callingUid = Binder.getCallingUid(); + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid, + userId, "createMediaView"); + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + try { + getAdSessionLocked(sessionToken, callingUid, resolvedUserId) + .createMediaView(windowToken, frame); + } catch (RemoteException | SessionNotFoundException e) { + Slog.e(TAG, "error in createMediaView", e); + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void relayoutMediaView(IBinder sessionToken, Rect frame, int userId) { + final int callingUid = Binder.getCallingUid(); + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid, + userId, "relayoutMediaView"); + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + try { + getAdSessionLocked(sessionToken, callingUid, resolvedUserId) + .relayoutMediaView(frame); + } catch (RemoteException | SessionNotFoundException e) { + Slog.e(TAG, "error in relayoutMediaView", e); + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + + @Override + public void removeMediaView(IBinder sessionToken, int userId) { + final int callingUid = Binder.getCallingUid(); + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid, + userId, "removeMediaView"); + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + try { + getAdSessionLocked(sessionToken, callingUid, resolvedUserId) + .removeMediaView(); + } catch (RemoteException | SessionNotFoundException e) { + Slog.e(TAG, "error in removeMediaView", e); + } + } + } finally { + Binder.restoreCallingIdentity(identity); + } + } + } private final class BinderService extends ITvInteractiveAppManager.Stub { diff --git a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java index 51acc8e01cda..8549957f46b8 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperCropper.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperCropper.java @@ -410,9 +410,10 @@ public class WallpaperCropper { // adapt the entries in wallpaper.mCropHints for the actual display SparseArray<Rect> updatedCropHints = new SparseArray<>(); for (int i = 0; i < wallpaper.mCropHints.size(); i++) { - Rect defaultCrop = defaultDisplayCrops.valueAt(i); + int orientation = wallpaper.mCropHints.keyAt(i); + Rect defaultCrop = defaultDisplayCrops.get(orientation); if (defaultCrop != null) { - updatedCropHints.put(defaultDisplayCrops.keyAt(i), defaultCrop); + updatedCropHints.put(orientation, defaultCrop); } } wallpaper.mCropHints = updatedCropHints; diff --git a/services/core/java/com/android/server/wearable/WearableSensingManagerService.java b/services/core/java/com/android/server/wearable/WearableSensingManagerService.java index 106be5f124a0..4cc2c025575e 100644 --- a/services/core/java/com/android/server/wearable/WearableSensingManagerService.java +++ b/services/core/java/com/android/server/wearable/WearableSensingManagerService.java @@ -48,6 +48,7 @@ import com.android.server.pm.KnownPackages; import java.io.FileDescriptor; import java.util.Objects; import java.util.Set; +import java.util.function.Consumer; /** * System service for managing sensing {@link AmbientContextEvent}s on Wearables. @@ -191,9 +192,23 @@ public class WearableSensingManagerService extends } } + private void callPerUserServiceIfExist( + Consumer<WearableSensingManagerPerUserService> serviceConsumer, + RemoteCallback statusCallback) { + int userId = UserHandle.getCallingUserId(); + synchronized (mLock) { + WearableSensingManagerPerUserService service = getServiceForUserLocked(userId); + if (service == null) { + Slog.w(TAG, "Service not available for userId " + userId); + WearableSensingManagerPerUserService.notifyStatusCallback(statusCallback, + WearableSensingManager.STATUS_SERVICE_UNAVAILABLE); + return; + } + serviceConsumer.accept(service); + } + } + private final class WearableSensingManagerInternal extends IWearableSensingManager.Stub { - final WearableSensingManagerPerUserService mService = getServiceForUserLocked( - UserHandle.getCallingUserId()); @Override public void provideDataStream( @@ -210,7 +225,9 @@ public class WearableSensingManagerService extends WearableSensingManager.STATUS_SERVICE_UNAVAILABLE); return; } - mService.onProvideDataStream(parcelFileDescriptor, callback); + callPerUserServiceIfExist( + service -> service.onProvideDataStream(parcelFileDescriptor, callback), + callback); } @Override @@ -229,7 +246,9 @@ public class WearableSensingManagerService extends WearableSensingManager.STATUS_SERVICE_UNAVAILABLE); return; } - mService.onProvidedData(data, sharedMemory, callback); + callPerUserServiceIfExist( + service -> service.onProvidedData(data, sharedMemory, callback), + callback); } @Override diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 9b1f9c8441ad..3d492bbd4d37 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -141,6 +141,7 @@ import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_ORIENTATION; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_STARTING_WINDOW; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_STATES; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_SWITCH; +import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_WINDOW_TRANSITIONS_MIN; import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_ASPECT_RATIO; import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_FIXED_ORIENTATION; import static com.android.internal.util.FrameworkStatsLog.APP_COMPAT_STATE_CHANGED__STATE__LETTERBOXED_FOR_SIZE_COMPAT_MODE; @@ -991,6 +992,9 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A private CustomAppTransition mCustomOpenTransition; private CustomAppTransition mCustomCloseTransition; + /** Non-zero to pause dispatching configuration changes to the client. */ + int mPauseConfigurationDispatchCount = 0; + private final Runnable mPauseTimeoutRunnable = new Runnable() { @Override public void run() { @@ -3965,20 +3969,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A return removedFromHistory; } - boolean safelyDestroy(String reason) { - if (isDestroyable()) { - if (DEBUG_SWITCH) { - final Task task = getTask(); - Slog.v(TAG_SWITCH, "Safely destroying " + this + " in state " + getState() - + " resumed=" + task.getTopResumedActivity() - + " pausing=" + task.getTopPausingActivity() - + " for reason " + reason); - } - return destroyImmediately(reason); - } - return false; - } - /** Note: call {@link #cleanUp(boolean, boolean)} before this method. */ void removeFromHistory(String reason) { finishActivityResults(Activity.RESULT_CANCELED, @@ -4047,10 +4037,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } } - boolean isFinishing() { - return finishing; - } - /** * This method is to only be called from the client via binder when the activity is destroyed * AND finished. @@ -7978,6 +7964,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A if (mLetterboxUiController.shouldIgnoreRequestedOrientation(requestedOrientation)) { return; } + final int originalRelaunchingCount = mPendingRelaunchCount; // This is necessary in order to avoid going into size compat mode when the orientation // change request comes from the app if (getRequestedConfigurationOrientation(false, requestedOrientation) @@ -7995,8 +7982,10 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // the request is handled at task level with letterbox. if (!getMergedOverrideConfiguration().equals( mLastReportedConfiguration.getMergedConfiguration())) { - ensureActivityConfiguration( - false /* ignoreVisibility */, true /* isRequestedOrientationChanged */); + ensureActivityConfiguration(false /* ignoreVisibility */); + if (mPendingRelaunchCount > originalRelaunchingCount) { + mLetterboxUiController.setRelaunchingAfterRequestedOrientationChanged(true); + } if (mTransitionController.inPlayingTransition(this)) { mTransitionController.mValidateActivityCompat.add(this); } @@ -9291,6 +9280,59 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } } + @Override + void dispatchConfigurationToChild(WindowState child, Configuration config) { + if (isConfigurationDispatchPaused()) { + return; + } + super.dispatchConfigurationToChild(child, config); + } + + /** + * Pauses dispatch of configuration changes to the client. This includes any + * configuration-triggered lifecycle changes, WindowState configs, and surface changes. If + * a lifecycle change comes from another source (eg. stop), it will still run but will use the + * paused configuration. + * + * The main way this works is by blocking calls to {@link #updateReportedConfigurationAndSend}. + * That method is responsible for evaluating whether the activity needs to be relaunched and + * sending configurations. + */ + void pauseConfigurationDispatch() { + ++mPauseConfigurationDispatchCount; + if (mPauseConfigurationDispatchCount == 1) { + ProtoLog.v(WM_DEBUG_WINDOW_TRANSITIONS_MIN, "Pausing configuration dispatch for " + + " %s", this); + } + } + + /** @return `true` if configuration actually changed. */ + boolean resumeConfigurationDispatch() { + --mPauseConfigurationDispatchCount; + if (mPauseConfigurationDispatchCount > 0) { + return false; + } + ProtoLog.v(WM_DEBUG_WINDOW_TRANSITIONS_MIN, "Resuming configuration dispatch for %s", this); + if (mPauseConfigurationDispatchCount < 0) { + Slog.wtf(TAG, "Trying to resume non-paused configuration dispatch"); + mPauseConfigurationDispatchCount = 0; + return false; + } + if (mLastReportedDisplayId == getDisplayId() + && getConfiguration().equals(mLastReportedConfiguration.getMergedConfiguration())) { + return false; + } + for (int i = getChildCount() - 1; i >= 0; --i) { + dispatchConfigurationToChild(getChildAt(i), getConfiguration()); + } + updateReportedConfigurationAndSend(); + return true; + } + + boolean isConfigurationDispatchPaused() { + return mPauseConfigurationDispatchCount > 0; + } + private boolean applyAspectRatio(Rect outBounds, Rect containingAppBounds, Rect containingBounds) { return applyAspectRatio(outBounds, containingAppBounds, containingBounds, @@ -9502,11 +9544,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A return ensureActivityConfiguration(false /* ignoreVisibility */); } - boolean ensureActivityConfiguration(boolean ignoreVisibility) { - return ensureActivityConfiguration(ignoreVisibility, - false /* isRequestedOrientationChanged */); - } - /** * Make sure the given activity matches the current configuration. Ensures the HistoryRecord * is updated with the correct configuration and all other bookkeeping is handled. @@ -9515,13 +9552,10 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A * (stopped state). This is useful for the case where we know the * activity will be visible soon and we want to ensure its configuration * before we make it visible. - * @param isRequestedOrientationChanged whether this is triggered in response to an app calling - * {@link android.app.Activity#setRequestedOrientation}. * @return False if the activity was relaunched and true if it wasn't relaunched because we * can't or the app handles the specific configuration that is changing. */ - boolean ensureActivityConfiguration(boolean ignoreVisibility, - boolean isRequestedOrientationChanged) { + boolean ensureActivityConfiguration(boolean ignoreVisibility) { final Task rootTask = getRootTask(); if (rootTask.mConfigWillChange) { ProtoLog.v(WM_DEBUG_CONFIGURATION, "Skipping config check " @@ -9548,6 +9582,17 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A return true; } + if (isConfigurationDispatchPaused()) { + return true; + } + + return updateReportedConfigurationAndSend(); + } + + boolean updateReportedConfigurationAndSend() { + if (isConfigurationDispatchPaused()) { + Slog.wtf(TAG, "trying to update reported(client) config while dispatch is paused"); + } ProtoLog.v(WM_DEBUG_CONFIGURATION, "Ensuring correct " + "configuration: %s", this); @@ -9658,9 +9703,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } else { mRelaunchReason = RELAUNCH_REASON_NONE; } - if (isRequestedOrientationChanged) { - mLetterboxUiController.setRelaunchingAfterRequestedOrientationChanged(true); - } if (mState == PAUSING) { // A little annoying: we are waiting for this activity to finish pausing. Let's not // do anything now, but just flag that it needs to be restarted when done pausing. diff --git a/services/core/java/com/android/server/wm/ActivitySecurityModelFeatureFlags.java b/services/core/java/com/android/server/wm/ActivitySecurityModelFeatureFlags.java index f1a2159d6dbe..db27f607c867 100644 --- a/services/core/java/com/android/server/wm/ActivitySecurityModelFeatureFlags.java +++ b/services/core/java/com/android/server/wm/ActivitySecurityModelFeatureFlags.java @@ -43,7 +43,7 @@ class ActivitySecurityModelFeatureFlags { static final String DOC_LINK = "go/android-asm"; /** Used to determine which version of the ASM logic was used in logs while we iterate */ - static final int ASM_VERSION = 8; + static final int ASM_VERSION = 9; private static final String NAMESPACE = NAMESPACE_WINDOW_MANAGER; private static final String KEY_ASM_PREFIX = "ActivitySecurity__"; diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index f6d77ea33598..d6f52b89819e 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -2053,8 +2053,8 @@ class ActivityStarter { } if (!mSupervisor.getBackgroundActivityLaunchController().checkActivityAllowedToStart( - mSourceRecord, r, newTask, targetTask, mLaunchFlags, mBalCode, mCallingUid, - mRealCallingUid)) { + mSourceRecord, r, newTask, avoidMoveToFront(), targetTask, mLaunchFlags, mBalCode, + mCallingUid, mRealCallingUid)) { return START_ABORTED; } diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java index 2bd49bfa6219..a4d15e07a3ed 100644 --- a/services/core/java/com/android/server/wm/BackNavigationController.java +++ b/services/core/java/com/android/server/wm/BackNavigationController.java @@ -19,6 +19,7 @@ package com.android.server.wm; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.view.RemoteAnimationTarget.MODE_CLOSING; import static android.view.RemoteAnimationTarget.MODE_OPENING; +import static android.view.View.FOCUS_FORWARD; import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_OLD_NONE; @@ -60,6 +61,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.policy.TransitionAnimation; import com.android.internal.protolog.common.ProtoLog; import com.android.server.wm.utils.InsetUtils; +import com.android.window.flags.Flags; import java.io.PrintWriter; import java.util.ArrayList; @@ -167,6 +169,24 @@ class BackNavigationController { return null; } + // Move focus to the adjacent embedded window if it is higher than this window + final TaskFragment taskFragment = window.getTaskFragment(); + final TaskFragment adjacentTaskFragment = + taskFragment != null ? taskFragment.getAdjacentTaskFragment() : null; + if (adjacentTaskFragment != null && taskFragment.isEmbedded() + && Flags.embeddedActivityBackNavFlag()) { + final WindowContainer parent = taskFragment.getParent(); + if (parent.mChildren.indexOf(taskFragment) < parent.mChildren.indexOf( + adjacentTaskFragment)) { + mWindowManagerService.moveFocusToAdjacentWindow(window, FOCUS_FORWARD); + window = wmService.getFocusedWindowLocked(); + if (window == null) { + Slog.e(TAG, "Adjacent window is null, returning null."); + return null; + } + } + } + // This is needed to bridge the old and new back behavior with recents. While in // Overview with live tile enabled, the previous app is technically focused but we // add an input consumer to capture all input that would otherwise go to the apps diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java index 0f36d8eafbe4..9ac4a5c4ad5c 100644 --- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java +++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java @@ -34,6 +34,7 @@ import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLAS import static com.android.server.wm.ActivityTaskManagerService.APP_SWITCH_ALLOW; import static com.android.server.wm.ActivityTaskManagerService.APP_SWITCH_FG_ONLY; import static com.android.server.wm.ActivityTaskSupervisor.getApplicationLabel; +import static com.android.window.flags.Flags.balImproveRealCallerVisibilityCheck; import static com.android.window.flags.Flags.balRequireOptInByPendingIntentCreator; import static com.android.window.flags.Flags.balRequireOptInSameUid; import static com.android.window.flags.Flags.balShowToasts; @@ -57,6 +58,7 @@ import android.content.ComponentName; import android.content.Intent; import android.content.pm.PackageManager; import android.os.Process; +import android.os.SystemClock; import android.os.UserHandle; import android.provider.DeviceConfig; import android.util.ArraySet; @@ -69,7 +71,6 @@ import com.android.internal.util.FrameworkStatsLog; import com.android.internal.util.Preconditions; import com.android.server.UiThread; import com.android.server.am.PendingIntentRecord; -import com.android.window.flags.Flags; import java.lang.annotation.Retention; import java.util.HashMap; @@ -274,10 +275,13 @@ public class BackgroundActivityStartController { @BackgroundActivityStartMode int realCallerBackgroundActivityStartMode = checkedOptions.getPendingIntentBackgroundActivityStartMode(); - if (balRequireOptInByPendingIntentCreator() && originatingPendingIntent == null) { - mAutoOptInReason = "notPendingIntent"; - } else if (balRequireOptInByPendingIntentCreator() && mIsCallForResult) { + if (!balImproveRealCallerVisibilityCheck()) { + // without this fix the auto-opt ins below would violate CTS tests + mAutoOptInReason = null; + } else if (mIsCallForResult) { mAutoOptInReason = "callForResult"; + } else if (originatingPendingIntent == null) { + mAutoOptInReason = "notPendingIntent"; } else if (callingUid == realCallingUid && !balRequireOptInSameUid()) { mAutoOptInReason = "sameUid"; } else { @@ -949,7 +953,7 @@ public class BackgroundActivityStartController { // is allowed, or apps like live wallpaper with non app visible window will be allowed. final boolean appSwitchAllowedOrFg = state.mAppSwitchState == APP_SWITCH_ALLOW || state.mAppSwitchState == APP_SWITCH_FG_ONLY; - if (Flags.balImproveRealCallerVisibilityCheck()) { + if (balImproveRealCallerVisibilityCheck()) { if (appSwitchAllowedOrFg && state.mRealCallingUidHasAnyVisibleWindow) { return new BalVerdict(BAL_ALLOW_VISIBLE_WINDOW, /*background*/ false, "realCallingUid has visible window"); @@ -1042,8 +1046,9 @@ public class BackgroundActivityStartController { * create a new task or bring an existing one into the foreground */ boolean checkActivityAllowedToStart(@Nullable ActivityRecord sourceRecord, - @NonNull ActivityRecord targetRecord, boolean newTask, @NonNull Task targetTask, - int launchFlags, int balCode, int callingUid, int realCallingUid) { + @NonNull ActivityRecord targetRecord, boolean newTask, boolean avoidMoveTaskToFront, + @Nullable Task targetTask, int launchFlags, int balCode, int callingUid, + int realCallingUid) { // BAL Exception allowed in all cases if (balCode == BAL_ALLOW_ALLOWLISTED_UID) { return true; @@ -1067,14 +1072,36 @@ public class BackgroundActivityStartController { } if (balCode == BAL_ALLOW_GRACE_PERIOD) { + // Allow if launching into new task, and caller matches most recently finished activity if (taskToFront && mTopFinishedActivity != null && mTopFinishedActivity.mUid == callingUid) { return true; - } else if (!taskToFront) { - FinishedActivityEntry finishedEntry = - mTaskIdToFinishedActivity.get(targetTask.mTaskId); - if (finishedEntry != null && finishedEntry.mUid == callingUid) { - return true; + } + + // Launching into existing task - allow if matches most recently finished activity + // within the task. + // We can reach here multiple ways: + // 1. activity in fg fires intent (taskToFront = false, sourceRecord is available) + // 2. activity in bg fires intent (taskToFront = false, sourceRecord is available) + // 3. activity in bg fires intent with NEW_FLAG (taskToFront = true, + // avoidMoveTaskToFront = true, sourceRecord is available) + // 4. activity in bg fires PI (taskToFront = true, avoidMoveTaskToFront = true, + // sourceRecord is not available, targetTask may be available) + if (!taskToFront || avoidMoveTaskToFront) { + if (targetTask != null) { + FinishedActivityEntry finishedEntry = + mTaskIdToFinishedActivity.get(targetTask.mTaskId); + if (finishedEntry != null && finishedEntry.mUid == callingUid) { + return true; + } + } + + if (sourceRecord != null) { + FinishedActivityEntry finishedEntry = + mTaskIdToFinishedActivity.get(sourceRecord.getTask().mTaskId); + if (finishedEntry != null && finishedEntry.mUid == callingUid) { + return true; + } } } } @@ -1098,7 +1125,7 @@ public class BackgroundActivityStartController { bas = isTopActivityMatchingUidAbsentForAsm(taskToCheck, sourceRecord.getUid(), sourceRecord); } - } else if (!taskToFront) { + } else if (targetTask != null && (!taskToFront || avoidMoveTaskToFront)) { // We don't have a sourceRecord, and we're launching into an existing task. // Allow if callingUid is top of stack. bas = isTopActivityMatchingUidAbsentForAsm(targetTask, callingUid, @@ -1111,12 +1138,14 @@ public class BackgroundActivityStartController { // ASM rules have failed. Log why return logAsmFailureAndCheckFeatureEnabled(sourceRecord, callingUid, realCallingUid, - newTask, targetTask, targetRecord, balCode, launchFlags, bas, taskToFront); + newTask, avoidMoveTaskToFront, targetTask, targetRecord, balCode, launchFlags, + bas, taskToFront); } private boolean logAsmFailureAndCheckFeatureEnabled(ActivityRecord sourceRecord, int callingUid, - int realCallingUid, boolean newTask, Task targetTask, ActivityRecord targetRecord, - @BalCode int balCode, int launchFlags, BlockActivityStart bas, boolean taskToFront) { + int realCallingUid, boolean newTask, boolean avoidMoveTaskToFront, Task targetTask, + ActivityRecord targetRecord, @BalCode int balCode, int launchFlags, + BlockActivityStart bas, boolean taskToFront) { ActivityRecord targetTopActivity = targetTask == null ? null : targetTask.getActivity(ar -> !ar.finishing && !ar.isAlwaysOnTop()); @@ -1133,7 +1162,7 @@ public class BackgroundActivityStartController { String asmDebugInfo = getDebugInfoForActivitySecurity("Launch", sourceRecord, targetRecord, targetTask, targetTopActivity, realCallingUid, balCode, - blockActivityStartAndFeatureEnabled, taskToFront); + blockActivityStartAndFeatureEnabled, taskToFront, avoidMoveTaskToFront); FrameworkStatsLog.write(FrameworkStatsLog.ACTIVITY_ACTION_BLOCKED, /* caller_uid */ @@ -1265,7 +1294,7 @@ public class BackgroundActivityStartController { Slog.i(TAG, getDebugInfoForActivitySecurity("Clear Top", sourceRecord, targetRecord, targetTask, targetTaskTop, realCallingUid, balCode, shouldBlockActivityStart, - /* taskToFront */ true)); + /* taskToFront */ true, /* avoidMoveTaskToFront */ false)); } } @@ -1379,7 +1408,7 @@ public class BackgroundActivityStartController { private BlockActivityStart isTopActivityMatchingUidAbsentForAsm(@NonNull Task task, int uid, @Nullable ActivityRecord sourceRecord) { // If the source is visible, consider it 'top'. - if (sourceRecord != null && sourceRecord.isVisible()) { + if (sourceRecord != null && sourceRecord.isVisibleRequested()) { return new BlockActivityStart(false, false); } @@ -1389,6 +1418,12 @@ public class BackgroundActivityStartController { return new BlockActivityStart(false, false); } + // If UID is visible in target task, allow launch + if (task.forAllActivities((Predicate<ActivityRecord>) + ar -> ar.isUid(uid) && ar.isVisibleRequested())) { + return new BlockActivityStart(false, false); + } + // Consider the source activity, whether or not it is finishing. Do not consider any other // finishing activity. Predicate<ActivityRecord> topOfStackPredicate = (ar) -> ar.equals(sourceRecord) @@ -1480,27 +1515,26 @@ public class BackgroundActivityStartController { @Nullable ActivityRecord sourceRecord, @NonNull ActivityRecord targetRecord, @Nullable Task targetTask, @Nullable ActivityRecord targetTopActivity, int realCallingUid, @BalCode int balCode, - boolean blockActivityStartAndFeatureEnabled, boolean taskToFront) { + boolean blockActivityStartAndFeatureEnabled, boolean taskToFront, + boolean avoidMoveTaskToFront) { final String prefix = "[ASM] "; Function<ActivityRecord, String> recordToString = (ar) -> { if (ar == null) { return null; } - return (ar == sourceRecord ? " [source]=> " + + return (ar == sourceRecord ? " [source]=> " : ar == targetTopActivity ? " [ top ]=> " - : ar == targetRecord ? " [target]=> " - : " => ") - + ar - + " :: visible=" + ar.isVisible() - + ", finishing=" + ar.isFinishing() - + ", alwaysOnTop=" + ar.isAlwaysOnTop() - + ", taskFragment=" + ar.getTaskFragment(); + : ar == targetRecord ? " [target]=> " + : " => ") + + getDebugStringForActivityRecord(ar); }; StringJoiner joiner = new StringJoiner("\n"); joiner.add(prefix + "------ Activity Security " + action + " Debug Logging Start ------"); joiner.add(prefix + "Block Enabled: " + blockActivityStartAndFeatureEnabled); joiner.add(prefix + "ASM Version: " + ActivitySecurityModelFeatureFlags.ASM_VERSION); + joiner.add(prefix + "System Time: " + SystemClock.uptimeMillis()); boolean targetTaskMatchesSourceTask = targetTask != null && sourceRecord != null && sourceRecord.getTask() == targetTask; @@ -1512,6 +1546,8 @@ public class BackgroundActivityStartController { joiner.add(prefix + "Real Calling Uid Package: " + realCallingPackage); } else { joiner.add(prefix + "Source Record: " + recordToString.apply(sourceRecord)); + joiner.add(prefix + "Source Launch Package: " + sourceRecord.launchedFromPackage); + joiner.add(prefix + "Source Launch Intent: " + sourceRecord.intent); if (targetTaskMatchesSourceTask) { joiner.add(prefix + "Source/Target Task: " + sourceRecord.getTask()); joiner.add(prefix + "Source/Target Task Stack: "); @@ -1536,7 +1572,30 @@ public class BackgroundActivityStartController { joiner.add(prefix + "Target Record: " + recordToString.apply(targetRecord)); joiner.add(prefix + "Intent: " + targetRecord.intent); joiner.add(prefix + "TaskToFront: " + taskToFront); + joiner.add(prefix + "AvoidMoveToFront: " + avoidMoveTaskToFront); joiner.add(prefix + "BalCode: " + balCodeToString(balCode)); + joiner.add(prefix + "LastResumedActivity: " + + recordToString.apply(mService.mLastResumedActivity)); + + if (mTopFinishedActivity != null) { + joiner.add(prefix + "TopFinishedActivity: " + mTopFinishedActivity.mDebugInfo); + } + + if (!mTaskIdToFinishedActivity.isEmpty()) { + joiner.add(prefix + "TaskIdToFinishedActivity: "); + mTaskIdToFinishedActivity.values().forEach( + (fae) -> joiner.add(prefix + " " + fae.mDebugInfo)); + } + + if (balCode == BAL_ALLOW_VISIBLE_WINDOW || balCode == BAL_ALLOW_NON_APP_VISIBLE_WINDOW + || balCode == BAL_ALLOW_FOREGROUND) { + Task task = sourceRecord != null ? sourceRecord.getTask() : targetTask; + if (task != null && task.getDisplayArea() != null) { + joiner.add(prefix + "Tasks: "); + task.getDisplayArea().forAllTasks((Consumer<Task>) + t -> joiner.add(prefix + " T: " + t.toFullString())); + } + } joiner.add(prefix + "------ Activity Security " + action + " Debug Logging End ------"); return joiner.toString(); @@ -1620,7 +1679,7 @@ public class BackgroundActivityStartController { return; } - if (!finishActivity.mVisibleRequested + if (!finishActivity.isVisibleRequested() && finishActivity != finishActivity.getTask().getTopMostActivity()) { return; } @@ -1666,10 +1725,22 @@ public class BackgroundActivityStartController { } } + private static String getDebugStringForActivityRecord(ActivityRecord ar) { + return ar + + " :: visible=" + ar.isVisible() + + ", visibleRequested=" + ar.isVisibleRequested() + + ", finishing=" + ar.finishing + + ", alwaysOnTop=" + ar.isAlwaysOnTop() + + ", lastLaunchTime=" + ar.lastLaunchTime + + ", lastVisibleTime=" + ar.lastVisibleTime + + ", taskFragment=" + ar.getTaskFragment(); + } + private class FinishedActivityEntry { int mUid; int mTaskId; int mLaunchCount; + String mDebugInfo; FinishedActivityEntry(ActivityRecord ar) { FinishedActivityEntry entry = mTaskIdToFinishedActivity.get(ar.getTask().mTaskId); @@ -1677,6 +1748,7 @@ public class BackgroundActivityStartController { this.mUid = ar.getUid(); this.mTaskId = taskId; this.mLaunchCount = entry == null || !ar.isUid(entry.mUid) ? 1 : entry.mLaunchCount + 1; + this.mDebugInfo = getDebugStringForActivityRecord(ar); mService.mH.postDelayed(() -> { synchronized (mService.mGlobalLock) { diff --git a/services/core/java/com/android/server/wm/DisplayFrames.java b/services/core/java/com/android/server/wm/DisplayFrames.java index 7f785af1671e..a1799b473c95 100644 --- a/services/core/java/com/android/server/wm/DisplayFrames.java +++ b/services/core/java/com/android/server/wm/DisplayFrames.java @@ -92,35 +92,39 @@ public class DisplayFrames { mRotation = rotation; mWidth = w; mHeight = h; - final Rect unrestricted = mUnrestricted; - unrestricted.set(0, 0, w, h); - state.setDisplayFrame(unrestricted); + final Rect u = mUnrestricted; + u.set(0, 0, w, h); + state.setDisplayFrame(u); state.setDisplayCutout(displayCutout); state.setRoundedCorners(roundedCorners); state.setPrivacyIndicatorBounds(indicatorBounds); state.setDisplayShape(displayShape); state.getDisplayCutoutSafe(safe); - if (safe.left > unrestricted.left) { - state.getOrCreateSource(ID_DISPLAY_CUTOUT_LEFT, displayCutout()).setFrame( - unrestricted.left, unrestricted.top, safe.left, unrestricted.bottom); + if (safe.left > u.left) { + state.getOrCreateSource(ID_DISPLAY_CUTOUT_LEFT, displayCutout()) + .setFrame(u.left, u.top, safe.left, u.bottom) + .updateSideHint(u); } else { state.removeSource(ID_DISPLAY_CUTOUT_LEFT); } - if (safe.top > unrestricted.top) { - state.getOrCreateSource(ID_DISPLAY_CUTOUT_TOP, displayCutout()).setFrame( - unrestricted.left, unrestricted.top, unrestricted.right, safe.top); + if (safe.top > u.top) { + state.getOrCreateSource(ID_DISPLAY_CUTOUT_TOP, displayCutout()) + .setFrame(u.left, u.top, u.right, safe.top) + .updateSideHint(u); } else { state.removeSource(ID_DISPLAY_CUTOUT_TOP); } - if (safe.right < unrestricted.right) { - state.getOrCreateSource(ID_DISPLAY_CUTOUT_RIGHT, displayCutout()).setFrame( - safe.right, unrestricted.top, unrestricted.right, unrestricted.bottom); + if (safe.right < u.right) { + state.getOrCreateSource(ID_DISPLAY_CUTOUT_RIGHT, displayCutout()) + .setFrame(safe.right, u.top, u.right, u.bottom) + .updateSideHint(u); } else { state.removeSource(ID_DISPLAY_CUTOUT_RIGHT); } - if (safe.bottom < unrestricted.bottom) { - state.getOrCreateSource(ID_DISPLAY_CUTOUT_BOTTOM, displayCutout()).setFrame( - unrestricted.left, safe.bottom, unrestricted.right, unrestricted.bottom); + if (safe.bottom < u.bottom) { + state.getOrCreateSource(ID_DISPLAY_CUTOUT_BOTTOM, displayCutout()) + .setFrame(u.left, safe.bottom, u.right, u.bottom) + .updateSideHint(u); } else { state.removeSource(ID_DISPLAY_CUTOUT_BOTTOM); } diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java index 63ca5929e34d..e2bc59bb6550 100644 --- a/services/core/java/com/android/server/wm/DisplayPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayPolicy.java @@ -794,6 +794,9 @@ public class DisplayPolicy { } mService.mAtmService.mKeyguardController.updateDeferTransitionForAod( mAwake /* waiting */); + if (!awake) { + mDisplayContent.mWallpaperController.onDisplaySwitchFinished(); + } } } @@ -836,7 +839,8 @@ public class DisplayPolicy { mRemoteInsetsControllerControlsSystemBars = remoteInsetsControllerControlsSystemBars; } - public void screenTurnedOn(ScreenOnListener screenOnListener) { + /** Prepares to turn on screen. The given listener is used to notify that it is ready. */ + public void screenTurningOn(ScreenOnListener screenOnListener) { WindowProcessController visibleDozeUiProcess = null; synchronized (mLock) { mScreenOnEarly = true; @@ -858,6 +862,11 @@ public class DisplayPolicy { } } + /** It is called after {@link #finishScreenTurningOn}. This runs on PowerManager's thread. */ + public void screenTurnedOn() { + mDisplayContent.mWallpaperController.onDisplaySwitchFinished(); + } + public void screenTurnedOff() { synchronized (mLock) { mScreenOnEarly = false; diff --git a/services/core/java/com/android/server/wm/DragDropController.java b/services/core/java/com/android/server/wm/DragDropController.java index 32d60c5f52e6..6a3cf43438fd 100644 --- a/services/core/java/com/android/server/wm/DragDropController.java +++ b/services/core/java/com/android/server/wm/DragDropController.java @@ -469,8 +469,7 @@ class DragDropController { case MSG_REMOVE_DRAG_SURFACE_TIMEOUT: { synchronized (mService.mGlobalLock) { - mService.mTransactionFactory.get() - .reparent((SurfaceControl) msg.obj, null).apply(); + mService.mTransactionFactory.get().remove((SurfaceControl) msg.obj).apply(); } break; } diff --git a/services/core/java/com/android/server/wm/DragState.java b/services/core/java/com/android/server/wm/DragState.java index adbe3bc1d6b3..d302f0641b58 100644 --- a/services/core/java/com/android/server/wm/DragState.java +++ b/services/core/java/com/android/server/wm/DragState.java @@ -270,7 +270,7 @@ class DragState { } if (mSurfaceControl != null) { if (!mRelinquishDragSurfaceToDropTarget && !relinquishDragSurfaceToDragSource()) { - mTransaction.reparent(mSurfaceControl, null).apply(); + mTransaction.remove(mSurfaceControl).apply(); } else { mDragDropController.sendTimeoutMessage(MSG_REMOVE_DRAG_SURFACE_TIMEOUT, mSurfaceControl, DragDropController.DRAG_TIMEOUT_MS); diff --git a/services/core/java/com/android/server/wm/InsetsSourceProvider.java b/services/core/java/com/android/server/wm/InsetsSourceProvider.java index 9d5ddf3bf264..d9dda4aeb96a 100644 --- a/services/core/java/com/android/server/wm/InsetsSourceProvider.java +++ b/services/core/java/com/android/server/wm/InsetsSourceProvider.java @@ -62,6 +62,8 @@ import java.util.function.Consumer; */ class InsetsSourceProvider { + private static final Rect EMPTY_RECT = new Rect(); + protected final DisplayContent mDisplayContent; protected final @NonNull InsetsSource mSource; protected WindowContainer mWindowContainer; @@ -286,12 +288,15 @@ class InsetsSourceProvider { private void updateSourceFrameForServerVisibility() { // Make sure we set the valid source frame only when server visible is true, because the - // frame may not yet determined that server side doesn't think the window is ready to + // frame may not yet be determined that server side doesn't think the window is ready to // visible. (i.e. No surface, pending insets that were given during layout, etc..) - if (mServerVisible) { - mSource.setFrame(mSourceFrame); - } else { - mSource.setFrame(0, 0, 0, 0); + final Rect frame = mServerVisible ? mSourceFrame : EMPTY_RECT; + if (mSource.getFrame().equals(frame)) { + return; + } + mSource.setFrame(frame); + if (mWindowContainer != null) { + mSource.updateSideHint(mWindowContainer.getBounds()); } } @@ -631,7 +636,7 @@ class InsetsSourceProvider { } pw.print(prefix); pw.print("mIsLeashReadyForDispatching="); pw.print(mIsLeashReadyForDispatching); - pw.print("mHasPendingPosition="); pw.print(mHasPendingPosition); + pw.print(" mHasPendingPosition="); pw.print(mHasPendingPosition); pw.println(); if (mWindowContainer != null) { pw.print(prefix + "mWindowContainer="); diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java index fcc1e5b62221..f2796895d639 100644 --- a/services/core/java/com/android/server/wm/LetterboxUiController.java +++ b/services/core/java/com/android/server/wm/LetterboxUiController.java @@ -148,7 +148,7 @@ import java.util.function.Predicate; final class LetterboxUiController { private static final Predicate<ActivityRecord> FIRST_OPAQUE_NOT_FINISHING_ACTIVITY_PREDICATE = - activityRecord -> activityRecord.fillsParent() && !activityRecord.isFinishing(); + ActivityRecord::occludesParent; private static final String TAG = TAG_WITH_CLASS_NAME ? "LetterboxUiController" : TAG_ATM; diff --git a/services/core/java/com/android/server/wm/RootWindowContainer.java b/services/core/java/com/android/server/wm/RootWindowContainer.java index 02b3f15979ce..587cc7489763 100644 --- a/services/core/java/com/android/server/wm/RootWindowContainer.java +++ b/services/core/java/com/android/server/wm/RootWindowContainer.java @@ -2783,6 +2783,9 @@ class RootWindowContainer extends WindowContainer<DisplayContent> } else { throw new RuntimeException("Create the same sleep token twice: " + token); } + if (isSwappingDisplay) { + display.mWallpaperController.onDisplaySwitchStarted(); + } return token; } diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index 6371bb45ade9..0c6b174b2408 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -369,6 +369,15 @@ class TaskFragment extends WindowContainer<WindowContainer> { */ private boolean mMoveToBottomIfClearWhenLaunch; + /** + * If {@code true}, transitions are allowed even if this TaskFragment is empty. If + * {@code false}, transitions will wait until this TaskFragment becomes non-empty or other + * conditions are met. Default to {@code false}. + * + * @see #isReadyToTransit + */ + private boolean mAllowTransitionWhenEmpty; + /** When set, will force the task to report as invisible. */ static final int FLAG_FORCE_HIDDEN_FOR_PINNED_TASK = 1; static final int FLAG_FORCE_HIDDEN_FOR_TASK_ORG = 1 << 1; @@ -509,6 +518,19 @@ class TaskFragment extends WindowContainer<WindowContainer> { mIsolatedNav = isolatedNav; } + /** + * Sets whether transitions are allowed when the TaskFragment is empty. If {@code true}, + * transitions are allowed when the TaskFragment is empty. If {@code false}, transitions + * will wait until the TaskFragment becomes non-empty or other conditions are met. Default + * to {@code false}. + */ + void setAllowTransitionWhenEmpty(boolean allowTransitionWhenEmpty) { + if (!isEmbedded()) { + return; + } + mAllowTransitionWhenEmpty = allowTransitionWhenEmpty; + } + /** @see #mIsolatedNav */ boolean isIsolatedNav() { return isEmbedded() && mIsolatedNav; @@ -2827,8 +2849,9 @@ class TaskFragment extends WindowContainer<WindowContainer> { return true; } // We don't want to start the transition if the organized TaskFragment is empty, unless - // it is requested to be removed. - if (getTopNonFinishingActivity() != null || mIsRemovalRequested) { + // it is requested to be removed or the mAllowTransitionWhenEmpty flag is true. + if (getTopNonFinishingActivity() != null || mIsRemovalRequested + || mAllowTransitionWhenEmpty) { return true; } // Organizer shouldn't change embedded TaskFragment in PiP. diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java index d68f932400a2..0fc62a758c5e 100644 --- a/services/core/java/com/android/server/wm/WallpaperController.java +++ b/services/core/java/com/android/server/wm/WallpaperController.java @@ -16,6 +16,7 @@ package com.android.server.wm; +import static android.app.WallpaperManager.COMMAND_DISPLAY_SWITCH; import static android.app.WallpaperManager.COMMAND_FREEZE; import static android.app.WallpaperManager.COMMAND_UNFREEZE; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; @@ -120,6 +121,11 @@ class WallpaperController { private boolean mShouldOffsetWallpaperCenter; + /** + * Whether the wallpaper has been notified about a physical display switch event is started. + */ + private volatile boolean mIsWallpaperNotifiedOnDisplaySwitch; + private final Consumer<WindowState> mFindWallpapers = w -> { if (w.mAttrs.type == TYPE_WALLPAPER) { WallpaperWindowToken token = w.mToken.asWallpaperToken(); @@ -1083,6 +1089,52 @@ class WallpaperController { } /** + * Notifies the wallpaper that the display turns off when switching physical device. If the + * wallpaper is currently visible, its client visibility will be preserved until the display is + * confirmed to be off or on. + */ + void onDisplaySwitchStarted() { + mIsWallpaperNotifiedOnDisplaySwitch = notifyDisplaySwitch(true /* start */); + } + + /** + * Called when the screen has finished turning on or the device goes to sleep. This is no-op if + * the operation is not part of a display switch. + */ + void onDisplaySwitchFinished() { + // The method can be called outside WM lock (turned on), so only acquire lock if needed. + // This is to optimize the common cases that regular devices don't have display switch. + if (mIsWallpaperNotifiedOnDisplaySwitch) { + synchronized (mService.mGlobalLock) { + mIsWallpaperNotifiedOnDisplaySwitch = false; + notifyDisplaySwitch(false /* start */); + } + } + } + + private boolean notifyDisplaySwitch(boolean start) { + boolean notified = false; + for (int curTokenNdx = mWallpaperTokens.size() - 1; curTokenNdx >= 0; curTokenNdx--) { + final WallpaperWindowToken token = mWallpaperTokens.get(curTokenNdx); + for (int i = token.getChildCount() - 1; i >= 0; i--) { + final WindowState w = token.getChildAt(i); + if (start && !w.mWinAnimator.getShown()) { + continue; + } + try { + w.mClient.dispatchWallpaperCommand(COMMAND_DISPLAY_SWITCH, 0 /* x */, 0 /* y */, + start ? 1 : 0 /* use z as start or finish */, + null /* bundle */, false /* sync */); + } catch (RemoteException e) { + Slog.w(TAG, "Failed to dispatch COMMAND_DISPLAY_SWITCH " + e); + } + notified = true; + } + } + return notified; + } + + /** * Each window can request a zoom, example: * - User is in overview, zoomed out. * - User also pulls down the shade. diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index bdea1bc40f3a..286182eedf44 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -465,7 +465,7 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< } } final InsetsSource source = new InsetsSource(id, provider.getType()); - source.setFrame(provider.getArbitraryRectangle()); + source.setFrame(provider.getArbitraryRectangle()).updateSideHint(getBounds()); mLocalInsetsSources.put(id, source); mDisplayContent.getInsetsStateController().updateAboveInsetsState(true); } diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index f8ac8da710c8..9650b8bc2281 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -9156,55 +9156,63 @@ public class WindowManagerService extends IWindowManager.Stub if (fromWin == null || !fromWin.isFocused()) { return false; } - final TaskFragment fromFragment = fromWin.getTaskFragment(); - if (fromFragment == null) { - return false; - } - final TaskFragment adjacentFragment = fromFragment.getAdjacentTaskFragment(); - if (adjacentFragment == null || adjacentFragment.asTask() != null) { - // Don't move the focus to another task. - return false; - } - final Rect fromBounds = fromFragment.getBounds(); - final Rect adjacentBounds = adjacentFragment.getBounds(); - switch (direction) { - case View.FOCUS_LEFT: - if (adjacentBounds.left >= fromBounds.left) { - return false; - } - break; - case View.FOCUS_UP: - if (adjacentBounds.top >= fromBounds.top) { - return false; - } - break; - case View.FOCUS_RIGHT: - if (adjacentBounds.right <= fromBounds.right) { - return false; - } - break; - case View.FOCUS_DOWN: - if (adjacentBounds.bottom <= fromBounds.bottom) { - return false; - } - break; - case View.FOCUS_BACKWARD: - case View.FOCUS_FORWARD: - // These are not absolute directions. Skip checking the bounds. - break; - default: + return moveFocusToAdjacentWindow(fromWin, direction); + } + } + + boolean moveFocusToAdjacentWindow(WindowState fromWin, @FocusDirection int direction) { + final TaskFragment fromFragment = fromWin.getTaskFragment(); + if (fromFragment == null) { + return false; + } + final TaskFragment adjacentFragment = fromFragment.getAdjacentTaskFragment(); + if (adjacentFragment == null || adjacentFragment.asTask() != null) { + // Don't move the focus to another task. + return false; + } + if (adjacentFragment.isIsolatedNav()) { + // Don't move the focus if the adjacent TF is isolated navigation. + return false; + } + final Rect fromBounds = fromFragment.getBounds(); + final Rect adjacentBounds = adjacentFragment.getBounds(); + switch (direction) { + case View.FOCUS_LEFT: + if (adjacentBounds.left >= fromBounds.left) { return false; - } - final ActivityRecord topRunningActivity = adjacentFragment.topRunningActivity( - true /* focusableOnly */); - if (topRunningActivity == null) { - return false; - } - moveDisplayToTopInternal(topRunningActivity.getDisplayId()); - handleTaskFocusChange(topRunningActivity.getTask(), topRunningActivity); - if (fromWin.isFocused()) { + } + break; + case View.FOCUS_UP: + if (adjacentBounds.top >= fromBounds.top) { + return false; + } + break; + case View.FOCUS_RIGHT: + if (adjacentBounds.right <= fromBounds.right) { + return false; + } + break; + case View.FOCUS_DOWN: + if (adjacentBounds.bottom <= fromBounds.bottom) { + return false; + } + break; + case View.FOCUS_BACKWARD: + case View.FOCUS_FORWARD: + // These are not absolute directions. Skip checking the bounds. + break; + default: return false; - } + } + final ActivityRecord topRunningActivity = adjacentFragment.topRunningActivity( + true /* focusableOnly */); + if (topRunningActivity == null) { + return false; + } + moveDisplayToTopInternal(topRunningActivity.getDisplayId()); + handleTaskFocusChange(topRunningActivity.getTask(), topRunningActivity); + if (fromWin.isFocused()) { + return false; } return true; } diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index 205ed977f316..4ba52e4c0fd7 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -2202,6 +2202,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub } final TaskFragment taskFragment = new TaskFragment(mService, creationParams.getFragmentToken(), true /* createdByOrganizer */); + taskFragment.setAllowTransitionWhenEmpty(creationParams.getAllowTransitionWhenEmpty()); // Set task fragment organizer immediately, since it might have to be notified about further // actions. TaskFragmentOrganizerToken organizerToken = creationParams.getOrganizer(); diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 56f2bc3d3e3b..7ad87ed8f094 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -5187,6 +5187,11 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP if (mSurfaceControl == null) { return; } + if (mActivityRecord != null && mActivityRecord.isConfigurationDispatchPaused()) { + // Don't update surface-position while dispatch paused. This is calculated from + // the server-side activity configuration so return early. + return; + } if ((mWmService.mWindowPlacerLocked.isLayoutDeferred() || isGoneForLayout()) && !mSurfacePlacementNeeded) { diff --git a/services/core/java/com/android/server/wm/WindowToken.java b/services/core/java/com/android/server/wm/WindowToken.java index 5048cef3da1b..13e1ba785b87 100644 --- a/services/core/java/com/android/server/wm/WindowToken.java +++ b/services/core/java/com/android/server/wm/WindowToken.java @@ -639,9 +639,12 @@ class WindowToken extends WindowContainer<WindowState> { @Override void updateSurfacePosition(SurfaceControl.Transaction t) { + final ActivityRecord r = asActivityRecord(); + if (r != null && r.isConfigurationDispatchPaused()) { + return; + } super.updateSurfacePosition(t); if (!mTransitionController.isShellTransitionsEnabled() && isFixedRotationTransforming()) { - final ActivityRecord r = asActivityRecord(); final Task rootTask = r != null ? r.getRootTask() : null; // Don't transform the activity in PiP because the PiP task organizer will handle it. if (rootTask == null || !rootTask.inPinnedWindowingMode()) { diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp index 2049331e8efe..8bc41af8af62 100644 --- a/services/core/jni/com_android_server_input_InputManagerService.cpp +++ b/services/core/jni/com_android_server_input_InputManagerService.cpp @@ -281,6 +281,7 @@ public: void displayRemoved(JNIEnv* env, int32_t displayId); void setFocusedApplication(JNIEnv* env, int32_t displayId, jobject applicationHandleObj); void setFocusedDisplay(int32_t displayId); + void setMinTimeBetweenUserActivityPokes(int64_t intervalMillis); void setInputDispatchMode(bool enabled, bool frozen); void setSystemUiLightsOut(bool lightsOut); void setPointerDisplayId(int32_t displayId); @@ -1169,6 +1170,11 @@ void NativeInputManager::setFocusedDisplay(int32_t displayId) { mInputManager->getDispatcher().setFocusedDisplay(displayId); } +void NativeInputManager::setMinTimeBetweenUserActivityPokes(int64_t intervalMillis) { + mInputManager->getDispatcher().setMinTimeBetweenUserActivityPokes( + std::chrono::milliseconds(intervalMillis)); +} + void NativeInputManager::setInputDispatchMode(bool enabled, bool frozen) { mInputManager->getDispatcher().setInputDispatchMode(enabled, frozen); } @@ -2122,6 +2128,13 @@ static void nativeSetFocusedDisplay(JNIEnv* env, jobject nativeImplObj, jint dis im->setFocusedDisplay(displayId); } +static void nativeSetUserActivityPokeInterval(JNIEnv* env, jobject nativeImplObj, + jlong intervalMillis) { + NativeInputManager* im = getNativeInputManager(env, nativeImplObj); + + im->setMinTimeBetweenUserActivityPokes(intervalMillis); +} + static void nativeRequestPointerCapture(JNIEnv* env, jobject nativeImplObj, jobject tokenObj, jboolean enabled) { NativeInputManager* im = getNativeInputManager(env, nativeImplObj); @@ -2805,6 +2818,7 @@ static const JNINativeMethod gInputManagerMethods[] = { {"setFocusedApplication", "(ILandroid/view/InputApplicationHandle;)V", (void*)nativeSetFocusedApplication}, {"setFocusedDisplay", "(I)V", (void*)nativeSetFocusedDisplay}, + {"setMinTimeBetweenUserActivityPokes", "(J)V", (void*)nativeSetUserActivityPokeInterval}, {"requestPointerCapture", "(Landroid/os/IBinder;Z)V", (void*)nativeRequestPointerCapture}, {"setInputDispatchMode", "(ZZ)V", (void*)nativeSetInputDispatchMode}, {"setSystemUiLightsOut", "(Z)V", (void*)nativeSetSystemUiLightsOut}, diff --git a/services/core/jni/com_android_server_location_GnssLocationProvider.cpp b/services/core/jni/com_android_server_location_GnssLocationProvider.cpp index 11c40d7bcd9b..9c033e25c04e 100644 --- a/services/core/jni/com_android_server_location_GnssLocationProvider.cpp +++ b/services/core/jni/com_android_server_location_GnssLocationProvider.cpp @@ -675,7 +675,8 @@ static jboolean android_location_gnss_hal_GnssNative_start_measurement_collectio options.enableCorrVecOutputs = enableCorrVecOutputs; options.intervalMs = intervalMs; - return gnssMeasurementIface->setCallback(std::make_unique<gnss::GnssMeasurementCallback>(), + return gnssMeasurementIface->setCallback(std::make_unique<gnss::GnssMeasurementCallback>( + gnssMeasurementIface->getInterfaceVersion()), options); } diff --git a/services/core/jni/com_android_server_power_PowerManagerService.cpp b/services/core/jni/com_android_server_power_PowerManagerService.cpp index 6ab98feff210..d0b290c05ee9 100644 --- a/services/core/jni/com_android_server_power_PowerManagerService.cpp +++ b/services/core/jni/com_android_server_power_PowerManagerService.cpp @@ -31,6 +31,7 @@ #include <android_runtime/AndroidRuntime.h> #include <android_runtime/Log.h> #include <binder/IServiceManager.h> +#include <com_android_input_flags.h> #include <gui/SurfaceComposerClient.h> #include <hardware_legacy/power.h> #include <hidl/ServiceManagement.h> @@ -109,10 +110,12 @@ void android_server_PowerManagerService_userActivity(nsecs_t eventTime, int32_t eventTime = now; } - if (gLastEventTime[eventType] + MIN_TIME_BETWEEN_USERACTIVITIES > eventTime) { - return; + if (!com::android::input::flags::rate_limit_user_activity_poke_in_dispatcher()) { + if (gLastEventTime[eventType] + MIN_TIME_BETWEEN_USERACTIVITIES > eventTime) { + return; + } + gLastEventTime[eventType] = eventTime; } - gLastEventTime[eventType] = eventTime; // Tell the power HAL when user activity occurs. setPowerBoost(Boost::INTERACTION, 0); @@ -285,9 +288,11 @@ int register_android_server_PowerManagerService(JNIEnv* env) { GET_METHOD_ID(gPowerManagerServiceClassInfo.userActivityFromNative, clazz, "userActivityFromNative", "(JIII)V"); - // Initialize - for (int i = 0; i <= USER_ACTIVITY_EVENT_LAST; i++) { - gLastEventTime[i] = LLONG_MIN; + if (!com::android::input::flags::rate_limit_user_activity_poke_in_dispatcher()) { + // Initialize + for (int i = 0; i <= USER_ACTIVITY_EVENT_LAST; i++) { + gLastEventTime[i] = LLONG_MIN; + } } gPowerManagerServiceObj = NULL; return 0; diff --git a/services/core/jni/gnss/Gnss.cpp b/services/core/jni/gnss/Gnss.cpp index 8934c3a6abde..da8928b5f97f 100644 --- a/services/core/jni/gnss/Gnss.cpp +++ b/services/core/jni/gnss/Gnss.cpp @@ -196,7 +196,8 @@ void GnssHal::linkToDeath() { jboolean GnssHal::setCallback() { if (gnssHalAidl != nullptr) { - sp<IGnssCallbackAidl> gnssCbIfaceAidl = new GnssCallbackAidl(); + sp<IGnssCallbackAidl> gnssCbIfaceAidl = + new GnssCallbackAidl(gnssHalAidl->getInterfaceVersion()); auto status = gnssHalAidl->setCallback(gnssCbIfaceAidl); if (!checkAidlStatus(status, "IGnssAidl setCallback() failed.")) { return JNI_FALSE; diff --git a/services/core/jni/gnss/GnssCallback.cpp b/services/core/jni/gnss/GnssCallback.cpp index 60eed8e6d716..3d598f7a7203 100644 --- a/services/core/jni/gnss/GnssCallback.cpp +++ b/services/core/jni/gnss/GnssCallback.cpp @@ -120,7 +120,7 @@ void Gnss_class_init_once(JNIEnv* env, jclass& clazz) { Status GnssCallbackAidl::gnssSetCapabilitiesCb(const int capabilities) { ALOGD("%s: %du\n", __func__, capabilities); - bool isAdrCapabilityKnown = (getInterfaceVersion() >= 3) ? true : false; + bool isAdrCapabilityKnown = (interfaceVersion >= 3) ? true : false; JNIEnv* env = getJniEnv(); env->CallVoidMethod(mCallbacksObj, method_setTopHalCapabilities, capabilities, isAdrCapabilityKnown); @@ -178,7 +178,7 @@ Status GnssCallbackAidl::gnssLocationCb(const hardware::gnss::GnssLocation& loca Status GnssCallbackAidl::gnssNmeaCb(const int64_t timestamp, const std::string& nmea) { // In AIDL v1, if no listener is registered, do not report nmea to the framework. - if (getInterfaceVersion() <= 1) { + if (interfaceVersion <= 1) { if (!isNmeaRegistered) { return Status::ok(); } diff --git a/services/core/jni/gnss/GnssCallback.h b/services/core/jni/gnss/GnssCallback.h index 33acec8b5660..0622e533e5b5 100644 --- a/services/core/jni/gnss/GnssCallback.h +++ b/services/core/jni/gnss/GnssCallback.h @@ -60,6 +60,7 @@ void Gnss_class_init_once(JNIEnv* env, jclass& clazz); */ class GnssCallbackAidl : public hardware::gnss::BnGnssCallback { public: + GnssCallbackAidl(int version) : interfaceVersion(version){}; binder::Status gnssSetCapabilitiesCb(const int capabilities) override; binder::Status gnssSetSignalTypeCapabilitiesCb( const std::vector<android::hardware::gnss::GnssSignalType>& signalTypes) override; @@ -73,6 +74,9 @@ public: binder::Status gnssRequestTimeCb() override; binder::Status gnssRequestLocationCb(const bool independentFromGnss, const bool isUserEmergency) override; + +private: + const int interfaceVersion; }; /* diff --git a/services/core/jni/gnss/GnssMeasurement.h b/services/core/jni/gnss/GnssMeasurement.h index 7a95db8ed7b6..20400fd8f587 100644 --- a/services/core/jni/gnss/GnssMeasurement.h +++ b/services/core/jni/gnss/GnssMeasurement.h @@ -41,6 +41,7 @@ public: const std::unique_ptr<GnssMeasurementCallback>& callback, const android::hardware::gnss::IGnssMeasurementInterface::Options& options) = 0; virtual jboolean close() = 0; + virtual int getInterfaceVersion() = 0; }; class GnssMeasurement : public GnssMeasurementInterface { @@ -50,6 +51,9 @@ public: const std::unique_ptr<GnssMeasurementCallback>& callback, const android::hardware::gnss::IGnssMeasurementInterface::Options& options) override; jboolean close() override; + int getInterfaceVersion() override { + return mIGnssMeasurement->getInterfaceVersion(); + } private: const sp<android::hardware::gnss::IGnssMeasurementInterface> mIGnssMeasurement; @@ -63,6 +67,9 @@ public: const std::unique_ptr<GnssMeasurementCallback>& callback, const android::hardware::gnss::IGnssMeasurementInterface::Options& options) override; jboolean close() override; + int getInterfaceVersion() override { + return 0; + } private: const sp<android::hardware::gnss::V1_0::IGnssMeasurement> mIGnssMeasurement_V1_0; diff --git a/services/core/jni/gnss/GnssMeasurementCallback.cpp b/services/core/jni/gnss/GnssMeasurementCallback.cpp index 2982546bfa47..ebab4c342ca0 100644 --- a/services/core/jni/gnss/GnssMeasurementCallback.cpp +++ b/services/core/jni/gnss/GnssMeasurementCallback.cpp @@ -392,7 +392,7 @@ void GnssMeasurementCallbackAidl::translateAndSetGnssData(const GnssData& data) jobjectArray gnssAgcArray = nullptr; gnssAgcArray = translateAllGnssAgcs(env, data.gnssAgcs); - if (this->getInterfaceVersion() >= 3) { + if (interfaceVersion >= 3) { setMeasurementData(env, mCallbacksObj, clock, measurementArray, gnssAgcArray, /*hasIsFullTracking=*/true, data.isFullTracking); } else { @@ -467,7 +467,7 @@ void GnssMeasurementCallbackAidl::translateSingleGnssMeasurement(JNIEnv* env, satellitePvt.tropoDelayMeters); } - if (this->getInterfaceVersion() >= 2) { + if (interfaceVersion >= 2) { callObjectMethodIgnoringResult(env, satellitePvtBuilderObject, method_satellitePvtBuilderSetTimeOfClock, satellitePvt.timeOfClockSeconds); diff --git a/services/core/jni/gnss/GnssMeasurementCallback.h b/services/core/jni/gnss/GnssMeasurementCallback.h index b3de486e6fa9..3cb47ce2fe18 100644 --- a/services/core/jni/gnss/GnssMeasurementCallback.h +++ b/services/core/jni/gnss/GnssMeasurementCallback.h @@ -53,7 +53,8 @@ void setMeasurementData(JNIEnv* env, jobject& callbacksObj, jobject clock, class GnssMeasurementCallbackAidl : public hardware::gnss::BnGnssMeasurementCallback { public: - GnssMeasurementCallbackAidl() : mCallbacksObj(getCallbacksObj()) {} + GnssMeasurementCallbackAidl(int version) + : mCallbacksObj(getCallbacksObj()), interfaceVersion(version) {} android::binder::Status gnssMeasurementCb(const hardware::gnss::GnssData& data) override; private: @@ -71,6 +72,7 @@ private: void translateGnssClock(JNIEnv* env, const hardware::gnss::GnssData& data, JavaObject& object); jobject& mCallbacksObj; + const int interfaceVersion; }; /* @@ -110,10 +112,10 @@ private: class GnssMeasurementCallback { public: - GnssMeasurementCallback() {} + GnssMeasurementCallback(int version) : interfaceVersion(version) {} sp<GnssMeasurementCallbackAidl> getAidl() { if (callbackAidl == nullptr) { - callbackAidl = sp<GnssMeasurementCallbackAidl>::make(); + callbackAidl = sp<GnssMeasurementCallbackAidl>::make(interfaceVersion); } return callbackAidl; } @@ -128,6 +130,7 @@ public: private: sp<GnssMeasurementCallbackAidl> callbackAidl; sp<GnssMeasurementCallbackHidl> callbackHidl; + const int interfaceVersion; }; template <class T> diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PersonalAppsSuspensionHelper.java b/services/devicepolicy/java/com/android/server/devicepolicy/PersonalAppsSuspensionHelper.java index 532823ad8367..e8c5658ca941 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/PersonalAppsSuspensionHelper.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/PersonalAppsSuspensionHelper.java @@ -17,6 +17,7 @@ package com.android.server.devicepolicy; import static android.accessibilityservice.AccessibilityServiceInfo.FEEDBACK_ALL_MASK; +import static android.app.admin.flags.Flags.defaultSmsPersonalAppSuspensionFixEnabled; import android.accessibilityservice.AccessibilityServiceInfo; import android.annotation.Nullable; @@ -42,6 +43,7 @@ import android.view.accessibility.IAccessibilityManager; import android.view.inputmethod.InputMethodInfo; import com.android.internal.R; +import com.android.internal.telephony.SmsApplication; import com.android.server.inputmethod.InputMethodManagerInternal; import com.android.server.utils.Slogf; @@ -97,7 +99,7 @@ public final class PersonalAppsSuspensionHelper { result.removeAll(getSystemLauncherPackages()); result.removeAll(getAccessibilityServices()); result.removeAll(getInputMethodPackages()); - result.remove(Telephony.Sms.getDefaultSmsPackage(mContext)); + result.remove(getDefaultSmsPackage()); result.remove(getSettingsPackageName()); final String[] unsuspendablePackages = @@ -202,6 +204,17 @@ public final class PersonalAppsSuspensionHelper { return resolveInfos != null && !resolveInfos.isEmpty(); } + private String getDefaultSmsPackage() { + //TODO(b/319449037): Unflag the following change. + if (defaultSmsPersonalAppSuspensionFixEnabled()) { + return SmsApplication.getDefaultSmsApplicationAsUser( + mContext, /*updateIfNeeded=*/ false, mContext.getUser()) + .getPackageName(); + } else { + return Telephony.Sms.getDefaultSmsPackage(mContext); + } + } + void dump(IndentingPrintWriter pw) { pw.println("PersonalAppsSuspensionHelper"); @@ -212,7 +225,7 @@ public final class PersonalAppsSuspensionHelper { DevicePolicyManagerService.dumpApps(pw, "accessibility services", getAccessibilityServices()); DevicePolicyManagerService.dumpApps(pw, "input method packages", getInputMethodPackages()); - pw.printf("SMS package: %s\n", Telephony.Sms.getDefaultSmsPackage(mContext)); + pw.printf("SMS package: %s\n", getDefaultSmsPackage()); pw.printf("Settings package: %s\n", getSettingsPackageName()); DevicePolicyManagerService.dumpApps(pw, "Packages subject to suspension", getPersonalAppsForSuspension()); diff --git a/services/permission/java/com/android/server/permission/access/permission/DevicePermissionPolicy.kt b/services/permission/java/com/android/server/permission/access/permission/DevicePermissionPolicy.kt index a0fb0138e5e5..24d49523b9d1 100644 --- a/services/permission/java/com/android/server/permission/access/permission/DevicePermissionPolicy.kt +++ b/services/permission/java/com/android/server/permission/access/permission/DevicePermissionPolicy.kt @@ -94,7 +94,9 @@ class DevicePermissionPolicy : SchemePolicy() { isSystemUpdated: Boolean ) { packageNames.forEachIndexed { _, packageName -> - val packageState = newState.externalState.packageStates[packageName]!! + // The package may still be removed even if it was once notified as installed. + val packageState = newState.externalState.packageStates[packageName] + ?: return@forEachIndexed trimPermissionStates(packageState.appId) } } @@ -245,6 +247,13 @@ class DevicePermissionPolicy : SchemePolicy() { flagMask: Int, flagValues: Int ): Boolean { + if (userId !in newState.userStates) { + // Despite that we check UserManagerInternal.exists() in PermissionService, we may still + // sometimes get race conditions between that check and the actual mutateState() call. + // This should rarely happen but at least we should not crash. + Slog.e(LOG_TAG, "Unable to update permission flags for missing user $userId") + return false + } val oldFlags = newState.userStates[userId]!! .appIdDevicePermissionFlags[appId] diff --git a/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt b/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt index f469ab547763..097d73a9a05b 100644 --- a/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt +++ b/services/permission/java/com/android/server/permission/access/permission/PermissionService.kt @@ -960,8 +960,8 @@ class PermissionService(private val service: AccessCheckingService) : if (permissionName !in androidPackage.requestedPermissions && oldFlags == 0) { if (reportError) { - throw SecurityException( - "Permission $permissionName isn't requested by package $packageName" + Slog.e( + LOG_TAG, "Permission $permissionName isn't requested by package $packageName" ) } return diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/AdditionalSubtypeUtilsTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/AdditionalSubtypeUtilsTest.java index 23e3e2560524..0edb3dfc0bc0 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/AdditionalSubtypeUtilsTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/AdditionalSubtypeUtilsTest.java @@ -55,7 +55,7 @@ public final class AdditionalSubtypeUtilsTest { // Save & load. AtomicFile atomicFile = new AtomicFile( new File(InstrumentationRegistry.getContext().getCacheDir(), "subtypes.xml")); - AdditionalSubtypeUtils.saveToFile(allSubtypes, methodMap, atomicFile); + AdditionalSubtypeUtils.saveToFile(allSubtypes, InputMethodMap.of(methodMap), atomicFile); ArrayMap<String, List<InputMethodSubtype>> loadedSubtypes = new ArrayMap<>(); AdditionalSubtypeUtils.loadFromFile(loadedSubtypes, atomicFile); diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceRestrictImeAmountTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceRestrictImeAmountTest.java index 570132f5a7d8..71752ba3b393 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceRestrictImeAmountTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceRestrictImeAmountTest.java @@ -126,11 +126,9 @@ public class InputMethodManagerServiceRestrictImeAmountTest extends List<String> enabledComponents) { final ArrayMap<String, List<InputMethodSubtype>> emptyAdditionalSubtypeMap = new ArrayMap<>(); - final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); - final ArrayList<InputMethodInfo> methodList = new ArrayList<>(); - InputMethodManagerService.filterInputMethodServices(emptyAdditionalSubtypeMap, methodMap, - methodList, enabledComponents, mContext, resolveInfoList); - return methodList; + final InputMethodMap methodMap = InputMethodManagerService.filterInputMethodServices( + emptyAdditionalSubtypeMap, enabledComponents, mContext, resolveInfoList); + return methodMap.values(); } private ResolveInfo createFakeSystemResolveInfo(String packageName, String componentName) { diff --git a/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodManagerServiceTests.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTests.java index fd65807a1976..a33e52f0dd75 100644 --- a/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodManagerServiceTests.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceTests.java @@ -25,23 +25,14 @@ import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFI import static org.junit.Assert.assertEquals; import static org.junit.Assert.fail; -import android.platform.test.annotations.Presubmit; - -import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; - import com.android.internal.inputmethod.SoftInputShowHideReason; import org.junit.Test; -import org.junit.runner.RunWith; import java.io.PrintWriter; import java.io.StringWriter; -@Presubmit -@SmallTest -@RunWith(AndroidJUnit4.class) -public class InputMethodManagerServiceTests { +public final class InputMethodManagerServiceTests { static final int SYSTEM_DECORATION_SUPPORT_DISPLAY_ID = 2; static final int NO_SYSTEM_DECORATION_SUPPORT_DISPLAY_ID = 3; diff --git a/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodSettingsTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSettingsTest.java index a55d1c409fb6..75118ead888a 100644 --- a/services/tests/servicestests/src/com/android/server/inputmethod/InputMethodSettingsTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSettingsTest.java @@ -22,14 +22,9 @@ import android.text.TextUtils; import android.util.IntArray; import androidx.annotation.NonNull; -import androidx.test.filters.SmallTest; -import androidx.test.runner.AndroidJUnit4; import org.junit.Test; -import org.junit.runner.RunWith; -@SmallTest -@RunWith(AndroidJUnit4.class) public final class InputMethodSettingsTest { private static void verifyUpdateEnabledImeString(@NonNull String expectedEnabledImeStr, @NonNull String initialEnabledImeStr, @NonNull String imeId, diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodUtilsTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodUtilsTest.java index ac485be5b5a1..2857619c70d3 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodUtilsTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodUtilsTest.java @@ -269,7 +269,7 @@ public final class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(LOCALE_EN_US), imi); assertEquals(1, result.size()); verifyEquality(autoSubtype, result.get(0)); @@ -293,7 +293,7 @@ public final class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(LOCALE_EN_US), imi); assertEquals(2, result.size()); verifyEquality(nonAutoEnUS, result.get(0)); @@ -317,7 +317,7 @@ public final class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(LOCALE_EN_GB), imi); assertEquals(2, result.size()); verifyEquality(nonAutoEnGB, result.get(0)); @@ -342,7 +342,7 @@ public final class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(LOCALE_FR), imi); assertEquals(2, result.size()); verifyEquality(nonAutoFrCA, result.get(0)); @@ -363,7 +363,7 @@ public final class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(LOCALE_FR_CA), imi); assertEquals(2, result.size()); verifyEquality(nonAutoFrCA, result.get(0)); @@ -385,7 +385,7 @@ public final class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(LOCALE_JA_JP), imi); assertEquals(3, result.size()); verifyEquality(nonAutoJa, result.get(0)); @@ -407,7 +407,7 @@ public final class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(LOCALE_JA_JP), imi); assertEquals(1, result.size()); verifyEquality(nonAutoHi, result.get(0)); @@ -424,7 +424,7 @@ public final class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(LOCALE_JA_JP), imi); assertEquals(1, result.size()); verifyEquality(nonAutoEnUS, result.get(0)); @@ -441,7 +441,7 @@ public final class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(LOCALE_JA_JP), imi); assertEquals(1, result.size()); verifyEquality(nonAutoEnUS, result.get(0)); @@ -463,7 +463,7 @@ public final class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(Locale.forLanguageTag("sr-Latn-RS")), imi); assertEquals(2, result.size()); assertThat(nonAutoSrLatn, is(in(result))); @@ -483,7 +483,7 @@ public final class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(Locale.forLanguageTag("sr-Cyrl-RS")), imi); assertEquals(2, result.size()); assertThat(nonAutoSrCyrl, is(in(result))); @@ -509,7 +509,7 @@ public final class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList( Locale.forLanguageTag("sr-Latn-RS-x-android"), Locale.forLanguageTag("ja-JP"), @@ -536,7 +536,7 @@ public final class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(LOCALE_FIL_PH), imi); assertEquals(1, result.size()); verifyEquality(nonAutoFil, result.get(0)); @@ -554,7 +554,7 @@ public final class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(LOCALE_FI), imi); assertEquals(1, result.size()); verifyEquality(nonAutoJa, result.get(0)); @@ -570,7 +570,7 @@ public final class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(LOCALE_IN), imi); assertEquals(1, result.size()); verifyEquality(nonAutoIn, result.get(0)); @@ -584,7 +584,7 @@ public final class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(LOCALE_ID), imi); assertEquals(1, result.size()); verifyEquality(nonAutoIn, result.get(0)); @@ -598,7 +598,7 @@ public final class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(LOCALE_IN), imi); assertEquals(1, result.size()); verifyEquality(nonAutoId, result.get(0)); @@ -612,7 +612,7 @@ public final class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(LOCALE_ID), imi); assertEquals(1, result.size()); verifyEquality(nonAutoId, result.get(0)); @@ -634,7 +634,7 @@ public final class InputMethodUtilsTest { "com.android.apps.inputmethod.latin", "FakeLatinIme", !IS_AUX, IS_DEFAULT, subtypes); final ArrayList<InputMethodSubtype> result = - SubtypeUtils.getImplicitlyApplicableSubtypesLocked( + SubtypeUtils.getImplicitlyApplicableSubtypes( new LocaleList(LOCALE_FR, LOCALE_EN_US, LOCALE_JA_JP), imi); assertThat(nonAutoFrCA, is(in(result))); assertThat(nonAutoEnUS, is(in(result))); @@ -796,19 +796,22 @@ public final class InputMethodUtilsTest { { final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); methodMap.put(systemIme.getId(), systemIme); - assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(methodMap, null, "")); + assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(InputMethodMap.of(methodMap), + null, "")); } // Returns null when the config value is empty. { final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); methodMap.put(systemIme.getId(), systemIme); - assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(methodMap, "", "")); + assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(InputMethodMap.of(methodMap), "", + "")); } // Returns null when the configured package doesn't have an IME. { - assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(new ArrayMap<>(), + assertNull(InputMethodInfoUtils.chooseSystemVoiceIme( + InputMethodMap.emptyMap(), systemIme.getPackageName(), "")); } @@ -816,7 +819,8 @@ public final class InputMethodUtilsTest { { final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); methodMap.put(systemIme.getId(), systemIme); - assertEquals(systemIme, InputMethodInfoUtils.chooseSystemVoiceIme(methodMap, + assertEquals(systemIme, InputMethodInfoUtils.chooseSystemVoiceIme( + InputMethodMap.of(methodMap), systemIme.getPackageName(), null)); } @@ -824,13 +828,15 @@ public final class InputMethodUtilsTest { { final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); methodMap.put(systemIme.getId(), systemIme); - assertEquals(systemIme, InputMethodInfoUtils.chooseSystemVoiceIme(methodMap, + assertEquals(systemIme, InputMethodInfoUtils.chooseSystemVoiceIme( + InputMethodMap.of(methodMap), systemIme.getPackageName(), "")); } // Returns null when the current default isn't found. { - assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(new ArrayMap<>(), + assertNull(InputMethodInfoUtils.chooseSystemVoiceIme( + InputMethodMap.emptyMap(), systemIme.getPackageName(), systemIme.getId())); } @@ -841,7 +847,7 @@ public final class InputMethodUtilsTest { final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); methodMap.put(systemIme.getId(), systemIme); methodMap.put(secondIme.getId(), secondIme); - assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(methodMap, + assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(InputMethodMap.of(methodMap), systemIme.getPackageName(), "")); } @@ -852,7 +858,8 @@ public final class InputMethodUtilsTest { final ArrayMap<String, InputMethodInfo> methodMap = new ArrayMap<>(); methodMap.put(systemIme.getId(), systemIme); methodMap.put(secondIme.getId(), secondIme); - assertEquals(systemIme, InputMethodInfoUtils.chooseSystemVoiceIme(methodMap, + assertEquals(systemIme, InputMethodInfoUtils.chooseSystemVoiceIme( + InputMethodMap.of(methodMap), systemIme.getPackageName(), systemIme.getId())); } @@ -862,7 +869,7 @@ public final class InputMethodUtilsTest { final InputMethodInfo nonSystemIme = createFakeInputMethodInfo("NonSystemIme", "fake.voice0", false /* isSystem */); methodMap.put(nonSystemIme.getId(), nonSystemIme); - assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(methodMap, + assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(InputMethodMap.of(methodMap), nonSystemIme.getPackageName(), nonSystemIme.getId())); } @@ -873,7 +880,7 @@ public final class InputMethodUtilsTest { "FakeDefaultAutoVoiceIme", "fake.voice0", false /* isSystem */); methodMap.put(systemIme.getId(), systemIme); methodMap.put(nonSystemIme.getId(), nonSystemIme); - assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(methodMap, + assertNull(InputMethodInfoUtils.chooseSystemVoiceIme(InputMethodMap.of(methodMap), nonSystemIme.getPackageName(), "")); } } diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ProcessObserverTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ProcessObserverTest.java new file mode 100644 index 000000000000..fcf761fb6607 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/am/ProcessObserverTest.java @@ -0,0 +1,275 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.am; + +import static android.os.Process.myPid; +import static android.os.Process.myUid; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; + +import android.app.ActivityManagerInternal; +import android.app.IApplicationThread; +import android.app.IProcessObserver; +import android.app.usage.UsageStatsManagerInternal; +import android.content.ComponentName; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManagerInternal; +import android.os.Binder; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.IBinder; +import android.util.Log; + +import androidx.test.filters.MediumTest; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.server.DropBoxManagerInternal; +import com.android.server.LocalServices; +import com.android.server.am.ActivityManagerService.Injector; +import com.android.server.appop.AppOpsService; +import com.android.server.wm.ActivityTaskManagerInternal; +import com.android.server.wm.ActivityTaskManagerService; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.io.File; +import java.util.Arrays; + + +/** + * Tests to verify that process events are dispatched to process observers. + */ +@MediumTest +@SuppressWarnings("GuardedBy") +public class ProcessObserverTest { + private static final String TAG = "ProcessObserverTest"; + + private static final String PACKAGE = "com.foo"; + + @Rule + public final ApplicationExitInfoTest.ServiceThreadRule + mServiceThreadRule = new ApplicationExitInfoTest.ServiceThreadRule(); + + private Context mContext; + private HandlerThread mHandlerThread; + + @Mock + private AppOpsService mAppOpsService; + @Mock + private DropBoxManagerInternal mDropBoxManagerInt; + @Mock + private PackageManagerInternal mPackageManagerInt; + @Mock + private UsageStatsManagerInternal mUsageStatsManagerInt; + @Mock + private ActivityManagerInternal mActivityManagerInt; + @Mock + private ActivityTaskManagerInternal mActivityTaskManagerInt; + @Mock + private BatteryStatsService mBatteryStatsService; + + private ActivityManagerService mRealAms; + private ActivityManagerService mAms; + + private ProcessList mRealProcessList = new ProcessList(); + private ProcessList mProcessList; + + final IProcessObserver mProcessObserver = mock(IProcessObserver.Stub.class); + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + mContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + + mHandlerThread = new HandlerThread(TAG); + mHandlerThread.start(); + + LocalServices.removeServiceForTest(DropBoxManagerInternal.class); + LocalServices.addService(DropBoxManagerInternal.class, mDropBoxManagerInt); + + LocalServices.removeServiceForTest(PackageManagerInternal.class); + LocalServices.addService(PackageManagerInternal.class, mPackageManagerInt); + + LocalServices.removeServiceForTest(ActivityManagerInternal.class); + LocalServices.addService(ActivityManagerInternal.class, mActivityManagerInt); + + LocalServices.removeServiceForTest(ActivityTaskManagerInternal.class); + LocalServices.addService(ActivityTaskManagerInternal.class, mActivityTaskManagerInt); + + doReturn(new ComponentName("", "")).when(mPackageManagerInt).getSystemUiServiceComponent(); + doReturn(true).when(mActivityTaskManagerInt).attachApplication(any()); + doNothing().when(mActivityTaskManagerInt).onProcessMapped(anyInt(), any()); + + mRealAms = new ActivityManagerService( + new TestInjector(mContext), mServiceThreadRule.getThread()); + mRealAms.mConstants.loadDeviceConfigConstants(); + mRealAms.mActivityTaskManager = new ActivityTaskManagerService(mContext); + mRealAms.mActivityTaskManager.initialize(null, null, mContext.getMainLooper()); + mRealAms.mAtmInternal = mActivityTaskManagerInt; + mRealAms.mPackageManagerInt = mPackageManagerInt; + mRealAms.mUsageStatsService = mUsageStatsManagerInt; + mRealAms.mProcessesReady = true; + mAms = spy(mRealAms); + mRealProcessList.mService = mAms; + mProcessList = spy(mRealProcessList); + + doReturn(mProcessObserver).when(mProcessObserver).asBinder(); + mProcessList.registerProcessObserver(mProcessObserver); + + doAnswer((invocation) -> { + Log.v(TAG, "Intercepting isProcStartValidLocked() for " + + Arrays.toString(invocation.getArguments())); + return null; + }).when(mProcessList).isProcStartValidLocked(any(), anyLong()); + } + + @After + public void tearDown() throws Exception { + mHandlerThread.quit(); + } + + private class TestInjector extends Injector { + TestInjector(Context context) { + super(context); + } + + @Override + public AppOpsService getAppOpsService(File recentAccessesFile, File storageFile, + Handler handler) { + return mAppOpsService; + } + + @Override + public Handler getUiHandler(ActivityManagerService service) { + return mHandlerThread.getThreadHandler(); + } + + @Override + public ProcessList getProcessList(ActivityManagerService service) { + return mRealProcessList; + } + + @Override + public BatteryStatsService getBatteryStatsService() { + return mBatteryStatsService; + } + } + + private ProcessRecord makeActiveProcessRecord(String packageName) + throws Exception { + final ApplicationInfo ai = makeApplicationInfo(packageName); + return makeActiveProcessRecord(ai); + } + + private ProcessRecord makeActiveProcessRecord(ApplicationInfo ai) + throws Exception { + final IApplicationThread thread = mock(IApplicationThread.class); + final IBinder threadBinder = new Binder(); + doReturn(threadBinder).when(thread).asBinder(); + doAnswer((invocation) -> { + Log.v(TAG, "Intercepting bindApplication() for " + + Arrays.toString(invocation.getArguments())); + if (mRealAms.mConstants.mEnableWaitForFinishAttachApplication) { + mRealAms.finishAttachApplication(0); + } + return null; + }).when(thread).bindApplication( + any(), any(), + any(), any(), anyBoolean(), + any(), any(), + any(), any(), + any(), + any(), anyInt(), + anyBoolean(), anyBoolean(), + anyBoolean(), anyBoolean(), any(), + any(), any(), any(), + any(), any(), + any(), any(), + any(), + anyLong(), anyLong()); + final ProcessRecord r = spy(new ProcessRecord(mAms, ai, ai.processName, ai.uid)); + r.setPid(myPid()); + r.setStartUid(myUid()); + r.setHostingRecord(new HostingRecord(HostingRecord.HOSTING_TYPE_BROADCAST)); + r.makeActive(thread, mAms.mProcessStats); + doNothing().when(r).killLocked(any(), any(), anyInt(), anyInt(), anyBoolean(), + anyBoolean()); + return r; + } + + static ApplicationInfo makeApplicationInfo(String packageName) { + final ApplicationInfo ai = new ApplicationInfo(); + ai.packageName = packageName; + ai.processName = packageName; + ai.uid = myUid(); + return ai; + } + + /** + * Verify that a process start event is dispatched to process observers. + */ + @Test + public void testNormal() throws Exception { + ProcessRecord app = startProcess(); + verify(mProcessObserver).onProcessStarted( + app.getPid(), app.uid, app.info.uid, PACKAGE, PACKAGE); + } + + private ProcessRecord startProcess() throws Exception { + final ProcessRecord app = makeActiveProcessRecord(PACKAGE); + final ApplicationInfo appInfo = makeApplicationInfo(PACKAGE); + mProcessList.handleProcessStartedLocked(app, app.getPid(), /* usingWrapper */ false, + /* expectedStartSeq */ 0, /* procAttached */ false); + app.getThread().bindApplication(PACKAGE, appInfo, + null, null, false, + null, + null, + null, null, + null, + null, 0, + false, false, + true, false, + null, + null, null, + null, + null, null, null, + null, null, + 0, 0); + return app; + } + + // TODO: [b/302724778] Remove manual JNI load + static { + System.loadLibrary("mockingservicestestjni"); + } +} diff --git a/services/tests/mockingservicestests/src/com/android/server/backup/SystemBackupAgentTest.java b/services/tests/mockingservicestests/src/com/android/server/backup/SystemBackupAgentTest.java index 4095be74d294..18dc114a8cd1 100644 --- a/services/tests/mockingservicestests/src/com/android/server/backup/SystemBackupAgentTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/backup/SystemBackupAgentTest.java @@ -20,11 +20,13 @@ import static com.google.common.truth.Truth.assertThat; import android.annotation.NonNull; import android.app.backup.BackupHelper; +import android.app.backup.BackupHelperWithLogger; import android.content.Context; import android.content.pm.PackageManager; import android.os.UserHandle; import android.os.UserManager; import android.platform.test.annotations.Presubmit; +import android.platform.test.flag.junit.SetFlagsRule; import android.util.ArraySet; import static org.mockito.Mockito.when; @@ -32,7 +34,10 @@ import static org.mockito.Mockito.when; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.server.backup.Flags; + import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -55,6 +60,9 @@ public class SystemBackupAgentTest { @Mock private PackageManager mPackageManagerMock; + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); @@ -71,7 +79,7 @@ public class SystemBackupAgentTest { mSystemBackupAgent.onCreate(userHandle, /* backupDestination= */ 0); - assertThat(mSystemBackupAgent.mAddedHelpers) + assertThat(mSystemBackupAgent.mAddedHelpersKey) .containsExactly( "account_sync_settings", "preferred_activities", @@ -96,7 +104,7 @@ public class SystemBackupAgentTest { mSystemBackupAgent.onCreate(userHandle, /* backupDestination= */ 0); - assertThat(mSystemBackupAgent.mAddedHelpers) + assertThat(mSystemBackupAgent.mAddedHelpersKey) .containsExactly( "account_sync_settings", "preferred_activities", @@ -118,7 +126,7 @@ public class SystemBackupAgentTest { mSystemBackupAgent.onCreate(userHandle, /* backupDestination= */ 0); - assertThat(mSystemBackupAgent.mAddedHelpers) + assertThat(mSystemBackupAgent.mAddedHelpersKey) .containsExactly( "account_sync_settings", "notifications", @@ -134,7 +142,7 @@ public class SystemBackupAgentTest { mSystemBackupAgent.onCreate(userHandle, /* backupDestination= */ 0); - assertThat(mSystemBackupAgent.mAddedHelpers) + assertThat(mSystemBackupAgent.mAddedHelpersKey) .containsExactly( "account_sync_settings", "preferred_activities", @@ -147,12 +155,42 @@ public class SystemBackupAgentTest { "companion"); } + @Test + public void onAddHelperIfEligibleForUser_flagIsOff_helpersHaveNoLogger() { + UserHandle userHandle = new UserHandle(UserHandle.USER_SYSTEM); + when(mUserManagerMock.isProfile()).thenReturn(false); + mSetFlagsRule.disableFlags(Flags.FLAG_ENABLE_METRICS_SYSTEM_BACKUP_AGENTS); + + mSystemBackupAgent.onCreate(userHandle, /* backupDestination= */ 0); + + for (BackupHelperWithLogger helper:mSystemBackupAgent.mAddedHelpers){ + assertThat(helper.isLoggerSet()).isFalse(); + } + } + + @Test + public void onAddHelperIfEligibleForUser_flagIsOn_helpersHaveLogger() { + UserHandle userHandle = new UserHandle(UserHandle.USER_SYSTEM); + when(mUserManagerMock.isProfile()).thenReturn(false); + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_METRICS_SYSTEM_BACKUP_AGENTS); + + mSystemBackupAgent.onCreate(userHandle, /* backupDestination= */ 0); + + for (BackupHelperWithLogger helper:mSystemBackupAgent.mAddedHelpers){ + assertThat(helper.isLoggerSet()).isTrue(); + } + } + private class TestableSystemBackupAgent extends SystemBackupAgent { - final Set<String> mAddedHelpers = new ArraySet<>(); + final Set<String> mAddedHelpersKey = new ArraySet<>(); + final Set<BackupHelperWithLogger> mAddedHelpers = new ArraySet<>(); @Override public void addHelper(String keyPrefix, BackupHelper helper) { - mAddedHelpers.add(keyPrefix); + mAddedHelpersKey.add(keyPrefix); + if (helper instanceof BackupHelperWithLogger) { + mAddedHelpers.add((BackupHelperWithLogger) helper); + } } @Override diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java index e989d7b060be..a65ef00f8a21 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java @@ -367,7 +367,7 @@ public class PackageArchiverTest { verify(mInstallerService).uninstall( eq(new VersionedPackage(PACKAGE, PackageManager.VERSION_CODE_HIGHEST)), eq(CALLER_PACKAGE), eq(DELETE_ARCHIVE | DELETE_KEEP_DATA), eq(mIntentSender), - eq(UserHandle.CURRENT.getIdentifier()), anyInt()); + eq(UserHandle.CURRENT.getIdentifier()), anyInt(), anyInt()); ArchiveState expectedArchiveState = createArchiveState(); ArchiveState actualArchiveState = mPackageSetting.readUserState( @@ -391,7 +391,7 @@ public class PackageArchiverTest { eq(CALLER_PACKAGE), eq(DELETE_ARCHIVE | DELETE_KEEP_DATA), eq(mIntentSender), - eq(UserHandle.CURRENT.getIdentifier()), anyInt()); + eq(UserHandle.CURRENT.getIdentifier()), anyInt(), anyInt()); ArchiveState expectedArchiveState = createArchiveState(); ArchiveState actualArchiveState = mPackageSetting.readUserState( @@ -552,22 +552,20 @@ public class PackageArchiverTest { when(mComputer.getPackageStateFiltered(eq(PACKAGE), anyInt(), anyInt())).thenReturn( null); - assertThat(mArchiveManager.getArchivedAppIcon(PACKAGE, UserHandle.CURRENT, - CALLER_PACKAGE)).isNull(); + assertThat(mArchiveManager.getArchivedAppIcon(PACKAGE, UserHandle.CURRENT)).isNull(); } @Test public void getArchivedAppIcon_notArchived() { - assertThat(mArchiveManager.getArchivedAppIcon(PACKAGE, UserHandle.CURRENT, - CALLER_PACKAGE)).isNull(); + assertThat(mArchiveManager.getArchivedAppIcon(PACKAGE, UserHandle.CURRENT)).isNull(); } @Test public void getArchivedAppIcon_success() { mUserState.setArchiveState(createArchiveState()).setInstalled(false); - assertThat(mArchiveManager.getArchivedAppIcon(PACKAGE, UserHandle.CURRENT, - CALLER_PACKAGE)).isEqualTo(mIcon); + assertThat(mArchiveManager.getArchivedAppIcon(PACKAGE, UserHandle.CURRENT)).isEqualTo( + mIcon); } diff --git a/services/tests/powerstatstests/Android.bp b/services/tests/powerstatstests/Android.bp index 654d7a8de168..f49f6383b3c8 100644 --- a/services/tests/powerstatstests/Android.bp +++ b/services/tests/powerstatstests/Android.bp @@ -44,6 +44,7 @@ android_test { "servicestests-utils", "platform-test-annotations", "flag-junit", + "ravenwood-junit", ], libs: [ diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java index ca162e0b46e1..ba2b53854cd7 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryUsageStatsRule.java @@ -32,6 +32,7 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.UidBatteryConsumer; import android.os.UserBatteryConsumer; +import android.platform.test.ravenwood.RavenwoodRule; import android.util.SparseArray; import androidx.test.InstrumentationRegistry; @@ -57,7 +58,8 @@ public class BatteryUsageStatsRule implements TestRule { private final PowerProfile mPowerProfile; private final MockClock mMockClock = new MockClock(); - private final MockBatteryStatsImpl mBatteryStats; + private final File mHistoryDir; + private MockBatteryStatsImpl mBatteryStats; private Handler mHandler; private BatteryUsageStats mBatteryUsageStats; @@ -66,6 +68,10 @@ public class BatteryUsageStatsRule implements TestRule { private SparseArray<int[]> mCpusByPolicy = new SparseArray<>(); private SparseArray<int[]> mFreqsByPolicy = new SparseArray<>(); + private int mDisplayCount = -1; + private int mPerUidModemModel = -1; + private NetworkStats mNetworkStats; + public BatteryUsageStatsRule() { this(0, null); } @@ -78,16 +84,38 @@ public class BatteryUsageStatsRule implements TestRule { mHandler = mock(Handler.class); mPowerProfile = spy(new PowerProfile()); mMockClock.currentTime = currentTime; - mBatteryStats = new MockBatteryStatsImpl(mMockClock, historyDir, mHandler); - mBatteryStats.setPowerProfile(mPowerProfile); + mHistoryDir = historyDir; + + if (!RavenwoodRule.isUnderRavenwood()) { + lateInitBatteryStats(); + } mCpusByPolicy.put(0, new int[]{0, 1, 2, 3}); mCpusByPolicy.put(4, new int[]{4, 5, 6, 7}); mFreqsByPolicy.put(0, new int[]{300000, 1000000, 2000000}); mFreqsByPolicy.put(4, new int[]{300000, 1000000, 2500000, 3000000}); + } + + private void lateInitBatteryStats() { + if (mBatteryStats != null) return; + + mBatteryStats = new MockBatteryStatsImpl(mMockClock, mHistoryDir, mHandler); + mBatteryStats.setPowerProfile(mPowerProfile); mBatteryStats.setCpuScalingPolicies(new CpuScalingPolicies(mCpusByPolicy, mFreqsByPolicy)); mBatteryStats.onSystemReady(); + + if (mDisplayCount != -1) { + mBatteryStats.setDisplayCountLocked(mDisplayCount); + } + if (mPerUidModemModel != -1) { + synchronized (mBatteryStats) { + mBatteryStats.setPerUidModemModel(mPerUidModemModel); + } + } + if (mNetworkStats != null) { + mBatteryStats.setNetworkStats(mNetworkStats); + } } public MockClock getMockClock() { @@ -112,7 +140,10 @@ public class BatteryUsageStatsRule implements TestRule { } mCpusByPolicy.put(policy, relatedCpus); mFreqsByPolicy.put(policy, frequencies); - mBatteryStats.setCpuScalingPolicies(new CpuScalingPolicies(mCpusByPolicy, mFreqsByPolicy)); + if (mBatteryStats != null) { + mBatteryStats.setCpuScalingPolicies( + new CpuScalingPolicies(mCpusByPolicy, mFreqsByPolicy)); + } return this; } @@ -174,13 +205,19 @@ public class BatteryUsageStatsRule implements TestRule { public BatteryUsageStatsRule setNumDisplays(int value) { when(mPowerProfile.getNumDisplays()).thenReturn(value); - mBatteryStats.setDisplayCountLocked(value); + mDisplayCount = value; + if (mBatteryStats != null) { + mBatteryStats.setDisplayCountLocked(mDisplayCount); + } return this; } public BatteryUsageStatsRule setPerUidModemModel(int perUidModemModel) { - synchronized (mBatteryStats) { - mBatteryStats.setPerUidModemModel(perUidModemModel); + mPerUidModemModel = perUidModemModel; + if (mBatteryStats != null) { + synchronized (mBatteryStats) { + mBatteryStats.setPerUidModemModel(mPerUidModemModel); + } } return this; } @@ -210,7 +247,10 @@ public class BatteryUsageStatsRule implements TestRule { } public void setNetworkStats(NetworkStats networkStats) { - mBatteryStats.setNetworkStats(networkStats); + mNetworkStats = networkStats; + if (mBatteryStats != null) { + mBatteryStats.setNetworkStats(mNetworkStats); + } } @Override @@ -225,6 +265,7 @@ public class BatteryUsageStatsRule implements TestRule { } private void before() { + lateInitBatteryStats(); HandlerThread bgThread = new HandlerThread("bg thread"); bgThread.start(); mHandler = new Handler(bgThread.getLooper()); diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp index 0831086b28ca..8958fac87bb6 100644 --- a/services/tests/servicestests/Android.bp +++ b/services/tests/servicestests/Android.bp @@ -31,6 +31,10 @@ android_test { "test-apps/SuspendTestApp/src/**/*.java", ], + + kotlincflags: [ + "-Werror", + ], static_libs: [ "frameworks-base-testutils", "services.accessibility", @@ -78,6 +82,7 @@ android_test { "securebox", "flag-junit", "ravenwood-junit", + "net_flags_lib", ], libs: [ diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java index 57c3a1d5f364..95cfc2a304d4 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java @@ -22,6 +22,7 @@ import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_ import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_NONE; import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW; import static android.view.accessibility.Flags.FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG; +import static android.view.accessibility.Flags.FLAG_SKIP_ACCESSIBILITY_WARNING_DIALOG_FOR_TRUSTED_SERVICES; import static com.android.internal.accessibility.AccessibilityShortcutController.ACCESSIBILITY_HEARING_AIDS_COMPONENT_NAME; import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME; @@ -82,6 +83,7 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.filters.SmallTest; import com.android.compatibility.common.util.TestUtils; +import com.android.internal.R; import com.android.internal.compat.IPlatformCompat; import com.android.server.LocalServices; import com.android.server.accessibility.AccessibilityManagerService.AccessibilityDisplayListener; @@ -857,8 +859,7 @@ public class AccessibilityManagerServiceTest { @RequiresFlagsEnabled(FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG) public void testIsAccessibilityServiceWarningRequired_requiredByDefault() { mockManageAccessibilityGranted(mTestableContext); - final AccessibilityServiceInfo info = new AccessibilityServiceInfo(); - info.setComponentName(COMPONENT_NAME); + final AccessibilityServiceInfo info = mockAccessibilityServiceInfo(COMPONENT_NAME); assertThat(mA11yms.isAccessibilityServiceWarningRequired(info)).isTrue(); } @@ -867,10 +868,9 @@ public class AccessibilityManagerServiceTest { @RequiresFlagsEnabled(FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG) public void testIsAccessibilityServiceWarningRequired_notRequiredIfAlreadyEnabled() { mockManageAccessibilityGranted(mTestableContext); - final AccessibilityServiceInfo info_a = new AccessibilityServiceInfo(); - info_a.setComponentName(COMPONENT_NAME); - final AccessibilityServiceInfo info_b = new AccessibilityServiceInfo(); - info_b.setComponentName(new ComponentName("package_b", "class_b")); + final AccessibilityServiceInfo info_a = mockAccessibilityServiceInfo(COMPONENT_NAME); + final AccessibilityServiceInfo info_b = mockAccessibilityServiceInfo( + new ComponentName("package_b", "class_b")); final AccessibilityUserState userState = mA11yms.getCurrentUserState(); userState.mEnabledServices.clear(); userState.mEnabledServices.add(info_b.getComponentName()); @@ -883,12 +883,12 @@ public class AccessibilityManagerServiceTest { @RequiresFlagsEnabled(FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG) public void testIsAccessibilityServiceWarningRequired_notRequiredIfExistingShortcut() { mockManageAccessibilityGranted(mTestableContext); - final AccessibilityServiceInfo info_a = new AccessibilityServiceInfo(); - info_a.setComponentName(new ComponentName("package_a", "class_a")); - final AccessibilityServiceInfo info_b = new AccessibilityServiceInfo(); - info_b.setComponentName(new ComponentName("package_b", "class_b")); - final AccessibilityServiceInfo info_c = new AccessibilityServiceInfo(); - info_c.setComponentName(new ComponentName("package_c", "class_c")); + final AccessibilityServiceInfo info_a = mockAccessibilityServiceInfo( + new ComponentName("package_a", "class_a")); + final AccessibilityServiceInfo info_b = mockAccessibilityServiceInfo( + new ComponentName("package_b", "class_b")); + final AccessibilityServiceInfo info_c = mockAccessibilityServiceInfo( + new ComponentName("package_c", "class_c")); final AccessibilityUserState userState = mA11yms.getCurrentUserState(); userState.mAccessibilityButtonTargets.clear(); userState.mAccessibilityButtonTargets.add(info_b.getComponentName().flattenToString()); @@ -900,6 +900,51 @@ public class AccessibilityManagerServiceTest { assertThat(mA11yms.isAccessibilityServiceWarningRequired(info_c)).isFalse(); } + @Test + @RequiresFlagsEnabled({ + FLAG_CLEANUP_ACCESSIBILITY_WARNING_DIALOG, + FLAG_SKIP_ACCESSIBILITY_WARNING_DIALOG_FOR_TRUSTED_SERVICES}) + public void testIsAccessibilityServiceWarningRequired_notRequiredIfAllowlisted() { + mockManageAccessibilityGranted(mTestableContext); + final AccessibilityServiceInfo info_a = mockAccessibilityServiceInfo( + new ComponentName("package_a", "class_a"), true); + final AccessibilityServiceInfo info_b = mockAccessibilityServiceInfo( + new ComponentName("package_b", "class_b"), false); + final AccessibilityServiceInfo info_c = mockAccessibilityServiceInfo( + new ComponentName("package_c", "class_c"), true); + mTestableContext.getOrCreateTestableResources().addOverride( + R.array.config_trustedAccessibilityServices, + new String[]{ + info_b.getComponentName().flattenToString(), + info_c.getComponentName().flattenToString()}); + + // info_a is not in the allowlist => require the warning + assertThat(mA11yms.isAccessibilityServiceWarningRequired(info_a)).isTrue(); + // info_b is not preinstalled => require the warning + assertThat(mA11yms.isAccessibilityServiceWarningRequired(info_b)).isTrue(); + // info_c is both in the allowlist and preinstalled => do not require the warning + assertThat(mA11yms.isAccessibilityServiceWarningRequired(info_c)).isFalse(); + } + + private static AccessibilityServiceInfo mockAccessibilityServiceInfo( + ComponentName componentName) { + return mockAccessibilityServiceInfo(componentName, false); + } + + private static AccessibilityServiceInfo mockAccessibilityServiceInfo( + ComponentName componentName, + boolean isSystemApp) { + AccessibilityServiceInfo accessibilityServiceInfo = + Mockito.spy(new AccessibilityServiceInfo()); + accessibilityServiceInfo.setComponentName(componentName); + ResolveInfo mockResolveInfo = Mockito.mock(ResolveInfo.class); + when(accessibilityServiceInfo.getResolveInfo()).thenReturn(mockResolveInfo); + mockResolveInfo.serviceInfo = Mockito.mock(ServiceInfo.class); + mockResolveInfo.serviceInfo.applicationInfo = Mockito.mock(ApplicationInfo.class); + when(mockResolveInfo.serviceInfo.applicationInfo.isSystemApp()).thenReturn(isSystemApp); + return accessibilityServiceInfo; + } + // Single package intents can trigger multiple PackageMonitor callbacks. // Collect the state of the lock in a set, since tests only care if calls // were all locked or all unlocked. diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthServiceTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthServiceTest.java index 88b2ed4f79c9..071db68704af 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/AuthServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthServiceTest.java @@ -16,6 +16,7 @@ package com.android.server.biometrics; +import static android.adaptiveauth.Flags.FLAG_REPORT_BIOMETRIC_AUTH_ATTEMPTS; import static android.Manifest.permission.MANAGE_BIOMETRIC; import static android.Manifest.permission.TEST_BIOMETRIC; import static android.Manifest.permission.USE_BIOMETRIC_INTERNAL; @@ -491,6 +492,22 @@ public class AuthServiceTest { } @Test + public void testRegisterAuthenticationStateListener_callsFaceService() throws Exception { + mSetFlagsRule.enableFlags(FLAG_REPORT_BIOMETRIC_AUTH_ATTEMPTS); + setInternalAndTestBiometricPermissions(mContext, true /* hasPermission */); + + mAuthService = new AuthService(mContext, mInjector); + mAuthService.onStart(); + + final AuthenticationStateListener listener = mock(AuthenticationStateListener.class); + + mAuthService.mImpl.registerAuthenticationStateListener(listener); + + waitForIdle(); + verify(mFaceService).registerAuthenticationStateListener(eq(listener)); + } + + @Test public void testRegisterKeyguardCallback_callsBiometricServiceRegisterKeyguardCallback() throws Exception { setInternalAndTestBiometricPermissions(mContext, true /* hasPermission */); diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClientTest.java index 3a3dd6ea2746..f8b5b04294cd 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClientTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClientTest.java @@ -16,6 +16,7 @@ package com.android.server.biometrics.sensors.face.aidl; +import static android.adaptiveauth.Flags.FLAG_REPORT_BIOMETRIC_AUTH_ATTEMPTS; import static android.hardware.biometrics.BiometricConstants.BIOMETRIC_ERROR_CANCELED; import static android.hardware.biometrics.BiometricFaceConstants.FACE_ERROR_LOCKOUT; import static android.hardware.biometrics.BiometricFaceConstants.FACE_ERROR_LOCKOUT_PERMANENT; @@ -49,6 +50,7 @@ import android.os.IBinder; import android.os.PowerManager; import android.os.RemoteException; import android.platform.test.annotations.Presubmit; +import android.platform.test.flag.junit.SetFlagsRule; import android.testing.TestableContext; import androidx.test.filters.SmallTest; @@ -58,6 +60,7 @@ import com.android.server.biometrics.log.BiometricContext; import com.android.server.biometrics.log.BiometricLogger; import com.android.server.biometrics.log.OperationContextExt; import com.android.server.biometrics.sensors.AuthSessionCoordinator; +import com.android.server.biometrics.sensors.AuthenticationStateListeners; import com.android.server.biometrics.sensors.ClientMonitorCallback; import com.android.server.biometrics.sensors.ClientMonitorCallbackConverter; import com.android.server.biometrics.sensors.LockoutTracker; @@ -81,6 +84,8 @@ import java.util.function.Consumer; @SmallTest public class FaceAuthenticationClientTest { + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + private static final int USER_ID = 12; private static final long OP_ID = 32; private static final int WAKE_REASON = WakeReason.LIFT; @@ -105,6 +110,8 @@ public class FaceAuthenticationClientTest { @Mock private ClientMonitorCallback mCallback; @Mock + private AuthenticationStateListeners mAuthenticationStateListeners; + @Mock private AidlResponseHandler mAidlResponseHandler; @Mock private ActivityTaskManager mActivityTaskManager; @@ -264,6 +271,29 @@ public class FaceAuthenticationClientTest { verify(mHal, never()).authenticate(anyInt()); } + @Test + public void testAuthenticationStateListeners_onAuthenticationSucceeded() + throws RemoteException { + mSetFlagsRule.enableFlags(FLAG_REPORT_BIOMETRIC_AUTH_ATTEMPTS); + final FaceAuthenticationClient client = createClient(); + client.start(mCallback); + client.onAuthenticated(new Face("friendly", 1 /* faceId */, 2 /* deviceId */), + true /* authenticated */, new ArrayList<>()); + + verify(mAuthenticationStateListeners).onAuthenticationSucceeded(anyInt(), anyInt()); + } + + @Test + public void testAuthenticationStateListeners_onAuthenticationFailed() throws RemoteException { + mSetFlagsRule.enableFlags(FLAG_REPORT_BIOMETRIC_AUTH_ATTEMPTS); + final FaceAuthenticationClient client = createClient(); + client.start(mCallback); + client.onAuthenticated(new Face("friendly", 1 /* faceId */, 2 /* deviceId */), + false /* authenticated */, new ArrayList<>()); + + verify(mAuthenticationStateListeners).onAuthenticationFailed(anyInt(), anyInt()); + } + private FaceAuthenticationClient createClient() throws RemoteException { return createClient(2 /* version */, mClientMonitorCallbackConverter, false /* allowBackgroundAuthentication */, @@ -311,7 +341,8 @@ public class FaceAuthenticationClientTest { false /* requireConfirmation */, mBiometricLogger, mBiometricContext, true /* isStrongBiometric */, mUsageStats, lockoutTracker, allowBackgroundAuthentication, - null /* sensorPrivacyManager */, 0 /* biometricStrength */) { + null /* sensorPrivacyManager */, 0 /* biometricStrength */, + mAuthenticationStateListeners) { @Override protected ActivityTaskManager getActivityTaskManager() { return mActivityTaskManager; diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java index 772ec8b73393..7648bd17f53c 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceProviderTest.java @@ -51,6 +51,7 @@ import androidx.test.filters.SmallTest; import com.android.internal.R; import com.android.server.biometrics.Flags; import com.android.server.biometrics.log.BiometricContext; +import com.android.server.biometrics.sensors.AuthenticationStateListeners; import com.android.server.biometrics.sensors.BaseClientMonitor; import com.android.server.biometrics.sensors.BiometricScheduler; import com.android.server.biometrics.sensors.BiometricStateCallback; @@ -89,6 +90,8 @@ public class FaceProviderTest { private BiometricContext mBiometricContext; @Mock private BiometricStateCallback mBiometricStateCallback; + @Mock + private AuthenticationStateListeners mAuthenticationStateListeners; private final TestLooper mLooper = new TestLooper(); private SensorProps[] mSensorProps; @@ -119,8 +122,8 @@ public class FaceProviderTest { mLockoutResetDispatcher = new LockoutResetDispatcher(mContext); mFaceProvider = new FaceProvider(mContext, mBiometricStateCallback, - mSensorProps, TAG, mLockoutResetDispatcher, mBiometricContext, - mDaemon, new Handler(mLooper.getLooper()), + mAuthenticationStateListeners, mSensorProps, TAG, mLockoutResetDispatcher, + mBiometricContext, mDaemon, new Handler(mLooper.getLooper()), false /* resetLockoutRequiresChallenge */, false /* testHalEnabled */); } @@ -154,7 +157,7 @@ public class FaceProviderTest { final HidlFaceSensorConfig[] hidlFaceSensorConfig = new HidlFaceSensorConfig[]{faceSensorConfig}; mFaceProvider = new FaceProvider(mContext, - mBiometricStateCallback, hidlFaceSensorConfig, TAG, + mBiometricStateCallback, mAuthenticationStateListeners, hidlFaceSensorConfig, TAG, mLockoutResetDispatcher, mBiometricContext, mDaemon, new Handler(mLooper.getLooper()), true /* resetLockoutRequiresChallenge */, diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/Face10Test.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/Face10Test.java index e558c4d64180..78c1e08ba832 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/Face10Test.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/hidl/Face10Test.java @@ -44,6 +44,7 @@ import androidx.test.filters.SmallTest; import com.android.internal.R; import com.android.server.biometrics.log.BiometricContext; +import com.android.server.biometrics.sensors.AuthenticationStateListeners; import com.android.server.biometrics.sensors.BiometricScheduler; import com.android.server.biometrics.sensors.BiometricStateCallback; import com.android.server.biometrics.sensors.LockoutResetDispatcher; @@ -81,6 +82,8 @@ public class Face10Test { private BiometricContext mBiometricContext; @Mock private BiometricStateCallback mBiometricStateCallback; + @Mock + private AuthenticationStateListeners mAuthenticationStateListeners; private final Handler mHandler = new Handler(Looper.getMainLooper()); private LockoutResetDispatcher mLockoutResetDispatcher; @@ -116,8 +119,8 @@ public class Face10Test { Face10.sSystemClock = Clock.fixed( Instant.ofEpochMilli(100), ZoneId.of("America/Los_Angeles")); - mFace10 = new Face10(mContext, mBiometricStateCallback, sensorProps, - mLockoutResetDispatcher, mHandler, mScheduler, mBiometricContext); + mFace10 = new Face10(mContext, mBiometricStateCallback, mAuthenticationStateListeners, + sensorProps, mLockoutResetDispatcher, mHandler, mScheduler, mBiometricContext); mBinder = new Binder(); } diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java index 774ea5bc6b16..4ed6f74d30fa 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClientTest.java @@ -16,6 +16,7 @@ package com.android.server.biometrics.sensors.fingerprint.aidl; +import static android.adaptiveauth.Flags.FLAG_REPORT_BIOMETRIC_AUTH_ATTEMPTS; import static android.hardware.biometrics.BiometricConstants.BIOMETRIC_ERROR_CANCELED; import static com.android.systemui.shared.Flags.FLAG_SIDEFPS_CONTROLLER_REFACTOR; @@ -451,6 +452,29 @@ public class FingerprintAuthenticationClientTest { } @Test + public void testAuthenticationStateListeners_onAuthenticationSucceeded() + throws RemoteException { + mSetFlagsRule.enableFlags(FLAG_REPORT_BIOMETRIC_AUTH_ATTEMPTS); + final FingerprintAuthenticationClient client = createClient(); + client.start(mCallback); + client.onAuthenticated(new Fingerprint("friendly", 1 /* fingerId */, + 2 /* deviceId */), true /* authenticated */, new ArrayList<>()); + + verify(mAuthenticationStateListeners).onAuthenticationSucceeded(anyInt(), anyInt()); + } + + @Test + public void testAuthenticationStateListeners_onAuthenticationFailed() throws RemoteException { + mSetFlagsRule.enableFlags(FLAG_REPORT_BIOMETRIC_AUTH_ATTEMPTS); + final FingerprintAuthenticationClient client = createClient(); + client.start(mCallback); + client.onAuthenticated(new Fingerprint("friendly", 1 /* fingerId */, + 2 /* deviceId */), false /* authenticated */, new ArrayList<>()); + + verify(mAuthenticationStateListeners).onAuthenticationFailed(anyInt(), anyInt()); + } + + @Test public void cancelsAuthWhenNotInForeground() throws Exception { final ActivityManager.RunningTaskInfo topTask = new ActivityManager.RunningTaskInfo(); topTask.topActivity = new ComponentName("other", "thing"); 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 0973d46283ed..5e380108aeb3 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceTvTest.java @@ -25,6 +25,7 @@ import static com.android.server.hdmi.Constants.ADDR_RECORDER_1; import static com.android.server.hdmi.Constants.ADDR_TV; import static com.android.server.hdmi.HdmiCecLocalDevice.ActiveSource; import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_ENABLE_CEC; +import static com.android.server.hdmi.HdmiControlService.INITIATED_BY_WAKE_UP_MESSAGE; import static com.google.common.truth.Truth.assertThat; @@ -1807,4 +1808,35 @@ public class HdmiCecLocalDeviceTvTest { // TV should only send <Give Osd Name> once assertEquals(1, Collections.frequency(mNativeWrapper.getResultMessages(), giveOsdName)); } + + @Test + public void initiateCecByWakeupMessage_selectInternalSourceAfterDelay_broadcastsActiveSource() { + HdmiCecMessage activeSourceFromTv = + HdmiCecMessageBuilder.buildActiveSource(ADDR_TV, 0x0000); + + mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_WAKE_UP_MESSAGE); + mTestLooper.dispatchAll(); + + mTestLooper.moveTimeForward(HdmiConfig.TIMEOUT_MS); + mTestLooper.dispatchAll(); + + mHdmiCecLocalDeviceTv.deviceSelect(ADDR_TV, new TestCallback()); + mTestLooper.dispatchAll(); + + assertThat(mNativeWrapper.getResultMessages()).contains(activeSourceFromTv); + } + + @Test + public void initiateCecByWakeupMessage_selectInternalSource_doesNotBroadcastActiveSource() { + HdmiCecMessage activeSourceFromTv = + HdmiCecMessageBuilder.buildActiveSource(ADDR_TV, 0x0000); + + mHdmiControlService.allocateLogicalAddress(mLocalDevices, INITIATED_BY_WAKE_UP_MESSAGE); + mTestLooper.dispatchAll(); + + mHdmiCecLocalDeviceTv.deviceSelect(ADDR_TV, new TestCallback()); + mTestLooper.dispatchAll(); + + assertThat(mNativeWrapper.getResultMessages()).doesNotContain(activeSourceFromTv); + } } diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java index 5a62d92e8e12..5081198f0058 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java @@ -16,6 +16,8 @@ package com.android.server.locksettings; +import static android.security.Flags.FLAG_REPORT_PRIMARY_AUTH_ATTEMPTS; + import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_NONE; import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PASSWORD; import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PATTERN; @@ -30,25 +32,30 @@ import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.PropertyInvalidatedCache; +import android.os.IBinder; import android.os.RemoteException; import android.platform.test.annotations.Presubmit; +import android.platform.test.flag.junit.SetFlagsRule; import android.service.gatekeeper.GateKeeperResponse; import android.text.TextUtils; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; +import com.android.internal.widget.ILockSettingsStateListener; import com.android.internal.widget.LockPatternUtils; import com.android.internal.widget.LockscreenCredential; import com.android.internal.widget.VerifyCredentialResponse; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -59,6 +66,7 @@ import org.junit.runner.RunWith; @Presubmit @RunWith(AndroidJUnit4.class) public class LockSettingsServiceTests extends BaseLockSettingsServiceTests { + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @Before public void setUp() { @@ -399,6 +407,60 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests { } @Test + public void testVerifyCredential_notifyLockSettingsStateListeners_whenGoodPassword() + throws Exception { + mSetFlagsRule.enableFlags(FLAG_REPORT_PRIMARY_AUTH_ATTEMPTS); + final LockscreenCredential password = newPassword("password"); + setCredential(PRIMARY_USER_ID, password); + final ILockSettingsStateListener listener = mockLockSettingsStateListener(); + mLocalService.registerLockSettingsStateListener(listener); + + assertEquals(VerifyCredentialResponse.RESPONSE_OK, + mService.verifyCredential(password, PRIMARY_USER_ID, 0 /* flags */) + .getResponseCode()); + + verify(listener).onAuthenticationSucceeded(PRIMARY_USER_ID); + } + + @Test + public void testVerifyCredential_notifyLockSettingsStateListeners_whenBadPassword() + throws Exception { + mSetFlagsRule.enableFlags(FLAG_REPORT_PRIMARY_AUTH_ATTEMPTS); + final LockscreenCredential password = newPassword("password"); + setCredential(PRIMARY_USER_ID, password); + final LockscreenCredential badPassword = newPassword("badPassword"); + final ILockSettingsStateListener listener = mockLockSettingsStateListener(); + mLocalService.registerLockSettingsStateListener(listener); + + assertEquals(VerifyCredentialResponse.RESPONSE_ERROR, + mService.verifyCredential(badPassword, PRIMARY_USER_ID, 0 /* flags */) + .getResponseCode()); + + verify(listener).onAuthenticationFailed(PRIMARY_USER_ID); + } + + @Test + public void testLockSettingsStateListener_registeredThenUnregistered() throws Exception { + mSetFlagsRule.enableFlags(FLAG_REPORT_PRIMARY_AUTH_ATTEMPTS); + final LockscreenCredential password = newPassword("password"); + setCredential(PRIMARY_USER_ID, password); + final LockscreenCredential badPassword = newPassword("badPassword"); + final ILockSettingsStateListener listener = mockLockSettingsStateListener(); + + mLocalService.registerLockSettingsStateListener(listener); + assertEquals(VerifyCredentialResponse.RESPONSE_OK, + mService.verifyCredential(password, PRIMARY_USER_ID, 0 /* flags */) + .getResponseCode()); + verify(listener).onAuthenticationSucceeded(PRIMARY_USER_ID); + + mLocalService.unregisterLockSettingsStateListener(listener); + assertEquals(VerifyCredentialResponse.RESPONSE_ERROR, + mService.verifyCredential(badPassword, PRIMARY_USER_ID, 0 /* flags */) + .getResponseCode()); + verify(listener, never()).onAuthenticationFailed(PRIMARY_USER_ID); + } + + @Test public void testSetCredentialNotPossibleInSecureFrpModeDuringSuw() { setUserSetupComplete(false); setSecureFrpMode(true); @@ -537,4 +599,12 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests { assertNotEquals(0, mGateKeeperService.getSecureUserId(userId)); } } + + private ILockSettingsStateListener mockLockSettingsStateListener() { + ILockSettingsStateListener listener = mock(ILockSettingsStateListener.Stub.class); + IBinder binder = mock(IBinder.class); + when(binder.isBinderAlive()).thenReturn(true); + when(listener.asBinder()).thenReturn(binder); + return listener; + } } diff --git a/services/tests/servicestests/src/com/android/server/net/NetworkManagementServiceTest.java b/services/tests/servicestests/src/com/android/server/net/NetworkManagementServiceTest.java index 13dc12032e7d..d6d2b6d9abd2 100644 --- a/services/tests/servicestests/src/com/android/server/net/NetworkManagementServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/net/NetworkManagementServiceTest.java @@ -16,6 +16,7 @@ package com.android.server.net; +import static android.net.ConnectivityManager.FIREWALL_CHAIN_BACKGROUND; import static android.net.ConnectivityManager.FIREWALL_CHAIN_DOZABLE; import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY; import static android.net.ConnectivityManager.FIREWALL_CHAIN_POWERSAVE; @@ -327,12 +328,20 @@ public class NetworkManagementServiceTest { isRestrictedForLowPowerStandby.put(INetd.FIREWALL_RULE_DENY, true); expected.put(FIREWALL_CHAIN_LOW_POWER_STANDBY, isRestrictedForLowPowerStandby); + // Background chain + final ArrayMap<Integer, Boolean> isRestrictedInBackground = new ArrayMap<>(); + isRestrictedInBackground.put(NetworkPolicyManager.FIREWALL_RULE_DEFAULT, true); + isRestrictedInBackground.put(INetd.FIREWALL_RULE_ALLOW, false); + isRestrictedInBackground.put(INetd.FIREWALL_RULE_DENY, true); + expected.put(FIREWALL_CHAIN_BACKGROUND, isRestrictedInBackground); + final int[] chains = { FIREWALL_CHAIN_STANDBY, FIREWALL_CHAIN_POWERSAVE, FIREWALL_CHAIN_DOZABLE, FIREWALL_CHAIN_RESTRICTED, - FIREWALL_CHAIN_LOW_POWER_STANDBY + FIREWALL_CHAIN_LOW_POWER_STANDBY, + FIREWALL_CHAIN_BACKGROUND }; final int[] states = { INetd.FIREWALL_RULE_ALLOW, diff --git a/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java index 2a764526a436..4451cae8db42 100644 --- a/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java @@ -26,12 +26,14 @@ import static android.app.ActivityManager.PROCESS_STATE_SERVICE; import static android.app.ActivityManager.PROCESS_STATE_TOP; import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_DATA_SAVER; import static android.net.ConnectivityManager.BLOCKED_METERED_REASON_USER_RESTRICTED; +import static android.net.ConnectivityManager.BLOCKED_REASON_APP_BACKGROUND; import static android.net.ConnectivityManager.BLOCKED_REASON_APP_STANDBY; import static android.net.ConnectivityManager.BLOCKED_REASON_BATTERY_SAVER; import static android.net.ConnectivityManager.BLOCKED_REASON_DOZE; import static android.net.ConnectivityManager.BLOCKED_REASON_LOW_POWER_STANDBY; import static android.net.ConnectivityManager.BLOCKED_REASON_NONE; import static android.net.ConnectivityManager.CONNECTIVITY_ACTION; +import static android.net.ConnectivityManager.FIREWALL_CHAIN_BACKGROUND; import static android.net.ConnectivityManager.FIREWALL_CHAIN_LOW_POWER_STANDBY; import static android.net.ConnectivityManager.FIREWALL_CHAIN_RESTRICTED; import static android.net.ConnectivityManager.TYPE_MOBILE; @@ -48,8 +50,13 @@ import static android.net.NetworkPolicyManager.ALLOWED_METERED_REASON_SYSTEM; import static android.net.NetworkPolicyManager.ALLOWED_REASON_FOREGROUND; import static android.net.NetworkPolicyManager.ALLOWED_REASON_LOW_POWER_STANDBY_ALLOWLIST; import static android.net.NetworkPolicyManager.ALLOWED_REASON_NONE; +import static android.net.NetworkPolicyManager.ALLOWED_REASON_NOT_IN_BACKGROUND; +import static android.net.NetworkPolicyManager.ALLOWED_REASON_POWER_SAVE_ALLOWLIST; +import static android.net.NetworkPolicyManager.ALLOWED_REASON_POWER_SAVE_EXCEPT_IDLE_ALLOWLIST; +import static android.net.NetworkPolicyManager.ALLOWED_REASON_RESTRICTED_MODE_PERMISSIONS; import static android.net.NetworkPolicyManager.ALLOWED_REASON_SYSTEM; import static android.net.NetworkPolicyManager.ALLOWED_REASON_TOP; +import static android.net.NetworkPolicyManager.BACKGROUND_THRESHOLD_STATE; import static android.net.NetworkPolicyManager.FIREWALL_RULE_DEFAULT; import static android.net.NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND; import static android.net.NetworkPolicyManager.POLICY_NONE; @@ -64,6 +71,7 @@ import static android.net.NetworkStats.METERED_YES; import static android.net.NetworkTemplate.MATCH_CARRIER; import static android.net.NetworkTemplate.MATCH_MOBILE; import static android.net.NetworkTemplate.MATCH_WIFI; +import static android.os.PowerExemptionManager.REASON_OTHER; import static android.telephony.CarrierConfigManager.ACTION_CARRIER_CONFIG_CHANGED; import static android.telephony.CarrierConfigManager.DATA_CYCLE_THRESHOLD_DISABLED; import static android.telephony.CarrierConfigManager.DATA_CYCLE_USE_PLATFORM_DEFAULT; @@ -146,6 +154,8 @@ import android.os.Build; import android.os.Handler; import android.os.INetworkManagementService; import android.os.PersistableBundle; +import android.os.PowerExemptionManager; +import android.os.PowerManager; import android.os.PowerManagerInternal; import android.os.PowerSaveState; import android.os.RemoteException; @@ -153,6 +163,9 @@ import android.os.SimpleClock; import android.os.UserHandle; import android.os.UserManager; 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.telephony.CarrierConfigManager; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; @@ -169,6 +182,7 @@ import android.util.Pair; import android.util.Range; import android.util.RecurrenceRule; import android.util.SparseArray; +import android.util.SparseIntArray; import androidx.test.InstrumentationRegistry; import androidx.test.filters.FlakyTest; @@ -243,6 +257,9 @@ import java.util.stream.Collectors; public class NetworkPolicyManagerServiceTest { private static final String TAG = "NetworkPolicyManagerServiceTest"; + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + private static final long TEST_START = 1194220800000L; private static final String TEST_IFACE = "test0"; private static final String TEST_WIFI_NETWORK_KEY = "TestWifiNetworkKey"; @@ -285,6 +302,7 @@ public class NetworkPolicyManagerServiceTest { private @Mock TelephonyManager mTelephonyManager; private @Mock UserManager mUserManager; private @Mock NetworkStatsManager mStatsManager; + private @Mock PowerExemptionManager mPowerExemptionManager; private TestDependencies mDeps; private ArgumentCaptor<ConnectivityManager.NetworkCallback> mNetworkCallbackCaptor = @@ -302,6 +320,7 @@ public class NetworkPolicyManagerServiceTest { private NetworkPolicyManagerService mService; private final ArraySet<BroadcastReceiver> mRegisteredReceivers = new ArraySet<>(); + private BroadcastReceiver mPowerAllowlistReceiver; /** * In some of the tests while initializing NetworkPolicyManagerService, @@ -446,6 +465,7 @@ public class NetworkPolicyManagerServiceTest { @Before public void callSystemReady() throws Exception { MockitoAnnotations.initMocks(this); + when(mPowerExemptionManager.getAllowListedAppIds(anyBoolean())).thenReturn(new int[0]); final Context context = InstrumentationRegistry.getContext(); @@ -482,6 +502,8 @@ public class NetworkPolicyManagerServiceTest { return mUserManager; case Context.NETWORK_STATS_SERVICE: return mStatsManager; + case Context.POWER_EXEMPTION_SERVICE: + return mPowerExemptionManager; default: return super.getSystemService(name); } @@ -495,6 +517,9 @@ public class NetworkPolicyManagerServiceTest { @Override public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter, String broadcastPermission, Handler scheduler) { + if (filter.hasAction(PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED)) { + mPowerAllowlistReceiver = receiver; + } mRegisteredReceivers.add(receiver); return super.registerReceiver(receiver, filter, broadcastPermission, scheduler); } @@ -2066,6 +2091,12 @@ public class NetworkPolicyManagerServiceTest { expectHasUseRestrictedNetworksPermission(UID_A, true); expectHasUseRestrictedNetworksPermission(UID_B, false); + // Set low enough proc-states to ensure these uids are allowed in the background chain. + // To maintain clean separation between separate firewall chains, the tests could + // check for the specific blockedReasons in the uidBlockedState. + callAndWaitOnUidStateChanged(UID_A, BACKGROUND_THRESHOLD_STATE - 1, 21); + callAndWaitOnUidStateChanged(UID_B, BACKGROUND_THRESHOLD_STATE - 1, 21); + Map<Integer, Integer> firewallUidRules = new ArrayMap<>(); doAnswer(arg -> { int[] uids = arg.getArgument(1); @@ -2113,7 +2144,111 @@ public class NetworkPolicyManagerServiceTest { } @Test + @RequiresFlagsEnabled(Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE) + public void testBackgroundChainEnabled() throws Exception { + verify(mNetworkManager).setFirewallChainEnabled(FIREWALL_CHAIN_BACKGROUND, true); + } + + + @Test + @RequiresFlagsEnabled(Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE) + public void testBackgroundChainOnProcStateChange() throws Exception { + // initialization calls setFirewallChainEnabled, so we want to reset the invocations. + clearInvocations(mNetworkManager); + + mService.mBackgroundRestrictionDelayMs = 500; // To avoid waiting too long in tests. + + // The app will be blocked when there is no prior proc-state. + assertTrue(mService.isUidNetworkingBlocked(UID_A, false)); + + int procStateSeq = 23; + callAndWaitOnUidStateChanged(UID_A, BACKGROUND_THRESHOLD_STATE - 1, procStateSeq++); + + verify(mNetworkManager).setFirewallUidRule(FIREWALL_CHAIN_BACKGROUND, UID_A, + FIREWALL_RULE_ALLOW); + assertFalse(mService.isUidNetworkingBlocked(UID_A, false)); + + callAndWaitOnUidStateChanged(UID_A, BACKGROUND_THRESHOLD_STATE + 1, procStateSeq++); + + // The app should be blocked after a delay. Posting a message just after the delay and + // waiting for it to complete to ensure that the blocking code has executed. + waitForDelayedMessageOnHandler(mService.mBackgroundRestrictionDelayMs + 1); + + verify(mNetworkManager).setFirewallUidRule(FIREWALL_CHAIN_BACKGROUND, UID_A, + FIREWALL_RULE_DEFAULT); + assertTrue(mService.isUidNetworkingBlocked(UID_A, false)); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE) + public void testBackgroundChainOnAllowlistChange() throws Exception { + // initialization calls setFirewallChainEnabled, so we want to reset the invocations. + clearInvocations(mNetworkManager); + + // The apps will be blocked when there is no prior proc-state. + assertTrue(mService.isUidNetworkingBlocked(UID_A, false)); + assertTrue(mService.isUidNetworkingBlocked(UID_B, false)); + + final int procStateSeq = 29; + callAndWaitOnUidStateChanged(UID_A, BACKGROUND_THRESHOLD_STATE + 1, procStateSeq); + assertTrue(mService.isUidNetworkingBlocked(UID_A, false)); + + when(mPowerExemptionManager.getAllowListedAppIds(anyBoolean())) + .thenReturn(new int[]{APP_ID_A, APP_ID_B}); + final SparseIntArray firewallUidRules = new SparseIntArray(); + doAnswer(arg -> { + final int[] uids = arg.getArgument(1); + final int[] rules = arg.getArgument(2); + assertTrue(uids.length == rules.length); + + for (int i = 0; i < uids.length; ++i) { + firewallUidRules.put(uids[i], rules[i]); + } + return null; + }).when(mNetworkManager).setFirewallUidRules(eq(FIREWALL_CHAIN_BACKGROUND), + any(int[].class), any(int[].class)); + + mPowerAllowlistReceiver.onReceive(mServiceContext, null); + + assertEquals(FIREWALL_RULE_ALLOW, firewallUidRules.get(UID_A, -1)); + assertEquals(FIREWALL_RULE_ALLOW, firewallUidRules.get(UID_B, -1)); + + assertFalse(mService.isUidNetworkingBlocked(UID_A, false)); + assertFalse(mService.isUidNetworkingBlocked(UID_B, false)); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE) + public void testBackgroundChainOnTempAllowlistChange() throws Exception { + // initialization calls setFirewallChainEnabled, so we want to reset the invocations. + clearInvocations(mNetworkManager); + + // The app will be blocked as is no prior proc-state. + assertTrue(mService.isUidNetworkingBlocked(UID_A, false)); + + final int procStateSeq = 19; + callAndWaitOnUidStateChanged(UID_A, BACKGROUND_THRESHOLD_STATE + 1, procStateSeq); + assertTrue(mService.isUidNetworkingBlocked(UID_A, false)); + + final NetworkPolicyManagerInternal internal = LocalServices.getService( + NetworkPolicyManagerInternal.class); + + internal.onTempPowerSaveWhitelistChange(APP_ID_A, true, REASON_OTHER, "testing"); + + verify(mNetworkManager).setFirewallUidRule(FIREWALL_CHAIN_BACKGROUND, UID_A, + FIREWALL_RULE_ALLOW); + assertFalse(mService.isUidNetworkingBlocked(UID_A, false)); + + internal.onTempPowerSaveWhitelistChange(APP_ID_A, false, REASON_OTHER, "testing"); + + verify(mNetworkManager).setFirewallUidRule(FIREWALL_CHAIN_BACKGROUND, UID_A, + FIREWALL_RULE_DEFAULT); + assertTrue(mService.isUidNetworkingBlocked(UID_A, false)); + } + + @Test public void testLowPowerStandbyAllowlist() throws Exception { + // Chain background is also enabled but these procstates are important enough to be exempt. callAndWaitOnUidStateChanged(UID_A, PROCESS_STATE_TOP, 0); callAndWaitOnUidStateChanged(UID_B, ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, 0); callAndWaitOnUidStateChanged(UID_C, ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, 0); @@ -2200,7 +2335,21 @@ public class NetworkPolicyManagerServiceTest { ALLOWED_REASON_TOP), BLOCKED_REASON_NONE); effectiveBlockedReasons.put(Pair.create(BLOCKED_REASON_LOW_POWER_STANDBY, ALLOWED_REASON_LOW_POWER_STANDBY_ALLOWLIST), BLOCKED_REASON_NONE); - // TODO: test more combinations of blocked reasons. + + effectiveBlockedReasons.put(Pair.create(BLOCKED_REASON_APP_BACKGROUND, + ALLOWED_REASON_NOT_IN_BACKGROUND), BLOCKED_REASON_NONE); + effectiveBlockedReasons.put(Pair.create(BLOCKED_REASON_APP_BACKGROUND + | BLOCKED_REASON_BATTERY_SAVER, ALLOWED_REASON_NOT_IN_BACKGROUND), + BLOCKED_REASON_BATTERY_SAVER); + effectiveBlockedReasons.put(Pair.create(BLOCKED_REASON_APP_BACKGROUND + | BLOCKED_REASON_DOZE, ALLOWED_REASON_NOT_IN_BACKGROUND), + BLOCKED_REASON_DOZE); + effectiveBlockedReasons.put(Pair.create(BLOCKED_REASON_APP_BACKGROUND, + ALLOWED_REASON_RESTRICTED_MODE_PERMISSIONS), BLOCKED_REASON_APP_BACKGROUND); + effectiveBlockedReasons.put(Pair.create(BLOCKED_REASON_APP_BACKGROUND, + ALLOWED_REASON_POWER_SAVE_ALLOWLIST), BLOCKED_REASON_NONE); + effectiveBlockedReasons.put(Pair.create(BLOCKED_REASON_APP_BACKGROUND, + ALLOWED_REASON_POWER_SAVE_EXCEPT_IDLE_ALLOWLIST), BLOCKED_REASON_NONE); for (Map.Entry<Pair<Integer, Integer>, Integer> test : effectiveBlockedReasons.entrySet()) { final int expectedEffectiveBlockedReasons = test.getValue(); @@ -2529,7 +2678,6 @@ public class NetworkPolicyManagerServiceTest { private FutureIntent mRestrictBackgroundChanged; private void postMsgAndWaitForCompletion() throws InterruptedException { - final Handler handler = mService.getHandlerForTesting(); final CountDownLatch latch = new CountDownLatch(1); mService.getHandlerForTesting().post(latch::countDown); if (!latch.await(5, TimeUnit.SECONDS)) { @@ -2537,6 +2685,14 @@ public class NetworkPolicyManagerServiceTest { } } + private void waitForDelayedMessageOnHandler(long delayMs) throws InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); + mService.getHandlerForTesting().postDelayed(latch::countDown, delayMs); + if (!latch.await(delayMs + 5_000, TimeUnit.MILLISECONDS)) { + fail("Timed out waiting for delayed msg to be handled"); + } + } + private void setSubscriptionPlans(int subId, SubscriptionPlan[] plans, String callingPackage) throws InterruptedException { mService.setSubscriptionPlans(subId, plans, 0, callingPackage); diff --git a/services/tests/servicestests/src/com/android/server/om/OverlayActorEnforcerTests.kt b/services/tests/servicestests/src/com/android/server/om/OverlayActorEnforcerTests.kt index 10f27ca02aaa..72fa949301cc 100644 --- a/services/tests/servicestests/src/com/android/server/om/OverlayActorEnforcerTests.kt +++ b/services/tests/servicestests/src/com/android/server/om/OverlayActorEnforcerTests.kt @@ -80,7 +80,7 @@ class OverlayActorEnforcerTests { @BeforeClass @JvmStatic fun checkAllCasesUniquelyNamed() { - val duplicateCaseNames = CASES.mapIndexed { caseIndex, testCase -> + val duplicateCaseNames = CASES.mapIndexed { _, testCase -> testCase.failures.map { makeTestName(testCase, it.first, Params.Type.FAILURE) } + testCase.allowed.map { diff --git a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java index 81df597f3f33..080548520b0c 100644 --- a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java @@ -157,7 +157,6 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { return mMockDevicePolicyManager; case Context.APP_SEARCH_SERVICE: case Context.ROLE_SERVICE: - case Context.APP_OPS_SERVICE: // RoleManager is final and cannot be mocked, so we only override the inject // accessor methods in ShortcutService. return getTestContext().getSystemService(name); diff --git a/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigNamedActorTest.kt b/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigNamedActorTest.kt index 150822bdff6b..c07c4d7618b7 100644 --- a/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigNamedActorTest.kt +++ b/services/tests/servicestests/src/com/android/server/systemconfig/SystemConfigNamedActorTest.kt @@ -18,12 +18,13 @@ package com.android.server.systemconfig import android.content.Context import android.util.Xml -import androidx.test.InstrumentationRegistry +import androidx.test.platform.app.InstrumentationRegistry import com.android.server.SystemConfig import com.google.common.truth.Truth.assertThat +import org.junit.Assert.assertEquals +import org.junit.Assert.assertThrows import org.junit.Rule import org.junit.Test -import org.junit.rules.ExpectedException import org.junit.rules.TemporaryFolder class SystemConfigNamedActorTest { @@ -37,14 +38,11 @@ class SystemConfigNamedActorTest { private const val PACKAGE_TWO = "com.test.actor.two" } - private val context: Context = InstrumentationRegistry.getContext() + private val context: Context = InstrumentationRegistry.getInstrumentation().context @get:Rule val tempFolder = TemporaryFolder(context.filesDir) - @get:Rule - val expected = ExpectedException.none() - private var uniqueCounter = 0 @Test @@ -193,11 +191,9 @@ class SystemConfigNamedActorTest { </config> """.write() - expected.expect(IllegalStateException::class.java) - expected.expectMessage("Defining $ACTOR_ONE as $PACKAGE_ONE " + + val exc = assertThrows(IllegalStateException::class.java) { assertPermissions() } + assertEquals(exc.message, "Defining $ACTOR_ONE as $PACKAGE_ONE " + "for the android namespace is not allowed") - - assertPermissions() } @Test @@ -217,11 +213,9 @@ class SystemConfigNamedActorTest { </config> """.write() - expected.expect(IllegalStateException::class.java) - expected.expectMessage("Duplicate actor definition for $NAMESPACE_TEST/$ACTOR_ONE;" + + val exc = assertThrows(IllegalStateException::class.java) { assertPermissions() } + assertEquals(exc.message, "Duplicate actor definition for $NAMESPACE_TEST/$ACTOR_ONE;" + " defined as both $PACKAGE_ONE and $PACKAGE_TWO") - - assertPermissions() } private fun String.write() = tempFolder.root.resolve("${uniqueCounter++}.xml") @@ -230,5 +224,5 @@ class SystemConfigNamedActorTest { private fun assertPermissions() = SystemConfig(false).apply { val parser = Xml.newPullParser() readPermissions(parser, tempFolder.root, 0) - }. let { assertThat(it.namedActors) } + }.let { assertThat(it.namedActors) } } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java index bfd2df2d2b7d..e75afccfdfdf 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java @@ -27,12 +27,13 @@ import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_LIGHTS; import static android.media.AudioAttributes.USAGE_NOTIFICATION; import static android.media.AudioAttributes.USAGE_NOTIFICATION_RINGTONE; -import static junit.framework.Assert.assertFalse; -import static junit.framework.Assert.assertNull; -import static junit.framework.Assert.assertTrue; +import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.any; @@ -43,6 +44,7 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.after; +import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; @@ -59,7 +61,10 @@ import android.app.Notification.Builder; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; +import android.content.BroadcastReceiver; import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; import android.content.pm.PackageManager; import android.content.pm.UserInfo; import android.graphics.Color; @@ -80,6 +85,7 @@ import android.provider.Settings; import android.service.notification.NotificationListenerService; import android.service.notification.StatusBarNotification; import android.test.suitebuilder.annotation.SmallTest; +import android.util.Pair; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.IAccessibilityManager; @@ -100,6 +106,7 @@ import com.android.server.pm.PackageManagerService; import java.util.List; import java.util.Objects; +import java.util.Set; import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -132,6 +139,8 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { KeyguardManager mKeyguardManager; @Mock private UserManager mUserManager; + @Mock + private PackageManager mPackageManager; NotificationRecordLoggerFake mNotificationRecordLogger = new NotificationRecordLoggerFake(); private InstanceIdSequence mNotificationInstanceIdSequence = new InstanceIdSequenceFake( 1 << 30); @@ -171,11 +180,14 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { private static final int CUSTOM_LIGHT_OFF = 10000; private static final int MAX_VIBRATION_DELAY = 1000; private static final float DEFAULT_VOLUME = 1.0f; + private BroadcastReceiver mAvalancheBroadcastReceiver; @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); getContext().addMockSystemService(Vibrator.class, mVibrator); + getContext().addMockSystemService(PackageManager.class, mPackageManager); + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_TELEPHONY)).thenReturn(false); when(mAudioManager.isAudioFocusExclusive()).thenReturn(false); when(mAudioManager.getRingtonePlayer()).thenReturn(mRingtonePlayer); @@ -214,8 +226,9 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { private void initAttentionHelper(TestableFlagResolver flagResolver) { mAttentionHelper = new NotificationAttentionHelper(getContext(), mock(LightsManager.class), - mAccessibilityManager, getContext().getPackageManager(), mUserManager, mUsageStats, - mService.mNotificationManagerPrivate, mock(ZenModeHelper.class), flagResolver); + mAccessibilityManager, mPackageManager, mUserManager, mUsageStats, + mService.mNotificationManagerPrivate, mock(ZenModeHelper.class), flagResolver); + mAttentionHelper.onSystemReady(); mAttentionHelper.setVibratorHelper(spy(new VibratorHelper(getContext()))); mAttentionHelper.setAudioManager(mAudioManager); mAttentionHelper.setSystemReady(true); @@ -226,6 +239,29 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { mAttentionHelper.setScreenOn(false); mAttentionHelper.setInCallStateOffHook(false); mAttentionHelper.mNotificationPulseEnabled = true; + + if (Flags.crossAppPoliteNotifications()) { + // Capture BroadcastReceiver for avalanche triggers + ArgumentCaptor<BroadcastReceiver> broadcastReceiverCaptor = + ArgumentCaptor.forClass(BroadcastReceiver.class); + ArgumentCaptor<IntentFilter> intentFilterCaptor = + ArgumentCaptor.forClass(IntentFilter.class); + verify(getContext(), atLeastOnce()).registerReceiverAsUser( + broadcastReceiverCaptor.capture(), + any(), intentFilterCaptor.capture(), any(), any()); + List<BroadcastReceiver> broadcastReceivers = broadcastReceiverCaptor.getAllValues(); + List<IntentFilter> intentFilters = intentFilterCaptor.getAllValues(); + + assertThat(broadcastReceivers.size()).isAtLeast(1); + assertThat(intentFilters.size()).isAtLeast(1); + for (int i = 0; i < intentFilters.size(); i++) { + final IntentFilter filter = intentFilters.get(i); + if (filter.hasAction(Intent.ACTION_AIRPLANE_MODE_CHANGED)) { + mAvalancheBroadcastReceiver = broadcastReceivers.get(i); + } + } + assertThat(mAvalancheBroadcastReceiver).isNotNull(); + } } // @@ -2040,7 +2076,7 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { } @Test - public void testBeepVolume_politeNotif_GlobalStrategy() throws Exception { + public void testBeepVolume_politeNotif_AvalancheStrategy() throws Exception { mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS); mSetFlagsRule.enableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS); TestableFlagResolver flagResolver = new TestableFlagResolver(); @@ -2048,6 +2084,11 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME2, 0); initAttentionHelper(flagResolver); + // Trigger avalanche trigger intent + final Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED); + intent.putExtra("state", false); + mAvalancheBroadcastReceiver.onReceive(getContext(), intent); + NotificationRecord r = getBeepyNotification(); // set up internal state @@ -2078,7 +2119,8 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { } @Test - public void testBeepVolume_politeNotif_GlobalStrategy_ChannelHasUserSound() throws Exception { + public void testBeepVolume_politeNotif_AvalancheStrategy_ChannelHasUserSound() + throws Exception { mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS); mSetFlagsRule.enableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS); TestableFlagResolver flagResolver = new TestableFlagResolver(); @@ -2086,6 +2128,11 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME2, 0); initAttentionHelper(flagResolver); + // Trigger avalanche trigger intent + final Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED); + intent.putExtra("state", false); + mAvalancheBroadcastReceiver.onReceive(getContext(), intent); + NotificationRecord r = getBeepyNotification(); // set up internal state @@ -2364,6 +2411,82 @@ public class NotificationAttentionHelperTest extends UiServiceTestCase { assertNotEquals(-1, r.getLastAudiblyAlertedMs()); } + @Test + public void testAvalancheStrategyTriggers() throws Exception { + mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS); + mSetFlagsRule.enableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS); + TestableFlagResolver flagResolver = new TestableFlagResolver(); + final int avalancheTimeoutMs = 100; + flagResolver.setFlagOverride(NotificationFlags.NOTIF_AVALANCHE_TIMEOUT, avalancheTimeoutMs); + initAttentionHelper(flagResolver); + + // Trigger avalanche trigger intents + for (String intentAction + : NotificationAttentionHelper.NOTIFICATION_AVALANCHE_TRIGGER_INTENTS) { + // Set the action and extras to trigger the avalanche strategy + Intent intent = new Intent(intentAction); + Pair<String, Boolean> extras = + NotificationAttentionHelper.NOTIFICATION_AVALANCHE_TRIGGER_EXTRAS + .get(intentAction); + if (extras != null) { + intent.putExtra(extras.first, extras.second); + } + mAvalancheBroadcastReceiver.onReceive(getContext(), intent); + assertThat(mAttentionHelper.getPolitenessStrategy().isActive()).isTrue(); + + // Wait for avalanche timeout + Thread.sleep(avalancheTimeoutMs + 1); + + // Check that avalanche strategy is inactive + assertThat(mAttentionHelper.getPolitenessStrategy().isActive()).isFalse(); + } + } + + @Test + public void testAvalancheStrategyTriggers_disabledExtras() throws Exception { + mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS); + mSetFlagsRule.enableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS); + TestableFlagResolver flagResolver = new TestableFlagResolver(); + initAttentionHelper(flagResolver); + + for (String intentAction + : NotificationAttentionHelper.NOTIFICATION_AVALANCHE_TRIGGER_INTENTS) { + Intent intent = new Intent(intentAction); + Pair<String, Boolean> extras = + NotificationAttentionHelper.NOTIFICATION_AVALANCHE_TRIGGER_EXTRAS + .get(intentAction); + // Test only for intents with extras + if (extras != null) { + // Set the action extras to NOT trigger the avalanche strategy + intent.putExtra(extras.first, !extras.second); + mAvalancheBroadcastReceiver.onReceive(getContext(), intent); + // Check that avalanche strategy is inactive + assertThat(mAttentionHelper.getPolitenessStrategy().isActive()).isFalse(); + } + } + } + + @Test + public void testAvalancheStrategyTriggers_nonAvalancheIntents() throws Exception { + mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS); + mSetFlagsRule.enableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS); + TestableFlagResolver flagResolver = new TestableFlagResolver(); + initAttentionHelper(flagResolver); + + // Broadcast intents that are not avalanche triggers + final Set<String> notAvalancheTriggerIntents = Set.of( + Intent.ACTION_USER_ADDED, + Intent.ACTION_SCREEN_ON, + Intent.ACTION_POWER_CONNECTED + ); + for (String intentAction : notAvalancheTriggerIntents) { + Intent intent = new Intent(intentAction); + mAvalancheBroadcastReceiver.onReceive(getContext(), intent); + // Check that avalanche strategy is inactive + assertThat(mAttentionHelper.getPolitenessStrategy().isActive()).isFalse(); + } + } + static class VibrateRepeatMatcher implements ArgumentMatcher<VibrationEffect> { private final int mRepeatIndex; diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java index ba7b52e368f3..31d6fa3e91f8 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -1762,32 +1762,6 @@ public class ActivityRecordTests extends WindowTestsBase { assertEquals(1, task.getChildCount()); } - /** - * Test that an activity will not be destroyed if it is marked as non-destroyable. - */ - @Test - public void testSafelyDestroy_nonDestroyable() { - final ActivityRecord activity = createActivityWithTask(); - doReturn(false).when(activity).isDestroyable(); - - activity.safelyDestroy("test"); - - verify(activity, never()).destroyImmediately(anyString()); - } - - /** - * Test that an activity will not be destroyed if it is marked as non-destroyable. - */ - @Test - public void testSafelyDestroy_destroyable() { - final ActivityRecord activity = createActivityWithTask(); - doReturn(true).when(activity).isDestroyable(); - - activity.safelyDestroy("test"); - - verify(activity).destroyImmediately(anyString()); - } - @Test public void testRemoveImmediately() { final Consumer<Consumer<ActivityRecord>> test = setup -> { @@ -3748,6 +3722,68 @@ public class ActivityRecordTests extends WindowTestsBase { assertFalse(ar.moveFocusableActivityToTop("test")); } + @Test + public void testPauseConfigDispatch() throws RemoteException { + final Task task = new TaskBuilder(mSupervisor) + .setDisplay(mDisplayContent).setCreateActivity(true).build(); + final ActivityRecord activity = task.getTopNonFinishingActivity(); + final WindowManager.LayoutParams attrs = new WindowManager.LayoutParams( + TYPE_BASE_APPLICATION); + attrs.setTitle("AppWindow"); + final TestWindowState appWindow = createWindowState(attrs, activity); + activity.addWindow(appWindow); + + clearInvocations(mClientLifecycleManager); + clearInvocations(activity); + + Configuration ro = activity.getRequestedOverrideConfiguration(); + ro.windowConfiguration.setBounds(new Rect(20, 0, 120, 200)); + activity.onRequestedOverrideConfigurationChanged(ro); + activity.ensureActivityConfiguration(); + mWm.mRoot.performSurfacePlacement(); + + // policy will center the bounds, so just check for matching size here. + assertEquals(100, activity.getWindowConfiguration().getBounds().width()); + assertEquals(100, appWindow.getWindowConfiguration().getBounds().width()); + // No scheduled transactions since it asked for a restart. + verify(mClientLifecycleManager, times(1)).scheduleTransaction(any()); + verify(activity, times(1)).setLastReportedConfiguration(any(), any()); + assertTrue(appWindow.mResizeReported); + + // act like everything drew and went idle + appWindow.mResizeReported = false; + makeLastConfigReportedToClient(appWindow, true); + + // Now pause dispatch and try to resize + activity.pauseConfigurationDispatch(); + + ro.windowConfiguration.setBounds(new Rect(20, 0, 150, 200)); + activity.onRequestedOverrideConfigurationChanged(ro); + activity.ensureActivityConfiguration(); + mWm.mRoot.performSurfacePlacement(); + + // Activity should get new config (core-side) + assertEquals(130, activity.getWindowConfiguration().getBounds().width()); + // But windows should not get new config. + assertEquals(100, appWindow.getWindowConfiguration().getBounds().width()); + // The client shouldn't receive any changes + verify(mClientLifecycleManager, times(1)).scheduleTransaction(any()); + // and lastReported shouldn't be set. + verify(activity, times(1)).setLastReportedConfiguration(any(), any()); + // There should be no resize reported to client. + assertFalse(appWindow.mResizeReported); + + // Now resume dispatch + activity.resumeConfigurationDispatch(); + mWm.mRoot.performSurfacePlacement(); + + // Windows and client should now receive updates + verify(activity, times(2)).setLastReportedConfiguration(any(), any()); + verify(mClientLifecycleManager, times(2)).scheduleTransaction(any()); + assertEquals(130, appWindow.getWindowConfiguration().getBounds().width()); + assertTrue(appWindow.mResizeReported); + } + private ICompatCameraControlCallback getCompatCameraControlCallback() { return new ICompatCameraControlCallback.Stub() { @Override diff --git a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java index 402cbccbca01..c44be7b9db51 100644 --- a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java @@ -56,6 +56,7 @@ import android.os.Bundle; import android.os.RemoteCallback; import android.os.RemoteException; import android.platform.test.annotations.Presubmit; +import android.platform.test.annotations.RequiresFlagsEnabled; import android.util.ArraySet; import android.view.WindowManager; import android.window.BackAnimationAdapter; @@ -69,6 +70,7 @@ import android.window.TaskSnapshot; import android.window.WindowOnBackInvokedDispatcher; import com.android.server.LocalServices; +import com.android.window.flags.Flags; import org.junit.Before; import org.junit.Test; @@ -81,6 +83,12 @@ import java.util.ArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +/** + * Tests for the {@link BackNavigationController} class. + * + * Build/Install/Run: + * atest WmTests:BackNavigationControllerTests + */ @Presubmit @RunWith(WindowTestRunner.class) public class BackNavigationControllerTests extends WindowTestsBase { @@ -623,6 +631,22 @@ public class BackNavigationControllerTests extends WindowTestsBase { 0, navigationObserver.getCount()); } + @Test + @RequiresFlagsEnabled(Flags.FLAG_EMBEDDED_ACTIVITY_BACK_NAV_FLAG) + public void testAdjacentFocusInActivityEmbedding() { + Task task = createTask(mDefaultDisplay); + TaskFragment primary = createTaskFragmentWithActivity(task); + TaskFragment secondary = createTaskFragmentWithActivity(task); + primary.setAdjacentTaskFragment(secondary); + secondary.setAdjacentTaskFragment(primary); + + WindowState windowState = mock(WindowState.class); + doReturn(windowState).when(mWm).getFocusedWindowLocked(); + doReturn(primary).when(windowState).getTaskFragment(); + + startBackNavigation(); + verify(mWm).moveFocusToAdjacentWindow(any(), anyInt()); + } /** * Test with diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java index 782d89cdcd29..95850ac2f3b2 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayContentTests.java @@ -2131,8 +2131,8 @@ public class DisplayContentTests extends WindowTestsBase { // Once transition starts, rotation is applied and transition shows DC rotating. testPlayer.startTransition(); waitUntilHandlersIdle(); - verify(activity1).ensureActivityConfiguration(anyBoolean(), anyBoolean()); - verify(activity2).ensureActivityConfiguration(anyBoolean(), anyBoolean()); + verify(activity1).ensureActivityConfiguration(anyBoolean()); + verify(activity2).ensureActivityConfiguration(anyBoolean()); assertNotEquals(origRot, dc.getConfiguration().windowConfiguration.getRotation()); assertNotNull(testPlayer.mLastReady); assertTrue(testPlayer.mController.isPlaying()); diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java index be96e60917a3..9e00f927a568 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java @@ -283,12 +283,12 @@ public class DisplayPolicyTests extends WindowTestsBase { policy.screenTurnedOff(); policy.setAwake(false); - policy.screenTurnedOn(null /* screenOnListener */); + policy.screenTurningOn(null /* screenOnListener */); assertTrue(wpc.isShowingUiWhileDozing()); policy.screenTurnedOff(); assertFalse(wpc.isShowingUiWhileDozing()); - policy.screenTurnedOn(null /* screenOnListener */); + policy.screenTurningOn(null /* screenOnListener */); assertTrue(wpc.isShowingUiWhileDozing()); policy.setAwake(true); assertFalse(wpc.isShowingUiWhileDozing()); 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 5518c604446d..752dc5e8e7f6 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java @@ -197,10 +197,10 @@ public class SizeCompatTests extends WindowTestsBase { mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); // Translucent Activity final ActivityRecord translucentActivity = new ActivityBuilder(mAtm) + .setActivityTheme(android.R.style.Theme_Translucent) .setLaunchedFromUid(mActivity.getUid()) .setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT) .build(); - doReturn(false).when(translucentActivity).fillsParent(); mTask.addChild(translucentActivity); translucentActivity.setState(DESTROYED, "testing"); @@ -225,10 +225,10 @@ public class SizeCompatTests extends WindowTestsBase { // Translucent Activity final ActivityRecord translucentActivity = new ActivityBuilder(mAtm) + .setActivityTheme(android.R.style.Theme_Translucent) .setLaunchedFromUid(mActivity.getUid()) .setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT) .build(); - doReturn(false).when(translucentActivity).fillsParent(); mTask.addChild(translucentActivity); spyOn(translucentActivity.mLetterboxUiController); @@ -300,10 +300,10 @@ public class SizeCompatTests extends WindowTestsBase { // Translucent Activity final ActivityRecord translucentActivity = new ActivityBuilder(mAtm) + .setActivityTheme(android.R.style.Theme_Translucent) .setLaunchedFromUid(mActivity.getUid()) .setScreenOrientation(SCREEN_ORIENTATION_LANDSCAPE) .build(); - doReturn(false).when(translucentActivity).fillsParent(); mTask.addChild(translucentActivity); spyOn(translucentActivity.mLetterboxUiController); @@ -376,10 +376,10 @@ public class SizeCompatTests extends WindowTestsBase { // Launch translucent Activity final ActivityRecord translucentActivity = new ActivityBuilder(mAtm) + .setActivityTheme(android.R.style.Theme_Translucent) .setLaunchedFromUid(mActivity.getUid()) .setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT) .build(); - doReturn(false).when(translucentActivity).fillsParent(); mTask.addChild(translucentActivity); // Transparent strategy applied assertTrue(translucentActivity.mLetterboxUiController.hasInheritedLetterboxBehavior()); @@ -404,10 +404,10 @@ public class SizeCompatTests extends WindowTestsBase { // Launch translucent Activity final ActivityRecord translucentActivity = new ActivityBuilder(mAtm) + .setActivityTheme(android.R.style.Theme_Translucent) .setLaunchedFromUid(mActivity.getUid()) .setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT) .build(); - doReturn(false).when(translucentActivity).fillsParent(); mTask.addChild(translucentActivity); // Transparent strategy applied assertTrue(translucentActivity.mLetterboxUiController.hasInheritedLetterboxBehavior()); @@ -441,10 +441,10 @@ public class SizeCompatTests extends WindowTestsBase { // Launch translucent Activity final ActivityRecord translucentActivity = new ActivityBuilder(mAtm) + .setActivityTheme(android.R.style.Theme_Translucent) .setLaunchedFromUid(mActivity.getUid()) .setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT) .build(); - doReturn(false).when(translucentActivity).fillsParent(); mTask.addChild(translucentActivity); // Transparent strategy applied assertTrue(translucentActivity.mLetterboxUiController.hasInheritedLetterboxBehavior()); @@ -465,12 +465,12 @@ public class SizeCompatTests extends WindowTestsBase { mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); // Translucent Activity final ActivityRecord translucentActivity = new ActivityBuilder(mAtm) + .setActivityTheme(android.R.style.Theme_Translucent) .setLaunchedFromUid(mActivity.getUid()) .setScreenOrientation(SCREEN_ORIENTATION_LANDSCAPE) .setMinAspectRatio(1.1f) .setMaxAspectRatio(3f) .build(); - doReturn(false).when(translucentActivity).fillsParent(); mTask.addChild(translucentActivity); // We check bounds final Rect opaqueBounds = mActivity.getConfiguration().windowConfiguration.getBounds(); @@ -493,9 +493,9 @@ public class SizeCompatTests extends WindowTestsBase { mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); // Translucent Activity final ActivityRecord translucentActivity = new ActivityBuilder(mAtm) + .setActivityTheme(android.R.style.Theme_Translucent) .setLaunchedFromUid(mActivity.getUid()) .build(); - doReturn(false).when(translucentActivity).fillsParent(); final Configuration requestedConfig = translucentActivity.getRequestedOverrideConfiguration(); final WindowConfiguration translucentWinConf = requestedConfig.windowConfiguration; @@ -525,12 +525,12 @@ public class SizeCompatTests extends WindowTestsBase { mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); // Translucent Activity final ActivityRecord translucentActivity = new ActivityBuilder(mAtm) + .setActivityTheme(android.R.style.Theme_Translucent) .setLaunchedFromUid(mActivity.getUid()) .setScreenOrientation(SCREEN_ORIENTATION_LANDSCAPE) .setMinAspectRatio(1.1f) .setMaxAspectRatio(3f) .build(); - doReturn(false).when(translucentActivity).fillsParent(); mTask.addChild(translucentActivity); // We check bounds final Rect opaqueBounds = mActivity.getConfiguration().windowConfiguration.getBounds(); @@ -538,10 +538,10 @@ public class SizeCompatTests extends WindowTestsBase { assertEquals(opaqueBounds, translucentRequestedBounds); // Launch another translucent activity final ActivityRecord translucentActivity2 = new ActivityBuilder(mAtm) + .setActivityTheme(android.R.style.Theme_Translucent) .setLaunchedFromUid(mActivity.getUid()) .setScreenOrientation(SCREEN_ORIENTATION_LANDSCAPE) .build(); - doReturn(false).when(translucentActivity2).fillsParent(); mTask.addChild(translucentActivity2); // We check bounds final Rect translucent2RequestedBounds = translucentActivity2.getRequestedOverrideBounds(); @@ -558,9 +558,9 @@ public class SizeCompatTests extends WindowTestsBase { // simplicity. doReturn(true).when(mActivity).isEmbedded(); // Translucent Activity - final ActivityRecord translucentActivity = new ActivityBuilder(mAtm).build(); + final ActivityRecord translucentActivity = new ActivityBuilder(mAtm) + .setActivityTheme(android.R.style.Theme_Translucent).build(); doReturn(false).when(translucentActivity).matchParentBounds(); - doReturn(false).when(translucentActivity).fillsParent(); mTask.addChild(translucentActivity); // Check the strategy has not being applied assertFalse(translucentActivity.mLetterboxUiController.hasInheritedLetterboxBehavior()); @@ -580,10 +580,10 @@ public class SizeCompatTests extends WindowTestsBase { assertFalse(mActivity.inSizeCompatMode()); // We launch a transparent activity final ActivityRecord translucentActivity = new ActivityBuilder(mAtm) + .setActivityTheme(android.R.style.Theme_Translucent) .setLaunchedFromUid(mActivity.getUid()) .setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT) .build(); - doReturn(false).when(translucentActivity).fillsParent(); mTask.addChild(translucentActivity); // It should not be in SCM assertFalse(translucentActivity.inSizeCompatMode()); @@ -600,12 +600,16 @@ public class SizeCompatTests extends WindowTestsBase { mActivity.mDisplayContent.setIgnoreOrientationRequest(true /* ignoreOrientationRequest */); // Translucent Activity final ActivityRecord translucentActivity = new ActivityBuilder(mAtm) + .setActivityTheme(android.R.style.Theme_Translucent) .setLaunchedFromUid(mActivity.getUid()) .build(); - doReturn(false).when(translucentActivity).fillsParent(); - spyOn(mActivity); + assertFalse(translucentActivity.fillsParent()); + assertTrue(mActivity.fillsParent()); + mActivity.finishing = true; + assertFalse(mActivity.occludesParent()); mTask.addChild(translucentActivity); - verify(mActivity).isFinishing(); + // The translucent activity won't inherit letterbox behavior from a finishing activity. + assertFalse(translucentActivity.mLetterboxUiController.hasInheritedLetterboxBehavior()); } @Test @@ -619,10 +623,10 @@ public class SizeCompatTests extends WindowTestsBase { prepareUnresizable(mActivity, SCREEN_ORIENTATION_PORTRAIT); // We launch a transparent activity final ActivityRecord translucentActivity = new ActivityBuilder(mAtm) + .setActivityTheme(android.R.style.Theme_Translucent) .setLaunchedFromUid(mActivity.getUid()) .setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT) .build(); - doReturn(false).when(translucentActivity).fillsParent(); mTask.addChild(translucentActivity); assertEquals(translucentActivity.getBounds(), mActivity.getBounds()); @@ -655,10 +659,10 @@ public class SizeCompatTests extends WindowTestsBase { // We launch a transparent activity final ActivityRecord translucentActivity = new ActivityBuilder(mAtm) + .setActivityTheme(android.R.style.Theme_Translucent) .setLaunchedFromUid(mActivity.getUid()) .setScreenOrientation(SCREEN_ORIENTATION_PORTRAIT) .build(); - doReturn(false).when(translucentActivity).fillsParent(); mTask.addChild(translucentActivity); // The transparent activity inherits the compat display insets of the opaque activity @@ -1020,8 +1024,17 @@ public class SizeCompatTests extends WindowTestsBase { // Activity is sandboxed due to fixed aspect ratio. assertActivityMaxBoundsSandboxed(); + // Prepare the states for verifying relaunching after changing orientation. + mActivity.finishRelaunching(); + mActivity.setState(RESUMED, "testFixedAspectRatioOrientationChangeOrientation"); + mActivity.setLastReportedConfiguration(mAtm.getGlobalConfiguration(), + mActivity.getConfiguration()); + // Change the fixed orientation. mActivity.setRequestedOrientation(SCREEN_ORIENTATION_LANDSCAPE); + assertTrue(mActivity.isRelaunching()); + assertTrue(mActivity.mLetterboxUiController + .getIsRelaunchingAfterRequestedOrientationChanged()); assertFitted(); assertEquals(originalBounds.width(), mActivity.getBounds().height()); @@ -4781,6 +4794,7 @@ public class SizeCompatTests extends WindowTestsBase { new WindowManager.LayoutParams(TYPE_STATUS_BAR); final Binder owner = new Binder(); attrs.gravity = android.view.Gravity.TOP; + attrs.height = STATUS_BAR_HEIGHT; attrs.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; attrs.setFitInsetsTypes(0 /* types */); diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java index 7c7e562426c2..245b2c5a7d6b 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java @@ -444,6 +444,10 @@ public class TaskFragmentTest extends WindowTestsBase { // Not ready if the task is still visible when the TaskFragment becomes empty. doReturn(true).when(task).isVisibleRequested(); assertFalse(taskFragment.isReadyToTransit()); + + // Ready if the mAllowTransitionWhenEmpty flag is true. + taskFragment.setAllowTransitionWhenEmpty(true); + assertTrue(taskFragment.isReadyToTransit()); } @Test diff --git a/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java b/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java index 45ecc3f762ec..00ecd008cde7 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java +++ b/services/tests/wmtests/src/com/android/server/wm/TestDisplayContent.java @@ -215,7 +215,7 @@ class TestDisplayContent extends DisplayContent { doReturn(false).when(newDisplay).supportsSystemDecorations(); } // Update the display policy to make the screen fully turned on so animation is allowed - displayPolicy.screenTurnedOn(null /* screenOnListener */); + displayPolicy.screenTurningOn(null /* screenOnListener */); displayPolicy.finishKeyguardDrawn(); displayPolicy.finishWindowsDrawn(); displayPolicy.finishScreenTurningOn(); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java index c7c791337bb4..a0bafb64090f 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowTestsBase.java @@ -229,7 +229,7 @@ class WindowTestsBase extends SystemServiceTestsBase { mDefaultDisplay = mWm.mRoot.getDefaultDisplay(); // Update the display policy to make the screen fully turned on so animation is allowed final DisplayPolicy displayPolicy = mDefaultDisplay.getDisplayPolicy(); - displayPolicy.screenTurnedOn(null /* screenOnListener */); + displayPolicy.screenTurningOn(null /* screenOnListener */); displayPolicy.finishKeyguardDrawn(); displayPolicy.finishWindowsDrawn(); displayPolicy.finishScreenTurningOn(); diff --git a/telecomm/java/android/telecom/CallControl.java b/telecomm/java/android/telecom/CallControl.java index fe699af86f1d..a14078697c71 100644 --- a/telecomm/java/android/telecom/CallControl.java +++ b/telecomm/java/android/telecom/CallControl.java @@ -21,7 +21,6 @@ import static android.telecom.CallException.TRANSACTION_EXCEPTION_KEY; import android.annotation.CallbackExecutor; import android.annotation.FlaggedApi; import android.annotation.NonNull; -import android.annotation.Nullable; import android.annotation.SuppressLint; import android.os.Binder; import android.os.Bundle; @@ -31,7 +30,6 @@ import android.os.RemoteException; import android.os.ResultReceiver; import android.text.TextUtils; -import com.android.internal.telecom.ClientTransactionalServiceRepository; import com.android.internal.telecom.ICallControl; import com.android.server.telecom.flags.Flags; @@ -52,20 +50,13 @@ import java.util.concurrent.Executor; @SuppressLint("NotCloseable") public final class CallControl { private static final String TAG = CallControl.class.getSimpleName(); - private static final String INTERFACE_ERROR_MSG = "Call Control is not available"; private final String mCallId; private final ICallControl mServerInterface; - private final PhoneAccountHandle mPhoneAccountHandle; - private final ClientTransactionalServiceRepository mRepository; /** @hide */ - public CallControl(@NonNull String callId, @Nullable ICallControl serverInterface, - @NonNull ClientTransactionalServiceRepository repository, - @NonNull PhoneAccountHandle pah) { + public CallControl(@NonNull String callId, @NonNull ICallControl serverInterface) { mCallId = callId; mServerInterface = serverInterface; - mRepository = repository; - mPhoneAccountHandle = pah; } /** @@ -97,16 +88,14 @@ public final class CallControl { */ public void setActive(@CallbackExecutor @NonNull Executor executor, @NonNull OutcomeReceiver<Void, CallException> callback) { - if (mServerInterface != null) { - try { - mServerInterface.setActive(mCallId, - new CallControlResultReceiver("setActive", executor, callback)); + Objects.requireNonNull(executor); + Objects.requireNonNull(callback); + try { + mServerInterface.setActive(mCallId, + new CallControlResultReceiver("setActive", executor, callback)); - } catch (RemoteException e) { - throw e.rethrowAsRuntimeException(); - } - } else { - throw new IllegalStateException(INTERFACE_ERROR_MSG); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); } } @@ -134,16 +123,12 @@ public final class CallControl { validateVideoState(videoState); Objects.requireNonNull(executor); Objects.requireNonNull(callback); - if (mServerInterface != null) { - try { - mServerInterface.answer(videoState, mCallId, - new CallControlResultReceiver("answer", executor, callback)); + try { + mServerInterface.answer(videoState, mCallId, + new CallControlResultReceiver("answer", executor, callback)); - } catch (RemoteException e) { - throw e.rethrowAsRuntimeException(); - } - } else { - throw new IllegalStateException(INTERFACE_ERROR_MSG); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); } } @@ -165,16 +150,14 @@ public final class CallControl { */ public void setInactive(@CallbackExecutor @NonNull Executor executor, @NonNull OutcomeReceiver<Void, CallException> callback) { - if (mServerInterface != null) { - try { - mServerInterface.setInactive(mCallId, - new CallControlResultReceiver("setInactive", executor, callback)); + Objects.requireNonNull(executor); + Objects.requireNonNull(callback); + try { + mServerInterface.setInactive(mCallId, + new CallControlResultReceiver("setInactive", executor, callback)); - } catch (RemoteException e) { - throw e.rethrowAsRuntimeException(); - } - } else { - throw new IllegalStateException(INTERFACE_ERROR_MSG); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); } } @@ -213,15 +196,11 @@ public final class CallControl { Objects.requireNonNull(executor); Objects.requireNonNull(callback); validateDisconnectCause(disconnectCause); - if (mServerInterface != null) { - try { - mServerInterface.disconnect(mCallId, disconnectCause, - new CallControlResultReceiver("disconnect", executor, callback)); - } catch (RemoteException e) { - throw e.rethrowAsRuntimeException(); - } - } else { - throw new IllegalStateException(INTERFACE_ERROR_MSG); + try { + mServerInterface.disconnect(mCallId, disconnectCause, + new CallControlResultReceiver("disconnect", executor, callback)); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); } } @@ -245,15 +224,13 @@ public final class CallControl { */ public void startCallStreaming(@CallbackExecutor @NonNull Executor executor, @NonNull OutcomeReceiver<Void, CallException> callback) { - if (mServerInterface != null) { - try { - mServerInterface.startCallStreaming(mCallId, - new CallControlResultReceiver("startCallStreaming", executor, callback)); - } catch (RemoteException e) { - throw e.rethrowAsRuntimeException(); - } - } else { - throw new IllegalStateException(INTERFACE_ERROR_MSG); + Objects.requireNonNull(executor); + Objects.requireNonNull(callback); + try { + mServerInterface.startCallStreaming(mCallId, + new CallControlResultReceiver("startCallStreaming", executor, callback)); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); } } @@ -281,15 +258,11 @@ public final class CallControl { Objects.requireNonNull(callEndpoint); Objects.requireNonNull(executor); Objects.requireNonNull(callback); - if (mServerInterface != null) { - try { - mServerInterface.requestCallEndpointChange(callEndpoint, - new CallControlResultReceiver("endpointChange", executor, callback)); - } catch (RemoteException e) { - throw e.rethrowAsRuntimeException(); - } - } else { - throw new IllegalStateException(INTERFACE_ERROR_MSG); + try { + mServerInterface.requestCallEndpointChange(callEndpoint, + new CallControlResultReceiver("requestCallEndpointChange", executor, callback)); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); } } @@ -313,20 +286,16 @@ public final class CallControl { * passed that details why the operation failed. */ @FlaggedApi(Flags.FLAG_SET_MUTE_STATE) - public void setMuteState(boolean isMuted, @CallbackExecutor @NonNull Executor executor, + public void requestMuteState(boolean isMuted, @CallbackExecutor @NonNull Executor executor, @NonNull OutcomeReceiver<Void, CallException> callback) { Objects.requireNonNull(executor); Objects.requireNonNull(callback); - if (mServerInterface != null) { - try { - mServerInterface.setMuteState(isMuted, - new CallControlResultReceiver("setMuteState", executor, callback)); + try { + mServerInterface.setMuteState(isMuted, + new CallControlResultReceiver("requestMuteState", executor, callback)); - } catch (RemoteException e) { - throw e.rethrowAsRuntimeException(); - } - } else { - throw new IllegalStateException(INTERFACE_ERROR_MSG); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); } } @@ -352,14 +321,10 @@ public final class CallControl { public void sendEvent(@NonNull String event, @NonNull Bundle extras) { Objects.requireNonNull(event); Objects.requireNonNull(extras); - if (mServerInterface != null) { - try { - mServerInterface.sendEvent(mCallId, event, extras); - } catch (RemoteException e) { - throw e.rethrowAsRuntimeException(); - } - } else { - throw new IllegalStateException(INTERFACE_ERROR_MSG); + try { + mServerInterface.sendEvent(mCallId, event, extras); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); } } diff --git a/telecomm/java/android/telecom/PhoneAccount.java b/telecomm/java/android/telecom/PhoneAccount.java index a089f5c9d641..63db29713825 100644 --- a/telecomm/java/android/telecom/PhoneAccount.java +++ b/telecomm/java/android/telecom/PhoneAccount.java @@ -580,6 +580,9 @@ public final class PhoneAccount implements Parcelable { mExtras = phoneAccount.getExtras(); mGroupId = phoneAccount.getGroupId(); mSupportedAudioRoutes = phoneAccount.getSupportedAudioRoutes(); + if (phoneAccount.hasSimultaneousCallingRestriction()) { + mSimultaneousCallingRestriction = phoneAccount.getSimultaneousCallingRestriction(); + } } /** diff --git a/telecomm/java/com/android/internal/telecom/ClientTransactionalServiceWrapper.java b/telecomm/java/com/android/internal/telecom/ClientTransactionalServiceWrapper.java index 71e9184b7c54..467e89c78810 100644 --- a/telecomm/java/com/android/internal/telecom/ClientTransactionalServiceWrapper.java +++ b/telecomm/java/com/android/internal/telecom/ClientTransactionalServiceWrapper.java @@ -208,8 +208,7 @@ public class ClientTransactionalServiceWrapper { if (resultCode == TELECOM_TRANSACTION_SUCCESS) { // create the interface object that the client will interact with - CallControl control = new CallControl(callId, callControl, mRepository, - mPhoneAccountHandle); + CallControl control = new CallControl(callId, callControl); // give the client the object via the OR that was passed into addCall pendingControl.onResult(control); diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java index 1badf674c8ce..a73c46b12c53 100644 --- a/telephony/java/android/telephony/CarrierConfigManager.java +++ b/telephony/java/android/telephony/CarrierConfigManager.java @@ -9430,16 +9430,6 @@ public class CarrierConfigManager { "missed_incoming_call_sms_originator_string_array"; /** - * String array of Apn Type configurations. - * The entries should be of form "APN_TYPE_NAME:priority". - * priority is an integer that is sorted from highest to lowest. - * example: cbs:5 - * - * @hide - */ - public static final String KEY_APN_PRIORITY_STRING_ARRAY = "apn_priority_string_array"; - - /** * Network capability priority for determine the satisfy order in telephony. The priority is * from the lowest 0 to the highest 100. The long-lived network shall have the lowest priority. * This allows other short-lived requests like MMS requests to be established. Emergency request @@ -10755,17 +10745,14 @@ public class CarrierConfigManager { TimeUnit.DAYS.toMillis(1)); sDefaults.putStringArray(KEY_MISSED_INCOMING_CALL_SMS_ORIGINATOR_STRING_ARRAY, new String[0]); - sDefaults.putStringArray(KEY_APN_PRIORITY_STRING_ARRAY, new String[] { - "enterprise:0", "default:1", "mms:2", "supl:2", "dun:2", "hipri:3", "fota:2", - "ims:2", "cbs:2", "ia:2", "emergency:2", "mcx:3", "xcap:3" - }); // Do not modify the priority unless you know what you are doing. This will have significant // impacts on the order of data network setup. sDefaults.putStringArray( KEY_TELEPHONY_NETWORK_CAPABILITY_PRIORITIES_STRING_ARRAY, new String[] { "eims:90", "supl:80", "mms:70", "xcap:70", "cbs:50", "mcx:50", "fota:50", - "ims:40", "dun:30", "enterprise:20", "internet:20" + "ims:40", "rcs:40", "dun:30", "enterprise:20", "internet:20", + "prioritize_bandwidth:20", "prioritize_latency:20" }); sDefaults.putStringArray( KEY_TELEPHONY_DATA_SETUP_RETRY_RULES_STRING_ARRAY, new String[] { @@ -10777,9 +10764,10 @@ public class CarrierConfigManager { // registration state changes) retry can still happen. "permanent_fail_causes=8|27|28|29|30|32|33|35|50|51|111|-5|-6|65537|65538|" + "-3|65543|65547|2252|2253|2254, retry_interval=2500", - "capabilities=mms|supl|cbs, retry_interval=2000", - "capabilities=internet|enterprise|dun|ims|fota, retry_interval=2500|3000|" - + "5000|10000|15000|20000|40000|60000|120000|240000|" + "capabilities=mms|supl|cbs|rcs, retry_interval=2000", + "capabilities=internet|enterprise|dun|ims|fota|xcap|mcx|" + + "prioritize_bandwidth|prioritize_latency, retry_interval=" + + "2500|3000|5000|10000|15000|20000|40000|60000|120000|240000|" + "600000|1200000|1800000, maximum_retries=20" }); sDefaults.putStringArray( diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java index 4dc77be425a4..1bf11df7059a 100644 --- a/telephony/java/android/telephony/SubscriptionManager.java +++ b/telephony/java/android/telephony/SubscriptionManager.java @@ -1954,7 +1954,6 @@ public class SubscriptionManager { * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. */ @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) - // @RequiresPermission(TODO(b/308809058)) public List<SubscriptionInfo> getActiveSubscriptionInfoList() { List<SubscriptionInfo> activeList = null; @@ -2011,6 +2010,9 @@ public class SubscriptionManager { * Create a new subscription manager instance that can see all subscriptions across * user profiles. * + * The permission check for accessing all subscriptions will be enforced upon calling the + * individual APIs linked below. + * * @return a SubscriptionManager that can see all subscriptions regardless its user profile * association. * @@ -2019,9 +2021,7 @@ public class SubscriptionManager { * @see UserHandle */ @FlaggedApi(Flags.FLAG_ENFORCE_SUBSCRIPTION_USER_FILTER) - // @RequiresPermission(TODO(b/308809058)) - // The permission check for accessing all subscriptions will be enforced upon calling the - // individual APIs linked above. + @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_PROFILES) @NonNull public SubscriptionManager createForAllUserProfiles() { return new SubscriptionManager(mContext, true/*isForAllUserProfiles*/); } @@ -2216,7 +2216,6 @@ public class SubscriptionManager { * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. */ @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) - // @RequiresPermission(TODO(b/308809058)) public int getActiveSubscriptionInfoCount() { int result = 0; diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java index b22d8ac3b48f..cbd552454642 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -13148,7 +13148,7 @@ public class TelephonyManager { */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P) public ServiceState getServiceStateForSubscriber(int subId) { - return getServiceStateForSubscriber(getSubId(), false, false); + return getServiceStateForSubscriber(subId, false, false); } /** diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java index 70047a6feb9c..a1ac477d3519 100644 --- a/telephony/java/android/telephony/satellite/SatelliteManager.java +++ b/telephony/java/android/telephony/satellite/SatelliteManager.java @@ -34,7 +34,6 @@ import android.os.ICancellationSignal; import android.os.OutcomeReceiver; import android.os.RemoteException; import android.os.ResultReceiver; -import android.os.ServiceSpecificException; import android.telephony.SubscriptionManager; import android.telephony.TelephonyCallback; import android.telephony.TelephonyFrameworkInitializer; @@ -336,6 +335,12 @@ public final class SatelliteManager { @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public static final int SATELLITE_RESULT_MODEM_BUSY = 22; + /** + * Telephony process is not currently available or satellite is not supported. + */ + @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) + public static final int SATELLITE_RESULT_ILLEGAL_STATE = 23; + /** @hide */ @IntDef(prefix = {"SATELLITE_RESULT_"}, value = { SATELLITE_RESULT_SUCCESS, @@ -360,7 +365,8 @@ public final class SatelliteManager { SATELLITE_RESULT_NOT_AUTHORIZED, SATELLITE_RESULT_NOT_SUPPORTED, SATELLITE_RESULT_REQUEST_IN_PROGRESS, - SATELLITE_RESULT_MODEM_BUSY + SATELLITE_RESULT_MODEM_BUSY, + SATELLITE_RESULT_ILLEGAL_STATE }) @Retention(RetentionPolicy.SOURCE) public @interface SatelliteResult {} @@ -510,7 +516,7 @@ public final class SatelliteManager { } } catch (RemoteException ex) { Rlog.e(TAG, "requestSatelliteEnabled() RemoteException: ", ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } } @@ -526,7 +532,6 @@ public final class SatelliteManager { * will return a {@link SatelliteException} with the {@link SatelliteResult}. * * @throws SecurityException if the caller doesn't have required permission. - * @throws IllegalStateException if the Telephony process is not currently available. */ @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) @@ -561,11 +566,12 @@ public final class SatelliteManager { }; telephony.requestIsSatelliteEnabled(mSubId, receiver); } else { - throw new IllegalStateException("telephony service is null."); + executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError( + new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE)))); } } catch (RemoteException ex) { loge("requestIsSatelliteEnabled() RemoteException: " + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } } @@ -581,7 +587,6 @@ public final class SatelliteManager { * will return a {@link SatelliteException} with the {@link SatelliteResult}. * * @throws SecurityException if the caller doesn't have required permission. - * @throws IllegalStateException if the Telephony process is not currently available. */ @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) @@ -616,11 +621,12 @@ public final class SatelliteManager { }; telephony.requestIsDemoModeEnabled(mSubId, receiver); } else { - throw new IllegalStateException("telephony service is null."); + executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError( + new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE)))); } } catch (RemoteException ex) { loge("requestIsDemoModeEnabled() RemoteException: " + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } } @@ -639,8 +645,6 @@ public final class SatelliteManager { * service is supported on the device and {@code false} otherwise. * If the request is not successful, {@link OutcomeReceiver#onError(Throwable)} * will return a {@link SatelliteException} with the {@link SatelliteResult}. - * - * @throws IllegalStateException if the Telephony process is not currently available. */ @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public void requestIsSatelliteSupported(@NonNull @CallbackExecutor Executor executor, @@ -674,11 +678,12 @@ public final class SatelliteManager { }; telephony.requestIsSatelliteSupported(mSubId, receiver); } else { - throw new IllegalStateException("telephony service is null."); + executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError( + new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE)))); } } catch (RemoteException ex) { loge("requestIsSatelliteSupported() RemoteException: " + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } } @@ -693,7 +698,6 @@ public final class SatelliteManager { * will return a {@link SatelliteException} with the {@link SatelliteResult}. * * @throws SecurityException if the caller doesn't have required permission. - * @throws IllegalStateException if the Telephony process is not currently available. */ @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) @@ -729,11 +733,12 @@ public final class SatelliteManager { }; telephony.requestSatelliteCapabilities(mSubId, receiver); } else { - throw new IllegalStateException("telephony service is null."); + executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError( + new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE)))); } } catch (RemoteException ex) { loge("requestSatelliteCapabilities() RemoteException: " + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } } @@ -959,7 +964,6 @@ public final class SatelliteManager { * @param callback The callback to notify of satellite transmission updates. * * @throws SecurityException if the caller doesn't have required permission. - * @throws IllegalStateException if the Telephony process is not currently available. */ @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) @@ -1009,11 +1013,12 @@ public final class SatelliteManager { telephony.startSatelliteTransmissionUpdates(mSubId, errorCallback, internalCallback); } else { - throw new IllegalStateException("telephony service is null."); + executor.execute(() -> Binder.withCleanCallingIdentity( + () -> resultListener.accept(SATELLITE_RESULT_ILLEGAL_STATE))); } } catch (RemoteException ex) { loge("startSatelliteTransmissionUpdates() RemoteException: " + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } } @@ -1029,7 +1034,6 @@ public final class SatelliteManager { * @param resultListener Listener for the {@link SatelliteResult} result of the operation. * * @throws SecurityException if the caller doesn't have required permission. - * @throws IllegalStateException if the Telephony process is not currently available. */ @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) @@ -1063,11 +1067,12 @@ public final class SatelliteManager { () -> resultListener.accept(SATELLITE_RESULT_INVALID_ARGUMENTS))); } } else { - throw new IllegalStateException("telephony service is null."); + executor.execute(() -> Binder.withCleanCallingIdentity( + () -> resultListener.accept(SATELLITE_RESULT_ILLEGAL_STATE))); } } catch (RemoteException ex) { loge("stopSatelliteTransmissionUpdates() RemoteException: " + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } } @@ -1112,11 +1117,12 @@ public final class SatelliteManager { cancelRemote = telephony.provisionSatelliteService(mSubId, token, provisionData, errorCallback); } else { - throw new IllegalStateException("telephony service is null."); + executor.execute(() -> Binder.withCleanCallingIdentity( + () -> resultListener.accept(SATELLITE_RESULT_ILLEGAL_STATE))); } } catch (RemoteException ex) { loge("provisionSatelliteService() RemoteException=" + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } if (cancellationSignal != null) { cancellationSignal.setRemote(cancelRemote); @@ -1138,7 +1144,6 @@ public final class SatelliteManager { * @param resultListener Listener for the {@link SatelliteResult} result of the operation. * * @throws SecurityException if the caller doesn't have required permission. - * @throws IllegalStateException if the Telephony process is not currently available. */ @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) @@ -1161,11 +1166,12 @@ public final class SatelliteManager { }; telephony.deprovisionSatelliteService(mSubId, token, errorCallback); } else { - throw new IllegalStateException("telephony service is null."); + executor.execute(() -> Binder.withCleanCallingIdentity( + () -> resultListener.accept(SATELLITE_RESULT_ILLEGAL_STATE))); } } catch (RemoteException ex) { loge("deprovisionSatelliteService() RemoteException=" + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } } @@ -1208,7 +1214,7 @@ public final class SatelliteManager { } } catch (RemoteException ex) { loge("registerForSatelliteProvisionStateChanged() RemoteException: " + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } return SATELLITE_RESULT_REQUEST_FAILED; } @@ -1244,7 +1250,7 @@ public final class SatelliteManager { } } catch (RemoteException ex) { loge("unregisterForSatelliteProvisionStateChanged() RemoteException: " + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } } @@ -1260,7 +1266,6 @@ public final class SatelliteManager { * will return a {@link SatelliteException} with the {@link SatelliteResult}. * * @throws SecurityException if the caller doesn't have required permission. - * @throws IllegalStateException if the Telephony process is not currently available. */ @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) @@ -1295,11 +1300,12 @@ public final class SatelliteManager { }; telephony.requestIsSatelliteProvisioned(mSubId, receiver); } else { - throw new IllegalStateException("telephony service is null."); + executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError( + new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE)))); } } catch (RemoteException ex) { loge("requestIsSatelliteProvisioned() RemoteException: " + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } } @@ -1340,7 +1346,7 @@ public final class SatelliteManager { } } catch (RemoteException ex) { loge("registerForSatelliteModemStateChanged() RemoteException:" + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } return SATELLITE_RESULT_REQUEST_FAILED; } @@ -1376,7 +1382,7 @@ public final class SatelliteManager { } } catch (RemoteException ex) { loge("unregisterForSatelliteModemStateChanged() RemoteException:" + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } } @@ -1436,7 +1442,7 @@ public final class SatelliteManager { } } catch (RemoteException ex) { loge("registerForSatelliteDatagram() RemoteException:" + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } return SATELLITE_RESULT_REQUEST_FAILED; } @@ -1471,7 +1477,7 @@ public final class SatelliteManager { } } catch (RemoteException ex) { loge("unregisterForSatelliteDatagram() RemoteException:" + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } } @@ -1488,7 +1494,6 @@ public final class SatelliteManager { * @param resultListener Listener for the {@link SatelliteResult} result of the operation. * * @throws SecurityException if the caller doesn't have required permission. - * @throws IllegalStateException if the Telephony process is not currently available. */ @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) @@ -1509,11 +1514,12 @@ public final class SatelliteManager { }; telephony.pollPendingSatelliteDatagrams(mSubId, internalCallback); } else { - throw new IllegalStateException("telephony service is null."); + executor.execute(() -> Binder.withCleanCallingIdentity( + () -> resultListener.accept(SATELLITE_RESULT_ILLEGAL_STATE))); } } catch (RemoteException ex) { loge("pollPendingSatelliteDatagrams() RemoteException:" + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } } @@ -1541,7 +1547,6 @@ public final class SatelliteManager { * @param resultListener Listener for the {@link SatelliteResult} result of the operation. * * @throws SecurityException if the caller doesn't have required permission. - * @throws IllegalStateException if the Telephony process is not currently available. */ @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) @@ -1566,11 +1571,12 @@ public final class SatelliteManager { telephony.sendSatelliteDatagram(mSubId, datagramType, datagram, needFullScreenPointingUI, internalCallback); } else { - throw new IllegalStateException("telephony service is null."); + executor.execute(() -> Binder.withCleanCallingIdentity( + () -> resultListener.accept(SATELLITE_RESULT_ILLEGAL_STATE))); } } catch (RemoteException ex) { loge("sendSatelliteDatagram() RemoteException:" + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } } @@ -1587,7 +1593,6 @@ public final class SatelliteManager { * will return a {@link SatelliteException} with the {@link SatelliteResult}. * * @throws SecurityException if the caller doesn't have required permission. - * @throws IllegalStateException if the Telephony process is not currently available. */ @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) @@ -1624,12 +1629,13 @@ public final class SatelliteManager { telephony.requestIsSatelliteCommunicationAllowedForCurrentLocation(mSubId, receiver); } else { - throw new IllegalStateException("telephony service is null."); + executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError( + new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE)))); } } catch (RemoteException ex) { loge("requestIsSatelliteCommunicationAllowedForCurrentLocation() RemoteException: " + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } } @@ -1645,7 +1651,6 @@ public final class SatelliteManager { * will return a {@link SatelliteException} with the {@link SatelliteResult}. * * @throws SecurityException if the caller doesn't have required permission. - * @throws IllegalStateException if the Telephony process is not currently available. */ @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) @@ -1681,11 +1686,12 @@ public final class SatelliteManager { }; telephony.requestTimeForNextSatelliteVisibility(mSubId, receiver); } else { - throw new IllegalStateException("telephony service is null."); + executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError( + new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE)))); } } catch (RemoteException ex) { loge("requestTimeForNextSatelliteVisibility() RemoteException: " + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } } @@ -1713,7 +1719,7 @@ public final class SatelliteManager { } } catch (RemoteException ex) { loge("informDeviceAlignedToSatellite() RemoteException:" + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } } @@ -1727,7 +1733,7 @@ public final class SatelliteManager { * <ul> * <li>Users want to enable it.</li> * <li>There is no satellite communication restriction, which is added by - * {@link #addSatelliteAttachRestrictionForCarrier(int, Executor, Consumer)}</li> + * {@link #addSatelliteAttachRestrictionForCarrier(int, int, Executor, Consumer)}</li> * <li>The carrier config {@link * android.telephony.CarrierConfigManager#KEY_SATELLITE_ATTACH_SUPPORTED_BOOL} is set to * {@code true}.</li> @@ -1739,7 +1745,6 @@ public final class SatelliteManager { * @param resultListener Listener for the {@link SatelliteResult} result of the operation. * * @throws SecurityException if the caller doesn't have required permission. - * @throws IllegalStateException if the Telephony process is not currently available. * @throws IllegalArgumentException if the subscription is invalid. */ @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @@ -1799,7 +1804,6 @@ public final class SatelliteManager { * @param resultListener Listener for the {@link SatelliteResult} result of the operation. * * @throws SecurityException if the caller doesn't have required permission. - * @throws IllegalStateException if the Telephony process is not currently available. * @throws IllegalArgumentException if the subscription is invalid. */ @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @@ -1824,11 +1828,12 @@ public final class SatelliteManager { }; telephony.addSatelliteAttachRestrictionForCarrier(subId, reason, errorCallback); } else { - throw new IllegalStateException("telephony service is null."); + executor.execute(() -> Binder.withCleanCallingIdentity( + () -> resultListener.accept(SATELLITE_RESULT_ILLEGAL_STATE))); } } catch (RemoteException ex) { loge("addSatelliteAttachRestrictionForCarrier() RemoteException:" + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } } @@ -1842,7 +1847,6 @@ public final class SatelliteManager { * @param resultListener Listener for the {@link SatelliteResult} result of the operation. * * @throws SecurityException if the caller doesn't have required permission. - * @throws IllegalStateException if the Telephony process is not currently available. * @throws IllegalArgumentException if the subscription is invalid. */ @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @@ -1867,17 +1871,18 @@ public final class SatelliteManager { }; telephony.removeSatelliteAttachRestrictionForCarrier(subId, reason, errorCallback); } else { - throw new IllegalStateException("telephony service is null."); + executor.execute(() -> Binder.withCleanCallingIdentity( + () -> resultListener.accept(SATELLITE_RESULT_ILLEGAL_STATE))); } } catch (RemoteException ex) { loge("removeSatelliteAttachRestrictionForCarrier() RemoteException:" + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } } /** * Get reasons for disallowing satellite attach, as requested by - * {@link #addSatelliteAttachRestrictionForCarrier(int, Executor, Consumer)} + * {@link #addSatelliteAttachRestrictionForCarrier(int, int, Executor, Consumer)} * * @param subId The subscription ID of the carrier. * @return Set of reasons for disallowing satellite communication. @@ -1910,7 +1915,7 @@ public final class SatelliteManager { } } catch (RemoteException ex) { loge("getSatelliteAttachRestrictionReasonsForCarrier() RemoteException: " + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } return new HashSet<>(); } @@ -1932,11 +1937,12 @@ public final class SatelliteManager { * The {@link NtnSignalStrength#NTN_SIGNAL_STRENGTH_NONE} will be returned if there is no * signal strength data available. * If the request is not successful, {@link OutcomeReceiver#onError(Throwable)} will return a - * {@link SatelliteException} with the {@link SatelliteResult}. + * {@link SatelliteException} with the {@link SatelliteResult}, or return a + * {@link IllegalStateException} if the Telephony process is not currently available or + * satellite is not supported, or return a {@link RuntimeException} when remote procedure call + * has failed. * * @throws SecurityException if the caller doesn't have required permission. - * @throws IllegalStateException if the Telephony process is not currently available or - * satellite is not supported. */ @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) @@ -1972,11 +1978,12 @@ public final class SatelliteManager { }; telephony.requestNtnSignalStrength(mSubId, receiver); } else { - throw new IllegalStateException("Telephony service is null."); + executor.execute(() -> Binder.withCleanCallingIdentity(() -> callback.onError( + new SatelliteException(SATELLITE_RESULT_ILLEGAL_STATE)))); } } catch (RemoteException ex) { loge("requestNtnSignalStrength() RemoteException: " + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } } @@ -1997,12 +2004,11 @@ public final class SatelliteManager { * * @throws SecurityException if the caller doesn't have required permission. * @throws IllegalStateException if the Telephony process is not currently available. - * @throws SatelliteException if the callback registration operation fails. */ @RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION) @FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public void registerForNtnSignalStrengthChanged(@NonNull @CallbackExecutor Executor executor, - @NonNull NtnSignalStrengthCallback callback) throws SatelliteException { + @NonNull NtnSignalStrengthCallback callback) { Objects.requireNonNull(executor); Objects.requireNonNull(callback); @@ -2024,12 +2030,9 @@ public final class SatelliteManager { } else { throw new IllegalStateException("Telephony service is null."); } - } catch (ServiceSpecificException ex) { - logd("registerForNtnSignalStrengthChanged() registration fails: " + ex.errorCode); - throw new SatelliteException(ex.errorCode); } catch (RemoteException ex) { loge("registerForNtnSignalStrengthChanged() RemoteException: " + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } } @@ -2072,7 +2075,7 @@ public final class SatelliteManager { } } catch (RemoteException ex) { loge("unregisterForNtnSignalStrengthChanged() RemoteException: " + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } } @@ -2113,7 +2116,7 @@ public final class SatelliteManager { } } catch (RemoteException ex) { loge("registerForSatelliteCapabilitiesChanged() RemoteException: " + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } return SATELLITE_RESULT_REQUEST_FAILED; } @@ -2149,7 +2152,7 @@ public final class SatelliteManager { } } catch (RemoteException ex) { loge("unregisterForSatelliteCapabilitiesChanged() RemoteException: " + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } } @@ -2177,7 +2180,7 @@ public final class SatelliteManager { } } catch (RemoteException ex) { loge("getAllSatellitePlmnsForCarrier() RemoteException: " + ex); - ex.rethrowFromSystemServer(); + ex.rethrowAsRuntimeException(); } return new ArrayList<>(); } diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp index 45dd02c9b7be..f3f183815d0b 100644 --- a/tools/aapt2/cmd/Link.cpp +++ b/tools/aapt2/cmd/Link.cpp @@ -56,6 +56,7 @@ #include "java/JavaClassGenerator.h" #include "java/ManifestClassGenerator.h" #include "java/ProguardRules.h" +#include "link/FeatureFlagsFilter.h" #include "link/Linkers.h" #include "link/ManifestFixer.h" #include "link/NoDefaultResourceRemover.h" @@ -1987,6 +1988,19 @@ class Linker { context_->SetNameManglerPolicy(NameManglerPolicy{context_->GetCompilationPackage()}); context_->SetSplitNameDependencies(app_info_.split_name_dependencies); + FeatureFlagsFilterOptions flags_filter_options; + if (context_->GetMinSdkVersion() > SDK_UPSIDE_DOWN_CAKE) { + // For API version > U, PackageManager will dynamically read the flag values and disable + // manifest elements accordingly when parsing the manifest. + // For API version <= U, we remove disabled elements from the manifest with the filter. + flags_filter_options.remove_disabled_elements = false; + flags_filter_options.flags_must_have_value = false; + } + FeatureFlagsFilter flags_filter(options_.feature_flag_values, flags_filter_options); + if (!flags_filter.Consume(context_, manifest_xml.get())) { + return 1; + } + // Override the package ID when it is "android". if (context_->GetCompilationPackage() == "android") { context_->SetPackageId(kAndroidPackageId); @@ -2531,7 +2545,7 @@ int LinkCommand::Action(const std::vector<std::string>& args) { } for (const std::string& arg : all_feature_flags_args) { - if (ParseFeatureFlagsParameter(arg, context.GetDiagnostics(), &options_.feature_flag_values)) { + if (!ParseFeatureFlagsParameter(arg, context.GetDiagnostics(), &options_.feature_flag_values)) { return 1; } } diff --git a/tools/aapt2/cmd/Link.h b/tools/aapt2/cmd/Link.h index 26713fd92264..dc18b1ccda60 100644 --- a/tools/aapt2/cmd/Link.h +++ b/tools/aapt2/cmd/Link.h @@ -330,7 +330,11 @@ class LinkCommand : public Command { "should only be used together with the --static-lib flag.", &options_.merge_only); AddOptionalSwitch("-v", "Enables verbose logging.", &verbose_); - AddOptionalFlagList("--feature-flags", "Placeholder, to be implemented.", &feature_flags_args_); + AddOptionalFlagList("--feature-flags", + "Specify the values of feature flags. The pairs in the argument\n" + "are separated by ',' and the name is separated from the value by '='.\n" + "Example: \"flag1=true,flag2=false,flag3=\" (flag3 has no given value).", + &feature_flags_args_); } int Action(const std::vector<std::string>& args) override; diff --git a/tools/aapt2/cmd/Link_test.cpp b/tools/aapt2/cmd/Link_test.cpp index 7096f5cc54e3..9323f3b95eac 100644 --- a/tools/aapt2/cmd/Link_test.cpp +++ b/tools/aapt2/cmd/Link_test.cpp @@ -16,11 +16,10 @@ #include "Link.h" -#include <android-base/file.h> - -#include "AppInfo.h" #include "Diagnostics.h" #include "LoadedApk.h" +#include "android-base/file.h" +#include "android-base/stringprintf.h" #include "test/Test.h" using testing::Eq; @@ -993,4 +992,213 @@ TEST_F(LinkTest, LocaleConfigWrongLocaleFormat) { ASSERT_FALSE(Link(link_args, &diag)); } +static void BuildSDKWithFeatureFlagAttr(const std::string& apk_path, const std::string& java_path, + CommandTestFixture* fixture, android::IDiagnostics* diag) { + const std::string android_values = + R"(<resources> + <staging-public-group type="attr" first-id="0x01fe0063"> + <public name="featureFlag" /> + </staging-public-group> + <attr name="featureFlag" format="string" /> + </resources>)"; + + SourceXML source_xml{.res_file_path = "/res/values/values.xml", .file_contents = android_values}; + BuildSDK({source_xml}, apk_path, java_path, fixture, diag); +} + +TEST_F(LinkTest, FeatureFlagDisabled_SdkAtMostUDC) { + StdErrDiagnostics diag; + const std::string android_apk = GetTestPath("android.apk"); + const std::string android_java = GetTestPath("android-java"); + BuildSDKWithFeatureFlagAttr(android_apk, android_java, this, &diag); + + const std::string manifest_contents = android::base::StringPrintf( + R"(<uses-sdk android:minSdkVersion="%d" />" + <permission android:name="FOO" android:featureFlag="flag" />)", + SDK_UPSIDE_DOWN_CAKE); + auto app_manifest = ManifestBuilder(this) + .SetPackageName("com.example.app") + .AddContents(manifest_contents) + .Build(); + + auto app_link_args = LinkCommandBuilder(this) + .SetManifestFile(app_manifest) + .AddParameter("-I", android_apk) + .AddParameter("--feature-flags", "flag=false"); + + const std::string app_apk = GetTestPath("app.apk"); + BuildApk({}, app_apk, std::move(app_link_args), this, &diag); + + // Permission element should be removed if flag is disabled + auto apk = LoadedApk::LoadApkFromPath(app_apk, &diag); + ASSERT_THAT(apk, NotNull()); + auto apk_manifest = apk->GetManifest(); + ASSERT_THAT(apk_manifest, NotNull()); + auto root = apk_manifest->root.get(); + ASSERT_THAT(root, NotNull()); + auto maybe_removed = root->FindChild({}, "permission"); + ASSERT_THAT(maybe_removed, IsNull()); +} + +TEST_F(LinkTest, FeatureFlagEnabled_SdkAtMostUDC) { + StdErrDiagnostics diag; + const std::string android_apk = GetTestPath("android.apk"); + const std::string android_java = GetTestPath("android-java"); + BuildSDKWithFeatureFlagAttr(android_apk, android_java, this, &diag); + + const std::string manifest_contents = android::base::StringPrintf( + R"(<uses-sdk android:minSdkVersion="%d" />" + <permission android:name="FOO" android:featureFlag="flag" />)", + SDK_UPSIDE_DOWN_CAKE); + auto app_manifest = ManifestBuilder(this) + .SetPackageName("com.example.app") + .AddContents(manifest_contents) + .Build(); + + auto app_link_args = LinkCommandBuilder(this) + .SetManifestFile(app_manifest) + .AddParameter("-I", android_apk) + .AddParameter("--feature-flags", "flag=true"); + + const std::string app_apk = GetTestPath("app.apk"); + BuildApk({}, app_apk, std::move(app_link_args), this, &diag); + + // Permission element should be kept if flag is enabled + auto apk = LoadedApk::LoadApkFromPath(app_apk, &diag); + ASSERT_THAT(apk, NotNull()); + auto apk_manifest = apk->GetManifest(); + ASSERT_THAT(apk_manifest, NotNull()); + auto root = apk_manifest->root.get(); + ASSERT_THAT(root, NotNull()); + auto maybe_removed = root->FindChild({}, "permission"); + ASSERT_THAT(maybe_removed, NotNull()); +} + +TEST_F(LinkTest, FeatureFlagWithNoValue_SdkAtMostUDC) { + StdErrDiagnostics diag; + const std::string android_apk = GetTestPath("android.apk"); + const std::string android_java = GetTestPath("android-java"); + BuildSDKWithFeatureFlagAttr(android_apk, android_java, this, &diag); + + const std::string manifest_contents = android::base::StringPrintf( + R"(<uses-sdk android:minSdkVersion="%d" />" + <permission android:name="FOO" android:featureFlag="flag" />)", + SDK_UPSIDE_DOWN_CAKE); + auto app_manifest = ManifestBuilder(this) + .SetPackageName("com.example.app") + .AddContents(manifest_contents) + .Build(); + + auto app_link_args = LinkCommandBuilder(this) + .SetManifestFile(app_manifest) + .AddParameter("-I", android_apk) + .AddParameter("--feature-flags", "flag="); + + // Flags must have values if <= UDC + const std::string app_apk = GetTestPath("app.apk"); + ASSERT_FALSE(Link(app_link_args.Build(app_apk), &diag)); +} + +TEST_F(LinkTest, FeatureFlagDisabled_SdkAfterUDC) { + StdErrDiagnostics diag; + const std::string android_apk = GetTestPath("android.apk"); + const std::string android_java = GetTestPath("android-java"); + BuildSDKWithFeatureFlagAttr(android_apk, android_java, this, &diag); + + const std::string manifest_contents = android::base::StringPrintf( + R"(<uses-sdk android:minSdkVersion="%d" />" + <permission android:name="FOO" android:featureFlag="flag" />)", + SDK_CUR_DEVELOPMENT); + auto app_manifest = ManifestBuilder(this) + .SetPackageName("com.example.app") + .AddContents(manifest_contents) + .Build(); + + auto app_link_args = LinkCommandBuilder(this) + .SetManifestFile(app_manifest) + .AddParameter("-I", android_apk) + .AddParameter("--feature-flags", "flag=false"); + + const std::string app_apk = GetTestPath("app.apk"); + BuildApk({}, app_apk, std::move(app_link_args), this, &diag); + + // Permission element should be kept if > UDC, regardless of flag value + auto apk = LoadedApk::LoadApkFromPath(app_apk, &diag); + ASSERT_THAT(apk, NotNull()); + auto apk_manifest = apk->GetManifest(); + ASSERT_THAT(apk_manifest, NotNull()); + auto root = apk_manifest->root.get(); + ASSERT_THAT(root, NotNull()); + auto maybe_removed = root->FindChild({}, "permission"); + ASSERT_THAT(maybe_removed, NotNull()); +} + +TEST_F(LinkTest, FeatureFlagEnabled_SdkAfterUDC) { + StdErrDiagnostics diag; + const std::string android_apk = GetTestPath("android.apk"); + const std::string android_java = GetTestPath("android-java"); + BuildSDKWithFeatureFlagAttr(android_apk, android_java, this, &diag); + + const std::string manifest_contents = android::base::StringPrintf( + R"(<uses-sdk android:minSdkVersion="%d" />" + <permission android:name="FOO" android:featureFlag="flag" />)", + SDK_CUR_DEVELOPMENT); + auto app_manifest = ManifestBuilder(this) + .SetPackageName("com.example.app") + .AddContents(manifest_contents) + .Build(); + + auto app_link_args = LinkCommandBuilder(this) + .SetManifestFile(app_manifest) + .AddParameter("-I", android_apk) + .AddParameter("--feature-flags", "flag=true"); + + const std::string app_apk = GetTestPath("app.apk"); + BuildApk({}, app_apk, std::move(app_link_args), this, &diag); + + // Permission element should be kept if > UDC, regardless of flag value + auto apk = LoadedApk::LoadApkFromPath(app_apk, &diag); + ASSERT_THAT(apk, NotNull()); + auto apk_manifest = apk->GetManifest(); + ASSERT_THAT(apk_manifest, NotNull()); + auto root = apk_manifest->root.get(); + ASSERT_THAT(root, NotNull()); + auto maybe_removed = root->FindChild({}, "permission"); + ASSERT_THAT(maybe_removed, NotNull()); +} + +TEST_F(LinkTest, FeatureFlagWithNoValue_SdkAfterUDC) { + StdErrDiagnostics diag; + const std::string android_apk = GetTestPath("android.apk"); + const std::string android_java = GetTestPath("android-java"); + BuildSDKWithFeatureFlagAttr(android_apk, android_java, this, &diag); + + const std::string manifest_contents = android::base::StringPrintf( + R"(<uses-sdk android:minSdkVersion="%d" />" + <permission android:name="FOO" android:featureFlag="flag" />)", + SDK_CUR_DEVELOPMENT); + auto app_manifest = ManifestBuilder(this) + .SetPackageName("com.example.app") + .AddContents(manifest_contents) + .Build(); + + auto app_link_args = LinkCommandBuilder(this) + .SetManifestFile(app_manifest) + .AddParameter("-I", android_apk) + .AddParameter("--feature-flags", "flag="); + + const std::string app_apk = GetTestPath("app.apk"); + BuildApk({}, app_apk, std::move(app_link_args), this, &diag); + + // Permission element should be kept if > UDC, regardless of flag value + auto apk = LoadedApk::LoadApkFromPath(app_apk, &diag); + ASSERT_THAT(apk, NotNull()); + auto apk_manifest = apk->GetManifest(); + ASSERT_THAT(apk_manifest, NotNull()); + auto root = apk_manifest->root.get(); + ASSERT_THAT(root, NotNull()); + auto maybe_removed = root->FindChild({}, "permission"); + ASSERT_THAT(maybe_removed, NotNull()); +} + } // namespace aapt diff --git a/tools/aapt2/java/AnnotationProcessor.cpp b/tools/aapt2/java/AnnotationProcessor.cpp index 8c644cf83339..a7f6f5524e21 100644 --- a/tools/aapt2/java/AnnotationProcessor.cpp +++ b/tools/aapt2/java/AnnotationProcessor.cpp @@ -64,29 +64,31 @@ static std::array<AnnotationRule, 3> sAnnotationRules = {{ {"@FlaggedApi", AnnotationRule::kFlaggedApi, "@android.annotation.FlaggedApi", true}, }}; -void AnnotationProcessor::AppendCommentLine(std::string comment) { +void AnnotationProcessor::AppendCommentLine(std::string comment, bool add_api_annotations) { static constexpr std::string_view sDeprecated = "@deprecated"; - // Treat deprecated specially, since we don't remove it from the source comment. - if (comment.find(sDeprecated) != std::string::npos) { - annotation_parameter_map_[AnnotationRule::kDeprecated] = ""; - } + if (add_api_annotations) { + // Treat deprecated specially, since we don't remove it from the source comment. + if (comment.find(sDeprecated) != std::string::npos) { + annotation_parameter_map_[AnnotationRule::kDeprecated] = ""; + } - for (const AnnotationRule& rule : sAnnotationRules) { - std::string::size_type idx = comment.find(rule.doc_str.data()); - if (idx != std::string::npos) { - // Captures all parameters associated with the specified annotation rule - // by matching the first pair of parentheses after the rule. - std::regex re(std::string(rule.doc_str).append(R"(\s*\((.+)\))")); - std::smatch match_result; - const bool is_match = std::regex_search(comment, match_result, re); - if (is_match && rule.preserve_params) { - annotation_parameter_map_[rule.bit_mask] = match_result[1].str(); - comment.erase(comment.begin() + match_result.position(), - comment.begin() + match_result.position() + match_result.length()); - } else { - annotation_parameter_map_[rule.bit_mask] = ""; - comment.erase(comment.begin() + idx, comment.begin() + idx + rule.doc_str.size()); + for (const AnnotationRule& rule : sAnnotationRules) { + std::string::size_type idx = comment.find(rule.doc_str.data()); + if (idx != std::string::npos) { + // Captures all parameters associated with the specified annotation rule + // by matching the first pair of parentheses after the rule. + std::regex re(std::string(rule.doc_str).append(R"(\s*\((.+)\))")); + std::smatch match_result; + const bool is_match = std::regex_search(comment, match_result, re); + if (is_match && rule.preserve_params) { + annotation_parameter_map_[rule.bit_mask] = match_result[1].str(); + comment.erase(comment.begin() + match_result.position(), + comment.begin() + match_result.position() + match_result.length()); + } else { + annotation_parameter_map_[rule.bit_mask] = ""; + comment.erase(comment.begin() + idx, comment.begin() + idx + rule.doc_str.size()); + } } } } @@ -109,12 +111,12 @@ void AnnotationProcessor::AppendCommentLine(std::string comment) { comment_ << "\n * " << std::move(comment); } -void AnnotationProcessor::AppendComment(StringPiece comment) { +void AnnotationProcessor::AppendComment(StringPiece comment, bool add_api_annotations) { // We need to process line by line to clean-up whitespace and append prefixes. for (StringPiece line : util::Tokenize(comment, '\n')) { line = util::TrimWhitespace(line); if (!line.empty()) { - AppendCommentLine(std::string(line)); + AppendCommentLine(std::string(line), add_api_annotations); } } } diff --git a/tools/aapt2/java/AnnotationProcessor.h b/tools/aapt2/java/AnnotationProcessor.h index db3437e3b5b1..2217ab35705a 100644 --- a/tools/aapt2/java/AnnotationProcessor.h +++ b/tools/aapt2/java/AnnotationProcessor.h @@ -60,7 +60,10 @@ class AnnotationProcessor { // Adds more comments. Resources can have value definitions for various configurations, and // each of the definitions may have comments that need to be processed. - void AppendComment(android::StringPiece comment); + // + // If add_api_annotations is false, annotations found in the comment (e.g., "@SystemApi") + // will NOT be converted to Java annotations. + void AppendComment(android::StringPiece comment, bool add_api_annotations = true); void AppendNewLine(); @@ -73,7 +76,7 @@ class AnnotationProcessor { bool has_comments_ = false; std::unordered_map<uint32_t, std::string> annotation_parameter_map_; - void AppendCommentLine(std::string line); + void AppendCommentLine(std::string line, bool add_api_annotations); }; } // namespace aapt diff --git a/tools/aapt2/java/AnnotationProcessor_test.cpp b/tools/aapt2/java/AnnotationProcessor_test.cpp index e98e96ba3bc3..e5eee34f451c 100644 --- a/tools/aapt2/java/AnnotationProcessor_test.cpp +++ b/tools/aapt2/java/AnnotationProcessor_test.cpp @@ -136,7 +136,28 @@ TEST(AnnotationProcessorTest, NotEmitSystemApiAnnotation) { EXPECT_THAT(annotations, HasSubstr("This is a system API")); } -TEST(AnnotationProcessor, ExtractsFirstSentence) { +TEST(AnnotationProcessorTest, DoNotAddApiAnnotations) { + AnnotationProcessor processor; + processor.AppendComment( + "@SystemApi This is a system API\n" + "@FlaggedApi This is a flagged API\n" + "@TestApi This is a test API\n" + "@deprecated Deprecate me\n", /*add_api_annotations=*/ + false); + + std::string annotations; + StringOutputStream out(&annotations); + Printer printer(&out); + processor.Print(&printer); + out.Flush(); + + EXPECT_THAT(annotations, Not(HasSubstr("@android.annotation.SystemApi"))); + EXPECT_THAT(annotations, Not(HasSubstr("@android.annotation.FlaggedApi"))); + EXPECT_THAT(annotations, Not(HasSubstr("@android.annotation.TestApi"))); + EXPECT_THAT(annotations, Not(HasSubstr("@Deprecated"))); +} + +TEST(AnnotationProcessorTest, ExtractsFirstSentence) { EXPECT_THAT(AnnotationProcessor::ExtractFirstSentence("This is the only sentence"), Eq("This is the only sentence")); EXPECT_THAT(AnnotationProcessor::ExtractFirstSentence( diff --git a/tools/aapt2/java/JavaClassGenerator.cpp b/tools/aapt2/java/JavaClassGenerator.cpp index 58f656458177..6e73b017cce2 100644 --- a/tools/aapt2/java/JavaClassGenerator.cpp +++ b/tools/aapt2/java/JavaClassGenerator.cpp @@ -180,7 +180,10 @@ static void AddAttributeFormatDoc(AnnotationProcessor* processor, Attribute* att << "<td>" << std::hex << symbol.value << std::dec << "</td>" << "<td>" << util::TrimWhitespace(symbol.symbol.GetComment()) << "</td></tr>"; - processor->AppendComment(line.str()); + // add_api_annotations is false since we don't want any annotations + // (e.g., "@deprecated")/ found in the enum/flag values to be propagated + // up to the attribute. + processor->AppendComment(line.str(), /*add_api_annotations=*/false); } processor->AppendComment("</table>"); } diff --git a/tools/aapt2/java/JavaClassGenerator_test.cpp b/tools/aapt2/java/JavaClassGenerator_test.cpp index 40395ed64fe3..bca9f4bc58c4 100644 --- a/tools/aapt2/java/JavaClassGenerator_test.cpp +++ b/tools/aapt2/java/JavaClassGenerator_test.cpp @@ -324,7 +324,58 @@ TEST(JavaClassGeneratorTest, CommentsForSimpleResourcesArePresent) { EXPECT_THAT(output, HasSubstr(expected_text)); } -TEST(JavaClassGeneratorTest, CommentsForEnumAndFlagAttributesArePresent) {} +TEST(JavaClassGeneratorTest, CommentsForEnumAndFlagAttributesArePresent) { + std::unique_ptr<Attribute> flagAttr = + test::AttributeBuilder() + .SetTypeMask(android::ResTable_map::TYPE_FLAGS) + .SetComment("Flag attribute") + .AddItemWithComment("flagOne", 0x01, "Flag comment 1") + .AddItemWithComment("flagTwo", 0x02, "@deprecated Flag comment 2") + .Build(); + std::unique_ptr<Attribute> enumAttr = + test::AttributeBuilder() + .SetTypeMask(android::ResTable_map::TYPE_ENUM) + .SetComment("Enum attribute") + .AddItemWithComment("enumOne", 0x01, "@TestApi Enum comment 1") + .AddItemWithComment("enumTwo", 0x02, "Enum comment 2") + .Build(); + + std::unique_ptr<ResourceTable> table = test::ResourceTableBuilder() + .AddValue("android:attr/one", std::move(flagAttr)) + .AddValue("android:attr/two", std::move(enumAttr)) + .Build(); + + std::unique_ptr<IAaptContext> context = + test::ContextBuilder() + .AddSymbolSource(util::make_unique<ResourceTableSymbolSource>(table.get())) + .SetNameManglerPolicy(NameManglerPolicy{"android"}) + .Build(); + JavaClassGeneratorOptions options; + options.use_final = false; + JavaClassGenerator generator(context.get(), table.get(), options); + + std::string output; + StringOutputStream out(&output); + ASSERT_TRUE(generator.Generate("android", &out)); + out.Flush(); + + // Special annotations from the enum/flag values should NOT generate + // annotations for the attribute value. + EXPECT_THAT(output, Not(HasSubstr("@Deprecated"))); + EXPECT_THAT(output, Not(HasSubstr("@android.annotation.TestApi"))); + + EXPECT_THAT(output, HasSubstr("Flag attribute")); + EXPECT_THAT(output, HasSubstr("flagOne")); + EXPECT_THAT(output, HasSubstr("Flag comment 1")); + EXPECT_THAT(output, HasSubstr("flagTwo")); + EXPECT_THAT(output, HasSubstr("@deprecated Flag comment 2")); + + EXPECT_THAT(output, HasSubstr("Enum attribute")); + EXPECT_THAT(output, HasSubstr("enumOne")); + EXPECT_THAT(output, HasSubstr("@TestApi Enum comment 1")); + EXPECT_THAT(output, HasSubstr("enumTwo")); + EXPECT_THAT(output, HasSubstr("Enum comment 2")); +} TEST(JavaClassGeneratorTest, CommentsForStyleablesAndNestedAttributesArePresent) { Attribute attr; diff --git a/tools/aapt2/test/Builders.cpp b/tools/aapt2/test/Builders.cpp index 65f63dc68e54..b5934e40a2a3 100644 --- a/tools/aapt2/test/Builders.cpp +++ b/tools/aapt2/test/Builders.cpp @@ -177,12 +177,25 @@ AttributeBuilder& AttributeBuilder::SetWeak(bool weak) { return *this; } +AttributeBuilder& AttributeBuilder::SetComment(StringPiece comment) { + attr_->SetComment(comment); + return *this; +} + AttributeBuilder& AttributeBuilder::AddItem(StringPiece name, uint32_t value) { attr_->symbols.push_back( Attribute::Symbol{Reference(ResourceName({}, ResourceType::kId, name)), value}); return *this; } +AttributeBuilder& AttributeBuilder::AddItemWithComment(StringPiece name, uint32_t value, + StringPiece comment) { + Reference ref(ResourceName({}, ResourceType::kId, name)); + ref.SetComment(comment); + attr_->symbols.push_back(Attribute::Symbol{ref, value}); + return *this; +} + std::unique_ptr<Attribute> AttributeBuilder::Build() { return std::move(attr_); } diff --git a/tools/aapt2/test/Builders.h b/tools/aapt2/test/Builders.h index 098535d8526f..9ee44ba6d04c 100644 --- a/tools/aapt2/test/Builders.h +++ b/tools/aapt2/test/Builders.h @@ -116,7 +116,10 @@ class AttributeBuilder { AttributeBuilder(); AttributeBuilder& SetTypeMask(uint32_t typeMask); AttributeBuilder& SetWeak(bool weak); + AttributeBuilder& SetComment(android::StringPiece comment); AttributeBuilder& AddItem(android::StringPiece name, uint32_t value); + AttributeBuilder& AddItemWithComment(android::StringPiece name, uint32_t value, + android::StringPiece comment); std::unique_ptr<Attribute> Build(); private: diff --git a/tools/aapt2/util/Files.cpp b/tools/aapt2/util/Files.cpp index 93c1b61f9a57..02e4beaed949 100644 --- a/tools/aapt2/util/Files.cpp +++ b/tools/aapt2/util/Files.cpp @@ -251,10 +251,13 @@ bool AppendArgsFromFile(StringPiece path, std::vector<std::string>* out_arglist, return false; } - for (StringPiece line : util::Tokenize(contents, ' ')) { + for (StringPiece line : util::Tokenize(contents, '\n')) { line = util::TrimWhitespace(line); - if (!line.empty()) { - out_arglist->emplace_back(line); + for (StringPiece arg : util::Tokenize(line, ' ')) { + arg = util::TrimWhitespace(arg); + if (!arg.empty()) { + out_arglist->emplace_back(arg); + } } } return true; @@ -270,10 +273,13 @@ bool AppendSetArgsFromFile(StringPiece path, std::unordered_set<std::string>* ou return false; } - for (StringPiece line : util::Tokenize(contents, ' ')) { + for (StringPiece line : util::Tokenize(contents, '\n')) { line = util::TrimWhitespace(line); - if (!line.empty()) { - out_argset->emplace(line); + for (StringPiece arg : util::Tokenize(line, ' ')) { + arg = util::TrimWhitespace(arg); + if (!arg.empty()) { + out_argset->emplace(arg); + } } } return true; diff --git a/tools/aapt2/util/Files_test.cpp b/tools/aapt2/util/Files_test.cpp index 6c380808c0df..618a3e0d86ae 100644 --- a/tools/aapt2/util/Files_test.cpp +++ b/tools/aapt2/util/Files_test.cpp @@ -25,6 +25,9 @@ using ::android::base::StringPrintf; +using ::testing::ElementsAre; +using ::testing::UnorderedElementsAre; + namespace aapt { namespace file { @@ -34,9 +37,11 @@ constexpr const char sTestDirSep = '\\'; constexpr const char sTestDirSep = '/'; #endif -class FilesTest : public ::testing::Test { +class FilesTest : public TestDirectoryFixture { public: void SetUp() override { + TestDirectoryFixture::SetUp(); + std::stringstream builder; builder << "hello" << sDirSep << "there"; expected_path_ = builder.str(); @@ -66,6 +71,42 @@ TEST_F(FilesTest, AppendPathWithLeadingOrTrailingSeparators) { EXPECT_EQ(expected_path_, base); } +TEST_F(FilesTest, AppendArgsFromFile) { + const std::string args_file = GetTestPath("args.txt"); + WriteFile(args_file, + " \n" + "arg1 arg2 arg3 \n" + " arg4 arg5"); + std::vector<std::string> args; + std::string error; + ASSERT_TRUE(AppendArgsFromFile(args_file, &args, &error)); + EXPECT_THAT(args, ElementsAre("arg1", "arg2", "arg3", "arg4", "arg5")); +} + +TEST_F(FilesTest, AppendArgsFromFile_InvalidFile) { + std::vector<std::string> args; + std::string error; + ASSERT_FALSE(AppendArgsFromFile(GetTestPath("not_found.txt"), &args, &error)); +} + +TEST_F(FilesTest, AppendSetArgsFromFile) { + const std::string args_file = GetTestPath("args.txt"); + WriteFile(args_file, + " \n" + "arg2 arg4 arg1 \n" + " arg5 arg3"); + std::unordered_set<std::string> args; + std::string error; + ASSERT_TRUE(AppendSetArgsFromFile(args_file, &args, &error)); + EXPECT_THAT(args, UnorderedElementsAre("arg1", "arg2", "arg3", "arg4", "arg5")); +} + +TEST_F(FilesTest, AppendSetArgsFromFile_InvalidFile) { + std::unordered_set<std::string> args; + std::string error; + ASSERT_FALSE(AppendSetArgsFromFile(GetTestPath("not_found.txt"), &args, &error)); +} + #ifdef _WIN32 TEST_F(FilesTest, WindowsMkdirsLongPath) { // Creating directory paths longer than the Windows maximum path length (260 charatcers) should diff --git a/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/SystemProperties_host.java b/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/SystemProperties_host.java index 1ec1d5f307e1..2f6a361e3609 100644 --- a/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/SystemProperties_host.java +++ b/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/SystemProperties_host.java @@ -15,42 +15,181 @@ */ package com.android.hoststubgen.nativesubstitution; +import android.util.SparseArray; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.util.Preconditions; + +import java.util.Map; +import java.util.Objects; +import java.util.function.Predicate; + public class SystemProperties_host { + private static final Object sLock = new Object(); + + /** Active system property values */ + @GuardedBy("sLock") + private static Map<String, String> sValues; + /** Predicate tested to determine if a given key can be read. */ + @GuardedBy("sLock") + private static Predicate<String> sKeyReadablePredicate; + /** Predicate tested to determine if a given key can be written. */ + @GuardedBy("sLock") + private static Predicate<String> sKeyWritablePredicate; + /** Callback to trigger when values are changed */ + @GuardedBy("sLock") + private static Runnable sChangeCallback; + + /** + * Reverse mapping that provides a way back to an original key from the + * {@link System#identityHashCode(Object)} of {@link String#intern}. + */ + @GuardedBy("sLock") + private static SparseArray<String> sKeyHandles = new SparseArray<>(); + + public static void native_init$ravenwood(Map<String, String> values, + Predicate<String> keyReadablePredicate, Predicate<String> keyWritablePredicate, + Runnable changeCallback) { + synchronized (sLock) { + sValues = Objects.requireNonNull(values); + sKeyReadablePredicate = Objects.requireNonNull(keyReadablePredicate); + sKeyWritablePredicate = Objects.requireNonNull(keyWritablePredicate); + sChangeCallback = Objects.requireNonNull(changeCallback); + sKeyHandles.clear(); + } + } + + public static void native_reset$ravenwood() { + synchronized (sLock) { + sValues = null; + sKeyReadablePredicate = null; + sKeyWritablePredicate = null; + sChangeCallback = null; + sKeyHandles.clear(); + } + } + + public static void native_set(String key, String val) { + synchronized (sLock) { + Objects.requireNonNull(key); + Preconditions.requireNonNullViaRavenwoodRule(sValues); + if (!sKeyWritablePredicate.test(key)) { + throw new IllegalArgumentException( + "Write access to system property '" + key + "' denied via RavenwoodRule"); + } + if (key.startsWith("ro.") && sValues.containsKey(key)) { + throw new IllegalArgumentException( + "System property '" + key + "' already defined once; cannot redefine"); + } + if ((val == null) || val.isEmpty()) { + sValues.remove(key); + } else { + sValues.put(key, val); + } + sChangeCallback.run(); + } + } + public static String native_get(String key, String def) { - throw new RuntimeException("Not implemented yet"); + synchronized (sLock) { + Objects.requireNonNull(key); + Preconditions.requireNonNullViaRavenwoodRule(sValues); + if (!sKeyReadablePredicate.test(key)) { + throw new IllegalArgumentException( + "Read access to system property '" + key + "' denied via RavenwoodRule"); + } + return sValues.getOrDefault(key, def); + } } + public static int native_get_int(String key, int def) { - throw new RuntimeException("Not implemented yet"); + try { + return Integer.parseInt(native_get(key, "")); + } catch (NumberFormatException ignored) { + return def; + } } + public static long native_get_long(String key, long def) { - throw new RuntimeException("Not implemented yet"); + try { + return Long.parseLong(native_get(key, "")); + } catch (NumberFormatException ignored) { + return def; + } } + public static boolean native_get_boolean(String key, boolean def) { - throw new RuntimeException("Not implemented yet"); + return parseBoolean(native_get(key, ""), def); } public static long native_find(String name) { - throw new RuntimeException("Not implemented yet"); + synchronized (sLock) { + Preconditions.requireNonNullViaRavenwoodRule(sValues); + if (sValues.containsKey(name)) { + name = name.intern(); + final int handle = System.identityHashCode(name); + sKeyHandles.put(handle, name); + return handle; + } else { + return 0; + } + } } + public static String native_get(long handle) { - throw new RuntimeException("Not implemented yet"); + synchronized (sLock) { + return native_get(sKeyHandles.get((int) handle), ""); + } } + public static int native_get_int(long handle, int def) { - throw new RuntimeException("Not implemented yet"); + synchronized (sLock) { + return native_get_int(sKeyHandles.get((int) handle), def); + } } + public static long native_get_long(long handle, long def) { - throw new RuntimeException("Not implemented yet"); + synchronized (sLock) { + return native_get_long(sKeyHandles.get((int) handle), def); + } } + public static boolean native_get_boolean(long handle, boolean def) { - throw new RuntimeException("Not implemented yet"); - } - public static void native_set(String key, String def) { - throw new RuntimeException("Not implemented yet"); + synchronized (sLock) { + return native_get_boolean(sKeyHandles.get((int) handle), def); + } } + public static void native_add_change_callback() { - throw new RuntimeException("Not implemented yet"); + // Ignored; callback always registered via init above } + public static void native_report_sysprop_change() { - throw new RuntimeException("Not implemented yet"); + // Report through callback always registered via init above + synchronized (sLock) { + Preconditions.requireNonNullViaRavenwoodRule(sValues); + sChangeCallback.run(); + } + } + + private static boolean parseBoolean(String val, boolean def) { + // Matches system/libbase/include/android-base/parsebool.h + if (val == null) return def; + switch (val) { + case "1": + case "on": + case "true": + case "y": + case "yes": + return true; + case "0": + case "false": + case "n": + case "no": + case "off": + return false; + default: + return def; + } } } diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/AndroidHeuristicsFilter.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/AndroidHeuristicsFilter.kt index 8ca4732f57c4..76bac9286a1f 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/AndroidHeuristicsFilter.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/AndroidHeuristicsFilter.kt @@ -24,6 +24,7 @@ class AndroidHeuristicsFilter( private val classes: ClassNodes, val aidlPolicy: FilterPolicyWithReason?, val featureFlagsPolicy: FilterPolicyWithReason?, + val syspropsPolicy: FilterPolicyWithReason?, fallback: OutputFilter ) : DelegatingFilter(fallback) { override fun getPolicyForClass(className: String): FilterPolicyWithReason { @@ -33,6 +34,9 @@ class AndroidHeuristicsFilter( if (featureFlagsPolicy != null && classes.isFeatureFlagsClass(className)) { return featureFlagsPolicy } + if (syspropsPolicy != null && classes.isSyspropsClass(className)) { + return syspropsPolicy + } return super.getPolicyForClass(className) } } @@ -57,3 +61,13 @@ private fun ClassNodes.isFeatureFlagsClass(className: String): Boolean { || className.endsWith("/FeatureFlagsImpl") || className.endsWith("/FakeFeatureFlagsImpl"); } + +/** + * @return if a given class "seems like" a sysprops class. + */ +private fun ClassNodes.isSyspropsClass(className: String): Boolean { + // Matches template classes defined here: + // https://cs.android.com/android/platform/superproject/main/+/main:system/tools/sysprop/ + return className.startsWith("android/sysprop/") + && className.endsWith("Properties") +} diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt index d38a6e34e09f..7fdd944770c6 100644 --- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt +++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt @@ -64,6 +64,7 @@ fun createFilterFromTextPolicyFile( var aidlPolicy: FilterPolicyWithReason? = null var featureFlagsPolicy: FilterPolicyWithReason? = null + var syspropsPolicy: FilterPolicyWithReason? = null try { BufferedReader(FileReader(filename)).use { reader -> @@ -141,6 +142,14 @@ fun createFilterFromTextPolicyFile( featureFlagsPolicy = policy.withReason("$FILTER_REASON (feature flags)") } + SpecialClass.Sysprops -> { + if (syspropsPolicy != null) { + throw ParseException( + "Policy for sysprops already defined") + } + syspropsPolicy = + policy.withReason("$FILTER_REASON (sysprops)") + } } } } @@ -205,10 +214,10 @@ fun createFilterFromTextPolicyFile( } var ret: OutputFilter = imf - if (aidlPolicy != null || featureFlagsPolicy != null) { + if (aidlPolicy != null || featureFlagsPolicy != null || syspropsPolicy != null) { log.d("AndroidHeuristicsFilter enabled") ret = AndroidHeuristicsFilter( - classes, aidlPolicy, featureFlagsPolicy, imf) + classes, aidlPolicy, featureFlagsPolicy, syspropsPolicy, imf) } return ret } @@ -218,6 +227,7 @@ private enum class SpecialClass { NotSpecial, Aidl, FeatureFlags, + Sysprops, } private fun resolveSpecialClass(className: String): SpecialClass { @@ -227,6 +237,7 @@ private fun resolveSpecialClass(className: String): SpecialClass { when (className.lowercase()) { ":aidl" -> return SpecialClass.Aidl ":feature_flags" -> return SpecialClass.FeatureFlags + ":sysprops" -> return SpecialClass.Sysprops } throw ParseException("Invalid special class name \"$className\"") } |