diff options
503 files changed, 13311 insertions, 2802 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index f5bf437a738d..beb11fc3ee35 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -64,7 +64,9 @@ aconfig_srcjars = [ ":com.android.input.flags-aconfig-java{.generated_srcjars}", ":com.android.internal.foldables.flags-aconfig-java{.generated_srcjars}", ":com.android.media.flags.bettertogether-aconfig-java{.generated_srcjars}", + ":com.android.media.flags.editing-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 +135,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", @@ -536,6 +539,21 @@ java_aconfig_library { defaults: ["framework-minus-apex-aconfig-java-defaults"], } +// Media Editing +aconfig_declarations { + name: "com.android.media.flags.editing-aconfig", + package: "com.android.media.editing.flags", + srcs: [ + "media/java/android/media/flags/editing.aconfig", + ], +} + +java_aconfig_library { + name: "com.android.media.flags.editing-aconfig-java", + aconfig_declarations: "com.android.media.flags.editing-aconfig", + defaults: ["framework-minus-apex-aconfig-java-defaults"], +} + // Media TV aconfig_declarations { name: "android.media.tv.flags-aconfig", @@ -760,12 +778,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/apex/jobscheduler/framework/aconfig/job.aconfig b/apex/jobscheduler/framework/aconfig/job.aconfig index e73b434042af..788e82407926 100644 --- a/apex/jobscheduler/framework/aconfig/job.aconfig +++ b/apex/jobscheduler/framework/aconfig/job.aconfig @@ -13,3 +13,10 @@ flag { description: "Add APIs to let apps attach debug information to jobs" bug: "293491637" } + +flag { + name: "backup_jobs_exemption" + namespace: "backstage_power" + description: "Introduce a new RUN_BACKUP_JOBS permission and exemption logic allowing for longer running jobs for apps whose primary purpose is to backup or sync content." + bug: "318731461" +} 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 7a92cca74795..7f5bb5ce47a7 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java @@ -5448,6 +5448,9 @@ public class JobSchedulerService extends com.android.server.SystemService pw.print(Flags.FLAG_THROW_ON_UNSUPPORTED_BIAS_USAGE, Flags.throwOnUnsupportedBiasUsage()); pw.println(); + pw.print(android.app.job.Flags.FLAG_BACKUP_JOBS_EXEMPTION, + android.app.job.Flags.backupJobsExemption()); + pw.println(); pw.decreaseIndent(); 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 6f2393adfc7b..0cf6a7a8a4f6 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerShellCommand.java @@ -356,6 +356,9 @@ public final class JobSchedulerShellCommand extends BasicShellCommandHandler { case com.android.server.job.Flags.FLAG_THROW_ON_UNSUPPORTED_BIAS_USAGE: pw.println(com.android.server.job.Flags.throwOnUnsupportedBiasUsage()); break; + case android.app.job.Flags.FLAG_BACKUP_JOBS_EXEMPTION: + pw.println(android.app.job.Flags.backupJobsExemption()); + break; default: pw.println("Unknown flag: " + flagName); break; diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java index 14cce19ef676..6883d18cd937 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/FlexibilityController.java @@ -972,6 +972,20 @@ public final class FlexibilityController extends StateController { synchronized (mLock) { final long earliest = getLifeCycleBeginningElapsedLocked(js); final long latest = getLifeCycleEndElapsedLocked(js, nowElapsed, earliest); + if (latest <= earliest) { + // Something has gone horribly wrong. This has only occurred on incorrectly + // configured tests, but add a check here for safety. + Slog.wtf(TAG, "Got invalid latest when scheduling alarm." + + " Prefetch=" + js.getJob().isPrefetch()); + // Since things have gone wrong, the safest and most reliable thing to do is + // stop applying flex policy to the job. + mFlexibilityTracker.setNumDroppedFlexibleConstraints(js, + js.getNumAppliedFlexibleConstraints()); + mJobsToCheck.add(js); + mHandler.sendEmptyMessage(MSG_CHECK_JOBS); + return; + } + final long nextTimeElapsed = getNextConstraintDropTimeElapsedLocked(js, earliest, latest); diff --git a/core/api/Android.bp b/core/api/Android.bp index 907916a125da..8d8a82b69b55 100644 --- a/core/api/Android.bp +++ b/core/api/Android.bp @@ -96,21 +96,3 @@ filegroup { name: "non-updatable-test-lint-baseline.txt", srcs: ["test-lint-baseline.txt"], } - -java_api_contribution { - name: "api-stubs-docs-non-updatable-public-stubs", - api_surface: "public", - api_file: "current.txt", - visibility: [ - "//build/orchestrator/apis", - ], -} - -java_api_contribution { - name: "frameworks-base-core-api-module-lib-stubs", - api_surface: "module-lib", - api_file: "module-lib-current.txt", - visibility: [ - "//build/orchestrator/apis", - ], -} diff --git a/core/api/current.txt b/core/api/current.txt index 3acdb3265987..9e3919d76930 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -277,6 +277,7 @@ package android { 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 @FlaggedApi("android.app.job.backup_jobs_exemption") public static final String RUN_BACKUP_JOBS = "android.permission.RUN_BACKUP_JOBS"; field public static final String RUN_USER_INITIATED_JOBS = "android.permission.RUN_USER_INITIATED_JOBS"; field public static final String SCHEDULE_EXACT_ALARM = "android.permission.SCHEDULE_EXACT_ALARM"; field public static final String SEND_RESPOND_VIA_MESSAGE = "android.permission.SEND_RESPOND_VIA_MESSAGE"; @@ -284,6 +285,7 @@ package android { field public static final String SET_ALARM = "com.android.alarm.permission.SET_ALARM"; field public static final String SET_ALWAYS_FINISH = "android.permission.SET_ALWAYS_FINISH"; field public static final String SET_ANIMATION_SCALE = "android.permission.SET_ANIMATION_SCALE"; + field @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public static final String SET_BIOMETRIC_DIALOG_LOGO = "android.permission.SET_BIOMETRIC_DIALOG_LOGO"; field public static final String SET_DEBUG_APP = "android.permission.SET_DEBUG_APP"; field @Deprecated public static final String SET_PREFERRED_APPLICATIONS = "android.permission.SET_PREFERRED_APPLICATIONS"; field public static final String SET_PROCESS_LIMIT = "android.permission.SET_PROCESS_LIMIT"; @@ -18722,8 +18724,8 @@ package android.hardware.biometrics { method @Nullable public int getAllowedAuthenticators(); method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @Nullable public android.hardware.biometrics.PromptContentView getContentView(); method @Nullable public CharSequence getDescription(); - method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @Nullable @RequiresPermission("android.permission.MANAGE_BIOMETRIC_DIALOG") public android.graphics.Bitmap getLogoBitmap(); - method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @DrawableRes @RequiresPermission("android.permission.MANAGE_BIOMETRIC_DIALOG") public int getLogoRes(); + method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @Nullable @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_LOGO) public android.graphics.Bitmap getLogoBitmap(); + method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @DrawableRes @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_LOGO) public int getLogoRes(); method @Nullable public CharSequence getNegativeButtonText(); method @Nullable public CharSequence getSubtitle(); method @NonNull public CharSequence getTitle(); @@ -18773,8 +18775,8 @@ package android.hardware.biometrics { method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setContentView(@NonNull android.hardware.biometrics.PromptContentView); method @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setDescription(@NonNull CharSequence); method @Deprecated @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setDeviceCredentialAllowed(boolean); - method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @NonNull @RequiresPermission("android.permission.MANAGE_BIOMETRIC_DIALOG") public android.hardware.biometrics.BiometricPrompt.Builder setLogo(@DrawableRes int); - method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @NonNull @RequiresPermission("android.permission.MANAGE_BIOMETRIC_DIALOG") public android.hardware.biometrics.BiometricPrompt.Builder setLogo(@NonNull android.graphics.Bitmap); + method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @NonNull @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_LOGO) public android.hardware.biometrics.BiometricPrompt.Builder setLogoBitmap(@NonNull android.graphics.Bitmap); + method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @NonNull @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_LOGO) public android.hardware.biometrics.BiometricPrompt.Builder setLogoRes(@DrawableRes int); method @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setNegativeButton(@NonNull CharSequence, @NonNull java.util.concurrent.Executor, @NonNull android.content.DialogInterface.OnClickListener); method @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setSubtitle(@NonNull CharSequence); method @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setTitle(@NonNull CharSequence); @@ -18800,14 +18802,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; @@ -18818,7 +18820,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(); @@ -18831,7 +18833,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); } } @@ -24299,7 +24301,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 String, @NonNull android.os.UserHandle); + method @FlaggedApi("com.android.media.flags.enable_cross_user_routing_in_media_router2") @Nullable @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); 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(); @@ -25715,9 +25717,48 @@ package android.media.metrics { field public static final String KEY_STATSD_ATOM = "bundlesession-statsd-atom"; } + @FlaggedApi("com.android.media.editing.flags.add_media_metrics_editing") public final class EditingEndedEvent extends android.media.metrics.Event implements android.os.Parcelable { + method public int describeContents(); + method public int getErrorCode(); + method public int getFinalState(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.media.metrics.EditingEndedEvent> CREATOR; + field public static final int ERROR_CODE_AUDIO_PROCESSING_FAILED = 18; // 0x12 + field public static final int ERROR_CODE_DECODER_INIT_FAILED = 11; // 0xb + field public static final int ERROR_CODE_DECODING_FAILED = 12; // 0xc + field public static final int ERROR_CODE_DECODING_FORMAT_UNSUPPORTED = 13; // 0xd + field public static final int ERROR_CODE_ENCODER_INIT_FAILED = 14; // 0xe + field public static final int ERROR_CODE_ENCODING_FAILED = 15; // 0xf + field public static final int ERROR_CODE_ENCODING_FORMAT_UNSUPPORTED = 16; // 0x10 + field public static final int ERROR_CODE_FAILED_RUNTIME_CHECK = 2; // 0x2 + field public static final int ERROR_CODE_IO_BAD_HTTP_STATUS = 6; // 0x6 + field public static final int ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED = 9; // 0x9 + field public static final int ERROR_CODE_IO_FILE_NOT_FOUND = 7; // 0x7 + field public static final int ERROR_CODE_IO_NETWORK_CONNECTION_FAILED = 4; // 0x4 + field public static final int ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT = 5; // 0x5 + field public static final int ERROR_CODE_IO_NO_PERMISSION = 8; // 0x8 + field public static final int ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE = 10; // 0xa + field public static final int ERROR_CODE_IO_UNSPECIFIED = 3; // 0x3 + field public static final int ERROR_CODE_MUXING_FAILED = 19; // 0x13 + field public static final int ERROR_CODE_NONE = 1; // 0x1 + field public static final int ERROR_CODE_VIDEO_FRAME_PROCESSING_FAILED = 17; // 0x11 + field public static final int FINAL_STATE_CANCELED = 2; // 0x2 + field public static final int FINAL_STATE_ERROR = 3; // 0x3 + field public static final int FINAL_STATE_SUCCEEDED = 1; // 0x1 + } + + @FlaggedApi("com.android.media.editing.flags.add_media_metrics_editing") public static final class EditingEndedEvent.Builder { + ctor public EditingEndedEvent.Builder(int); + method @NonNull public android.media.metrics.EditingEndedEvent build(); + method @NonNull public android.media.metrics.EditingEndedEvent.Builder setErrorCode(int); + method @NonNull public android.media.metrics.EditingEndedEvent.Builder setMetricsBundle(@NonNull android.os.Bundle); + method @NonNull public android.media.metrics.EditingEndedEvent.Builder setTimeSinceCreatedMillis(@IntRange(from=0xffffffff) long); + } + public final class EditingSession implements java.lang.AutoCloseable { method public void close(); method @NonNull public android.media.metrics.LogSessionId getSessionId(); + method @FlaggedApi("com.android.media.editing.flags.add_media_metrics_editing") public void reportEditingEndedEvent(@NonNull android.media.metrics.EditingEndedEvent); } public abstract class Event { @@ -40575,7 +40616,7 @@ package android.service.notification { method public int getPriorityCategoryReminders(); method public int getPriorityCategoryRepeatCallers(); method public int getPriorityCategorySystem(); - method @FlaggedApi("android.app.modes_api") public int getPriorityChannels(); + method @FlaggedApi("android.app.modes_api") public int getPriorityChannelsAllowed(); method public int getPriorityConversationSenders(); method public int getPriorityMessageSenders(); method public int getVisualEffectAmbient(); @@ -41703,10 +41744,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>); } diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 0b17e03c147d..ea008ac2b5c0 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"; @@ -871,6 +871,10 @@ package android.app { field @NonNull public static final android.os.Parcelable.Creator<android.app.AppOpsManager.PackageOps> CREATOR; } + @FlaggedApi("android.app.bic_client") public final class BackgroundInstallControlManager { + method @FlaggedApi("android.app.bic_client") @NonNull @RequiresPermission(android.Manifest.permission.GET_BACKGROUND_INSTALLED_PACKAGES) public java.util.List<android.content.pm.PackageInfo> getBackgroundInstalledPackages(long); + } + public class BroadcastOptions { method public void clearRequireCompatChange(); method public int getPendingIntentBackgroundActivityStartMode(); @@ -3388,11 +3392,10 @@ package android.companion.virtual.camera { } @FlaggedApi("android.companion.virtual.flags.virtual_camera") public static final class VirtualCameraConfig.Builder { - ctor public VirtualCameraConfig.Builder(); + ctor public VirtualCameraConfig.Builder(@NonNull String); method @NonNull public android.companion.virtual.camera.VirtualCameraConfig.Builder addStreamConfig(@IntRange(from=1) int, @IntRange(from=1) int, int, @IntRange(from=1) int); method @NonNull public android.companion.virtual.camera.VirtualCameraConfig build(); method @NonNull public android.companion.virtual.camera.VirtualCameraConfig.Builder setLensFacing(int); - method @NonNull public android.companion.virtual.camera.VirtualCameraConfig.Builder setName(@NonNull String); method @NonNull public android.companion.virtual.camera.VirtualCameraConfig.Builder setSensorOrientation(int); method @NonNull public android.companion.virtual.camera.VirtualCameraConfig.Builder setVirtualCameraCallback(@NonNull java.util.concurrent.Executor, @NonNull android.companion.virtual.camera.VirtualCameraCallback); } @@ -6886,7 +6889,6 @@ package android.media { public final class MediaRouter2 { method @NonNull public java.util.List<android.media.MediaRoute2Info> getAllRoutes(); method @Nullable public String getClientPackageName(); - method @Nullable @RequiresPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL) public static android.media.MediaRouter2 getInstance(@NonNull android.content.Context, @NonNull String); method @RequiresPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL) public void setRouteVolume(@NonNull android.media.MediaRoute2Info, int); method @RequiresPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL) public void startScan(); method @RequiresPermission(android.Manifest.permission.MEDIA_CONTENT_CONTROL) public void stopScan(); @@ -14774,6 +14776,7 @@ package android.telephony { method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean matchesCurrentSimOperator(@NonNull String, int, @Nullable String); method public boolean needsOtaServiceProvisioning(); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void notifyOtaEmergencyNumberDbInstalled(); + method @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") @RequiresPermission(android.Manifest.permission.DUMP) public void persistEmergencyCallDiagnosticData(@NonNull String, @NonNull android.telephony.TelephonyManager.EmergencyCallDiagnosticParams); method @RequiresPermission(android.Manifest.permission.REBOOT) public int prepareForUnattendedReboot(); method @Deprecated @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public boolean rebootRadio(); method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public void registerCarrierPrivilegesCallback(int, @NonNull java.util.concurrent.Executor, @NonNull android.telephony.TelephonyManager.CarrierPrivilegesCallback); @@ -14952,6 +14955,21 @@ package android.telephony { method public default void onCarrierServiceChanged(@Nullable String, int); } + @FlaggedApi("com.android.server.telecom.flags.telecom_resolve_hidden_dependencies") public static final class TelephonyManager.EmergencyCallDiagnosticParams { + method public long getLogcatCollectionStartTimeMillis(); + method public boolean isLogcatCollectionEnabled(); + method public boolean isTelecomDumpSysCollectionEnabled(); + method public boolean isTelephonyDumpSysCollectionEnabled(); + } + + public static final class TelephonyManager.EmergencyCallDiagnosticParams.Builder { + ctor public TelephonyManager.EmergencyCallDiagnosticParams.Builder(); + method @NonNull public android.telephony.TelephonyManager.EmergencyCallDiagnosticParams build(); + method @NonNull public android.telephony.TelephonyManager.EmergencyCallDiagnosticParams.Builder setLogcatCollectionStartTimeMillis(long); + method @NonNull public android.telephony.TelephonyManager.EmergencyCallDiagnosticParams.Builder setTelecomDumpSysCollectionEnabled(boolean); + method @NonNull public android.telephony.TelephonyManager.EmergencyCallDiagnosticParams.Builder setTelephonyDumpSysCollectionEnabled(boolean); + } + public static class TelephonyManager.ModemActivityInfoException extends java.lang.Exception { ctor public TelephonyManager.ModemActivityInfoException(int); method public int getErrorCode(); @@ -17159,7 +17177,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); @@ -17226,6 +17244,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 e288b42f7ec7..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]); } @@ -6859,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 ccd8456129eb..00c4b0f6515f 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -1548,9 +1548,16 @@ public class AppOpsManager { public static final int OP_READ_SYSTEM_GRAMMATICAL_GENDER = AppProtoEnums.APP_OP_READ_SYSTEM_GRAMMATICAL_GENDER; + /** + * Allows an app whose primary use case is to backup or sync content to run longer jobs. + * + * @hide + */ + public static final int OP_RUN_BACKUP_JOBS = AppProtoEnums.APP_OP_RUN_BACKUP_JOBS; + /** @hide */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) - public static final int _NUM_OP = 144; + public static final int _NUM_OP = 145; /** * All app ops represented as strings. @@ -1700,6 +1707,7 @@ public class AppOpsManager { OPSTR_ENABLE_MOBILE_DATA_BY_USER, OPSTR_RESERVED_FOR_TESTING, OPSTR_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER, + OPSTR_RUN_BACKUP_JOBS, }) public @interface AppOpString {} @@ -2392,6 +2400,13 @@ public class AppOpsManager { public static final String OPSTR_READ_SYSTEM_GRAMMATICAL_GENDER = "android:read_system_grammatical_gender"; + /** + * Allows an app whose primary use case is to backup or sync content to run longer jobs. + * + * @hide + */ + public static final String OPSTR_RUN_BACKUP_JOBS = "android:run_backup_jobs"; + /** {@link #sAppOpsToNote} not initialized yet for this op */ private static final byte SHOULD_COLLECT_NOTE_OP_NOT_INITIALIZED = 0; /** Should not collect noting of this app-op in {@link #sAppOpsToNote} */ @@ -2504,6 +2519,7 @@ public class AppOpsManager { OP_RECEIVE_SANDBOXED_DETECTION_TRAINING_DATA, OP_MEDIA_ROUTING_CONTROL, OP_READ_SYSTEM_GRAMMATICAL_GENDER, + OP_RUN_BACKUP_JOBS, }; static final AppOpInfo[] sAppOpInfos = new AppOpInfo[]{ @@ -2961,6 +2977,8 @@ public class AppOpsManager { // will make it an app-op permission in the future. // .setPermission(Manifest.permission.READ_SYSTEM_GRAMMATICAL_GENDER) .build(), + new AppOpInfo.Builder(OP_RUN_BACKUP_JOBS, OPSTR_RUN_BACKUP_JOBS, "RUN_BACKUP_JOBS") + .setPermission(Manifest.permission.RUN_BACKUP_JOBS).build(), }; // The number of longs needed to form a full bitmask of app ops diff --git a/core/java/android/app/BackgroundInstallControlManager.java b/core/java/android/app/BackgroundInstallControlManager.java new file mode 100644 index 000000000000..664fcebcfc05 --- /dev/null +++ b/core/java/android/app/BackgroundInstallControlManager.java @@ -0,0 +1,102 @@ +/* + * 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 static android.Manifest.permission.GET_BACKGROUND_INSTALLED_PACKAGES; +import static android.annotation.SystemApi.Client.PRIVILEGED_APPS; + +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.annotation.RequiresPermission; +import android.annotation.SystemApi; +import android.annotation.SystemService; +import android.content.Context; +import android.content.pm.IBackgroundInstallControlService; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.os.RemoteException; +import android.os.ServiceManager; + +import java.util.List; + +/** + * BackgroundInstallControlManager client allows apps to query apps installed in background. + * + * <p>Any applications that was installed without an accompanying installer UI activity paired + * with recorded user interaction event is considered background installed. This is determined by + * analysis of user-activity logs. + * + * <p>Warning: BackgroundInstallControl should not be considered a definitive + * authority of identifying background installed applications. Consumers can use this as a + * supplementary signal, but must perform additional due diligence to confirm the install nature + * of the package. + * + * @hide + */ +@FlaggedApi(Flags.FLAG_BIC_CLIENT) +@SystemApi(client = PRIVILEGED_APPS) +@SystemService(Context.BACKGROUND_INSTALL_CONTROL_SERVICE) +public final class BackgroundInstallControlManager { + + private static final String TAG = "BackgroundInstallControlManager"; + private static IBackgroundInstallControlService sService; + private final Context mContext; + + BackgroundInstallControlManager(Context context) { + mContext = context; + } + + private static IBackgroundInstallControlService getService() { + if (sService == null) { + sService = + IBackgroundInstallControlService.Stub.asInterface( + ServiceManager.getService(Context.BACKGROUND_INSTALL_CONTROL_SERVICE)); + } + return sService; + } + + /** + * Returns a full list of {@link PackageInfo} of apps currently installed for the current user + * that are considered installed in the background. + * + * <p>Refer to top level doc {@link BackgroundInstallControlManager} for more details on + * background-installed applications. + * <p> + * + * @param flags - Flags will be used to call + * {@link PackageManager#getInstalledPackages(PackageInfoFlags)} to retrieve installed packages. + * @return A list of packages retrieved from {@link PackageManager} with non-background + * installed app filter applied. + * + * @hide + */ + @FlaggedApi(Flags.FLAG_BIC_CLIENT) + @SystemApi + @RequiresPermission(GET_BACKGROUND_INSTALLED_PACKAGES) + public @NonNull List<PackageInfo> getBackgroundInstalledPackages( + @PackageManager.PackageInfoFlagsBits long flags) { + List<PackageInfo> backgroundInstalledPackages; + try { + return getService() + .getBackgroundInstalledPackages(flags, mContext.getUserId()) + .getList(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + +} diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index ed0cfbe3d9c3..a81ad3c429ea 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -5487,6 +5487,15 @@ public class Notification implements Parcelable return mColors; } + /** + * @param isHeader If the notification is a notification header + * @return An instance of mColors after resolving the palette + */ + private Colors getColors(boolean isHeader) { + mColors.resolvePalette(mContext, mN.color, !isHeader && mN.isColorized(), mInNightMode); + return mColors; + } + private void updateBackgroundColor(RemoteViews contentView, StandardTemplateParams p) { if (isBackgroundColorized(p)) { @@ -6618,6 +6627,23 @@ public class Notification implements Parcelable return getColors(p).getContrastColor(); } + /** + * Gets the foreground color of the small icon. If the notification is colorized, this + * is the primary text color, otherwise it's the contrast-adjusted app-provided color. + * @hide + */ + public @ColorInt int getSmallIconColor(boolean isHeader) { + return getColors(/* isHeader = */ isHeader).getContrastColor(); + } + + /** + * Gets the background color of the notification. + * @hide + */ + public @ColorInt int getBackgroundColor(boolean isHeader) { + return getColors(/* isHeader = */ isHeader).getBackgroundColor(); + } + /** @return the theme's accent color for colored UI elements. */ private @ColorInt int getPrimaryAccentColor(StandardTemplateParams p) { return getColors(p).getPrimaryAccentColor(); @@ -8532,6 +8558,8 @@ public class Notification implements Parcelable boolean isImportantConversation = mConversationType == CONVERSATION_TYPE_IMPORTANT; boolean isHeaderless = !isConversationLayout && isCollapsed; + //TODO (b/217799515): ensure mConversationTitle always returns the correct + // conversationTitle, probably set mConversationTitle = conversationTitle after this CharSequence conversationTitle = !TextUtils.isEmpty(super.mBigContentTitle) ? super.mBigContentTitle : mConversationTitle; diff --git a/core/java/android/app/OWNERS b/core/java/android/app/OWNERS index 0760d4db9169..3b5bba20a10b 100644 --- a/core/java/android/app/OWNERS +++ b/core/java/android/app/OWNERS @@ -90,8 +90,8 @@ per-file InstantAppResolveInfo.aidl = file:/services/core/java/com/android/serve per-file pinner-client.aconfig = file:/core/java/android/app/pinner/OWNERS # BackgroundInstallControlManager -per-file BackgroundInstallControlManager.java = file:/services/core/java/com/android/server/pm/OWNERS -per-file background_install_control_manager.aconfig = file:/services/core/java/com/android/server/pm/OWNERS +per-file BackgroundInstallControlManager.java = file:/services/core/java/com/android/server/pm/BACKGROUND_INSTALL_OWNERS +per-file background_install_control_manager.aconfig = file:/services/core/java/com/android/server/pm/BACKGROUND_INSTALL_OWNERS # ResourcesManager per-file ResourcesManager.java = file:RESOURCES_OWNERS diff --git a/core/java/android/companion/virtual/camera/VirtualCameraConfig.java b/core/java/android/companion/virtual/camera/VirtualCameraConfig.java index 350cf3d832d6..06a0f5c09e18 100644 --- a/core/java/android/companion/virtual/camera/VirtualCameraConfig.java +++ b/core/java/android/companion/virtual/camera/VirtualCameraConfig.java @@ -196,13 +196,12 @@ public final class VirtualCameraConfig implements Parcelable { * <li>At least one stream must be added with {@link #addStreamConfig(int, int, int, int)}. * <li>A callback must be set with {@link #setVirtualCameraCallback(Executor, * VirtualCameraCallback)} - * <li>A camera name must be set with {@link #setName(String)} * <li>A lens facing must be set with {@link #setLensFacing(int)} */ @FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA) public static final class Builder { - private String mName; + private final String mName; private final ArraySet<VirtualCameraStreamConfig> mStreamConfigurations = new ArraySet<>(); private Executor mCallbackExecutor; private VirtualCameraCallback mCallback; @@ -210,12 +209,12 @@ public final class VirtualCameraConfig implements Parcelable { private int mLensFacing = LENS_FACING_UNKNOWN; /** - * Sets the name of the virtual camera instance. + * Creates a new instance of {@link Builder}. + * + * @param name The name of the {@link VirtualCamera}. */ - @NonNull - public Builder setName(@NonNull String name) { - mName = requireNonNull(name, "Display name cannot be null"); - return this; + public Builder(@NonNull String name) { + mName = requireNonNull(name, "Name cannot be null"); } /** diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index 4724e866f094..8744eaee4341 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -128,6 +128,7 @@ import java.util.function.Function; * <a href="/training/basics/intents/package-visibility">manage package visibility</a>. * </p> */ +@android.ravenwood.annotation.RavenwoodKeepPartialClass public abstract class PackageManager { private static final String TAG = "PackageManager"; @@ -5492,6 +5493,7 @@ public abstract class PackageManager { * application info. * @hide */ + @android.ravenwood.annotation.RavenwoodKeepWholeClass public static class Flags { final long mValue; protected Flags(long value) { @@ -5506,6 +5508,7 @@ public abstract class PackageManager { * Specific flags used for retrieving package info. Example: * {@code PackageManager.getPackageInfo(packageName, PackageInfoFlags.of(0)} */ + @android.ravenwood.annotation.RavenwoodKeepWholeClass public final static class PackageInfoFlags extends Flags { private PackageInfoFlags(@PackageInfoFlagsBits long value) { super(value); @@ -5519,6 +5522,7 @@ public abstract class PackageManager { /** * Specific flags used for retrieving application info. */ + @android.ravenwood.annotation.RavenwoodKeepWholeClass public final static class ApplicationInfoFlags extends Flags { private ApplicationInfoFlags(@ApplicationInfoFlagsBits long value) { super(value); @@ -5532,6 +5536,7 @@ public abstract class PackageManager { /** * Specific flags used for retrieving component info. */ + @android.ravenwood.annotation.RavenwoodKeepWholeClass public final static class ComponentInfoFlags extends Flags { private ComponentInfoFlags(@ComponentInfoFlagsBits long value) { super(value); @@ -5545,6 +5550,7 @@ public abstract class PackageManager { /** * Specific flags used for retrieving resolve info. */ + @android.ravenwood.annotation.RavenwoodKeepWholeClass public final static class ResolveInfoFlags extends Flags { private ResolveInfoFlags(@ResolveInfoFlagsBits long value) { super(value); diff --git a/core/java/android/content/pm/UserInfo.java b/core/java/android/content/pm/UserInfo.java index 8fd78bd4276c..3e9f260566bd 100644 --- a/core/java/android/content/pm/UserInfo.java +++ b/core/java/android/content/pm/UserInfo.java @@ -52,6 +52,7 @@ import java.lang.annotation.RetentionPolicy; * @hide */ @TestApi +@android.ravenwood.annotation.RavenwoodKeepWholeClass public class UserInfo implements Parcelable { /** @@ -438,6 +439,7 @@ public class UserInfo implements Parcelable { /** * @return true if this user can be switched to. **/ + @android.ravenwood.annotation.RavenwoodThrow public boolean supportsSwitchTo() { if (partial || !isEnabled()) { // Don't support switching to disabled or partial users, which includes users with @@ -455,6 +457,7 @@ public class UserInfo implements Parcelable { * @return true if user is of type {@link UserManager#USER_TYPE_SYSTEM_HEADLESS} and * {@link com.android.internal.R.bool.config_canSwitchToHeadlessSystemUser} is true. */ + @android.ravenwood.annotation.RavenwoodThrow private boolean canSwitchToHeadlessSystemUser() { return UserManager.USER_TYPE_SYSTEM_HEADLESS.equals(userType) && Resources.getSystem() .getBoolean(com.android.internal.R.bool.config_canSwitchToHeadlessSystemUser); @@ -465,6 +468,7 @@ public class UserInfo implements Parcelable { * @deprecated Use {@link UserInfo#supportsSwitchTo} instead. */ @Deprecated + @android.ravenwood.annotation.RavenwoodThrow public boolean supportsSwitchToByUser() { return supportsSwitchTo(); } diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig index e4e9fbaf2c55..fd872906f53b 100644 --- a/core/java/android/content/pm/flags.aconfig +++ b/core/java/android/content/pm/flags.aconfig @@ -153,3 +153,18 @@ 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 +} + +flag { + name: "allow_sdk_sandbox_query_intent_activities" + namespace: "package_manager_service" + description: "Feature flag to allow the sandbox SDK to query intent activities of the client app." + bug: "295842134" +} diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig index 9644d8095a4d..c08343713abb 100644 --- a/core/java/android/content/pm/multiuser.aconfig +++ b/core/java/android/content/pm/multiuser.aconfig @@ -85,6 +85,7 @@ flag { description: "Enable auto-locking private space on device restarts" bug: "296993385" } + flag { name: "enable_system_user_only_for_services_and_providers" namespace: "multiuser" @@ -92,3 +93,10 @@ flag { bug: "302354856" is_fixed_read_only: true } + +flag { + name: "allow_private_profile_apis" + namespace: "profile_experiences" + description: "Enable only the API changes to support private space" + bug: "299069460" +} diff --git a/core/java/android/content/res/FontScaleConverterFactory.java b/core/java/android/content/res/FontScaleConverterFactory.java index cbe4c62d7069..625d7cb66900 100644 --- a/core/java/android/content/res/FontScaleConverterFactory.java +++ b/core/java/android/content/res/FontScaleConverterFactory.java @@ -58,6 +58,16 @@ public class FontScaleConverterFactory { synchronized (LOOKUP_TABLES_WRITE_LOCK) { putInto( sLookupTables, + /* scaleKey= */ 1.1f, + new FontScaleConverterImpl( + /* fromSp= */ + new float[] { 8f, 10f, 12f, 14f, 18f, 20f, 24f, 30f, 100}, + /* toDp= */ + new float[] { 8.8f, 11f, 13.2f, 15.6f, 19.2f, 21.2f, 24.8f, 30f, 100}) + ); + + putInto( + sLookupTables, /* scaleKey= */ 1.15f, new FontScaleConverterImpl( /* fromSp= */ diff --git a/core/java/android/credentials/ui/AuthenticationEntry.java b/core/java/android/credentials/ui/AuthenticationEntry.java index b1a382c8ea96..9bd0871b3d3b 100644 --- a/core/java/android/credentials/ui/AuthenticationEntry.java +++ b/core/java/android/credentials/ui/AuthenticationEntry.java @@ -34,15 +34,24 @@ import java.lang.annotation.RetentionPolicy; /** * An authentication entry. * + * Applicable only for credential retrieval flow, authentication entries are a special type of + * entries that require the user to unlock the given provider before its credential options can + * be fully rendered. + * * @hide */ @TestApi public final class AuthenticationEntry implements Parcelable { - @NonNull private final String mKey; - @NonNull private final String mSubkey; - @NonNull private final @Status int mStatus; - @Nullable private Intent mFrameworkExtrasIntent; - @NonNull private final Slice mSlice; + @NonNull + private final String mKey; + @NonNull + private final String mSubkey; + @NonNull + private final @Status int mStatus; + @Nullable + private Intent mFrameworkExtrasIntent; + @NonNull + private final Slice mSlice; /** @hide **/ @IntDef(prefix = {"STATUS_"}, value = { @@ -51,15 +60,21 @@ public final class AuthenticationEntry implements Parcelable { STATUS_UNLOCKED_BUT_EMPTY_MOST_RECENT, }) @Retention(RetentionPolicy.SOURCE) - public @interface Status {} + public @interface Status { + } /** This entry is still locked, as initially supplied by the provider. */ public static final int STATUS_LOCKED = 0; - /** This entry was unlocked but didn't contain any credential. Meanwhile, "less recent" means - * there is another such entry that was unlocked more recently. */ + /** + * This entry was unlocked but didn't contain any credential. Meanwhile, "less recent" means + * there is another such entry that was unlocked more recently. + */ public static final int STATUS_UNLOCKED_BUT_EMPTY_LESS_RECENT = 1; - /** This is the most recent entry that was unlocked but didn't contain any credential. - * There should be at most one authentication entry with this status. */ + /** + * This is the most recent entry that was unlocked but didn't contain any credential. + * + * There will be at most one authentication entry with this status. + */ public static final int STATUS_UNLOCKED_BUT_EMPTY_MOST_RECENT = 2; private AuthenticationEntry(@NonNull Parcel in) { @@ -74,9 +89,11 @@ public final class AuthenticationEntry implements Parcelable { AnnotationValidations.validate(NonNull.class, null, mSlice); } - /** Constructor to be used for an entry that does not require further activities + /** + * Constructor to be used for an entry that does not require further activities * to be invoked when selected. */ + // TODO(b/322065508): remove this constructor. public AuthenticationEntry(@NonNull String key, @NonNull String subkey, @NonNull Slice slice, @Status int status) { mKey = key; @@ -95,9 +112,9 @@ public final class AuthenticationEntry implements Parcelable { } /** - * Returns the identifier of this entry that's unique within the context of the CredentialManager - * request. - */ + * Returns the identifier of this entry that's unique within the context of the + * CredentialManager request. + */ @NonNull public String getKey() { return mKey; @@ -111,23 +128,23 @@ public final class AuthenticationEntry implements Parcelable { return mSubkey; } - /** - * Returns the Slice to be rendered. - */ + /** Returns the Slice to be rendered. */ @NonNull public Slice getSlice() { return mSlice; } - /** - * Returns the entry status. - */ + /** Returns the entry status, depending on which the entry will be rendered differently. */ @NonNull @Status public int getStatus() { return mStatus; } + /** + * Returns the framework intent to be filled in when launching this entry's provider + * PendingIntent. + */ @Nullable @SuppressLint("IntentBuilderName") // Not building a new intent. public Intent getFrameworkExtrasIntent() { diff --git a/core/java/android/credentials/ui/BaseDialogResult.java b/core/java/android/credentials/ui/BaseDialogResult.java index e8cf5abd5239..e985a4666d31 100644 --- a/core/java/android/credentials/ui/BaseDialogResult.java +++ b/core/java/android/credentials/ui/BaseDialogResult.java @@ -24,8 +24,6 @@ import android.os.IBinder; import android.os.Parcel; import android.os.Parcelable; -import com.android.internal.util.AnnotationValidations; - import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -46,7 +44,7 @@ public class BaseDialogResult implements Parcelable { /** * Used for the UX to construct the {@code resultData Bundle} to send via the {@code - * ResultReceiver}. + * ResultReceiver}. */ public static void addToBundle(@NonNull BaseDialogResult result, @NonNull Bundle bundle) { bundle.putParcelable(EXTRA_BASE_RESULT, result); @@ -66,13 +64,14 @@ public class BaseDialogResult implements Parcelable { RESULT_CODE_DATA_PARSING_FAILURE, }) @Retention(RetentionPolicy.SOURCE) - public @interface ResultCode {} + public @interface ResultCode { + } /** User intentionally canceled the dialog. */ public static final int RESULT_CODE_DIALOG_USER_CANCELED = 0; /** - * The user has consented to switching to a new default provider. The provider info is in the - * {@code resultData}. + * The UI was stopped since the user has chosen to navigate to the Settings UI to reconfigure + * their providers. */ public static final int RESULT_CODE_CANCELED_AND_LAUNCHED_SETTINGS = 1; /** @@ -86,6 +85,7 @@ public class BaseDialogResult implements Parcelable { public static final int RESULT_CODE_DATA_PARSING_FAILURE = 3; @Nullable + @Deprecated private final IBinder mRequestToken; public BaseDialogResult(@Nullable IBinder requestToken) { @@ -94,6 +94,7 @@ public class BaseDialogResult implements Parcelable { /** Returns the unique identifier for the request that launched the operation. */ @Nullable + @Deprecated public IBinder getRequestToken() { return mRequestToken; } @@ -115,14 +116,14 @@ public class BaseDialogResult implements Parcelable { public static final @NonNull Creator<BaseDialogResult> CREATOR = new Creator<BaseDialogResult>() { - @Override - public BaseDialogResult createFromParcel(@NonNull Parcel in) { - return new BaseDialogResult(in); - } - - @Override - public BaseDialogResult[] newArray(int size) { - return new BaseDialogResult[size]; - } - }; + @Override + public BaseDialogResult createFromParcel(@NonNull Parcel in) { + return new BaseDialogResult(in); + } + + @Override + public BaseDialogResult[] newArray(int size) { + return new BaseDialogResult[size]; + } + }; } diff --git a/core/java/android/credentials/ui/CancelUiRequest.java b/core/java/android/credentials/ui/CancelUiRequest.java index d4c249e58c8a..712424ced41b 100644 --- a/core/java/android/credentials/ui/CancelUiRequest.java +++ b/core/java/android/credentials/ui/CancelUiRequest.java @@ -24,7 +24,7 @@ import android.os.Parcelable; import com.android.internal.util.AnnotationValidations; /** - * A request to cancel any ongoing UI matching this request. + * A request to cancel the ongoing UI matching the identifier token in this request. * * @hide */ @@ -33,9 +33,12 @@ public final class CancelUiRequest implements Parcelable { /** * The intent extra key for the {@code CancelUiRequest} object when launching the UX * activities. + * + * @hide */ - @NonNull public static final String EXTRA_CANCEL_UI_REQUEST = - "android.credentials.ui.extra.EXTRA_CANCEL_UI_REQUEST"; + @NonNull + public static final String EXTRA_CANCEL_UI_REQUEST = + "android.credentials.ui.extra.CANCEL_UI_REQUEST"; @NonNull private final IBinder mToken; @@ -51,6 +54,10 @@ public final class CancelUiRequest implements Parcelable { return mToken; } + /** + * Returns the app package name invoking this request, that can be used to derive display + * metadata (e.g. "Cancelled by `App Name`"). + */ @NonNull public String getAppPackageName() { return mAppPackageName; @@ -64,6 +71,7 @@ public final class CancelUiRequest implements Parcelable { return mShouldShowCancellationUi; } + /** Constructs a {@link CancelUiRequest}. */ public CancelUiRequest(@NonNull IBinder token, boolean shouldShowCancellationUi, @NonNull String appPackageName) { mToken = token; @@ -91,7 +99,8 @@ public final class CancelUiRequest implements Parcelable { return 0; } - @NonNull public static final Creator<CancelUiRequest> CREATOR = new Creator<>() { + @NonNull + public static final Creator<CancelUiRequest> CREATOR = new Creator<>() { @Override public CancelUiRequest createFromParcel(@NonNull Parcel in) { return new CancelUiRequest(in); diff --git a/core/java/android/credentials/ui/Constants.java b/core/java/android/credentials/ui/Constants.java index 37f850bc46c5..68f28e74dad0 100644 --- a/core/java/android/credentials/ui/Constants.java +++ b/core/java/android/credentials/ui/Constants.java @@ -36,7 +36,5 @@ public class Constants { public static final String EXTRA_REQ_FOR_ALL_OPTIONS = "android.credentials.ui.extra.REQ_FOR_ALL_OPTIONS"; - /** The intent action for when the enabled Credential Manager providers has been updated. */ - public static final String CREDMAN_ENABLED_PROVIDERS_UPDATED = - "android.credentials.ui.action.CREDMAN_ENABLED_PROVIDERS_UPDATED"; + private Constants() {} } diff --git a/core/java/android/credentials/ui/CreateCredentialProviderData.java b/core/java/android/credentials/ui/CreateCredentialProviderData.java index 2508d8eb20ab..d7a4f5bdbfca 100644 --- a/core/java/android/credentials/ui/CreateCredentialProviderData.java +++ b/core/java/android/credentials/ui/CreateCredentialProviderData.java @@ -47,6 +47,17 @@ public final class CreateCredentialProviderData extends ProviderData implements mRemoteEntry = remoteEntry; } + /** + * Converts the instance to a {@link CreateCredentialProviderInfo}. + * + * @hide + */ + @NonNull + public CreateCredentialProviderInfo toCreateCredentialProviderInfo() { + return new CreateCredentialProviderInfo( + getProviderFlattenedComponentName(), mSaveEntries, mRemoteEntry); + } + @NonNull public List<Entry> getSaveEntries() { return mSaveEntries; diff --git a/core/java/android/credentials/ui/CreateCredentialProviderInfo.java b/core/java/android/credentials/ui/CreateCredentialProviderInfo.java new file mode 100644 index 000000000000..41ca852c2351 --- /dev/null +++ b/core/java/android/credentials/ui/CreateCredentialProviderInfo.java @@ -0,0 +1,113 @@ +/* + * Copyright 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 android.credentials.ui; + +import android.annotation.NonNull; +import android.annotation.Nullable; + +import com.android.internal.util.Preconditions; + +import java.util.ArrayList; +import java.util.List; + +/** + * Information pertaining to a specific provider during the given create-credential flow. + * + * This includes provider metadata and its credential creation options for display purposes. + * + * @hide + */ +public final class CreateCredentialProviderInfo { + + @NonNull + private final String mProviderName; + + @NonNull + private final List<Entry> mSaveEntries; + @Nullable + private final Entry mRemoteEntry; + + CreateCredentialProviderInfo( + @NonNull String providerName, @NonNull List<Entry> saveEntries, + @Nullable Entry remoteEntry) { + mProviderName = Preconditions.checkStringNotEmpty(providerName); + mSaveEntries = new ArrayList<>(saveEntries); + mRemoteEntry = remoteEntry; + } + + /** Returns the fully-qualified provider (component or package) name. */ + @NonNull + public String getProviderName() { + return mProviderName; + } + + /** Returns all the options this provider has, to which the credential can be saved. */ + @NonNull + public List<Entry> getSaveEntries() { + return mSaveEntries; + } + + /** + * Returns the remote credential saving option, if any. + * + * Notice that only one system configured provider can set this option, and when set, it means + * that the system service has already validated the provider's eligibility. + */ + @Nullable + public Entry getRemoteEntry() { + return mRemoteEntry; + } + + /** + * Builder for {@link CreateCredentialProviderInfo}. + * + * @hide + */ + public static final class Builder { + @NonNull + private String mProviderName; + @NonNull + private List<Entry> mSaveEntries = new ArrayList<>(); + @Nullable + private Entry mRemoteEntry = null; + + /** Constructor with required properties. */ + public Builder(@NonNull String providerName) { + mProviderName = Preconditions.checkStringNotEmpty(providerName); + } + + /** Sets the list of options for credential saving to be displayed to the user. */ + @NonNull + public Builder setSaveEntries(@NonNull List<Entry> credentialEntries) { + mSaveEntries = credentialEntries; + return this; + } + + /** Sets the remote entry of the provider. */ + @NonNull + public Builder setRemoteEntry(@Nullable Entry remoteEntry) { + mRemoteEntry = remoteEntry; + return this; + } + + /** Builds a {@link CreateCredentialProviderInfo}. */ + @NonNull + public CreateCredentialProviderInfo build() { + return new CreateCredentialProviderInfo(mProviderName, mSaveEntries, mRemoteEntry); + } + } +} diff --git a/core/java/android/credentials/ui/DisabledProviderData.java b/core/java/android/credentials/ui/DisabledProviderData.java index c266fd56acef..8bccdc9a199f 100644 --- a/core/java/android/credentials/ui/DisabledProviderData.java +++ b/core/java/android/credentials/ui/DisabledProviderData.java @@ -34,6 +34,16 @@ public final class DisabledProviderData extends ProviderData implements Parcelab super(providerFlattenedComponentName); } + /** + * Converts the instance to a {@link DisabledProviderInfo}. + * + * @hide + */ + @NonNull + public DisabledProviderInfo toDisabledProviderInfo() { + return new DisabledProviderInfo(getProviderFlattenedComponentName()); + } + private DisabledProviderData(@NonNull Parcel in) { super(in); } diff --git a/core/java/android/credentials/ui/DisabledProviderInfo.java b/core/java/android/credentials/ui/DisabledProviderInfo.java new file mode 100644 index 000000000000..7ce63681cf1c --- /dev/null +++ b/core/java/android/credentials/ui/DisabledProviderInfo.java @@ -0,0 +1,50 @@ +/* + * Copyright 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 android.credentials.ui; + +import android.annotation.NonNull; + +import com.android.internal.util.Preconditions; + +/** + * Information pertaining to a specific provider that is disabled from the user settings. + * + * Currently, disabled provider data is only propagated in the create-credential flow. + * + * @hide + */ +public final class DisabledProviderInfo { + + @NonNull + private final String mProviderName; + + /** + * Constructs a {@link DisabledProviderInfo}. + * + * @throws IllegalArgumentException if {@code providerName} is empty + */ + public DisabledProviderInfo( + @NonNull String providerName) { + mProviderName = Preconditions.checkStringNotEmpty(providerName); + } + + /** Returns the fully-qualified provider (component or package) name. */ + @NonNull + public String getProviderName() { + return mProviderName; + } +} diff --git a/core/java/android/credentials/ui/Entry.java b/core/java/android/credentials/ui/Entry.java index 55f2a3eb490b..84694471ce70 100644 --- a/core/java/android/credentials/ui/Entry.java +++ b/core/java/android/credentials/ui/Entry.java @@ -35,10 +35,14 @@ import com.android.internal.util.AnnotationValidations; */ @TestApi public final class Entry implements Parcelable { - @NonNull private final String mKey; - @NonNull private final String mSubkey; - @Nullable private PendingIntent mPendingIntent; - @Nullable private Intent mFrameworkExtrasIntent; + @NonNull + private final String mKey; + @NonNull + private final String mSubkey; + @Nullable + private PendingIntent mPendingIntent; + @Nullable + private Intent mFrameworkExtrasIntent; @NonNull private final Slice mSlice; @@ -58,16 +62,19 @@ public final class Entry implements Parcelable { mFrameworkExtrasIntent = in.readTypedObject(Intent.CREATOR); } - /** Constructor to be used for an entry that does not require further activities + /** + * Constructor to be used for an entry that does not require further activities * to be invoked when selected. */ + // TODO(b/322065508): deprecate this constructor. public Entry(@NonNull String key, @NonNull String subkey, @NonNull Slice slice) { mKey = key; mSubkey = subkey; mSlice = slice; } - /** Constructor to be used for an entry that requires a pending intent to be invoked + /** + * Constructor to be used for an entry that requires a pending intent to be invoked * when clicked. */ public Entry(@NonNull String key, @NonNull String subkey, @NonNull Slice slice, @@ -77,9 +84,12 @@ public final class Entry implements Parcelable { } /** - * Returns the identifier of this entry that's unique within the context of the CredentialManager - * request. - */ + * Returns the identifier of this entry that's unique within the context of the + * CredentialManager + * request. + * + * Generally used when sending the user selection result back to the system service. + */ @NonNull public String getKey() { return mKey; @@ -87,25 +97,33 @@ public final class Entry implements Parcelable { /** * Returns the sub-identifier of this entry that's unique within the context of the {@code key}. + * + * Generally used when sending the user selection result back to the system service. */ @NonNull public String getSubkey() { return mSubkey; } - /** - * Returns the Slice to be rendered. - */ + /** Returns the Slice to be rendered. */ @NonNull public Slice getSlice() { return mSlice; } + /** + * Returns the provider PendingIntent to launch once this entry is selected. + */ + // TODO(b/322065508): deprecate this bit. @Nullable public PendingIntent getPendingIntent() { return mPendingIntent; } + /** + * Returns the framework fill in intent to add to the provider PendingIntent to launch, once + * this entry is selected. + */ @Nullable @SuppressLint("IntentBuilderName") // Not building a new intent. public Intent getFrameworkExtrasIntent() { @@ -126,7 +144,7 @@ public final class Entry implements Parcelable { return 0; } - public static final @NonNull Creator<Entry> CREATOR = new Creator<Entry>() { + public static final @NonNull Creator<Entry> CREATOR = new Creator<>() { @Override public Entry createFromParcel(@NonNull Parcel in) { return new Entry(in); diff --git a/core/java/android/credentials/ui/FailureDialogResult.java b/core/java/android/credentials/ui/FailureDialogResult.java new file mode 100644 index 000000000000..abd5a92415d8 --- /dev/null +++ b/core/java/android/credentials/ui/FailureDialogResult.java @@ -0,0 +1,97 @@ +/* + * Copyright 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 android.credentials.ui; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Bundle; +import android.os.IBinder; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Result data when the selector UI has encountered a failure. + * + * @hide + */ +public final class FailureDialogResult extends BaseDialogResult implements Parcelable { + /** Parses and returns a UserSelectionDialogResult from the given resultData. */ + @Nullable + public static FailureDialogResult fromResultData(@NonNull Bundle resultData) { + return resultData.getParcelable( + EXTRA_FAILURE_RESULT, FailureDialogResult.class); + } + + /** + * Used for the UX to construct the {@code resultData Bundle} to send via the {@code + * ResultReceiver}. + */ + public static void addToBundle( + @NonNull FailureDialogResult result, @NonNull Bundle bundle) { + bundle.putParcelable(EXTRA_FAILURE_RESULT, result); + } + + /** + * The intent extra key for the {@code UserSelectionDialogResult} object when the credential + * selector activity finishes. + */ + private static final String EXTRA_FAILURE_RESULT = + "android.credentials.ui.extra.FAILURE_RESULT"; + + @Nullable + private final String mErrorMessage; + + public FailureDialogResult(@Nullable IBinder requestToken, @Nullable String errorMessage) { + super(requestToken); + mErrorMessage = errorMessage; + } + + /** Returns provider package name whose entry was selected by the user. */ + @Nullable + public String getErrorMessage() { + return mErrorMessage; + } + + protected FailureDialogResult(@NonNull Parcel in) { + super(in); + mErrorMessage = in.readString8(); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeString8(mErrorMessage); + } + + @Override + public int describeContents() { + return 0; + } + + public static final @NonNull Creator<FailureDialogResult> CREATOR = + new Creator<>() { + @Override + public FailureDialogResult createFromParcel(@NonNull Parcel in) { + return new FailureDialogResult(in); + } + + @Override + public FailureDialogResult[] newArray(int size) { + return new FailureDialogResult[size]; + } + }; +} diff --git a/core/java/android/credentials/ui/FailureResult.java b/core/java/android/credentials/ui/FailureResult.java new file mode 100644 index 000000000000..ec584170fba2 --- /dev/null +++ b/core/java/android/credentials/ui/FailureResult.java @@ -0,0 +1,96 @@ +/* + * Copyright 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 android.credentials.ui; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Failure or cancellation result encountered during a UI flow. + * + * @hide + */ +public final class FailureResult implements UiResult { + @Nullable + private final String mErrorMessage; + @NonNull + private final int mErrorCode; + + /** @hide **/ + @IntDef(prefix = {"ERROR_CODE_"}, value = { + ERROR_CODE_DIALOG_CANCELED_BY_USER, + ERROR_CODE_CANCELED_AND_LAUNCHED_SETTINGS, + ERROR_CODE_UI_FAILURE, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ErrorCode { + } + + /** + * The UI was stopped due to a failure, e.g. because it failed to parse the incoming data, + * or it encountered an irrecoverable internal issue. + */ + public static final int ERROR_CODE_UI_FAILURE = 0; + /** The user intentionally canceled the dialog. */ + public static final int ERROR_CODE_DIALOG_CANCELED_BY_USER = 1; + /** + * The UI was stopped since the user has chosen to navigate to the Settings UI to reconfigure + * their providers. + */ + public static final int ERROR_CODE_CANCELED_AND_LAUNCHED_SETTINGS = 2; + + /** + * Constructs a {@link FailureResult}. + * + * @throws IllegalArgumentException if {@code providerId} is empty + */ + public FailureResult(@ErrorCode int errorCode, @Nullable String errorMessage) { + mErrorCode = errorCode; + mErrorMessage = errorMessage; + } + + /** Returns the error code. */ + @ErrorCode + public int getErrorCode() { + return mErrorCode; + } + + /** Returns the error message. */ + @Nullable + public String getErrorMessage() { + return mErrorMessage; + } + + FailureDialogResult toFailureDialogResult() { + return new FailureDialogResult(/*requestToken=*/null, mErrorMessage); + } + + int errorCodeToResultCode() { + switch (mErrorCode) { + case ERROR_CODE_DIALOG_CANCELED_BY_USER: + return BaseDialogResult.RESULT_CODE_DIALOG_USER_CANCELED; + case ERROR_CODE_CANCELED_AND_LAUNCHED_SETTINGS: + return BaseDialogResult.RESULT_CODE_CANCELED_AND_LAUNCHED_SETTINGS; + default: + return BaseDialogResult.RESULT_CODE_DATA_PARSING_FAILURE; + } + } +} diff --git a/core/java/android/credentials/ui/GetCredentialProviderData.java b/core/java/android/credentials/ui/GetCredentialProviderData.java index 181475c7ce5a..481419b4f732 100644 --- a/core/java/android/credentials/ui/GetCredentialProviderData.java +++ b/core/java/android/credentials/ui/GetCredentialProviderData.java @@ -55,6 +55,17 @@ public final class GetCredentialProviderData extends ProviderData implements Par mRemoteEntry = remoteEntry; } + /** + * Converts the instance to a {@link GetCredentialProviderInfo}. + * + * @hide + */ + @NonNull + public GetCredentialProviderInfo toGetCredentialProviderInfo() { + return new GetCredentialProviderInfo(getProviderFlattenedComponentName(), + mCredentialEntries, mActionChips, mAuthenticationEntries, mRemoteEntry); + } + @NonNull public List<Entry> getCredentialEntries() { return mCredentialEntries; @@ -83,12 +94,12 @@ public final class GetCredentialProviderData extends ProviderData implements Par mCredentialEntries = credentialEntries; AnnotationValidations.validate(NonNull.class, null, mCredentialEntries); - List<Entry> actionChips = new ArrayList<>(); + List<Entry> actionChips = new ArrayList<>(); in.readTypedList(actionChips, Entry.CREATOR); mActionChips = actionChips; AnnotationValidations.validate(NonNull.class, null, mActionChips); - List<AuthenticationEntry> authenticationEntries = new ArrayList<>(); + List<AuthenticationEntry> authenticationEntries = new ArrayList<>(); in.readTypedList(authenticationEntries, AuthenticationEntry.CREATOR); mAuthenticationEntries = authenticationEntries; AnnotationValidations.validate(NonNull.class, null, mAuthenticationEntries); @@ -113,16 +124,16 @@ public final class GetCredentialProviderData extends ProviderData implements Par public static final @NonNull Creator<GetCredentialProviderData> CREATOR = new Creator<GetCredentialProviderData>() { - @Override - public GetCredentialProviderData createFromParcel(@NonNull Parcel in) { - return new GetCredentialProviderData(in); - } + @Override + public GetCredentialProviderData createFromParcel(@NonNull Parcel in) { + return new GetCredentialProviderData(in); + } - @Override - public GetCredentialProviderData[] newArray(int size) { - return new GetCredentialProviderData[size]; - } - }; + @Override + public GetCredentialProviderData[] newArray(int size) { + return new GetCredentialProviderData[size]; + } + }; /** * Builder for {@link GetCredentialProviderData}. @@ -131,11 +142,16 @@ public final class GetCredentialProviderData extends ProviderData implements Par */ @TestApi public static final class Builder { - @NonNull private String mProviderFlattenedComponentName; - @NonNull private List<Entry> mCredentialEntries = new ArrayList<>(); - @NonNull private List<Entry> mActionChips = new ArrayList<>(); - @NonNull private List<AuthenticationEntry> mAuthenticationEntries = new ArrayList<>(); - @Nullable private Entry mRemoteEntry = null; + @NonNull + private String mProviderFlattenedComponentName; + @NonNull + private List<Entry> mCredentialEntries = new ArrayList<>(); + @NonNull + private List<Entry> mActionChips = new ArrayList<>(); + @NonNull + private List<AuthenticationEntry> mAuthenticationEntries = new ArrayList<>(); + @Nullable + private Entry mRemoteEntry = null; /** Constructor with required properties. */ public Builder(@NonNull String providerFlattenedComponentName) { diff --git a/core/java/android/credentials/ui/GetCredentialProviderInfo.java b/core/java/android/credentials/ui/GetCredentialProviderInfo.java new file mode 100644 index 000000000000..bac71472acd1 --- /dev/null +++ b/core/java/android/credentials/ui/GetCredentialProviderInfo.java @@ -0,0 +1,168 @@ +/* + * Copyright 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 android.credentials.ui; + +import android.annotation.NonNull; +import android.annotation.Nullable; + +import com.android.internal.util.Preconditions; + +import java.util.ArrayList; +import java.util.List; + +/** + * Information pertaining to a specific provider during the given create-credential flow. + * + * This includes provider metadata and its credential creation options for display purposes. + * + * @hide + */ +public final class GetCredentialProviderInfo { + + @NonNull + private final String mProviderName; + + @NonNull + private final List<Entry> mCredentialEntries; + @NonNull + private final List<Entry> mActionChips; + @NonNull + private final List<AuthenticationEntry> mAuthenticationEntries; + @Nullable + private final Entry mRemoteEntry; + + GetCredentialProviderInfo( + @NonNull String providerName, @NonNull List<Entry> credentialEntries, + @NonNull List<Entry> actionChips, + @NonNull List<AuthenticationEntry> authenticationEntries, + @Nullable Entry remoteEntry) { + mProviderName = Preconditions.checkStringNotEmpty(providerName); + mCredentialEntries = new ArrayList<>(credentialEntries); + mActionChips = new ArrayList<>(actionChips); + mAuthenticationEntries = new ArrayList<>(authenticationEntries); + mRemoteEntry = remoteEntry; + } + + /** Returns the fully-qualified provider (component or package) name. */ + @NonNull + public String getProviderName() { + return mProviderName; + } + + /** Returns the display information for all the candidate credentials this provider has. */ + @NonNull + public List<Entry> getCredentialEntries() { + return mCredentialEntries; + } + + /** + * Returns a list of actions defined by the provider that intent into the provider's app for + * specific user actions, each of which should eventually lead to an actual credential. + */ + @NonNull + public List<Entry> getActionChips() { + return mActionChips; + } + + /** + * Returns a list of authentication actions that each intents into a provider authentication + * activity. + * + * When the authentication activity succeeds, the provider will return a list of actual + * credential candidates to render. However, the UI should not attempt to parse the result + * itself, but rather send the result back to the system service, which will then process the + * new candidates and relaunch the UI with updated display data. + */ + @NonNull + public List<AuthenticationEntry> getAuthenticationEntries() { + return mAuthenticationEntries; + } + + /** + * Returns the remote credential retrieval option, if any. + * + * Notice that only one system configured provider can set this option, and when set, it means + * that the system service has already validated the provider's eligibility. + */ + @Nullable + public Entry getRemoteEntry() { + return mRemoteEntry; + } + + /** + * Builder for {@link GetCredentialProviderInfo}. + * + * @hide + */ + public static final class Builder { + @NonNull + private String mProviderName; + @NonNull + private List<Entry> mCredentialEntries = new ArrayList<>(); + @NonNull + private List<Entry> mActionChips = new ArrayList<>(); + @NonNull + private List<AuthenticationEntry> mAuthenticationEntries = new ArrayList<>(); + @Nullable + private Entry mRemoteEntry = null; + + /** + * Constructs a {@link GetCredentialProviderInfo.Builder}. + * + * @throws IllegalArgumentException if {@code providerName} is null or empty + */ + public Builder(@NonNull String providerName) { + mProviderName = Preconditions.checkStringNotEmpty(providerName); + } + + /** Sets the list of credential candidates to be displayed to the user. */ + @NonNull + public Builder setCredentialEntries(@NonNull List<Entry> credentialEntries) { + mCredentialEntries = credentialEntries; + return this; + } + + /** Sets the list of action chips to be displayed to the user. */ + @NonNull + public Builder setActionChips(@NonNull List<Entry> actionChips) { + mActionChips = actionChips; + return this; + } + + /** Sets the authentication entry to be displayed to the user. */ + @NonNull + public Builder setAuthenticationEntries( + @NonNull List<AuthenticationEntry> authenticationEntry) { + mAuthenticationEntries = authenticationEntry; + return this; + } + + /** Sets the remote entry to be displayed to the user. */ + @NonNull + public Builder setRemoteEntry(@Nullable Entry remoteEntry) { + mRemoteEntry = remoteEntry; + return this; + } + + /** Builds a {@link GetCredentialProviderInfo}. */ + @NonNull + public GetCredentialProviderInfo build() { + return new GetCredentialProviderInfo(mProviderName, + mCredentialEntries, mActionChips, mAuthenticationEntries, mRemoteEntry); + } + } +} diff --git a/core/java/android/credentials/ui/IntentFactory.java b/core/java/android/credentials/ui/IntentFactory.java index 49321d514128..5e1e0efe39c4 100644 --- a/core/java/android/credentials/ui/IntentFactory.java +++ b/core/java/android/credentials/ui/IntentFactory.java @@ -113,25 +113,6 @@ public class IntentFactory { } /** - * Notify the UI that providers have been enabled/disabled. - * - * @hide - */ - @NonNull - public static Intent createProviderUpdateIntent() { - Intent intent = new Intent(); - ComponentName componentName = - ComponentName.unflattenFromString( - Resources.getSystem() - .getString( - com.android.internal.R.string - .config_credentialManagerReceiverComponent)); - intent.setComponent(componentName); - intent.setAction(Constants.CREDMAN_ENABLED_PROVIDERS_UPDATED); - return intent; - } - - /** * Convert an instance of a "locally-defined" ResultReceiver to an instance of {@link * android.os.ResultReceiver} itself, which the receiving process will be able to unmarshall. */ diff --git a/core/java/android/credentials/ui/IntentHelper.java b/core/java/android/credentials/ui/IntentHelper.java new file mode 100644 index 000000000000..c5f34c1440a7 --- /dev/null +++ b/core/java/android/credentials/ui/IntentHelper.java @@ -0,0 +1,97 @@ +/* + * 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.credentials.ui; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SuppressLint; +import android.content.Intent; +import android.os.ResultReceiver; + +import java.util.List; + +/** + * Utilities for parsing the intent data used to launch the UI activity. + * + * @hide + */ +public final class IntentHelper { + /** + * Attempts to extract a {@link CancelUiRequest} from the given intent; returns null + * if not found. + */ + @Nullable + public static CancelUiRequest extractCancelUiRequest(@NonNull Intent intent) { + return intent.getParcelableExtra(CancelUiRequest.EXTRA_CANCEL_UI_REQUEST, + CancelUiRequest.class); + } + + /** + * Attempts to extract a {@link RequestInfo} from the given intent; returns null + * if not found. + */ + @Nullable + public static RequestInfo extractRequestInfo(@NonNull Intent intent) { + return intent.getParcelableExtra(RequestInfo.EXTRA_REQUEST_INFO, + RequestInfo.class); + } + + /** + * Attempts to extract the list of {@link GetCredentialProviderInfo} from the given intent; + * returns null if not found. + */ + @Nullable + @SuppressLint("NullableCollection") // To be consistent with the nullable Intent extra APIs + // and the other APIs in this class. + public static List<GetCredentialProviderInfo> extractGetCredentialProviderDataList( + @NonNull Intent intent) { + List<GetCredentialProviderData> providerList = intent.getParcelableArrayListExtra( + ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST, + GetCredentialProviderData.class); + return providerList == null ? null : providerList.stream().map( + GetCredentialProviderData::toGetCredentialProviderInfo).toList(); + } + + /** + * Attempts to extract the list of {@link CreateCredentialProviderInfo} from the given intent; + * returns null if not found. + */ + @Nullable + @SuppressLint("NullableCollection") // To be consistent with the nullable Intent extra APIs + // and the other APIs in this class. + public static List<CreateCredentialProviderInfo> extractCreateCredentialProviderDataList( + @NonNull Intent intent) { + List<CreateCredentialProviderData> providerList = intent.getParcelableArrayListExtra( + ProviderData.EXTRA_ENABLED_PROVIDER_DATA_LIST, + CreateCredentialProviderData.class); + return providerList == null ? null : providerList.stream().map( + CreateCredentialProviderData::toCreateCredentialProviderInfo).toList(); + } + + /** + * Attempts to extract a {@link android.os.ResultReceiver} from the given intent, which should + * be used to send back UI results; returns null if not found. + */ + @Nullable + public static ResultReceiver extractResultReceiver(@NonNull Intent intent) { + return intent.getParcelableExtra(Constants.EXTRA_RESULT_RECEIVER, + ResultReceiver.class); + } + + private IntentHelper() { + } +} diff --git a/core/java/android/credentials/ui/ProviderDialogResult.java b/core/java/android/credentials/ui/ProviderDialogResult.java deleted file mode 100644 index 53f1864f7bc6..000000000000 --- a/core/java/android/credentials/ui/ProviderDialogResult.java +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright 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 android.credentials.ui; - -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.os.Bundle; -import android.os.IBinder; -import android.os.Parcel; -import android.os.Parcelable; - -import com.android.internal.util.AnnotationValidations; - -/** - * Result data matching {@link BaseDialogResult#RESULT_CODE_PROVIDER_ENABLED}, or {@link - * BaseDialogResult#RESULT_CODE_DEFAULT_PROVIDER_CHANGED}. - * - * @hide - */ -public final class ProviderDialogResult extends BaseDialogResult implements Parcelable { - /** Parses and returns a ProviderDialogResult from the given resultData. */ - @Nullable - public static ProviderDialogResult fromResultData(@NonNull Bundle resultData) { - return resultData.getParcelable(EXTRA_PROVIDER_RESULT, ProviderDialogResult.class); - } - - /** - * Used for the UX to construct the {@code resultData Bundle} to send via the {@code - * ResultReceiver}. - */ - public static void addToBundle( - @NonNull ProviderDialogResult result, @NonNull Bundle bundle) { - bundle.putParcelable(EXTRA_PROVIDER_RESULT, result); - } - - /** - * The intent extra key for the {@code ProviderDialogResult} object when the credential - * selector activity finishes. - */ - private static final String EXTRA_PROVIDER_RESULT = - "android.credentials.ui.extra.PROVIDER_RESULT"; - - @NonNull - private final String mProviderId; - - public ProviderDialogResult(@NonNull IBinder requestToken, @NonNull String providerId) { - super(requestToken); - mProviderId = providerId; - } - - @NonNull - public String getProviderId() { - return mProviderId; - } - - protected ProviderDialogResult(@NonNull Parcel in) { - super(in); - String providerId = in.readString8(); - mProviderId = providerId; - AnnotationValidations.validate(NonNull.class, null, mProviderId); - } - - @Override - public void writeToParcel(@NonNull Parcel dest, int flags) { - super.writeToParcel(dest, flags); - dest.writeString8(mProviderId); - } - - @Override - public int describeContents() { - return 0; - } - - public static final @NonNull Creator<ProviderDialogResult> CREATOR = - new Creator<ProviderDialogResult>() { - @Override - public ProviderDialogResult createFromParcel(@NonNull Parcel in) { - return new ProviderDialogResult(in); - } - - @Override - public ProviderDialogResult[] newArray(int size) { - return new ProviderDialogResult[size]; - } - }; -} diff --git a/core/java/android/credentials/ui/ProviderPendingIntentResponse.java b/core/java/android/credentials/ui/ProviderPendingIntentResponse.java index 47936c48f927..11cc21f9d2db 100644 --- a/core/java/android/credentials/ui/ProviderPendingIntentResponse.java +++ b/core/java/android/credentials/ui/ProviderPendingIntentResponse.java @@ -16,15 +16,20 @@ package android.credentials.ui; +import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SuppressLint; import android.content.Intent; import android.os.Parcel; import android.os.Parcelable; -import androidx.annotation.NonNull; - /** - * Response from a provider's pending intent + * Result of launching a provider's PendingIntent associated with an {@link Entry} after it is + * selected by the user. + * + * The provider sets the credential creation / retrieval result through + * {@link android.app.Activity#setResult(int, Intent)}, which is then directly propagated back + * through this data structure. * * @hide */ @@ -33,20 +38,21 @@ public final class ProviderPendingIntentResponse implements Parcelable { @Nullable private final Intent mResultData; + /** Constructs a {@link ProviderPendingIntentResponse}. */ public ProviderPendingIntentResponse(int resultCode, @Nullable Intent resultData) { mResultCode = resultCode; mResultData = resultData; } - protected ProviderPendingIntentResponse(Parcel in) { + private ProviderPendingIntentResponse(@NonNull Parcel in) { mResultCode = in.readInt(); mResultData = in.readTypedObject(Intent.CREATOR); } - public static final Creator<ProviderPendingIntentResponse> CREATOR = - new Creator<ProviderPendingIntentResponse>() { + public static final @NonNull Creator<ProviderPendingIntentResponse> CREATOR = + new Creator<>() { @Override - public ProviderPendingIntentResponse createFromParcel(Parcel in) { + public ProviderPendingIntentResponse createFromParcel(@NonNull Parcel in) { return new ProviderPendingIntentResponse(in); } @@ -67,13 +73,15 @@ public final class ProviderPendingIntentResponse implements Parcelable { dest.writeTypedObject(mResultData, flags); } - /** Returns the result code associated with this pending intent activity result. */ + /** Returns the result code associated with this provider PendingIntent activity result. */ public int getResultCode() { return mResultCode; } - /** Returns the result data associated with this pending intent activity result. */ - @NonNull public Intent getResultData() { + /** Returns the result data associated with this provider PendingIntent activity result. */ + @SuppressLint("IntentBuilderName") // Not building a new intent. + @NonNull + public Intent getResultData() { return mResultData; } } diff --git a/core/java/android/credentials/ui/RequestInfo.java b/core/java/android/credentials/ui/RequestInfo.java index 4fedc8353bf7..f65158444e48 100644 --- a/core/java/android/credentials/ui/RequestInfo.java +++ b/core/java/android/credentials/ui/RequestInfo.java @@ -48,17 +48,24 @@ public final class RequestInfo implements Parcelable { @NonNull public static final String EXTRA_REQUEST_INFO = "android.credentials.ui.extra.REQUEST_INFO"; - /** Type value for any request that does not require UI. */ + /** + * Type value for any request that does not require UI. + */ @NonNull public static final String TYPE_UNDEFINED = "android.credentials.ui.TYPE_UNDEFINED"; - /** Type value for a getCredential request. */ + /** + * Type value for a getCredential request. + */ @NonNull public static final String TYPE_GET = "android.credentials.ui.TYPE_GET"; - /** Type value for a getCredential request that utilizes the credential registry. + /** + * Type value for a getCredential request that utilizes the credential registry. * * @hide - **/ + */ @NonNull public static final String TYPE_GET_VIA_REGISTRY = "android.credentials.ui.TYPE_GET_VIA_REGISTRY"; - /** Type value for a createCredential request. */ + /** + * Type value for a createCredential request. + */ @NonNull public static final String TYPE_CREATE = "android.credentials.ui.TYPE_CREATE"; /** @hide */ diff --git a/core/java/android/credentials/ui/ResultHelper.java b/core/java/android/credentials/ui/ResultHelper.java new file mode 100644 index 000000000000..7b9d5e87d666 --- /dev/null +++ b/core/java/android/credentials/ui/ResultHelper.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.credentials.ui; + +import android.annotation.NonNull; +import android.content.Intent; +import android.os.Bundle; +import android.os.ResultReceiver; + +/** + * Utilities for sending the UI results back to the system service. + * + * @hide + */ +public final class ResultHelper { + /** + * Sends the {@code failureResult} that caused the UI to stop back to the CredentialManager + * service. + * + * The {code resultReceiver} for a UI flow can be extracted from the UI launch intent via + * {@link IntentHelper#extractResultReceiver(Intent)}. + */ + public static void sendFailureResult(@NonNull ResultReceiver resultReceiver, + @NonNull FailureResult failureResult) { + FailureDialogResult result = failureResult.toFailureDialogResult(); + Bundle resultData = new Bundle(); + FailureDialogResult.addToBundle(result, resultData); + resultReceiver.send(failureResult.errorCodeToResultCode(), + resultData); + } + + /** + * Sends the completed {@code userSelectionResult} back to the CredentialManager service. + * + * The {code resultReceiver} for a UI flow can be extracted from the UI launch intent via + * {@link IntentHelper#extractResultReceiver(Intent)}. + */ + public static void sendUserSelectionResult(@NonNull ResultReceiver resultReceiver, + @NonNull UserSelectionResult userSelectionResult) { + UserSelectionDialogResult result = userSelectionResult.toUserSelectionDialogResult(); + Bundle resultData = new Bundle(); + UserSelectionDialogResult.addToBundle(result, resultData); + resultReceiver.send(BaseDialogResult.RESULT_CODE_DIALOG_COMPLETE_WITH_SELECTION, + resultData); + } + + private ResultHelper() {} +} diff --git a/core/java/android/credentials/ui/UiResult.java b/core/java/android/credentials/ui/UiResult.java new file mode 100644 index 000000000000..692584d1a561 --- /dev/null +++ b/core/java/android/credentials/ui/UiResult.java @@ -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 android.credentials.ui; + +/** + * Base class for different types of ui results. + * + * @hide + */ +public interface UiResult {} diff --git a/core/java/android/credentials/ui/UserSelectionResult.java b/core/java/android/credentials/ui/UserSelectionResult.java new file mode 100644 index 000000000000..431dc631f3f6 --- /dev/null +++ b/core/java/android/credentials/ui/UserSelectionResult.java @@ -0,0 +1,84 @@ +/* + * Copyright 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 android.credentials.ui; + +import android.annotation.NonNull; +import android.annotation.Nullable; + +import com.android.internal.util.Preconditions; + +/** + * Result sent back from the UI after the user chose an option and completed the following + * transaction launched through the provider PendingIntent associated with that option. + * + * @hide + */ +public final class UserSelectionResult implements UiResult { + @NonNull + private final String mProviderId; + @NonNull + private final String mEntryKey; + @NonNull + private final String mEntrySubkey; + @Nullable + private ProviderPendingIntentResponse mProviderPendingIntentResponse; + + /** + * Constructs a {@link UserSelectionResult}. + * + * @throws IllegalArgumentException if {@code providerId} is empty + */ + + public UserSelectionResult(@NonNull String providerId, + @NonNull String entryKey, @NonNull String entrySubkey, + @Nullable ProviderPendingIntentResponse providerPendingIntentResponse) { + mProviderId = Preconditions.checkStringNotEmpty(providerId); + mEntryKey = Preconditions.checkNotNull(entryKey); + mEntrySubkey = Preconditions.checkNotNull(entrySubkey); + mProviderPendingIntentResponse = providerPendingIntentResponse; + } + + /** Returns provider package name whose entry was selected by the user. */ + @NonNull + public String getProviderId() { + return mProviderId; + } + + /** Returns the key of the visual entry that the user selected. */ + @NonNull + public String getEntryKey() { + return mEntryKey; + } + + /** Returns the subkey of the visual entry that the user selected. */ + @NonNull + public String getEntrySubkey() { + return mEntrySubkey; + } + + /** Returns the pending intent response from the provider. */ + @Nullable + public ProviderPendingIntentResponse getPendingIntentProviderResponse() { + return mProviderPendingIntentResponse; + } + + @NonNull + UserSelectionDialogResult toUserSelectionDialogResult() { + return new UserSelectionDialogResult(/*requestToken=*/null, mProviderId, mEntryKey, + mEntrySubkey, mProviderPendingIntentResponse); + } +} diff --git a/core/java/android/hardware/HardwareBuffer.java b/core/java/android/hardware/HardwareBuffer.java index 0047b7d69282..ce0f9f598897 100644 --- a/core/java/android/hardware/HardwareBuffer.java +++ b/core/java/android/hardware/HardwareBuffer.java @@ -115,14 +115,16 @@ public final class HardwareBuffer implements Parcelable, AutoCloseable { @FlaggedApi(com.android.graphics.hwui.flags.Flags.FLAG_REQUESTED_FORMATS_V) public static final int R_8 = 0x38; /** - * Format: 16 bits red. Bits should be represented in unsigned integer, instead of the - * implicit unsigned normalized. + * Format: 16 bits red. When sampled on the GPU this is represented as an + * unsigned integer instead of implicit unsigned normalize. + * For more information see https://www.khronos.org/opengl/wiki/Normalized_Integer */ @FlaggedApi(com.android.graphics.hwui.flags.Flags.FLAG_REQUESTED_FORMATS_V) 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. + * Format: 16 bits each red, green. When sampled on the GPU this is represented + * as an unsigned integer instead of implicit unsigned normalize. + * For more information see https://www.khronos.org/opengl/wiki/Normalized_Integer */ @FlaggedApi(com.android.graphics.hwui.flags.Flags.FLAG_REQUESTED_FORMATS_V) public static final int RG_1616 = 0x3a; 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/BiometricPrompt.java b/core/java/android/hardware/biometrics/BiometricPrompt.java index c0424dbeb813..bdaf9d789960 100644 --- a/core/java/android/hardware/biometrics/BiometricPrompt.java +++ b/core/java/android/hardware/biometrics/BiometricPrompt.java @@ -16,7 +16,7 @@ package android.hardware.biometrics; -import static android.Manifest.permission.MANAGE_BIOMETRIC_DIALOG; +import static android.Manifest.permission.SET_BIOMETRIC_DIALOG_LOGO; import static android.Manifest.permission.TEST_BIOMETRIC; import static android.Manifest.permission.USE_BIOMETRIC; import static android.Manifest.permission.USE_BIOMETRIC_INTERNAL; @@ -174,9 +174,9 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan * @return This builder. */ @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) - @RequiresPermission(MANAGE_BIOMETRIC_DIALOG) + @RequiresPermission(SET_BIOMETRIC_DIALOG_LOGO) @NonNull - public BiometricPrompt.Builder setLogo(@DrawableRes int logoRes) { + public BiometricPrompt.Builder setLogoRes(@DrawableRes int logoRes) { mPromptInfo.setLogoRes(logoRes); return this; } @@ -193,9 +193,9 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan * @return This builder. */ @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) - @RequiresPermission(MANAGE_BIOMETRIC_DIALOG) + @RequiresPermission(SET_BIOMETRIC_DIALOG_LOGO) @NonNull - public BiometricPrompt.Builder setLogo(@NonNull Bitmap logoBitmap) { + public BiometricPrompt.Builder setLogoBitmap(@NonNull Bitmap logoBitmap) { mPromptInfo.setLogoBitmap(logoBitmap); return this; } @@ -719,25 +719,25 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan /** * Gets the drawable resource of the logo for the prompt, as set by - * {@link Builder#setLogo(int)}. Currently for system applications use only. + * {@link Builder#setLogoRes(int)}. Currently for system applications use only. * * @return The drawable resource of the logo, or -1 if the prompt has no logo resource set. */ @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) - @RequiresPermission(MANAGE_BIOMETRIC_DIALOG) + @RequiresPermission(SET_BIOMETRIC_DIALOG_LOGO) @DrawableRes public int getLogoRes() { return mPromptInfo.getLogoRes(); } /** - * Gets the logo bitmap for the prompt, as set by {@link Builder#setLogo(Bitmap)}. Currently for - * system applications use only. + * Gets the logo bitmap for the prompt, as set by {@link Builder#setLogoBitmap(Bitmap)}. + * Currently for system applications use only. * * @return The logo bitmap of the prompt, or null if the prompt has no logo bitmap set. */ @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) - @RequiresPermission(MANAGE_BIOMETRIC_DIALOG) + @RequiresPermission(SET_BIOMETRIC_DIALOG_LOGO) @Nullable public Bitmap getLogoBitmap() { return mPromptInfo.getLogoBitmap(); 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/PromptInfo.java b/core/java/android/hardware/biometrics/PromptInfo.java index d788b37c781d..0f9cadc52608 100644 --- a/core/java/android/hardware/biometrics/PromptInfo.java +++ b/core/java/android/hardware/biometrics/PromptInfo.java @@ -166,9 +166,9 @@ public class PromptInfo implements Parcelable { } /** - * Returns whether MANAGE_BIOMETRIC_DIALOG is contained. + * Returns whether SET_BIOMETRIC_DIALOG_LOGO is contained. */ - public boolean containsManageBioApiConfigurations() { + public boolean containsSetLogoApiConfigurations() { if (mLogoRes != -1) { return true; } else if (mLogoBitmap != null) { 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/hardware/input/InputSettings.java b/core/java/android/hardware/input/InputSettings.java index d93953231eaf..fdbd3197fb79 100644 --- a/core/java/android/hardware/input/InputSettings.java +++ b/core/java/android/hardware/input/InputSettings.java @@ -17,6 +17,7 @@ package android.hardware.input; import static com.android.hardware.input.Flags.keyboardA11yBounceKeysFlag; +import static com.android.hardware.input.Flags.keyboardA11ySlowKeysFlag; import static com.android.hardware.input.Flags.keyboardA11yStickyKeysFlag; import static com.android.input.flags.Flags.enableInputFilterRustImpl; @@ -68,6 +69,12 @@ public class InputSettings { */ public static final int MAX_ACCESSIBILITY_BOUNCE_KEYS_THRESHOLD_MILLIS = 5000; + /** + * The maximum allowed Accessibility slow keys threshold. + * @hide + */ + public static final int MAX_ACCESSIBILITY_SLOW_KEYS_THRESHOLD_MILLIS = 5000; + private InputSettings() { } @@ -419,6 +426,86 @@ public class InputSettings { } /** + * Whether Accessibility slow keys feature flags is enabled. + * + * <p> + * 'Slow keys' is an accessibility feature to aid users who have physical disabilities, that + * allows the user to specify the duration for which one must press-and-hold a key before the + * system accepts the keypress. + * </p> + * + * @hide + */ + public static boolean isAccessibilitySlowKeysFeatureFlagEnabled() { + return keyboardA11ySlowKeysFlag() && enableInputFilterRustImpl(); + } + + /** + * Whether Accessibility slow keys is enabled. + * + * <p> + * 'Slow keys' is an accessibility feature to aid users who have physical disabilities, that + * allows the user to specify the duration for which one must press-and-hold a key before the + * system accepts the keypress. + * </p> + * + * @hide + */ + public static boolean isAccessibilitySlowKeysEnabled(@NonNull Context context) { + return getAccessibilitySlowKeysThreshold(context) != 0; + } + + /** + * Get Accessibility slow keys threshold duration in milliseconds. + * + * <p> + * 'Slow keys' is an accessibility feature to aid users who have physical disabilities, that + * allows the user to specify the duration for which one must press-and-hold a key before the + * system accepts the keypress. + * </p> + * + * @hide + */ + public static int getAccessibilitySlowKeysThreshold(@NonNull Context context) { + if (!isAccessibilitySlowKeysFeatureFlagEnabled()) { + return 0; + } + return Settings.Secure.getIntForUser(context.getContentResolver(), + Settings.Secure.ACCESSIBILITY_SLOW_KEYS, 0, UserHandle.USER_CURRENT); + } + + /** + * Set Accessibility slow keys threshold duration in milliseconds. + * @param thresholdTimeMillis time duration for which a key should be pressed to be registered + * in the system. The threshold must be between 0 and + * {@link MAX_ACCESSIBILITY_SLOW_KEYS_THRESHOLD_MILLIS} + * + * <p> + * 'Slow keys' is an accessibility feature to aid users who have physical disabilities, that + * allows the user to specify the duration for which one must press-and-hold a key before the + * system accepts the keypress. + * </p> + * + * @hide + */ + @RequiresPermission(Manifest.permission.WRITE_SETTINGS) + public static void setAccessibilitySlowKeysThreshold(@NonNull Context context, + int thresholdTimeMillis) { + if (!isAccessibilitySlowKeysFeatureFlagEnabled()) { + return; + } + if (thresholdTimeMillis < 0 + || thresholdTimeMillis > MAX_ACCESSIBILITY_SLOW_KEYS_THRESHOLD_MILLIS) { + throw new IllegalArgumentException( + "Provided Slow keys threshold should be in range [0, " + + MAX_ACCESSIBILITY_SLOW_KEYS_THRESHOLD_MILLIS + "]"); + } + Settings.Secure.putIntForUser(context.getContentResolver(), + Settings.Secure.ACCESSIBILITY_SLOW_KEYS, thresholdTimeMillis, + UserHandle.USER_CURRENT); + } + + /** * Whether Accessibility sticky keys feature is enabled. * * <p> diff --git a/core/java/android/hardware/input/input_framework.aconfig b/core/java/android/hardware/input/input_framework.aconfig index 362fe78b14b8..0ed6569afd2a 100644 --- a/core/java/android/hardware/input/input_framework.aconfig +++ b/core/java/android/hardware/input/input_framework.aconfig @@ -29,4 +29,11 @@ flag { name: "pointer_coords_is_resampled_api" description: "Makes MotionEvent.PointerCoords#isResampled() a public API" bug: "298197511" +} + +flag { + namespace: "input_native" + name: "keyboard_a11y_slow_keys_flag" + description: "Controls if the slow keys accessibility feature for physical keyboard is available to the user" + bug: "294546335" }
\ No newline at end of file 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/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/PersistableBundle.java b/core/java/android/os/PersistableBundle.java index 02704f5b346b..236194d16ad8 100644 --- a/core/java/android/os/PersistableBundle.java +++ b/core/java/android/os/PersistableBundle.java @@ -294,6 +294,43 @@ public final class PersistableBundle extends BaseBundle implements Cloneable, Pa XmlUtils.writeMapXml(mMap, out, this); } + /** + * Checks whether all keys and values are within the given character limit. + * Note: Maximum character limit of String that can be saved to XML as part of bundle is 65535. + * Otherwise IOException is thrown. + * @param limit length of String keys and values in the PersistableBundle, including nested + * PersistableBundles to check against. + * + * @hide + */ + public boolean isBundleContentsWithinLengthLimit(int limit) { + unparcel(); + if (mMap == null) { + return true; + } + for (int i = 0; i < mMap.size(); i++) { + if (mMap.keyAt(i) != null && mMap.keyAt(i).length() > limit) { + return false; + } + final Object value = mMap.valueAt(i); + if (value instanceof String && ((String) value).length() > limit) { + return false; + } else if (value instanceof String[]) { + String[] stringArray = (String[]) value; + for (int j = 0; j < stringArray.length; j++) { + if (stringArray[j] != null + && stringArray[j].length() > limit) { + return false; + } + } + } else if (value instanceof PersistableBundle + && !((PersistableBundle) value).isBundleContentsWithinLengthLimit(limit)) { + return false; + } + } + return true; + } + /** @hide */ static class MyReadMapCallback implements XmlUtils.ReadMapCallback { @Override diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java index dd0436cbb2f2..1f3a1620a9f2 100644 --- a/core/java/android/os/Process.java +++ b/core/java/android/os/Process.java @@ -1589,7 +1589,15 @@ public class Process { @UnsupportedAppUsage public static final native long getPss(int pid); - /** @hide */ + /** + * Gets the total Rss value for a given process, in bytes. + * + * @param pid the process to the Rss for + * @return an ordered array containing multiple values, they are: + * [total_rss, file, anon, swap, shmem]. + * or NULL if the value cannot be determined + * @hide + */ public static final native long[] getRss(int pid); /** 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/os/UserManager.java b/core/java/android/os/UserManager.java index c280d132ae38..533946d89706 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -88,6 +88,7 @@ import java.util.Set; * See {@link DevicePolicyManager#ACTION_PROVISION_MANAGED_PROFILE} for more on managed profiles. */ @SystemService(Context.USER_SERVICE) +@android.ravenwood.annotation.RavenwoodKeepPartialClass public class UserManager { private static final String TAG = "UserManager"; @@ -106,6 +107,21 @@ public class UserManager { /** Whether the device is in headless system user mode; null until cached. */ private static Boolean sIsHeadlessSystemUser = null; + /** Maximum length of username. + * @hide + */ + public static final int MAX_USER_NAME_LENGTH = 100; + + /** Maximum length of user property String value. + * @hide + */ + public static final int MAX_ACCOUNT_STRING_LENGTH = 500; + + /** Maximum length of account options String values. + * @hide + */ + public static final int MAX_ACCOUNT_OPTIONS_LENGTH = 1000; + /** * User type representing a {@link UserHandle#USER_SYSTEM system} user that is a human user. * This type of user cannot be created; it can only pre-exist on first boot. @@ -2906,6 +2922,7 @@ public class UserManager { * {@link UserManager#USER_TYPE_PROFILE_MANAGED managed profile}. * @hide */ + @android.ravenwood.annotation.RavenwoodKeep public static boolean isUserTypeManagedProfile(@Nullable String userType) { return USER_TYPE_PROFILE_MANAGED.equals(userType); } @@ -2914,6 +2931,7 @@ public class UserManager { * Returns whether the user type is a {@link UserManager#USER_TYPE_FULL_GUEST guest user}. * @hide */ + @android.ravenwood.annotation.RavenwoodKeep public static boolean isUserTypeGuest(@Nullable String userType) { return USER_TYPE_FULL_GUEST.equals(userType); } @@ -2923,6 +2941,7 @@ public class UserManager { * {@link UserManager#USER_TYPE_FULL_RESTRICTED restricted user}. * @hide */ + @android.ravenwood.annotation.RavenwoodKeep public static boolean isUserTypeRestricted(@Nullable String userType) { return USER_TYPE_FULL_RESTRICTED.equals(userType); } @@ -2931,6 +2950,7 @@ public class UserManager { * Returns whether the user type is a {@link UserManager#USER_TYPE_FULL_DEMO demo user}. * @hide */ + @android.ravenwood.annotation.RavenwoodKeep public static boolean isUserTypeDemo(@Nullable String userType) { return USER_TYPE_FULL_DEMO.equals(userType); } @@ -2939,6 +2959,7 @@ public class UserManager { * Returns whether the user type is a {@link UserManager#USER_TYPE_PROFILE_CLONE clone user}. * @hide */ + @android.ravenwood.annotation.RavenwoodKeep public static boolean isUserTypeCloneProfile(@Nullable String userType) { return USER_TYPE_PROFILE_CLONE.equals(userType); } @@ -2948,6 +2969,7 @@ public class UserManager { * {@link UserManager#USER_TYPE_PROFILE_COMMUNAL communal profile}. * @hide */ + @android.ravenwood.annotation.RavenwoodKeep public static boolean isUserTypeCommunalProfile(@Nullable String userType) { return USER_TYPE_PROFILE_COMMUNAL.equals(userType); } @@ -2958,6 +2980,7 @@ public class UserManager { * * @hide */ + @android.ravenwood.annotation.RavenwoodKeep public static boolean isUserTypePrivateProfile(@Nullable String userType) { return USER_TYPE_PROFILE_PRIVATE.equals(userType); } @@ -4423,15 +4446,15 @@ public class UserManager { * This API should only be called if the current user is an {@link #isAdminUser() admin} user, * as otherwise the returned intent will not be able to create a user. * - * @param userName Optional name to assign to the user. + * @param userName Optional name to assign to the user. Character limit is 100. * @param accountName Optional account name that will be used by the setup wizard to initialize - * the user. + * the user. Character limit is 500. * @param accountType Optional account type for the account to be created. This is required - * if the account name is specified. + * if the account name is specified. Character limit is 500. * @param accountOptions Optional bundle of data to be passed in during account creation in the * new user via {@link AccountManager#addAccount(String, String, String[], * Bundle, android.app.Activity, android.accounts.AccountManagerCallback, - * Handler)}. + * Handler)}. Character limit is 1000. * @return An Intent that can be launched from an Activity. * @see #USER_CREATION_FAILED_NOT_PERMITTED * @see #USER_CREATION_FAILED_NO_MORE_USERS diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index ecd6f22607b6..11edcafecdee 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -444,6 +444,18 @@ public final class Settings { "android.settings.ACCESSIBILITY_DETAILS_SETTINGS"; /** + * Activity Action: Show settings to allow configuration of an accessibility + * shortcut belonging to an accessibility feature or features. + * <p> + * Input: ":settings:show_fragment_args" must contain "targets" denoting the services to edit. + * <p> + * Output: Nothing. + * @hide + **/ + public static final String ACTION_ACCESSIBILITY_SHORTCUT_SETTINGS = + "android.settings.ACCESSIBILITY_SHORTCUT_SETTINGS"; + + /** * Activity Action: Show settings to allow configuration of accessibility color and motion. * <p> * In some cases, a matching Activity may not exist, so ensure you @@ -4513,10 +4525,11 @@ public final class Settings { /** @hide */ public static void adjustConfigurationForUser(ContentResolver cr, Configuration outConfig, int userHandle, boolean updateSettingsIfEmpty) { + final float defaultFontScale = getDefaultFontScale(cr, userHandle); outConfig.fontScale = Settings.System.getFloatForUser( - cr, FONT_SCALE, DEFAULT_FONT_SCALE, userHandle); + cr, FONT_SCALE, defaultFontScale, userHandle); if (outConfig.fontScale < 0) { - outConfig.fontScale = DEFAULT_FONT_SCALE; + outConfig.fontScale = defaultFontScale; } outConfig.fontWeightAdjustment = Settings.Secure.getIntForUser( cr, Settings.Secure.FONT_WEIGHT_ADJUSTMENT, DEFAULT_FONT_WEIGHT, userHandle); @@ -4541,6 +4554,12 @@ public final class Settings { } } + private static float getDefaultFontScale(ContentResolver cr, int userHandle) { + return com.android.window.flags.Flags.configurableFontScaleDefault() + ? Settings.System.getFloatForUser(cr, DEFAULT_DEVICE_FONT_SCALE, + DEFAULT_FONT_SCALE, userHandle) : DEFAULT_FONT_SCALE; + } + /** * @hide Erase the fields in the Configuration that should be applied * by the settings. @@ -4907,6 +4926,15 @@ public final class Settings { public static final String FONT_SCALE = "font_scale"; /** + * Default scaling factor for fonts for the specific device, float. + * The value is read from the {@link R.dimen.def_device_font_scale} + * configuration property. + * + * @hide + */ + public static final String DEFAULT_DEVICE_FONT_SCALE = "device_font_scale"; + + /** * The serialized system locale value. * * Do not use this value directory. @@ -6245,6 +6273,7 @@ public final class Settings { PRIVATE_SETTINGS.add(CAMERA_FLASH_NOTIFICATION); PRIVATE_SETTINGS.add(SCREEN_FLASH_NOTIFICATION); PRIVATE_SETTINGS.add(SCREEN_FLASH_NOTIFICATION_COLOR); + PRIVATE_SETTINGS.add(DEFAULT_DEVICE_FONT_SCALE); } /** @@ -7881,6 +7910,17 @@ public final class Settings { public static final String ACCESSIBILITY_BOUNCE_KEYS = "accessibility_bounce_keys"; /** + * Whether to enable slow keys for Physical Keyboard accessibility. + * + * If set to non-zero value, any key press on physical keyboard needs to be pressed and + * held for the provided threshold duration (in milliseconds) to be registered in the + * system. + * + * @hide + */ + public static final String ACCESSIBILITY_SLOW_KEYS = "accessibility_slow_keys"; + + /** * Whether to enable sticky keys for Physical Keyboard accessibility. * * This is a boolean value that determines if Sticky keys feature is enabled. @@ -12276,6 +12316,8 @@ public final class Settings { CLONE_TO_MANAGED_PROFILE.add(LOCATION_MODE); CLONE_TO_MANAGED_PROFILE.add(SHOW_IME_WITH_HARD_KEYBOARD); CLONE_TO_MANAGED_PROFILE.add(ACCESSIBILITY_BOUNCE_KEYS); + CLONE_TO_MANAGED_PROFILE.add(ACCESSIBILITY_SLOW_KEYS); + CLONE_TO_MANAGED_PROFILE.add(ACCESSIBILITY_STICKY_KEYS); CLONE_TO_MANAGED_PROFILE.add(NOTIFICATION_BUBBLES); CLONE_TO_MANAGED_PROFILE.add(NOTIFICATION_HISTORY_ENABLED); } diff --git a/core/java/android/service/autofill/FillResponse.java b/core/java/android/service/autofill/FillResponse.java index 7ea74d375ffb..09ec933880d4 100644 --- a/core/java/android/service/autofill/FillResponse.java +++ b/core/java/android/service/autofill/FillResponse.java @@ -28,6 +28,7 @@ import android.annotation.StringRes; import android.annotation.SuppressLint; import android.annotation.TestApi; import android.app.Activity; +import android.app.PendingIntent; import android.content.Intent; import android.content.IntentSender; import android.content.pm.ParceledListSlice; @@ -116,6 +117,7 @@ public final class FillResponse implements Parcelable { private final boolean mShowFillDialogIcon; private final boolean mShowSaveDialogIcon; private final @Nullable FieldClassification[] mDetectedFieldTypes; + private final @Nullable PendingIntent mDialogPendingIntent; /** * Creates a shollow copy of the provided FillResponse. @@ -150,7 +152,8 @@ public final class FillResponse implements Parcelable { r.mServiceDisplayNameResourceId, r.mShowFillDialogIcon, r.mShowSaveDialogIcon, - r.mDetectedFieldTypes); + r.mDetectedFieldTypes, + r.mDialogPendingIntent); } private FillResponse(ParceledListSlice<Dataset> datasets, SaveInfo saveInfo, Bundle clientState, @@ -163,7 +166,7 @@ public final class FillResponse implements Parcelable { int[] cancelIds, boolean supportsInlineSuggestions, int iconResourceId, int serviceDisplayNameResourceId, boolean showFillDialogIcon, boolean showSaveDialogIcon, - FieldClassification[] detectedFieldTypes) { + FieldClassification[] detectedFieldTypes, PendingIntent dialogPendingIntent) { mDatasets = datasets; mSaveInfo = saveInfo; mClientState = clientState; @@ -190,6 +193,7 @@ public final class FillResponse implements Parcelable { mShowFillDialogIcon = showFillDialogIcon; mShowSaveDialogIcon = showSaveDialogIcon; mDetectedFieldTypes = detectedFieldTypes; + mDialogPendingIntent = dialogPendingIntent; } private FillResponse(@NonNull Builder builder) { @@ -219,6 +223,7 @@ public final class FillResponse implements Parcelable { mShowFillDialogIcon = builder.mShowFillDialogIcon; mShowSaveDialogIcon = builder.mShowSaveDialogIcon; mDetectedFieldTypes = builder.mDetectedFieldTypes; + mDialogPendingIntent = builder.mDialogPendingIntent; } /** @hide */ @@ -399,6 +404,7 @@ public final class FillResponse implements Parcelable { private boolean mShowFillDialogIcon = true; private boolean mShowSaveDialogIcon = true; private FieldClassification[] mDetectedFieldTypes; + private PendingIntent mDialogPendingIntent; /** * Adds a new {@link FieldClassification} to this response, to @@ -1079,6 +1085,24 @@ public final class FillResponse implements Parcelable { } /** + * Sets credential dialog pending intent. Framework will use the intent to launch the + * selector UI. A replacement for previous fill bottom sheet. + * + * @throws IllegalStateException if {@link #build()} was already called. + * @throws NullPointerException if {@code pendingIntent} is {@code null}. + * + * @hide + */ + @NonNull + public Builder setDialogPendingIntent(@NonNull PendingIntent pendingIntent) { + throwIfDestroyed(); + Preconditions.checkNotNull(pendingIntent, + "can't pass a null object to setDialogPendingIntent"); + mDialogPendingIntent = pendingIntent; + return this; + } + + /** * Builds a new {@link FillResponse} instance. * * @throws IllegalStateException if any of the following conditions occur: @@ -1187,6 +1211,9 @@ public final class FillResponse implements Parcelable { if (mAuthentication != null) { builder.append(", hasAuthentication"); } + if (mDialogPendingIntent != null) { + builder.append(", hasDialogPendingIntent"); + } if (mAuthenticationIds != null) { builder.append(", authenticationIds=").append(Arrays.toString(mAuthenticationIds)); } @@ -1232,6 +1259,7 @@ public final class FillResponse implements Parcelable { parcel.writeParcelable(mInlineTooltipPresentation, flags); parcel.writeParcelable(mDialogPresentation, flags); parcel.writeParcelable(mDialogHeader, flags); + parcel.writeParcelable(mDialogPendingIntent, flags); parcel.writeParcelableArray(mFillDialogTriggerIds, flags); parcel.writeParcelable(mHeader, flags); parcel.writeParcelable(mFooter, flags); @@ -1282,6 +1310,11 @@ public final class FillResponse implements Parcelable { if (dialogHeader != null) { builder.setDialogHeader(dialogHeader); } + final PendingIntent dialogPendingIntent = parcel.readParcelable(null, + PendingIntent.class); + if (dialogPendingIntent != null) { + builder.setDialogPendingIntent(dialogPendingIntent); + } final AutofillId[] triggerIds = parcel.readParcelableArray(null, AutofillId.class); if (triggerIds != null) { builder.setFillDialogTriggerIds(triggerIds); diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java index 9895551a8672..9d19ef6bdc64 100644 --- a/core/java/android/service/notification/ZenModeConfig.java +++ b/core/java/android/service/notification/ZenModeConfig.java @@ -1053,7 +1053,7 @@ public class ZenModeConfig implements Parcelable { out); if (Flags.modesApi()) { - writeZenPolicyState(ALLOW_ATT_CHANNELS, policy.getPriorityChannels(), out); + writeZenPolicyState(ALLOW_ATT_CHANNELS, policy.getPriorityChannelsAllowed(), out); } } @@ -1381,7 +1381,7 @@ public class ZenModeConfig implements Parcelable { int state = defaultPolicy.state; if (Flags.modesApi()) { state = Policy.policyState(defaultPolicy.hasPriorityChannels(), - ZenPolicy.stateToBoolean(zenPolicy.getPriorityChannels(), + ZenPolicy.stateToBoolean(zenPolicy.getPriorityChannelsAllowed(), DEFAULT_ALLOW_PRIORITY_CHANNELS)); } diff --git a/core/java/android/service/notification/ZenPolicy.java b/core/java/android/service/notification/ZenPolicy.java index d8318a6bee7c..786d768bc55b 100644 --- a/core/java/android/service/notification/ZenPolicy.java +++ b/core/java/android/service/notification/ZenPolicy.java @@ -570,7 +570,7 @@ public final class ZenPolicy implements Parcelable { * with {@link NotificationChannel#canBypassDnd()} will be intercepted. */ @FlaggedApi(Flags.FLAG_MODES_API) - public @State int getPriorityChannels() { + public @State int getPriorityChannelsAllowed() { switch (mAllowChannels) { case CHANNEL_POLICY_PRIORITY: return STATE_ALLOW; @@ -1529,7 +1529,7 @@ public final class ZenPolicy implements Parcelable { proto.write(DNDPolicyProto.ALLOW_CONVERSATIONS_FROM, getPriorityConversationSenders()); if (Flags.modesApi()) { - proto.write(DNDPolicyProto.ALLOW_CHANNELS, getPriorityChannels()); + proto.write(DNDPolicyProto.ALLOW_CHANNELS, getPriorityChannelsAllowed()); } proto.flush(); diff --git a/core/java/android/util/Singleton.java b/core/java/android/util/Singleton.java index 92646b47cef2..d27bef9e9adc 100644 --- a/core/java/android/util/Singleton.java +++ b/core/java/android/util/Singleton.java @@ -25,6 +25,7 @@ import android.compat.annotation.UnsupportedAppUsage; * * @hide */ +@android.ravenwood.annotation.RavenwoodKeepWholeClass public abstract class Singleton<T> { @UnsupportedAppUsage diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java index 1908c64ce42d..fbadef3d19ef 100644 --- a/core/java/android/view/Display.java +++ b/core/java/android/view/Display.java @@ -2079,6 +2079,7 @@ public final class Display { * * @see Display#getSupportedModes() */ + @android.ravenwood.annotation.RavenwoodKeepWholeClass public static final class Mode implements Parcelable { /** * @hide @@ -2467,6 +2468,7 @@ public final class Display { * <p>You can get an instance for a given {@link Display} object with * {@link Display#getHdrCapabilities getHdrCapabilities()}. */ + @android.ravenwood.annotation.RavenwoodKeepWholeClass public static final class HdrCapabilities implements Parcelable { /** * Invalid luminance value. diff --git a/core/java/android/view/DisplayInfo.java b/core/java/android/view/DisplayInfo.java index 981911ec8880..5654bc159568 100644 --- a/core/java/android/view/DisplayInfo.java +++ b/core/java/android/view/DisplayInfo.java @@ -51,6 +51,7 @@ import java.util.Objects; * Describes the characteristics of a particular logical display. * @hide */ +@android.ravenwood.annotation.RavenwoodKeepWholeClass public final class DisplayInfo implements Parcelable { /** * The surface flinger layer stack associated with this logical display. diff --git a/core/java/android/view/IWindowSession.aidl b/core/java/android/view/IWindowSession.aidl index 1d81be17f580..d2c25cde35d6 100644 --- a/core/java/android/view/IWindowSession.aidl +++ b/core/java/android/view/IWindowSession.aidl @@ -235,7 +235,7 @@ interface IWindowSession { */ oneway void setWallpaperDisplayOffset(IBinder windowToken, int x, int y); - Bundle sendWallpaperCommand(IBinder window, String action, int x, int y, + oneway void sendWallpaperCommand(IBinder window, String action, int x, int y, int z, in Bundle extras, boolean sync); @UnsupportedAppUsage diff --git a/core/java/android/view/InsetsSource.java b/core/java/android/view/InsetsSource.java index 86ab21348e4f..bc33d5e2f6b1 100644 --- a/core/java/android/view/InsetsSource.java +++ b/core/java/android/view/InsetsSource.java @@ -17,6 +17,7 @@ package android.view; import static android.view.InsetsSourceProto.FRAME; +import static android.view.InsetsSourceProto.TYPE; import static android.view.InsetsSourceProto.TYPE_NUMBER; import static android.view.InsetsSourceProto.VISIBLE; import static android.view.InsetsSourceProto.VISIBLE_FRAME; @@ -442,6 +443,10 @@ public class InsetsSource implements Parcelable { */ public void dumpDebug(ProtoOutputStream proto, long fieldId) { final long token = proto.start(fieldId); + if (!android.os.Flags.androidOsBuildVanillaIceCream()) { + // Deprecated since V. + proto.write(TYPE, WindowInsets.Type.toString(mType)); + } mFrame.dumpDebug(proto, FRAME); if (mVisibleFrame != null) { mVisibleFrame.dumpDebug(proto, VISIBLE_FRAME); 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/WindowlessWindowManager.java b/core/java/android/view/WindowlessWindowManager.java index b95e4595d6b9..c4d18c6a8da5 100644 --- a/core/java/android/view/WindowlessWindowManager.java +++ b/core/java/android/view/WindowlessWindowManager.java @@ -534,9 +534,8 @@ public class WindowlessWindowManager implements IWindowSession { } @Override - public android.os.Bundle sendWallpaperCommand(android.os.IBinder window, + public void sendWallpaperCommand(android.os.IBinder window, java.lang.String action, int x, int y, int z, android.os.Bundle extras, boolean sync) { - return null; } @Override diff --git a/core/java/android/view/autofill/OWNERS b/core/java/android/view/autofill/OWNERS index 37c6f5bf3425..898947adcd1b 100644 --- a/core/java/android/view/autofill/OWNERS +++ b/core/java/android/view/autofill/OWNERS @@ -4,6 +4,7 @@ simranjit@google.com haoranzhang@google.com skxu@google.com yunicorn@google.com +reemabajwa@google.com # Bug component: 543785 = per-file *Augmented* per-file *Augmented* = wangqi@google.com 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 edfbea4f51a4..1de77f6d29e7 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 @@ -58,3 +58,11 @@ flag { bug: "319808237" is_fixed_read_only: true } + +flag { + name: "camera_compat_for_freeform" + namespace: "large_screen_experiences_app_compat" + description: "Whether to apply Camera Compat treatment to fixed-orientation apps in freeform windowing mode" + bug: "314952133" + is_fixed_read_only: true +} diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig index 2c5fbd7f3725..f234637a7d82 100644 --- a/core/java/android/window/flags/windowing_frontend.aconfig +++ b/core/java/android/window/flags/windowing_frontend.aconfig @@ -38,14 +38,6 @@ flag { } flag { - name: "draw_magnifier_border_outside_wmlock" - namespace: "windowing_frontend" - description: "Avoid holding WM locks for a long time when executing lockCanvas" - bug: "316075123" - is_fixed_read_only: true -} - -flag { name: "introduce_smoother_dimmer" namespace: "windowing_frontend" description: "Refactor dim to fix flickers" diff --git a/core/java/com/android/internal/app/ConfirmUserCreationActivity.java b/core/java/com/android/internal/app/ConfirmUserCreationActivity.java index 0a28997885ff..b4e87498f09b 100644 --- a/core/java/com/android/internal/app/ConfirmUserCreationActivity.java +++ b/core/java/com/android/internal/app/ConfirmUserCreationActivity.java @@ -116,6 +116,14 @@ public class ConfirmUserCreationActivity extends AlertActivity if (cantCreateUser) { setResult(UserManager.USER_CREATION_FAILED_NOT_PERMITTED); return null; + } else if (!(isUserPropertyWithinLimit(mUserName, UserManager.MAX_USER_NAME_LENGTH) + && isUserPropertyWithinLimit(mAccountName, UserManager.MAX_ACCOUNT_STRING_LENGTH) + && isUserPropertyWithinLimit(mAccountType, UserManager.MAX_ACCOUNT_STRING_LENGTH)) + || (mAccountOptions != null && !mAccountOptions.isBundleContentsWithinLengthLimit( + UserManager.MAX_ACCOUNT_OPTIONS_LENGTH))) { + setResult(UserManager.USER_CREATION_FAILED_NOT_PERMITTED); + Log.i(TAG, "User properties must not exceed their character limits"); + return null; } else if (cantCreateAnyMoreUsers) { setResult(UserManager.USER_CREATION_FAILED_NO_MORE_USERS); return null; @@ -144,4 +152,8 @@ public class ConfirmUserCreationActivity extends AlertActivity } finish(); } + + private boolean isUserPropertyWithinLimit(String property, int limit) { + return property == null || property.length() <= limit; + } } diff --git a/core/java/com/android/internal/display/BrightnessSynchronizer.java b/core/java/com/android/internal/display/BrightnessSynchronizer.java index 37aaa72cb7a0..006849034fbd 100644 --- a/core/java/com/android/internal/display/BrightnessSynchronizer.java +++ b/core/java/com/android/internal/display/BrightnessSynchronizer.java @@ -47,6 +47,7 @@ import java.io.PrintWriter; * (new) system for storing the brightness. It has methods to convert between the two and also * observes for when one of the settings is changed and syncs this with the other. */ +@android.ravenwood.annotation.RavenwoodKeepPartialClass public class BrightnessSynchronizer { private static final String TAG = "BrightnessSynchronizer"; @@ -282,6 +283,7 @@ public class BrightnessSynchronizer { * @param b second float to compare * @return whether the two values are within a small enough tolerance value */ + @android.ravenwood.annotation.RavenwoodKeep public static boolean floatEquals(float a, float b) { if (a == b) { return true; diff --git a/core/java/com/android/internal/jank/Cuj.java b/core/java/com/android/internal/jank/Cuj.java index 96740c59ec06..7b3565bff533 100644 --- a/core/java/com/android/internal/jank/Cuj.java +++ b/core/java/com/android/internal/jank/Cuj.java @@ -121,10 +121,11 @@ public class Cuj { public static final int CUJ_PREDICTIVE_BACK_CROSS_TASK = 85; public static final int CUJ_PREDICTIVE_BACK_HOME = 86; public static final int CUJ_LAUNCHER_SEARCH_QSB_OPEN = 87; + public static final int CUJ_BACK_PANEL_ARROW = 88; // When adding a CUJ, update this and make sure to also update CUJ_TO_STATSD_INTERACTION_TYPE. @VisibleForTesting - static final int LAST_CUJ = CUJ_LAUNCHER_SEARCH_QSB_OPEN; + static final int LAST_CUJ = CUJ_BACK_PANEL_ARROW; /** @hide */ @IntDef({ @@ -207,6 +208,7 @@ public class Cuj { CUJ_PREDICTIVE_BACK_CROSS_TASK, CUJ_PREDICTIVE_BACK_HOME, CUJ_LAUNCHER_SEARCH_QSB_OPEN, + CUJ_BACK_PANEL_ARROW, }) @Retention(RetentionPolicy.SOURCE) public @interface CujType { @@ -298,8 +300,8 @@ public class Cuj { CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__PREDICTIVE_BACK_CROSS_ACTIVITY; CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_PREDICTIVE_BACK_CROSS_TASK] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__PREDICTIVE_BACK_CROSS_TASK; CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_PREDICTIVE_BACK_HOME] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__PREDICTIVE_BACK_HOME; - CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_SEARCH_QSB_OPEN] = - FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_SEARCH_QSB_OPEN; + CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_LAUNCHER_SEARCH_QSB_OPEN] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__LAUNCHER_SEARCH_QSB_OPEN; + CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_BACK_PANEL_ARROW] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__BACK_PANEL_ARROW; } private Cuj() { @@ -474,6 +476,8 @@ public class Cuj { return "PREDICTIVE_BACK_HOME"; case CUJ_LAUNCHER_SEARCH_QSB_OPEN: return "LAUNCHER_SEARCH_QSB_OPEN"; + case CUJ_BACK_PANEL_ARROW: + return "BACK_PANEL_ARROW"; } return "UNKNOWN"; } diff --git a/core/java/com/android/internal/jank/InteractionMonitorDebugOverlay.java b/core/java/com/android/internal/jank/InteractionMonitorDebugOverlay.java index f3f16a0c662d..d9cac12c3372 100644 --- a/core/java/com/android/internal/jank/InteractionMonitorDebugOverlay.java +++ b/core/java/com/android/internal/jank/InteractionMonitorDebugOverlay.java @@ -28,6 +28,7 @@ import android.graphics.RecordingCanvas; import android.graphics.Rect; import android.os.Handler; import android.os.Trace; +import android.util.Log; import android.util.SparseArray; import android.util.SparseIntArray; import android.view.WindowCallbacks; @@ -52,6 +53,7 @@ import com.android.internal.jank.FrameTracker.Reasons; * @hide */ class InteractionMonitorDebugOverlay implements WindowCallbacks { + private static final String TAG = "InteractionMonitorDebug"; private static final int REASON_STILL_RUNNING = -1000; private final Object mLock; // Sparse array where the key in the CUJ and the value is the session status, or null if @@ -77,7 +79,7 @@ class InteractionMonitorDebugOverlay implements WindowCallbacks { mDebugPaint.setAntiAlias(false); mDebugFontMetrics = new Paint.FontMetrics(); final Context context = ActivityThread.currentApplication(); - mPackageName = context.getPackageName(); + mPackageName = context == null ? "null" : context.getPackageName(); } @UiThread @@ -153,8 +155,14 @@ class InteractionMonitorDebugOverlay implements WindowCallbacks { SparseArray<InteractionJankMonitor.RunningTracker> runningTrackers) { synchronized (mLock) { mRunningCujs.put(removedCuj, reason); + boolean isLoggable = Log.isLoggable(TAG, Log.DEBUG); + if (isLoggable) { + String cujName = Cuj.getNameOfCuj(removedCuj); + Log.d(TAG, cujName + (reason == REASON_END_NORMAL ? " ended" : " cancelled")); + } // If REASON_STILL_RUNNING is not in mRunningCujs, then all CUJs have ended if (mRunningCujs.indexOfValue(REASON_STILL_RUNNING) < 0) { + if (isLoggable) Log.d(TAG, "All CUJs ended"); mRunningCujs.clear(); dispose(); } else { @@ -186,6 +194,10 @@ class InteractionMonitorDebugOverlay implements WindowCallbacks { @UiThread void onTrackerAdded(@Cuj.CujType int addedCuj, InteractionJankMonitor.RunningTracker tracker) { + if (Log.isLoggable(TAG, Log.DEBUG)) { + String cujName = Cuj.getNameOfCuj(addedCuj); + Log.d(TAG, cujName + " started"); + } synchronized (mLock) { // Use REASON_STILL_RUNNING (not technically one of the '@Reasons') to indicate the CUJ // is still running 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/policy/SystemBarUtils.java b/core/java/com/android/internal/policy/SystemBarUtils.java index 7a1ac071a625..efa369715373 100644 --- a/core/java/com/android/internal/policy/SystemBarUtils.java +++ b/core/java/com/android/internal/policy/SystemBarUtils.java @@ -19,8 +19,9 @@ package com.android.internal.policy; import android.content.Context; import android.content.res.Resources; import android.graphics.Insets; -import android.util.RotationUtils; +import android.view.Display; import android.view.DisplayCutout; +import android.view.DisplayInfo; import android.view.Surface; import com.android.internal.R; @@ -56,21 +57,21 @@ public final class SystemBarUtils { */ public static int getStatusBarHeightForRotation( Context context, @Surface.Rotation int targetRot) { - final int rotation = context.getDisplay().getRotation(); - final DisplayCutout cutout = context.getDisplay().getCutout(); - - Insets insets = cutout == null ? Insets.NONE : Insets.of(cutout.getSafeInsets()); - Insets waterfallInsets = cutout == null ? Insets.NONE : cutout.getWaterfallInsets(); - // rotate insets to target rotation if needed. - if (rotation != targetRot) { - if (!insets.equals(Insets.NONE)) { - insets = RotationUtils.rotateInsets( - insets, RotationUtils.deltaRotation(rotation, targetRot)); - } - if (!waterfallInsets.equals(Insets.NONE)) { - waterfallInsets = RotationUtils.rotateInsets( - waterfallInsets, RotationUtils.deltaRotation(rotation, targetRot)); - } + final Display display = context.getDisplay(); + final int rotation = display.getRotation(); + final DisplayCutout cutout = display.getCutout(); + DisplayInfo info = new DisplayInfo(); + display.getDisplayInfo(info); + Insets insets; + Insets waterfallInsets; + if (cutout == null) { + insets = Insets.NONE; + waterfallInsets = Insets.NONE; + } else { + DisplayCutout rotated = + cutout.getRotated(info.logicalWidth, info.logicalHeight, rotation, targetRot); + insets = Insets.of(rotated.getSafeInsets()); + waterfallInsets = rotated.getWaterfallInsets(); } final int defaultSize = context.getResources().getDimensionPixelSize(R.dimen.status_bar_height_default); diff --git a/core/java/com/android/internal/widget/ConversationLayout.java b/core/java/com/android/internal/widget/ConversationLayout.java index 42be784d8baa..a8d0d37f78bd 100644 --- a/core/java/com/android/internal/widget/ConversationLayout.java +++ b/core/java/com/android/internal/widget/ConversationLayout.java @@ -105,6 +105,9 @@ public class ConversationLayout extends FrameLayout private int mConversationIconTopPaddingExpandedGroup; private int mConversationIconTopPadding; private int mExpandedGroupMessagePadding; + // TODO (b/217799515) Currently, mConversationText shows the conversation title, the actual + // conversation text is inside of mMessagingLinearLayout, which is misleading, we should rename + // this to mConversationTitleView private TextView mConversationText; private View mConversationIconBadge; private CachingIconView mConversationIconBadgeBg; @@ -125,6 +128,11 @@ public class ConversationLayout extends FrameLayout private int mNotificationBackgroundColor; private CharSequence mFallbackChatName; private CharSequence mFallbackGroupChatName; + //TODO (b/217799515) Currently, Notification.MessagingStyle, ConversationLayout, and + // HybridConversationNotificationView, each has their own definition of "ConversationTitle". + // What make things worse is that the term of "ConversationTitle" often confuses with + // "ConversationText". + // We need to unify them or differentiate the namings. private CharSequence mConversationTitle; private int mMessageSpacingStandard; private int mMessageSpacingGroup; @@ -160,12 +168,12 @@ public class ConversationLayout extends FrameLayout } public ConversationLayout(@NonNull Context context, @Nullable AttributeSet attrs, - @AttrRes int defStyleAttr) { + @AttrRes int defStyleAttr) { super(context, attrs, defStyleAttr); } public ConversationLayout(@NonNull Context context, @Nullable AttributeSet attrs, - @AttrRes int defStyleAttr, @StyleRes int defStyleRes) { + @AttrRes int defStyleAttr, @StyleRes int defStyleRes) { super(context, attrs, defStyleAttr, defStyleRes); } @@ -297,13 +305,17 @@ public class ConversationLayout extends FrameLayout mNameReplacement = nameReplacement; } - /** Sets this conversation as "important", adding some additional UI treatment. */ + /** + * Sets this conversation as "important", adding some additional UI treatment. + */ @RemotableViewMethod public void setIsImportantConversation(boolean isImportantConversation) { setIsImportantConversation(isImportantConversation, false); } - /** @hide **/ + /** + * @hide + **/ public void setIsImportantConversation(boolean isImportantConversation, boolean animate) { mImportantConversation = isImportantConversation; mImportanceRingView.setVisibility(isImportantConversation && mIcon.getVisibility() != GONE @@ -386,6 +398,7 @@ public class ConversationLayout extends FrameLayout /** * Set conversation data + * * @param extras Bundle contains conversation data */ @RemotableViewMethod(asyncImpl = "setDataAsync") @@ -427,6 +440,7 @@ public class ConversationLayout extends FrameLayout * RemotableViewMethod's asyncImpl of {@link #setData(Bundle)}. * This should be called on a background thread, and returns a Runnable which is then must be * called on the main thread to complete the operation and set text. + * * @param extras Bundle contains conversation data * @hide */ @@ -449,6 +463,7 @@ public class ConversationLayout extends FrameLayout /** * enable/disable precomputed text usage + * * @hide */ public void setPrecomputedTextEnabled(boolean precomputedTextEnabled) { @@ -466,7 +481,9 @@ public class ConversationLayout extends FrameLayout mImageResolver = resolver; } - /** @hide */ + /** + * @hide + */ public void setUnreadCount(int unreadCount) { mExpandButton.setNumber(unreadCount); } @@ -795,6 +812,10 @@ public class ConversationLayout extends FrameLayout mConversationTitle = conversationTitle != null ? conversationTitle.toString() : null; } + // TODO (b/217799515) getConversationTitle is not consistent with setConversationTitle + // if you call getConversationTitle() immediately after setConversationTitle(), the result + // will not correctly reflect the new change without calling updateConversationLayout, for + // example. public CharSequence getConversationTitle() { return mConversationText.getText(); } @@ -914,7 +935,7 @@ public class ConversationLayout extends FrameLayout } private void createGroupViews(List<List<MessagingMessage>> groups, - List<Person> senders, boolean showSpinner) { + List<Person> senders, boolean showSpinner) { mGroups.clear(); for (int groupIndex = 0; groupIndex < groups.size(); groupIndex++) { List<MessagingMessage> group = groups.get(groupIndex); @@ -963,8 +984,8 @@ public class ConversationLayout extends FrameLayout } private void findGroups(List<MessagingMessage> historicMessages, - List<MessagingMessage> messages, List<List<MessagingMessage>> groups, - List<Person> senders) { + List<MessagingMessage> messages, List<List<MessagingMessage>> groups, + List<Person> senders) { CharSequence currentSenderKey = null; List<MessagingMessage> currentGroup = null; int histSize = historicMessages.size(); diff --git a/core/java/com/android/internal/widget/ImageFloatingTextView.java b/core/java/com/android/internal/widget/ImageFloatingTextView.java index 0704cb8094d7..5da64350619c 100644 --- a/core/java/com/android/internal/widget/ImageFloatingTextView.java +++ b/core/java/com/android/internal/widget/ImageFloatingTextView.java @@ -18,9 +18,11 @@ package com.android.internal.widget; import android.annotation.Nullable; import android.content.Context; +import android.os.Build; import android.os.Trace; import android.text.BoringLayout; import android.text.Layout; +import android.text.PrecomputedText; import android.text.StaticLayout; import android.text.TextUtils; import android.text.method.TransformationMethod; @@ -48,6 +50,10 @@ public class ImageFloatingTextView extends TextView { private int mLayoutMaxLines = -1; private int mImageEndMargin; + private int mStaticLayoutCreationCountInOnMeasure = 0; + + private static final boolean TRACE_ONMEASURE = Build.isDebuggable(); + public ImageFloatingTextView(Context context) { this(context, null); } @@ -71,7 +77,10 @@ public class ImageFloatingTextView extends TextView { protected Layout makeSingleLayout(int wantWidth, BoringLayout.Metrics boring, int ellipsisWidth, Layout.Alignment alignment, boolean shouldEllipsize, TextUtils.TruncateAt effectiveEllipsize, boolean useSaved) { - Trace.beginSection("ImageFloatingTextView#makeSingleLayout"); + if (TRACE_ONMEASURE) { + Trace.beginSection("ImageFloatingTextView#makeSingleLayout"); + mStaticLayoutCreationCountInOnMeasure++; + } TransformationMethod transformationMethod = getTransformationMethod(); CharSequence text = getText(); if (transformationMethod != null) { @@ -79,7 +88,7 @@ public class ImageFloatingTextView extends TextView { } text = text == null ? "" : text; StaticLayout.Builder builder = StaticLayout.Builder.obtain(text, 0, text.length(), - getPaint(), wantWidth) + getPaint(), wantWidth) .setAlignment(alignment) .setTextDirection(getTextDirectionHeuristic()) .setLineSpacing(getLineSpacingExtra(), getLineSpacingMultiplier()) @@ -115,7 +124,10 @@ public class ImageFloatingTextView extends TextView { } final StaticLayout result = builder.build(); - Trace.endSection(); + if (TRACE_ONMEASURE) { + trackMaxLines(); + Trace.endSection(); + } return result; } @@ -141,7 +153,10 @@ public class ImageFloatingTextView extends TextView { @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - Trace.beginSection("ImageFloatingTextView#onMeasure"); + if (TRACE_ONMEASURE) { + Trace.beginSection("ImageFloatingTextView#onMeasure"); + } + mStaticLayoutCreationCountInOnMeasure = 0; int availableHeight = MeasureSpec.getSize(heightMeasureSpec) - mPaddingTop - mPaddingBottom; if (getLayout() != null && getLayout().getHeight() != availableHeight) { // We've been measured before and the new size is different than before, lets make sure @@ -168,7 +183,12 @@ public class ImageFloatingTextView extends TextView { super.onMeasure(widthMeasureSpec, heightMeasureSpec); } } - Trace.endSection(); + + + if (TRACE_ONMEASURE) { + trackParameters(); + Trace.endSection(); + } } @Override @@ -216,4 +236,37 @@ public class ImageFloatingTextView extends TextView { requestLayout(); } } + + private void trackParameters() { + if (!TRACE_ONMEASURE) { + return; + } + Trace.setCounter("ImageFloatingView#staticLayoutCreationCount", + mStaticLayoutCreationCountInOnMeasure); + Trace.setCounter("ImageFloatingView#isPrecomputedText", + isTextAPrecomputedText()); + } + /** + * @return 1 if {@link TextView#getText()} is PrecomputedText, else 0 + */ + private int isTextAPrecomputedText() { + final CharSequence text = getText(); + if (text == null) { + return 0; + } + + if (text instanceof PrecomputedText) { + return 1; + } + + return 0; + } + + private void trackMaxLines() { + if (!TRACE_ONMEASURE) { + return; + } + + Trace.setCounter("ImageFloatingView#layoutMaxLines", mLayoutMaxLines); + } } 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/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/internal/widget/PeopleHelper.java b/core/java/com/android/internal/widget/PeopleHelper.java index 85cedc362b99..3f5b4a0d61fe 100644 --- a/core/java/com/android/internal/widget/PeopleHelper.java +++ b/core/java/com/android/internal/widget/PeopleHelper.java @@ -22,6 +22,8 @@ import static com.android.internal.widget.MessagingPropertyAnimator.ALPHA_OUT; import android.annotation.ColorInt; import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.Notification; +import android.app.Person; import android.content.Context; import android.graphics.Bitmap; import android.graphics.Canvas; @@ -222,6 +224,72 @@ public class PeopleHelper { } /** + * A class that represents a map from unique sender names in the groups to the string 1- or + * 2-character prefix strings for the names. This class uses the String value of the + * CharSequence Names as the key. + */ + public class NameToPrefixMap { + Map<String, String> mMap; + NameToPrefixMap(Map<String, String> map) { + this.mMap = map; + } + + /** + * @param name the name + * @return the prefix of the given name + */ + public String getPrefix(CharSequence name) { + return mMap.get(name.toString()); + } + } + + /** + * Same functionality as mapUniqueNamesToPrefix, but takes list-represented message groups as + * the input. This method is better when inflating MessagingGroup from the UI thread is not + * an option. + * @param groups message groups represented by lists. A message group is some consecutive + * messages (>=3) from the same sender in a conversation. + */ + public NameToPrefixMap mapUniqueNamesToPrefixWithGroupList( + List<List<Notification.MessagingStyle.Message>> groups) { + // Map of unique names to their prefix + ArrayMap<String, String> uniqueNames = new ArrayMap<>(); + // Map of single-character string prefix to the only name which uses it, or null if multiple + ArrayMap<String, CharSequence> uniqueCharacters = new ArrayMap<>(); + for (int i = 0; i < groups.size(); i++) { + List<Notification.MessagingStyle.Message> group = groups.get(i); + if (group.isEmpty()) continue; + Person sender = group.get(0).getSenderPerson(); + if (sender == null) continue; + CharSequence senderName = sender.getName(); + if (sender.getIcon() != null || TextUtils.isEmpty(senderName)) { + continue; + } + String senderNameString = senderName.toString(); + if (!uniqueNames.containsKey(senderNameString)) { + String charPrefix = findNamePrefix(senderName, null); + if (charPrefix == null) { + continue; + } + if (uniqueCharacters.containsKey(charPrefix)) { + // this character was already used, lets make it more unique. We first need to + // resolve the existing character if it exists + CharSequence existingName = uniqueCharacters.get(charPrefix); + if (existingName != null) { + uniqueNames.put(existingName.toString(), findNameSplit(existingName)); + uniqueCharacters.put(charPrefix, null); + } + uniqueNames.put(senderNameString, findNameSplit(senderName)); + } else { + uniqueNames.put(senderNameString, charPrefix); + uniqueCharacters.put(charPrefix, senderName); + } + } + } + return new NameToPrefixMap(uniqueNames); + } + + /** * Update whether the groups can hide the sender if they are first * (happens only for 1:1 conversations where the given title matches the sender's name) */ diff --git a/core/jni/android_util_Process.cpp b/core/jni/android_util_Process.cpp index 6a640a5ab23b..d2e58bb62c46 100644 --- a/core/jni/android_util_Process.cpp +++ b/core/jni/android_util_Process.cpp @@ -1165,12 +1165,11 @@ static jlong android_os_Process_getPss(JNIEnv* env, jobject clazz, jint pid) static jlongArray android_os_Process_getRss(JNIEnv* env, jobject clazz, jint pid) { - // total, file, anon, swap - jlong rss[4] = {0, 0, 0, 0}; + // total, file, anon, swap, shmem + jlong rss[5] = {0, 0, 0, 0, 0}; std::string status_path = android::base::StringPrintf("/proc/%d/status", pid); UniqueFile file = MakeUniqueFile(status_path.c_str(), "re"); - char line[256]; while (file != nullptr && fgets(line, sizeof(line), file.get())) { jlong v; @@ -1182,17 +1181,18 @@ static jlongArray android_os_Process_getRss(JNIEnv* env, jobject clazz, jint pid rss[2] = v; } else if ( sscanf(line, "VmSwap: %" SCNd64 " kB", &v) == 1) { rss[3] = v; + } else if ( sscanf(line, "RssShmem: %" SCNd64 " kB", &v) == 1) { + rss[4] = v; } } - jlongArray rssArray = env->NewLongArray(4); + jlongArray rssArray = env->NewLongArray(5); if (rssArray == NULL) { jniThrowException(env, "java/lang/OutOfMemoryError", NULL); return NULL; } - env->SetLongArrayRegion(rssArray, 0, 4, rss); - + env->SetLongArrayRegion(rssArray, 0, 5, rss); return rssArray; } 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/proto/android/server/windowmanagerservice.proto b/core/proto/android/server/windowmanagerservice.proto index b63021d52f1c..c92435f61ab1 100644 --- a/core/proto/android/server/windowmanagerservice.proto +++ b/core/proto/android/server/windowmanagerservice.proto @@ -464,6 +464,7 @@ message WindowStateProto { repeated .android.graphics.RectProto keep_clear_areas = 45; repeated .android.graphics.RectProto unrestricted_keep_clear_areas = 46; repeated .android.view.InsetsSourceProto mergedLocalInsetsSources = 47; + optional int32 requested_visible_types = 48; } message IdentifierProto { diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 100259ef72b9..0e0af4db17a0 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -6654,7 +6654,14 @@ <!-- Allows the system to control the BiometricDialog (SystemUI). Reserved for the system. @hide --> <permission android:name="android.permission.MANAGE_BIOMETRIC_DIALOG" - android:protectionLevel="signature" /> + android:protectionLevel="signature" /> + + <!-- Allows an application to set the BiometricDialog (SystemUI) logo . + <p>Not for use by third-party applications. + @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") + --> + <permission android:name="android.permission.SET_BIOMETRIC_DIALOG_LOGO" + android:protectionLevel="signature" /> <!-- Allows an application to control keyguard. Only allowed for system processes. @hide --> @@ -7065,6 +7072,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" /> @@ -7800,6 +7808,16 @@ <permission android:name="android.permission.RUN_USER_INITIATED_JOBS" android:protectionLevel="normal"/> + <!-- @FlaggedApi("android.app.job.backup_jobs_exemption") + Gives applications whose <b>primary use case</b> is to backup or sync content increased + job execution allowance in order to complete the related work. The jobs must have a valid + content URI trigger and network constraint set. + <p>This is a special access permission that can be revoked by the system or the user. + <p>Protection level: signature|privileged|appop + --> + <permission android:name="android.permission.RUN_BACKUP_JOBS" + android:protectionLevel="signature|privileged|appop"/> + <!-- Allows an app access to the installer provided app metadata. @SystemApi @hide @@ -8373,6 +8391,16 @@ </intent-filter> </receiver> + <!-- Broadcast Receiver listens to sufficient verifier broadcast from Package Manager + when installing new SDK. Verification of SDK code during installation time is run + to determine compatibility with privacy sandbox restrictions. --> + <receiver android:name="com.android.server.sdksandbox.SdkSandboxVerifierReceiver" + android:exported="false"> + <intent-filter> + <action android:name="android.intent.action.PACKAGE_NEEDS_VERIFICATION"/> + </intent-filter> + </receiver> + <service android:name="android.hardware.location.GeofenceHardwareService" android:permission="android.permission.LOCATION_HARDWARE" android:exported="false" /> @@ -8414,6 +8442,10 @@ android:permission="android.permission.BIND_JOB_SERVICE"> </service> + <service android:name="com.android.server.selinux.SelinuxAuditLogsService" + android:permission="android.permission.BIND_JOB_SERVICE"> + </service> + <service android:name="com.android.server.compos.IsolatedCompilationJobService" android:permission="android.permission.BIND_JOB_SERVICE"> </service> diff --git a/core/res/res/color-night/notification_expand_button_state_tint.xml b/core/res/res/color-night/notification_expand_button_state_tint.xml deleted file mode 100644 index a794d53c7e71..000000000000 --- a/core/res/res/color-night/notification_expand_button_state_tint.xml +++ /dev/null @@ -1,21 +0,0 @@ -<!-- - ~ Copyright (C) 2023 The Android Open Source Project - ~ - ~ Licensed under the Apache License, Version 2.0 (the "License"); - ~ you may not use this file except in compliance with the License. - ~ You may obtain a copy of the License at - ~ - ~ http://www.apache.org/licenses/LICENSE-2.0 - ~ - ~ Unless required by applicable law or agreed to in writing, software - ~ distributed under the License is distributed on an "AS IS" BASIS, - ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - ~ See the License for the specific language governing permissions and - ~ limitations under the License. - --> - -<selector xmlns:android="http://schemas.android.com/apk/res/android"> - <item android:state_pressed="true" android:color="@android:color/system_on_surface_dark" android:alpha="0.06"/> - <item android:state_hovered="true" android:color="@android:color/system_on_surface_dark" android:alpha="0.03"/> - <item android:color="@android:color/system_on_surface_dark" android:alpha="0.00"/> -</selector>
\ No newline at end of file diff --git a/core/res/res/color/notification_expand_button_state_tint.xml b/core/res/res/color/notification_expand_button_state_tint.xml index 67b2c2568bb1..5a8594f0e461 100644 --- a/core/res/res/color/notification_expand_button_state_tint.xml +++ b/core/res/res/color/notification_expand_button_state_tint.xml @@ -14,8 +14,11 @@ ~ limitations under the License. --> -<selector xmlns:android="http://schemas.android.com/apk/res/android"> - <item android:state_pressed="true" android:color="@android:color/system_on_surface_light" android:alpha="0.12"/> - <item android:state_hovered="true" android:color="@android:color/system_on_surface_light" android:alpha="0.08"/> - <item android:color="@android:color/system_on_surface_light" android:alpha="0.00"/> +<selector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"> + <item android:state_pressed="true" android:color="?androidprv:attr/materialColorOnPrimaryFixed" + android:alpha="0.15"/> + <item android:state_hovered="true" android:color="?androidprv:attr/materialColorOnPrimaryFixed" + android:alpha="0.11"/> + <item android:color="@color/transparent" /> </selector>
\ No newline at end of file diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 0d1a98785695..23c78fdf108e 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -6611,7 +6611,7 @@ </string-array> <!-- Whether or not the monitoring on the apps' background battery drain is enabled --> - <bool name="config_bg_current_drain_monitor_enabled">true</bool> + <bool name="config_bg_current_drain_monitor_enabled">false</bool> <!-- The threshold of the background current drain (in percentage) to the restricted standby bucket. diff --git a/core/tests/InputMethodCoreTests/Android.bp b/core/tests/InputMethodCoreTests/Android.bp new file mode 100644 index 000000000000..ac6462589e16 --- /dev/null +++ b/core/tests/InputMethodCoreTests/Android.bp @@ -0,0 +1,66 @@ +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +android_test { + name: "InputMethodCoreTests", + + srcs: [ + "src/**/*.java", + "src/**/*.kt", + "src/**/I*.aidl", + ], + + dxflags: ["--core-library"], + + static_libs: [ + "collector-device-lib-platform", + "android-common", + "frameworks-core-util-lib", + "androidx.core_core", + "androidx.core_core-ktx", + "androidx.test.ext.junit", + "androidx.test.runner", + "androidx.test.rules", + "flag-junit", + "junit-params", + "kotlin-test", + "mockito-target-minus-junit4", + "platform-test-annotations", + "platform-compat-test-rules", + "truth", + "print-test-util-lib", + "testng", + "device-time-shell-utils", + "testables", + "flag-junit", + ], + + libs: [ + "android.test.runner", + "android.test.base", + "android.test.mock", + "framework", + "ext", + "framework-res", + ], + + sdk_version: "core_platform", + test_suites: [ + "device-tests", + "automotive-tests", + ], + + certificate: "platform", + + resource_dirs: ["res"], + + data: [ + ":com.android.cts.helpers.aosp", + ], +} diff --git a/core/tests/InputMethodCoreTests/AndroidManifest.xml b/core/tests/InputMethodCoreTests/AndroidManifest.xml new file mode 100644 index 000000000000..8d00d0f755bf --- /dev/null +++ b/core/tests/InputMethodCoreTests/AndroidManifest.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. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + android:installLocation="internalOnly" + package="com.android.frameworks.inputmethodcoretests" + android:sharedUserId="com.android.uid.test"> + + <application + android:supportsRtl="true" + android:enableOnBackInvokedCallback="true"> + <uses-library android:name="android.test.runner" /> + </application> + + <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.frameworks.inputmethodcoretests" + android:label="InputMethod Core Tests" /> +</manifest> diff --git a/core/tests/InputMethodCoreTests/AndroidTest.xml b/core/tests/InputMethodCoreTests/AndroidTest.xml new file mode 100644 index 000000000000..fa585d8d1075 --- /dev/null +++ b/core/tests/InputMethodCoreTests/AndroidTest.xml @@ -0,0 +1,38 @@ +<?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. +--> +<configuration description="Runs InputMethod Core Tests."> + <option name="test-suite-tag" value="apct" /> + <option name="test-suite-tag" value="apct-instrumentation" /> + + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="cleanup-apks" value="true" /> + <option name="test-file-name" value="InputMethodCoreTests.apk" /> + </target_preparer> + + <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"> + <!-- TODO(b/254155965): Design a mechanism to finally remove this command. --> + <option name="run-command" value="settings put global device_config_sync_disabled 0" /> + </target_preparer> + + <target_preparer class="com.android.compatibility.common.tradefed.targetprep.DeviceInteractionHelperInstaller" /> + + <option name="test-tag" value="InputMethodCoreTests" /> + <test class="com.android.tradefed.testtype.AndroidJUnitTest" > + <option name="package" value="com.android.frameworks.inputmethodcoretests" /> + <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> + <option name="hidden-api-checks" value="false"/> + </test> +</configuration> diff --git a/core/tests/InputMethodCoreTests/OWNERS b/core/tests/InputMethodCoreTests/OWNERS new file mode 100644 index 000000000000..5deb2ce8f24b --- /dev/null +++ b/core/tests/InputMethodCoreTests/OWNERS @@ -0,0 +1 @@ +include /core/java/android/view/inputmethod/OWNERS diff --git a/core/tests/coretests/res/xml/ime_meta.xml b/core/tests/InputMethodCoreTests/res/xml/ime_meta.xml index a975718c50e9..a975718c50e9 100644 --- a/core/tests/coretests/res/xml/ime_meta.xml +++ b/core/tests/InputMethodCoreTests/res/xml/ime_meta.xml diff --git a/core/tests/coretests/res/xml/ime_meta_inline_suggestions.xml b/core/tests/InputMethodCoreTests/res/xml/ime_meta_inline_suggestions.xml index e67bf6331bfe..e67bf6331bfe 100644 --- a/core/tests/coretests/res/xml/ime_meta_inline_suggestions.xml +++ b/core/tests/InputMethodCoreTests/res/xml/ime_meta_inline_suggestions.xml diff --git a/core/tests/coretests/res/xml/ime_meta_inline_suggestions_with_touch_exploration.xml b/core/tests/InputMethodCoreTests/res/xml/ime_meta_inline_suggestions_with_touch_exploration.xml index 34402089b47d..34402089b47d 100644 --- a/core/tests/coretests/res/xml/ime_meta_inline_suggestions_with_touch_exploration.xml +++ b/core/tests/InputMethodCoreTests/res/xml/ime_meta_inline_suggestions_with_touch_exploration.xml diff --git a/core/tests/coretests/res/xml/ime_meta_sw_next.xml b/core/tests/InputMethodCoreTests/res/xml/ime_meta_sw_next.xml index 2e2ee33e9933..2e2ee33e9933 100644 --- a/core/tests/coretests/res/xml/ime_meta_sw_next.xml +++ b/core/tests/InputMethodCoreTests/res/xml/ime_meta_sw_next.xml diff --git a/core/tests/coretests/res/xml/ime_meta_virtual_device_only.xml b/core/tests/InputMethodCoreTests/res/xml/ime_meta_virtual_device_only.xml index 1905365808bc..1905365808bc 100644 --- a/core/tests/coretests/res/xml/ime_meta_virtual_device_only.xml +++ b/core/tests/InputMethodCoreTests/res/xml/ime_meta_virtual_device_only.xml diff --git a/core/tests/coretests/res/xml/ime_meta_vr_only.xml b/core/tests/InputMethodCoreTests/res/xml/ime_meta_vr_only.xml index 653a8ffcb944..653a8ffcb944 100644 --- a/core/tests/coretests/res/xml/ime_meta_vr_only.xml +++ b/core/tests/InputMethodCoreTests/res/xml/ime_meta_vr_only.xml diff --git a/core/tests/coretests/src/android/view/inputmethod/BaseInputConnectionTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/BaseInputConnectionTest.java index f04f603564f6..f04f603564f6 100644 --- a/core/tests/coretests/src/android/view/inputmethod/BaseInputConnectionTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/BaseInputConnectionTest.java diff --git a/core/tests/coretests/src/android/view/inputmethod/CursorAnchorInfoTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/CursorAnchorInfoTest.java index 9d7d71d8d539..9d7d71d8d539 100644 --- a/core/tests/coretests/src/android/view/inputmethod/CursorAnchorInfoTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/CursorAnchorInfoTest.java diff --git a/core/tests/coretests/src/android/view/inputmethod/DeleteRangeGestureTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/DeleteRangeGestureTest.java index d7b911dda672..d7b911dda672 100644 --- a/core/tests/coretests/src/android/view/inputmethod/DeleteRangeGestureTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/DeleteRangeGestureTest.java diff --git a/core/tests/coretests/src/android/view/inputmethod/EditorInfoTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/EditorInfoTest.java index 4839dd27b283..4839dd27b283 100644 --- a/core/tests/coretests/src/android/view/inputmethod/EditorInfoTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/EditorInfoTest.java diff --git a/core/tests/coretests/src/android/view/inputmethod/InputMethodInfoTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodInfoTest.java index 909af7b4c5fb..a3f537ef5f1c 100644 --- a/core/tests/coretests/src/android/view/inputmethod/InputMethodInfoTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodInfoTest.java @@ -32,7 +32,7 @@ import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; -import com.android.frameworks.coretests.R; +import com.android.frameworks.inputmethodcoretests.R; import org.junit.Rule; import org.junit.Test; diff --git a/core/tests/coretests/src/android/view/inputmethod/InputMethodManagerTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodManagerTest.java index d70572444128..d70572444128 100644 --- a/core/tests/coretests/src/android/view/inputmethod/InputMethodManagerTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodManagerTest.java diff --git a/core/tests/coretests/src/android/view/inputmethod/InputMethodSubtypeArrayTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodSubtypeArrayTest.java index e7b1110f898a..e7b1110f898a 100644 --- a/core/tests/coretests/src/android/view/inputmethod/InputMethodSubtypeArrayTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodSubtypeArrayTest.java diff --git a/core/tests/coretests/src/android/view/inputmethod/InputMethodSubtypeTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodSubtypeTest.java index 5095cad1b607..5095cad1b607 100644 --- a/core/tests/coretests/src/android/view/inputmethod/InputMethodSubtypeTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InputMethodSubtypeTest.java diff --git a/core/tests/coretests/src/android/view/inputmethod/InsertGestureTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InsertGestureTest.java index 47a724d36038..47a724d36038 100644 --- a/core/tests/coretests/src/android/view/inputmethod/InsertGestureTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InsertGestureTest.java diff --git a/core/tests/coretests/src/android/view/inputmethod/InsertModeGestureTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InsertModeGestureTest.java index a94f8772fcd0..a94f8772fcd0 100644 --- a/core/tests/coretests/src/android/view/inputmethod/InsertModeGestureTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/InsertModeGestureTest.java diff --git a/core/tests/coretests/src/android/view/inputmethod/ParcelableHandwritingGestureTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/ParcelableHandwritingGestureTest.java index 90f7d06857c7..90f7d06857c7 100644 --- a/core/tests/coretests/src/android/view/inputmethod/ParcelableHandwritingGestureTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/ParcelableHandwritingGestureTest.java diff --git a/core/tests/coretests/src/android/view/inputmethod/SelectGestureTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/SelectGestureTest.java index b2eb07c0a9e7..b2eb07c0a9e7 100644 --- a/core/tests/coretests/src/android/view/inputmethod/SelectGestureTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/SelectGestureTest.java diff --git a/core/tests/coretests/src/android/view/inputmethod/SelectRangeGestureTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/SelectRangeGestureTest.java index df63a4aaaefe..df63a4aaaefe 100644 --- a/core/tests/coretests/src/android/view/inputmethod/SelectRangeGestureTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/SelectRangeGestureTest.java diff --git a/core/tests/coretests/src/android/view/inputmethod/SparseRectFArrayTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/SparseRectFArrayTest.java index f264cc630dc5..f264cc630dc5 100644 --- a/core/tests/coretests/src/android/view/inputmethod/SparseRectFArrayTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/SparseRectFArrayTest.java diff --git a/core/tests/coretests/src/android/view/inputmethod/SurroundingTextTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/SurroundingTextTest.java index 047f33074460..047f33074460 100644 --- a/core/tests/coretests/src/android/view/inputmethod/SurroundingTextTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/SurroundingTextTest.java diff --git a/core/tests/coretests/src/android/view/inputmethod/TextAppearanceInfoTest.java b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/TextAppearanceInfoTest.java index 0750cf1a64ab..0750cf1a64ab 100644 --- a/core/tests/coretests/src/android/view/inputmethod/TextAppearanceInfoTest.java +++ b/core/tests/InputMethodCoreTests/src/android/view/inputmethod/TextAppearanceInfoTest.java diff --git a/core/tests/coretests/src/com/android/internal/inputmethod/CompletableFutureUtilTest.kt b/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/CompletableFutureUtilTest.kt index 07471f08e0f5..07471f08e0f5 100644 --- a/core/tests/coretests/src/com/android/internal/inputmethod/CompletableFutureUtilTest.kt +++ b/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/CompletableFutureUtilTest.kt diff --git a/core/tests/coretests/src/com/android/internal/inputmethod/InputConnectionWrapperTest.java b/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/InputConnectionWrapperTest.java index a6262944e8b0..a6262944e8b0 100644 --- a/core/tests/coretests/src/com/android/internal/inputmethod/InputConnectionWrapperTest.java +++ b/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/InputConnectionWrapperTest.java diff --git a/core/tests/coretests/src/com/android/internal/inputmethod/InputMethodDebugTest.java b/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/InputMethodDebugTest.java index 32bfdcb7b217..32bfdcb7b217 100644 --- a/core/tests/coretests/src/com/android/internal/inputmethod/InputMethodDebugTest.java +++ b/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/InputMethodDebugTest.java diff --git a/core/tests/coretests/src/com/android/internal/inputmethod/InputMethodSubtypeHandleTest.java b/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/InputMethodSubtypeHandleTest.java index f111bf6fcd64..f111bf6fcd64 100644 --- a/core/tests/coretests/src/com/android/internal/inputmethod/InputMethodSubtypeHandleTest.java +++ b/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/InputMethodSubtypeHandleTest.java diff --git a/core/tests/coretests/src/com/android/internal/inputmethod/SubtypeLocaleUtilsTest.java b/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/SubtypeLocaleUtilsTest.java index ba6390808151..ba6390808151 100644 --- a/core/tests/coretests/src/com/android/internal/inputmethod/SubtypeLocaleUtilsTest.java +++ b/core/tests/InputMethodCoreTests/src/com/android/internal/inputmethod/SubtypeLocaleUtilsTest.java diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp index d1a90aea835c..f47679991418 100644 --- a/core/tests/coretests/Android.bp +++ b/core/tests/coretests/Android.bp @@ -209,11 +209,15 @@ android_ravenwood_test { "testng", ], srcs: [ + "src/android/content/pm/PackageManagerTest.java", + "src/android/content/pm/UserInfoTest.java", "src/android/database/CursorWindowTest.java", "src/android/os/**/*.java", + "src/android/telephony/PinResultTest.java", "src/android/util/**/*.java", + "src/android/view/DisplayInfoTest.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/content/pm/PackageManagerTest.java b/core/tests/coretests/src/android/content/pm/PackageManagerTest.java new file mode 100644 index 000000000000..20421d105db8 --- /dev/null +++ b/core/tests/coretests/src/android/content/pm/PackageManagerTest.java @@ -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 android.content.pm; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class PackageManagerTest { + @Test + public void testPackageInfoFlags() throws Exception { + assertThat(PackageManager.PackageInfoFlags.of(42L).getValue()).isEqualTo(42L); + } + + @Test + public void testApplicationInfoFlags() throws Exception { + assertThat(PackageManager.ApplicationInfoFlags.of(42L).getValue()).isEqualTo(42L); + } + + @Test + public void testComponentInfoFlags() throws Exception { + assertThat(PackageManager.ComponentInfoFlags.of(42L).getValue()).isEqualTo(42L); + } + + @Test + public void testResolveInfoFlags() throws Exception { + assertThat(PackageManager.ResolveInfoFlags.of(42L).getValue()).isEqualTo(42L); + } +} diff --git a/core/tests/coretests/src/android/content/pm/UserInfoTest.java b/core/tests/coretests/src/android/content/pm/UserInfoTest.java new file mode 100644 index 000000000000..af36dbb36379 --- /dev/null +++ b/core/tests/coretests/src/android/content/pm/UserInfoTest.java @@ -0,0 +1,71 @@ +/* + * 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.content.pm; + +import static com.google.common.truth.Truth.assertThat; + +import android.os.UserHandle; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class UserInfoTest { + @Test + public void testSimple() throws Exception { + final UserInfo ui = new UserInfo(10, "Test", UserInfo.FLAG_GUEST); + + assertThat(ui.getUserHandle()).isEqualTo(UserHandle.of(10)); + assertThat(ui.name).isEqualTo("Test"); + + // Derived based on userType field + assertThat(ui.isManagedProfile()).isEqualTo(false); + assertThat(ui.isGuest()).isEqualTo(true); + assertThat(ui.isRestricted()).isEqualTo(false); + assertThat(ui.isDemo()).isEqualTo(false); + assertThat(ui.isCloneProfile()).isEqualTo(false); + assertThat(ui.isCommunalProfile()).isEqualTo(false); + assertThat(ui.isPrivateProfile()).isEqualTo(false); + + // Derived based on flags field + assertThat(ui.isPrimary()).isEqualTo(false); + assertThat(ui.isAdmin()).isEqualTo(false); + assertThat(ui.isProfile()).isEqualTo(false); + assertThat(ui.isEnabled()).isEqualTo(true); + assertThat(ui.isQuietModeEnabled()).isEqualTo(false); + assertThat(ui.isEphemeral()).isEqualTo(false); + assertThat(ui.isForTesting()).isEqualTo(false); + assertThat(ui.isInitialized()).isEqualTo(false); + assertThat(ui.isFull()).isEqualTo(false); + assertThat(ui.isMain()).isEqualTo(false); + + // Derived dynamically + assertThat(ui.canHaveProfile()).isEqualTo(false); + } + + @Test + public void testDebug() throws Exception { + final UserInfo ui = new UserInfo(10, "Test", UserInfo.FLAG_GUEST); + + assertThat(ui.toString()).isNotEmpty(); + assertThat(ui.toFullString()).isNotEmpty(); + } +} diff --git a/core/tests/coretests/src/android/content/res/FontScaleConverterFactoryTest.kt b/core/tests/coretests/src/android/content/res/FontScaleConverterFactoryTest.kt index e32a57b1aefe..a2a5433eca24 100644 --- a/core/tests/coretests/src/android/content/res/FontScaleConverterFactoryTest.kt +++ b/core/tests/coretests/src/android/content/res/FontScaleConverterFactoryTest.kt @@ -145,7 +145,6 @@ class FontScaleConverterFactoryTest { fun unnecessaryFontScalesReturnsNull() { assertThat(FontScaleConverterFactory.forScale(0F)).isNull() assertThat(FontScaleConverterFactory.forScale(1F)).isNull() - assertThat(FontScaleConverterFactory.forScale(1.1F)).isNull() assertThat(FontScaleConverterFactory.forScale(0.85F)).isNull() } @@ -176,7 +175,7 @@ class FontScaleConverterFactoryTest { assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(-1f)).isFalse() assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(0.85f)).isFalse() assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(1.02f)).isFalse() - assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(1.10f)).isFalse() + assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(1.10f)).isTrue() assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(1.15f)).isTrue() assertThat(FontScaleConverterFactory.isNonLinearFontScalingActive(1.1499999f)) .isTrue() 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/telephony/PinResultTest.java b/core/tests/coretests/src/android/telephony/PinResultTest.java new file mode 100644 index 000000000000..c260807e5cbc --- /dev/null +++ b/core/tests/coretests/src/android/telephony/PinResultTest.java @@ -0,0 +1,34 @@ +/* + * 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.telephony; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class PinResultTest { + @Test + public void testSimple() throws Exception { + final PinResult res = new PinResult(PinResult.PIN_RESULT_TYPE_SUCCESS, 5); + assertThat(res.getResult()).isEqualTo(PinResult.PIN_RESULT_TYPE_SUCCESS); + assertThat(res.getAttemptsRemaining()).isEqualTo(5); + } +} diff --git a/core/tests/coretests/src/android/util/SingletonTest.java b/core/tests/coretests/src/android/util/SingletonTest.java new file mode 100644 index 000000000000..8c5a9639c23a --- /dev/null +++ b/core/tests/coretests/src/android/util/SingletonTest.java @@ -0,0 +1,41 @@ +/* + * 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.util; + +import static org.junit.Assert.assertTrue; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class SingletonTest { + @Test + public void testSimple() throws Exception { + final Singleton<Object> singleton = new Singleton<>() { + @Override + protected Object create() { + return new Object(); + } + }; + + final Object first = singleton.get(); + final Object second = singleton.get(); + assertTrue(first == second); + } +} diff --git a/core/tests/coretests/src/android/view/DisplayInfoTest.java b/core/tests/coretests/src/android/view/DisplayInfoTest.java index 803d38c4208a..4c5b7e508e34 100644 --- a/core/tests/coretests/src/android/view/DisplayInfoTest.java +++ b/core/tests/coretests/src/android/view/DisplayInfoTest.java @@ -21,9 +21,12 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import android.platform.test.ravenwood.RavenwoodRule; + import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -32,6 +35,9 @@ import org.junit.runner.RunWith; public class DisplayInfoTest { private static final float FLOAT_EQUAL_DELTA = 0.0001f; + @Rule + public final RavenwoodRule mRavenwood = new RavenwoodRule(); + @Test public void testDefaultDisplayInfosAreEqual() { DisplayInfo displayInfo1 = new DisplayInfo(); 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/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 2de305f8e925..62c9e16f753a 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -450,7 +450,7 @@ applications that come with the platform <!-- Permissions required for CTS test - android.server.biometrics --> <permission name="android.permission.USE_BIOMETRIC" /> <permission name="android.permission.TEST_BIOMETRIC" /> - <permission name="android.permission.MANAGE_BIOMETRIC_DIALOG" /> + <permission name="android.permission.SET_BIOMETRIC_DIALOG_LOGO" /> <permission name="android.permission.USE_BACKGROUND_FACE_AUTHENTICATION" /> <!-- Permissions required for CTS test - CtsContactsProviderTestCases --> <permission name="android.contacts.permission.MANAGE_SIM_ACCOUNTS" /> 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/libs/WindowManager/Shell/multivalentTests/OWNERS b/libs/WindowManager/Shell/multivalentTests/OWNERS new file mode 100644 index 000000000000..24c1a3a6d400 --- /dev/null +++ b/libs/WindowManager/Shell/multivalentTests/OWNERS @@ -0,0 +1,4 @@ +atsjenk@google.com +liranb@google.com +madym@google.com + diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDoubleTapHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipDoubleTapHelper.java index 1b1ebc39b558..4cbb78f2dae2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipDoubleTapHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipDoubleTapHelper.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 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,14 +14,12 @@ * limitations under the License. */ -package com.android.wm.shell.pip.phone; +package com.android.wm.shell.common.pip; import android.annotation.IntDef; import android.annotation.NonNull; import android.graphics.Rect; -import com.android.wm.shell.common.pip.PipBoundsState; - import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -50,9 +48,9 @@ public class PipDoubleTapHelper { @Retention(RetentionPolicy.SOURCE) @interface PipSizeSpec {} - static final int SIZE_SPEC_DEFAULT = 0; - static final int SIZE_SPEC_MAX = 1; - static final int SIZE_SPEC_CUSTOM = 2; + public static final int SIZE_SPEC_DEFAULT = 0; + public static final int SIZE_SPEC_MAX = 1; + public static final int SIZE_SPEC_CUSTOM = 2; /** * Returns MAX or DEFAULT {@link PipSizeSpec} to toggle to/from. @@ -84,7 +82,7 @@ public class PipDoubleTapHelper { * @return pip screen size to switch to */ @PipSizeSpec - static int nextSizeSpec(@NonNull PipBoundsState mPipBoundsState, + public static int nextSizeSpec(@NonNull PipBoundsState mPipBoundsState, @NonNull Rect userResizeBounds) { // is pip screen at its maximum boolean isScreenMax = mPipBoundsState.getBounds().width() diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java index 2dd27430e348..dbf7186def8a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java @@ -80,8 +80,7 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract { Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> onRestartButtonClicked) { super(context, taskInfo, syncQueue, taskListener, displayLayout); mCallback = callback; - mHasSizeCompat = taskInfo.appCompatTaskInfo.topActivityInSizeCompat - && shouldShowSizeCompatRestartButton(taskInfo); + mHasSizeCompat = taskInfo.appCompatTaskInfo.topActivityInSizeCompat; mCameraCompatControlState = taskInfo.appCompatTaskInfo.cameraCompatControlState; mCompatUIHintsState = compatUIHintsState; mCompatUIConfiguration = compatUIConfiguration; @@ -106,7 +105,8 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract { @Override protected boolean eligibleToShowLayout() { - return mHasSizeCompat || shouldShowCameraControl(); + return (mHasSizeCompat && shouldShowSizeCompatRestartButton(getLastTaskInfo())) + || shouldShowCameraControl(); } @Override @@ -114,11 +114,6 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract { mLayout = inflateLayout(); mLayout.inject(this); - final TaskInfo taskInfo = getLastTaskInfo(); - if (taskInfo != null) { - mHasSizeCompat = mHasSizeCompat && shouldShowSizeCompatRestartButton(taskInfo); - } - updateVisibilityOfViews(); if (mHasSizeCompat) { @@ -139,8 +134,7 @@ class CompatUIWindowManager extends CompatUIWindowManagerAbstract { boolean canShow) { final boolean prevHasSizeCompat = mHasSizeCompat; final int prevCameraCompatControlState = mCameraCompatControlState; - mHasSizeCompat = taskInfo.appCompatTaskInfo.topActivityInSizeCompat - && shouldShowSizeCompatRestartButton(taskInfo); + mHasSizeCompat = taskInfo.appCompatTaskInfo.topActivityInSizeCompat; mCameraCompatControlState = taskInfo.appCompatTaskInfo.cameraCompatControlState; if (!super.updateCompatInfo(taskInfo, taskListener, canShow)) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java index 180498c50c78..0564c95aef5c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java @@ -332,7 +332,7 @@ public abstract class CompatUIWindowManagerAbstract extends WindowlessWindowMana updateSurfacePosition(); } - @Nullable + @NonNull protected TaskInfo getLastTaskInfo() { return mTaskInfo; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java index 3b48c67a5bbd..7b98fa6523cb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java @@ -50,15 +50,16 @@ import java.util.Optional; public abstract class Pip2Module { @WMSingleton @Provides - static PipTransition providePipTransition(@NonNull ShellInit shellInit, + static PipTransition providePipTransition(Context context, + @NonNull ShellInit shellInit, @NonNull ShellTaskOrganizer shellTaskOrganizer, @NonNull Transitions transitions, PipBoundsState pipBoundsState, PipBoundsAlgorithm pipBoundsAlgorithm, Optional<PipController> pipController, @NonNull PipScheduler pipScheduler) { - return new PipTransition(shellInit, shellTaskOrganizer, transitions, pipBoundsState, null, - pipBoundsAlgorithm, pipScheduler); + return new PipTransition(context, shellInit, shellTaskOrganizer, transitions, + pipBoundsState, null, pipBoundsAlgorithm, pipScheduler); } @WMSingleton diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java index 04911c0bc064..0e7073688ec4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java @@ -47,6 +47,7 @@ import com.android.wm.shell.transition.Transitions; import java.io.PrintWriter; import java.util.ArrayList; import java.util.List; +import java.util.function.Consumer; /** * Responsible supplying PiP Transitions. @@ -116,6 +117,17 @@ public abstract class PipTransitionController implements Transitions.TransitionH } /** + * Called when the Shell wants to start resizing Pip transition/animation. + * + * @param onFinishResizeCallback callback guaranteed to execute when animation ends and + * client completes any potential draws upon WM state updates. + */ + public void startResizeTransition(WindowContainerTransaction wct, + Consumer<Rect> onFinishResizeCallback) { + // Default implementation does nothing. + } + + /** * Called when the transition animation can't continue (eg. task is removed during * animation) */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java index 452a41696fcf..81705e20a1df 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java @@ -52,6 +52,7 @@ import com.android.wm.shell.common.FloatingContentCoordinator; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; import com.android.wm.shell.common.pip.PipBoundsState; +import com.android.wm.shell.common.pip.PipDoubleTapHelper; import com.android.wm.shell.common.pip.PipUiEventLogger; import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.common.pip.SizeSpecSource; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java index 0b8f60e44c7e..57b73b3019f4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java @@ -24,10 +24,12 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.graphics.Rect; import android.view.SurfaceControl; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; +import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; @@ -36,6 +38,10 @@ import com.android.wm.shell.common.pip.PipBoundsState; import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.pip.PipTransitionController; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.function.Consumer; + /** * Scheduler for Shell initiated PiP transitions and animations. */ @@ -58,13 +64,37 @@ public class PipScheduler { private SurfaceControl mPinnedTaskLeash; /** - * A temporary broadcast receiver to initiate exit PiP via expand. - * This will later be modified to be triggered by the PiP menu. + * Temporary PiP CUJ codes to schedule PiP related transitions directly from Shell. + * This is used for a broadcast receiver to resolve intents. This should be removed once + * there is an equivalent of PipTouchHandler and PipResizeGestureHandler for PiP2. + */ + private static final int PIP_EXIT_VIA_EXPAND_CODE = 0; + private static final int PIP_DOUBLE_TAP = 1; + + @IntDef(value = { + PIP_EXIT_VIA_EXPAND_CODE, + PIP_DOUBLE_TAP + }) + @Retention(RetentionPolicy.SOURCE) + @interface PipUserJourneyCode {} + + /** + * A temporary broadcast receiver to initiate PiP CUJs. */ private class PipSchedulerReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { - scheduleExitPipViaExpand(); + int userJourneyCode = intent.getIntExtra("cuj_code_extra", 0); + switch (userJourneyCode) { + case PIP_EXIT_VIA_EXPAND_CODE: + scheduleExitPipViaExpand(); + break; + case PIP_DOUBLE_TAP: + scheduleDoubleTapToResize(); + break; + default: + throw new IllegalStateException("unexpected CUJ code=" + userJourneyCode); + } } } @@ -121,6 +151,23 @@ public class PipScheduler { } } + /** + * Schedules resize PiP via double tap. + */ + public void scheduleDoubleTapToResize() {} + + /** + * Animates resizing of the pinned stack given the duration. + */ + public void scheduleAnimateResizePip(Rect toBounds, Consumer<Rect> onFinishResizeCallback) { + if (mPipTaskToken == null) { + return; + } + WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.setBounds(mPipTaskToken, toBounds); + mPipTransitionController.startResizeTransition(wct, onFinishResizeCallback); + } + void onExitPip() { mPipTaskToken = null; mPinnedTaskLeash = null; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java index 3b0e7c139bed..f3d178aef4ea 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java @@ -22,10 +22,12 @@ import static android.view.WindowManager.TRANSIT_PIP; import static android.view.WindowManager.TRANSIT_TO_FRONT; import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP; +import static com.android.wm.shell.transition.Transitions.TRANSIT_RESIZE_PIP; import android.annotation.NonNull; import android.app.ActivityManager; import android.app.PictureInPictureParams; +import android.content.Context; import android.graphics.Rect; import android.os.IBinder; import android.view.SurfaceControl; @@ -36,6 +38,7 @@ import android.window.WindowContainerTransaction; import androidx.annotation.Nullable; +import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; import com.android.wm.shell.common.pip.PipBoundsState; @@ -45,25 +48,29 @@ import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; +import java.util.function.Consumer; + /** * Implementation of transitions for PiP on phone. */ public class PipTransition extends PipTransitionController { private static final String TAG = PipTransition.class.getSimpleName(); + private final Context mContext; private final PipScheduler mPipScheduler; @Nullable private WindowContainerToken mPipTaskToken; @Nullable private IBinder mEnterTransition; @Nullable - private IBinder mAutoEnterButtonNavTransition; - @Nullable private IBinder mExitViaExpandTransition; @Nullable - private IBinder mLegacyEnterTransition; + private IBinder mResizeTransition; + + private Consumer<Rect> mFinishResizeCallback; public PipTransition( + Context context, @NonNull ShellInit shellInit, @NonNull ShellTaskOrganizer shellTaskOrganizer, @NonNull Transitions transitions, @@ -74,6 +81,7 @@ public class PipTransition extends PipTransitionController { super(shellInit, shellTaskOrganizer, transitions, pipBoundsState, pipMenuController, pipBoundsAlgorithm); + mContext = context; mPipScheduler = pipScheduler; mPipScheduler.setPipTransitionController(this); } @@ -87,7 +95,7 @@ public class PipTransition extends PipTransitionController { @Override public void startExitTransition(int type, WindowContainerTransaction out, - @android.annotation.Nullable Rect destinationBounds) { + @Nullable Rect destinationBounds) { if (out == null) { return; } @@ -97,6 +105,16 @@ public class PipTransition extends PipTransitionController { } } + @Override + public void startResizeTransition(WindowContainerTransaction wct, + Consumer<Rect> onFinishResizeCallback) { + if (wct == null) { + return; + } + mResizeTransition = mTransitions.startTransition(TRANSIT_RESIZE_PIP, wct, this); + mFinishResizeCallback = onFinishResizeCallback; + } + @Nullable @Override public WindowContainerTransaction handleRequest(@NonNull IBinder transition, @@ -126,43 +144,6 @@ public class PipTransition extends PipTransitionController { public void onTransitionConsumed(@NonNull IBinder transition, boolean aborted, @Nullable SurfaceControl.Transaction finishT) {} - private WindowContainerTransaction getEnterPipTransaction(@NonNull IBinder transition, - @NonNull TransitionRequestInfo request) { - // cache the original task token to check for multi-activity case later - final ActivityManager.RunningTaskInfo pipTask = request.getPipTask(); - PictureInPictureParams pipParams = pipTask.pictureInPictureParams; - mPipBoundsState.setBoundsStateForEntry(pipTask.topActivity, pipTask.topActivityInfo, - pipParams, mPipBoundsAlgorithm); - - // calculate the entry bounds and notify core to move task to pinned with final bounds - final Rect entryBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); - mPipBoundsState.setBounds(entryBounds); - - WindowContainerTransaction wct = new WindowContainerTransaction(); - wct.movePipActivityToPinnedRootTask(pipTask.token, entryBounds); - return wct; - } - - private boolean isAutoEnterInButtonNavigation(@NonNull TransitionRequestInfo requestInfo) { - final ActivityManager.RunningTaskInfo pipTask = requestInfo.getPipTask(); - if (pipTask == null) { - return false; - } - if (pipTask.pictureInPictureParams == null) { - return false; - } - - // Assuming auto-enter is enabled and pipTask is non-null, the TRANSIT_OPEN request type - // implies that we are entering PiP in button navigation mode. This is guaranteed by - // TaskFragment#startPausing()` in Core which wouldn't get called in gesture nav. - return requestInfo.getType() == TRANSIT_OPEN - && pipTask.pictureInPictureParams.isAutoEnterEnabled(); - } - - private boolean isEnterPictureInPictureModeRequest(@NonNull TransitionRequestInfo requestInfo) { - return requestInfo.getType() == TRANSIT_PIP; - } - @Override public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, @@ -182,16 +163,48 @@ public class PipTransition extends PipTransitionController { } else if (transition == mExitViaExpandTransition) { mExitViaExpandTransition = null; return startExpandAnimation(info, startTransaction, finishTransaction, finishCallback); + } else if (transition == mResizeTransition) { + mResizeTransition = null; + return startResizeAnimation(info, startTransaction, finishTransaction, finishCallback); } return false; } - private boolean isLegacyEnter(@NonNull TransitionInfo info) { + private boolean startResizeAnimation(@NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { TransitionInfo.Change pipChange = getPipChange(info); - // If the only change in the changes list is a TO_FRONT mode PiP task, - // then this is legacy-enter PiP. - return pipChange != null && pipChange.getMode() == TRANSIT_TO_FRONT - && info.getChanges().size() == 1; + if (pipChange == null) { + return false; + } + SurfaceControl pipLeash = pipChange.getLeash(); + Rect destinationBounds = pipChange.getEndAbsBounds(); + + // Even though the final bounds and crop are applied with finishTransaction since + // this is a visible change, we still need to handle the app draw coming in. Snapshot + // covering app draw during collection will be removed by startTransaction. So we make + // the crop equal to the final bounds and then scale the leash back to starting bounds. + startTransaction.setWindowCrop(pipLeash, pipChange.getEndAbsBounds().width(), + pipChange.getEndAbsBounds().height()); + startTransaction.setScale(pipLeash, + (float) mPipBoundsState.getBounds().width() / destinationBounds.width(), + (float) mPipBoundsState.getBounds().height() / destinationBounds.height()); + startTransaction.apply(); + + finishTransaction.setScale(pipLeash, + (float) mPipBoundsState.getBounds().width() / destinationBounds.width(), + (float) mPipBoundsState.getBounds().height() / destinationBounds.height()); + + // We are done with the transition, but will continue animating leash to final bounds. + finishCallback.onTransitionFinished(null); + + // Animate the pip leash with the new buffer + final int duration = mContext.getResources().getInteger( + R.integer.config_pipResizeAnimationDuration); + // TODO: b/275910498 Couple this routine with a new implementation of the PiP animator. + startResizeAnimation(pipLeash, mPipBoundsState.getBounds(), destinationBounds, duration); + return true; } private boolean startBoundsTypeEnterAnimation(@NonNull TransitionInfo info, @@ -251,6 +264,57 @@ public class PipTransition extends PipTransitionController { return null; } + private WindowContainerTransaction getEnterPipTransaction(@NonNull IBinder transition, + @NonNull TransitionRequestInfo request) { + // cache the original task token to check for multi-activity case later + final ActivityManager.RunningTaskInfo pipTask = request.getPipTask(); + PictureInPictureParams pipParams = pipTask.pictureInPictureParams; + mPipBoundsState.setBoundsStateForEntry(pipTask.topActivity, pipTask.topActivityInfo, + pipParams, mPipBoundsAlgorithm); + + // calculate the entry bounds and notify core to move task to pinned with final bounds + final Rect entryBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); + mPipBoundsState.setBounds(entryBounds); + + WindowContainerTransaction wct = new WindowContainerTransaction(); + wct.movePipActivityToPinnedRootTask(pipTask.token, entryBounds); + return wct; + } + + private boolean isAutoEnterInButtonNavigation(@NonNull TransitionRequestInfo requestInfo) { + final ActivityManager.RunningTaskInfo pipTask = requestInfo.getPipTask(); + if (pipTask == null) { + return false; + } + if (pipTask.pictureInPictureParams == null) { + return false; + } + + // Assuming auto-enter is enabled and pipTask is non-null, the TRANSIT_OPEN request type + // implies that we are entering PiP in button navigation mode. This is guaranteed by + // TaskFragment#startPausing()` in Core which wouldn't get called in gesture nav. + return requestInfo.getType() == TRANSIT_OPEN + && pipTask.pictureInPictureParams.isAutoEnterEnabled(); + } + + private boolean isEnterPictureInPictureModeRequest(@NonNull TransitionRequestInfo requestInfo) { + return requestInfo.getType() == TRANSIT_PIP; + } + + private boolean isLegacyEnter(@NonNull TransitionInfo info) { + TransitionInfo.Change pipChange = getPipChange(info); + // If the only change in the changes list is a TO_FRONT mode PiP task, + // then this is legacy-enter PiP. + return pipChange != null && pipChange.getMode() == TRANSIT_TO_FRONT + && info.getChanges().size() == 1; + } + + /** + * TODO: b/275910498 Use a new implementation of the PiP animator here. + */ + private void startResizeAnimation(SurfaceControl leash, Rect startBounds, + Rect endBounds, int duration) {} + private void onExitPip() { mPipTaskToken = null; mPipScheduler.onExitPip(); 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/splitscreen/ISplitScreen.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl index 253acc49071a..0ca244c4b96a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/ISplitScreen.aidl @@ -158,5 +158,10 @@ interface ISplitScreen { * does not expect split to currently be running. */ RemoteAnimationTarget[] onStartingSplitLegacy(in RemoteAnimationTarget[] appTargets) = 14; + + /** + * Reverse the split. + */ + oneway void switchSplitPosition() = 22; } -// Last id = 21
\ No newline at end of file +// Last id = 22
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java index 2ec52bb028c6..70cb2fc6d52c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java @@ -1109,6 +1109,12 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, mStageCoordinator.onDroppedToSplit(position, dragSessionId); } + void switchSplitPosition(String reason) { + if (isSplitScreenVisible()) { + mStageCoordinator.switchSplitPosition(reason); + } + } + /** * Return the {@param exitReason} as a string. */ @@ -1473,5 +1479,11 @@ public class SplitScreenController implements DragAndDropPolicy.Starter, true /* blocking */); return out[0]; } + + @Override + public void switchSplitPosition() { + executeRemoteCallWithTaskPermission(mController, "switchSplitPosition", + (controller) -> controller.switchSplitPosition("remoteCall")); + } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenShellCommandHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenShellCommandHandler.java index 7fd03a9a306b..7f16c5e3592e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenShellCommandHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenShellCommandHandler.java @@ -43,6 +43,8 @@ public class SplitScreenShellCommandHandler implements return runRemoveFromSideStage(args, pw); case "setSideStagePosition": return runSetSideStagePosition(args, pw); + case "switchSplitPosition": + return runSwitchSplitPosition(); default: pw.println("Invalid command: " + args[0]); return false; @@ -84,6 +86,11 @@ public class SplitScreenShellCommandHandler implements return true; } + private boolean runSwitchSplitPosition() { + mController.switchSplitPosition("shellCommand"); + return true; + } + @Override public void printShellCommandHelp(PrintWriter pw, String prefix) { pw.println(prefix + "moveToSideStage <taskId> <SideStagePosition>"); @@ -92,5 +99,7 @@ public class SplitScreenShellCommandHandler implements pw.println(prefix + " Remove a task with given id in split-screen mode."); pw.println(prefix + "setSideStagePosition <SideStagePosition>"); pw.println(prefix + " Sets the position of the side-stage."); + pw.println(prefix + "switchSplitPosition"); + pw.println(prefix + " Reverses the split."); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java index 3fb0dbfaa63d..67fc7e2b4ea6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java @@ -175,6 +175,9 @@ public class Transitions implements RemoteCallable<Transitions>, /** Transition to animate task to desktop. */ public static final int TRANSIT_MOVE_TO_DESKTOP = WindowManager.TRANSIT_FIRST_CUSTOM + 15; + /** Transition to resize PiP task. */ + public static final int TRANSIT_RESIZE_PIP = TRANSIT_FIRST_CUSTOM + 16; + private final ShellTaskOrganizer mOrganizer; private final Context mContext; private final ShellExecutor mMainExecutor; 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/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java index 4ddc539eb220..dd358e757fde 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java @@ -30,6 +30,7 @@ import static org.mockito.Mockito.verify; import android.app.ActivityManager; import android.app.AppCompatTaskInfo.CameraCompatControlState; import android.app.TaskInfo; +import android.graphics.Rect; import android.testing.AndroidTestingRunner; import android.util.Pair; import android.view.LayoutInflater; @@ -83,6 +84,7 @@ public class CompatUILayoutTest extends ShellTestCase { @Before public void setUp() { MockitoAnnotations.initMocks(this); + doReturn(100).when(mCompatUIConfiguration).getHideSizeCompatRestartButtonTolerance(); mTaskInfo = createTaskInfo(/* hasSizeCompat= */ false, CAMERA_COMPAT_CONTROL_HIDDEN); mWindowManager = new CompatUIWindowManager(mContext, mTaskInfo, mSyncTransactionQueue, mCallback, mTaskListener, new DisplayLayout(), new CompatUIHintsState(), @@ -127,7 +129,6 @@ public class CompatUILayoutTest extends ShellTestCase { @Test public void testOnClickForSizeCompatHint() { mWindowManager.mHasSizeCompat = true; - doReturn(true).when(mWindowManager).shouldShowSizeCompatRestartButton(mTaskInfo); mWindowManager.createLayout(/* canShow= */ true); final LinearLayout sizeCompatHint = mLayout.findViewById(R.id.size_compat_hint); sizeCompatHint.performClick(); @@ -222,6 +223,9 @@ public class CompatUILayoutTest extends ShellTestCase { taskInfo.taskId = TASK_ID; taskInfo.appCompatTaskInfo.topActivityInSizeCompat = hasSizeCompat; taskInfo.appCompatTaskInfo.cameraCompatControlState = cameraCompatControlState; + taskInfo.appCompatTaskInfo.topActivityLetterboxHeight = 1000; + taskInfo.appCompatTaskInfo.topActivityLetterboxWidth = 1000; + taskInfo.configuration.windowConfiguration.setBounds(new Rect(0, 0, 2000, 2000)); return taskInfo; } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java index 2acfd83084ab..4f261cd79d39 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java @@ -20,7 +20,6 @@ import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_DISMISSED; import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_HIDDEN; import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_APPLIED; import static android.app.AppCompatTaskInfo.CAMERA_COMPAT_CONTROL_TREATMENT_SUGGESTED; -import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT; import static android.view.WindowInsets.Type.navigationBars; @@ -86,6 +85,8 @@ public class CompatUIWindowManagerTest extends ShellTestCase { public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT); private static final int TASK_ID = 1; + private static final int TASK_WIDTH = 2000; + private static final int TASK_HEIGHT = 2000; @Mock private SyncTransactionQueue mSyncTransactionQueue; @Mock private CompatUIController.CompatUICallback mCallback; @@ -101,6 +102,7 @@ public class CompatUIWindowManagerTest extends ShellTestCase { @Before public void setUp() { MockitoAnnotations.initMocks(this); + doReturn(100).when(mCompatUIConfiguration).getHideSizeCompatRestartButtonTolerance(); mTaskInfo = createTaskInfo(/* hasSizeCompat= */ false, CAMERA_COMPAT_CONTROL_HIDDEN); mWindowManager = new CompatUIWindowManager(mContext, mTaskInfo, mSyncTransactionQueue, mCallback, mTaskListener, new DisplayLayout(), new CompatUIHintsState(), @@ -115,7 +117,6 @@ public class CompatUIWindowManagerTest extends ShellTestCase { public void testCreateSizeCompatButton() { // Doesn't create layout if show is false. mWindowManager.mHasSizeCompat = true; - doReturn(true).when(mWindowManager).shouldShowSizeCompatRestartButton(mTaskInfo); assertTrue(mWindowManager.createLayout(/* canShow= */ false)); verify(mWindowManager, never()).inflateLayout(); @@ -147,6 +148,13 @@ public class CompatUIWindowManagerTest extends ShellTestCase { mWindowManager.mHasSizeCompat = false; assertFalse(mWindowManager.createLayout(/* canShow= */ true)); + // Returns false and doesn't create layout if restart button should be hidden. + clearInvocations(mWindowManager); + mWindowManager.mHasSizeCompat = true; + mTaskInfo.appCompatTaskInfo.topActivityLetterboxWidth = TASK_WIDTH; + mTaskInfo.appCompatTaskInfo.topActivityLetterboxHeight = TASK_HEIGHT; + assertFalse(mWindowManager.createLayout(/* canShow= */ true)); + verify(mWindowManager, never()).inflateLayout(); } @@ -293,8 +301,6 @@ public class CompatUIWindowManagerTest extends ShellTestCase { @Test public void testUpdateCompatInfoLayoutNotInflatedYet() { - mWindowManager.mHasSizeCompat = true; - doReturn(true).when(mWindowManager).shouldShowSizeCompatRestartButton(any()); mWindowManager.createLayout(/* canShow= */ false); verify(mWindowManager, never()).inflateLayout(); @@ -314,6 +320,15 @@ public class CompatUIWindowManagerTest extends ShellTestCase { mWindowManager.updateCompatInfo(taskInfo, mTaskListener, /* canShow= */ true); verify(mWindowManager).inflateLayout(); + + // Change shouldShowSizeCompatRestartButton to false and pass canShow true, layout + // shouldn't be inflated + clearInvocations(mWindowManager); + taskInfo.appCompatTaskInfo.topActivityLetterboxWidth = TASK_WIDTH; + taskInfo.appCompatTaskInfo.topActivityLetterboxHeight = TASK_HEIGHT; + mWindowManager.updateCompatInfo(taskInfo, mTaskListener, /* canShow= */ true); + + verify(mWindowManager, never()).inflateLayout(); } @Test @@ -364,7 +379,6 @@ public class CompatUIWindowManagerTest extends ShellTestCase { // Create button if it is not created. mWindowManager.mLayout = null; mWindowManager.mHasSizeCompat = true; - doReturn(true).when(mWindowManager).shouldShowSizeCompatRestartButton(mTaskInfo); mWindowManager.updateVisibility(/* canShow= */ true); verify(mWindowManager).createLayout(/* canShow= */ true); @@ -489,7 +503,6 @@ public class CompatUIWindowManagerTest extends ShellTestCase { TaskInfo taskInfo = createTaskInfo(true, CAMERA_COMPAT_CONTROL_HIDDEN); taskInfo.configuration.windowConfiguration.setBounds(new Rect(0, 0, 2000, 2000)); taskInfo.appCompatTaskInfo.topActivityLetterboxHeight = 2000; - taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); taskInfo.appCompatTaskInfo.topActivityLetterboxWidth = 1850; assertFalse(mWindowManager.shouldShowSizeCompatRestartButton(taskInfo)); @@ -514,6 +527,11 @@ public class CompatUIWindowManagerTest extends ShellTestCase { taskInfo.appCompatTaskInfo.topActivityInSizeCompat = hasSizeCompat; taskInfo.appCompatTaskInfo.cameraCompatControlState = cameraCompatControlState; taskInfo.configuration.uiMode &= ~Configuration.UI_MODE_TYPE_DESK; + // Letterboxed activity that takes half the screen should show size compat restart button + taskInfo.configuration.windowConfiguration.setBounds( + new Rect(0, 0, TASK_WIDTH, TASK_HEIGHT)); + taskInfo.appCompatTaskInfo.topActivityLetterboxHeight = 1000; + taskInfo.appCompatTaskInfo.topActivityLetterboxWidth = 1000; return taskInfo; } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipDoubleTapHelperTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipDoubleTapHelperTest.java index 0f8db85dcef4..b583acda1c9a 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipDoubleTapHelperTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipDoubleTapHelperTest.java @@ -16,10 +16,10 @@ package com.android.wm.shell.pip.phone; -import static com.android.wm.shell.pip.phone.PipDoubleTapHelper.SIZE_SPEC_CUSTOM; -import static com.android.wm.shell.pip.phone.PipDoubleTapHelper.SIZE_SPEC_DEFAULT; -import static com.android.wm.shell.pip.phone.PipDoubleTapHelper.SIZE_SPEC_MAX; -import static com.android.wm.shell.pip.phone.PipDoubleTapHelper.nextSizeSpec; +import static com.android.wm.shell.common.pip.PipDoubleTapHelper.SIZE_SPEC_CUSTOM; +import static com.android.wm.shell.common.pip.PipDoubleTapHelper.SIZE_SPEC_DEFAULT; +import static com.android.wm.shell.common.pip.PipDoubleTapHelper.SIZE_SPEC_MAX; +import static com.android.wm.shell.common.pip.PipDoubleTapHelper.nextSizeSpec; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -30,6 +30,7 @@ import android.testing.AndroidTestingRunner; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.pip.PipBoundsState; +import com.android.wm.shell.common.pip.PipDoubleTapHelper; import org.junit.Assert; import org.junit.Before; @@ -38,7 +39,7 @@ import org.junit.runner.RunWith; import org.mockito.Mock; /** - * Unit test against {@link PipDoubleTapHelper}. + * Unit test against {@link com.android.wm.shell.common.pip.PipDoubleTapHelper}. */ @RunWith(AndroidTestingRunner.class) public class PipDoubleTapHelperTest extends ShellTestCase { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java index 12a5594ae1da..7f3bfbb0e81d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java @@ -421,6 +421,15 @@ public class SplitScreenControllerTests extends ShellTestCase { assertEquals(false, controller.supportsMultiInstanceSplit(component)); } + @Test + public void testSwitchSplitPosition_checksIsSplitScreenVisible() { + final String reason = "test"; + when(mSplitScreenController.isSplitScreenVisible()).thenReturn(true, false); + mSplitScreenController.switchSplitPosition(reason); + mSplitScreenController.switchSplitPosition(reason); + verify(mStageCoordinator, times(1)).switchSplitPosition(reason); + } + private Intent createStartIntent(String activityName) { Intent intent = new Intent(); intent.setComponent(new ComponentName(mContext, activityName)); diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp index b40b73c111d0..0abb6f5ed011 100644 --- a/libs/hwui/Android.bp +++ b/libs/hwui/Android.bp @@ -229,15 +229,6 @@ filegroup { path: "apex/java", } -java_api_contribution { - name: "framework-graphics-public-stubs", - api_surface: "public", - api_file: "api/current.txt", - visibility: [ - "//build/orchestrator/apis", - ], -} - // ------------------------ // APEX // ------------------------ diff --git a/media/java/android/media/MediaRouter2.java b/media/java/android/media/MediaRouter2.java index 691aa7784d7a..425db06ce55f 100644 --- a/media/java/android/media/MediaRouter2.java +++ b/media/java/android/media/MediaRouter2.java @@ -183,23 +183,23 @@ public final class MediaRouter2 { * preference} passed by a proxy router. Use {@link RouteDiscoveryPreference#EMPTY} when * setting a route callback. * <li> - * <p>Methods returning non-system {@link RoutingController controllers} always return - * new instances with the latest data. Do not attempt to compare or store them. Instead, - * use {@link #getController(String)} or {@link #getControllers()} to query the most + * <p>Methods returning non-system {@link RoutingController controllers} always return new + * instances with the latest data. Do not attempt to compare or store them. Instead, use + * {@link #getController(String)} or {@link #getControllers()} to query the most * up-to-date state. * <li> * <p>Calls to {@link #setOnGetControllerHintsListener} are ignored. * </ul> * * @param clientPackageName the package name of the app to control - * @throws SecurityException if the caller doesn't have {@link - * Manifest.permission#MEDIA_CONTENT_CONTROL MEDIA_CONTENT_CONTROL} permission. - * @hide + * @return a proxy MediaRouter2 instance if {@code clientPackageName} exists or {@code null}. */ - // TODO (b/311711420): Deprecate once #getInstance(Context, String, UserHandle) reaches public - // SDK. - @SystemApi - @RequiresPermission(Manifest.permission.MEDIA_CONTENT_CONTROL) + @FlaggedApi(FLAG_ENABLE_CROSS_USER_ROUTING_IN_MEDIA_ROUTER2) + @RequiresPermission( + anyOf = { + Manifest.permission.MEDIA_CONTENT_CONTROL, + Manifest.permission.MEDIA_ROUTING_CONTROL + }) @Nullable public static MediaRouter2 getInstance( @NonNull Context context, @NonNull String clientPackageName) { @@ -226,9 +226,9 @@ public final class MediaRouter2 { * {@link RouteDiscoveryPreference.Builder#setPreferredFeatures(List) preferred features} * when setting a route callback. * <li> - * <p>Methods returning non-system {@link RoutingController controllers} always return - * new instances with the latest data. Do not attempt to compare or store them. Instead, - * use {@link #getController(String)} or {@link #getControllers()} to query the most + * <p>Methods returning non-system {@link RoutingController controllers} always return new + * instances with the latest data. Do not attempt to compare or store them. Instead, use + * {@link #getController(String)} or {@link #getControllers()} to query the most * up-to-date state. * <li> * <p>Calls to {@link #setOnGetControllerHintsListener} are ignored. @@ -242,8 +242,8 @@ public final class MediaRouter2 { * @throws SecurityException if {@code user} does not match {@link Process#myUserHandle()} and * the caller does not hold {@code Manifest.permission#INTERACT_ACROSS_USERS_FULL}. * @throws IllegalArgumentException if {@code clientPackageName} does not exist in {@code user}. + * @hide */ - @FlaggedApi(FLAG_ENABLE_CROSS_USER_ROUTING_IN_MEDIA_ROUTER2) @RequiresPermission( anyOf = { Manifest.permission.MEDIA_CONTENT_CONTROL, @@ -251,9 +251,7 @@ public final class MediaRouter2 { }) @NonNull public static MediaRouter2 getInstance( - @NonNull Context context, - @NonNull String clientPackageName, - @NonNull UserHandle user) { + @NonNull Context context, @NonNull String clientPackageName, @NonNull UserHandle user) { return findOrCreateProxyInstanceForCallingUser(context, clientPackageName, user); } diff --git a/media/java/android/media/RoutingSessionInfo.java b/media/java/android/media/RoutingSessionInfo.java index d28c26df6749..2202766ef016 100644 --- a/media/java/android/media/RoutingSessionInfo.java +++ b/media/java/android/media/RoutingSessionInfo.java @@ -182,7 +182,7 @@ public final class RoutingSessionInfo implements Parcelable { mControlHints = src.readBundle(); mIsSystemSession = src.readBoolean(); mTransferReason = src.readInt(); - mTransferInitiatorUserHandle = src.readParcelable(null, android.os.UserHandle.class); + mTransferInitiatorUserHandle = UserHandle.readFromParcel(src); mTransferInitiatorPackageName = src.readString(); } @@ -417,11 +417,7 @@ public final class RoutingSessionInfo implements Parcelable { dest.writeBundle(mControlHints); dest.writeBoolean(mIsSystemSession); dest.writeInt(mTransferReason); - if (mTransferInitiatorUserHandle != null) { - mTransferInitiatorUserHandle.writeToParcel(dest, /* flags= */ 0); - } else { - dest.writeParcelable(null, /* flags= */ 0); - } + UserHandle.writeToParcel(mTransferInitiatorUserHandle, dest); dest.writeString(mTransferInitiatorPackageName); } diff --git a/media/java/android/media/flags/editing.aconfig b/media/java/android/media/flags/editing.aconfig new file mode 100644 index 000000000000..c3997e94622d --- /dev/null +++ b/media/java/android/media/flags/editing.aconfig @@ -0,0 +1,8 @@ +package: "com.android.media.editing.flags" + +flag { + name: "add_media_metrics_editing" + namespace: "media_solutions" + description: "Add media metrics for transcoding/editing events." + bug: "297487694" +} diff --git a/media/java/android/media/metrics/EditingEndedEvent.aidl b/media/java/android/media/metrics/EditingEndedEvent.aidl new file mode 100644 index 000000000000..e099deaa6836 --- /dev/null +++ b/media/java/android/media/metrics/EditingEndedEvent.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.media.metrics; + +parcelable EditingEndedEvent; diff --git a/media/java/android/media/metrics/EditingEndedEvent.java b/media/java/android/media/metrics/EditingEndedEvent.java new file mode 100644 index 000000000000..72e6db8d987f --- /dev/null +++ b/media/java/android/media/metrics/EditingEndedEvent.java @@ -0,0 +1,325 @@ +/* + * 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.media.metrics; + +import static com.android.media.editing.flags.Flags.FLAG_ADD_MEDIA_METRICS_EDITING; + +import android.annotation.FlaggedApi; +import android.annotation.IntDef; +import android.annotation.IntRange; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; + +import java.lang.annotation.Retention; +import java.util.Objects; + +/** Event for an editing operation having ended. */ +@FlaggedApi(FLAG_ADD_MEDIA_METRICS_EDITING) +public final class EditingEndedEvent extends Event implements Parcelable { + + // The special value 0 is reserved for the field being unspecified in the proto. + + /** The editing operation was successful. */ + public static final int FINAL_STATE_SUCCEEDED = 1; + + /** The editing operation was canceled. */ + public static final int FINAL_STATE_CANCELED = 2; + + /** The editing operation failed due to an error. */ + public static final int FINAL_STATE_ERROR = 3; + + /** @hide */ + @IntDef( + prefix = {"FINAL_STATE_"}, + value = { + FINAL_STATE_SUCCEEDED, + FINAL_STATE_CANCELED, + FINAL_STATE_ERROR, + }) + @Retention(java.lang.annotation.RetentionPolicy.SOURCE) + public @interface FinalState {} + + private final @FinalState int mFinalState; + + // The special value 0 is reserved for the field being unspecified in the proto. + + /** Special value representing that no error occurred. */ + public static final int ERROR_CODE_NONE = 1; + + /** Error code for unexpected runtime errors. */ + public static final int ERROR_CODE_FAILED_RUNTIME_CHECK = 2; + + /** Error code for non-specific errors during input/output. */ + public static final int ERROR_CODE_IO_UNSPECIFIED = 3; + + /** Error code for network connection failures. */ + public static final int ERROR_CODE_IO_NETWORK_CONNECTION_FAILED = 4; + + /** Error code for network timeouts. */ + public static final int ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT = 5; + + /** Caused by an HTTP server returning an unexpected HTTP response status code. */ + public static final int ERROR_CODE_IO_BAD_HTTP_STATUS = 6; + + /** Caused by a non-existent file. */ + public static final int ERROR_CODE_IO_FILE_NOT_FOUND = 7; + + /** + * Caused by lack of permission to perform an IO operation. For example, lack of permission to + * access internet or external storage. + */ + public static final int ERROR_CODE_IO_NO_PERMISSION = 8; + + /** */ + public static final int ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED = 9; + + /** Caused by reading data out of the data bounds. */ + public static final int ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE = 10; + + /** Caused by a decoder initialization failure. */ + public static final int ERROR_CODE_DECODER_INIT_FAILED = 11; + + /** Caused by a failure while trying to decode media samples. */ + public static final int ERROR_CODE_DECODING_FAILED = 12; + + /** Caused by trying to decode content whose format is not supported. */ + public static final int ERROR_CODE_DECODING_FORMAT_UNSUPPORTED = 13; + + /** Caused by an encoder initialization failure. */ + public static final int ERROR_CODE_ENCODER_INIT_FAILED = 14; + + /** Caused by a failure while trying to encode media samples. */ + public static final int ERROR_CODE_ENCODING_FAILED = 15; + + /** Caused by trying to encode content whose format is not supported. */ + public static final int ERROR_CODE_ENCODING_FORMAT_UNSUPPORTED = 16; + + /** Caused by a video frame processing failure. */ + public static final int ERROR_CODE_VIDEO_FRAME_PROCESSING_FAILED = 17; + + /** Caused by an audio processing failure. */ + public static final int ERROR_CODE_AUDIO_PROCESSING_FAILED = 18; + + /** Caused by a failure while muxing media samples. */ + public static final int ERROR_CODE_MUXING_FAILED = 19; + + /** @hide */ + @IntDef( + prefix = {"ERROR_CODE_"}, + value = { + ERROR_CODE_NONE, + ERROR_CODE_FAILED_RUNTIME_CHECK, + ERROR_CODE_IO_UNSPECIFIED, + ERROR_CODE_IO_NETWORK_CONNECTION_FAILED, + ERROR_CODE_IO_NETWORK_CONNECTION_TIMEOUT, + ERROR_CODE_IO_BAD_HTTP_STATUS, + ERROR_CODE_IO_FILE_NOT_FOUND, + ERROR_CODE_IO_NO_PERMISSION, + ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED, + ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE, + ERROR_CODE_DECODER_INIT_FAILED, + ERROR_CODE_DECODING_FAILED, + ERROR_CODE_DECODING_FORMAT_UNSUPPORTED, + ERROR_CODE_ENCODER_INIT_FAILED, + ERROR_CODE_ENCODING_FAILED, + ERROR_CODE_ENCODING_FORMAT_UNSUPPORTED, + ERROR_CODE_VIDEO_FRAME_PROCESSING_FAILED, + ERROR_CODE_AUDIO_PROCESSING_FAILED, + ERROR_CODE_MUXING_FAILED, + }) + @Retention(java.lang.annotation.RetentionPolicy.SOURCE) + public @interface ErrorCode {} + + private final @ErrorCode int mErrorCode; + @SuppressWarnings("HidingField") // Hiding field from superclass as for playback events. + private final long mTimeSinceCreatedMillis; + + private EditingEndedEvent( + @FinalState int finalState, + @ErrorCode int errorCode, + long timeSinceCreatedMillis, + @NonNull Bundle extras) { + mFinalState = finalState; + mErrorCode = errorCode; + mTimeSinceCreatedMillis = timeSinceCreatedMillis; + mMetricsBundle = extras.deepCopy(); + } + + /** Returns the state of the editing session when it ended. */ + @FinalState + public int getFinalState() { + return mFinalState; + } + + /** Returns the error code for a {@linkplain #FINAL_STATE_ERROR failed} editing session. */ + @ErrorCode + public int getErrorCode() { + return mErrorCode; + } + + /** + * Gets the elapsed time since creating of the editing session, in milliseconds, or -1 if + * unknown. + * + * @return The elapsed time since creating the editing session, in milliseconds, or -1 if + * unknown. + * @see LogSessionId + * @see EditingSession + */ + @Override + @IntRange(from = -1) + public long getTimeSinceCreatedMillis() { + return mTimeSinceCreatedMillis; + } + + /** + * Gets metrics-related information that is not supported by dedicated methods. + * + * <p>It is intended to be used for backwards compatibility by the metrics infrastructure. + */ + @Override + @NonNull + public Bundle getMetricsBundle() { + return mMetricsBundle; + } + + @Override + @NonNull + public String toString() { + return "PlaybackErrorEvent { " + + "finalState = " + + mFinalState + + ", " + + "errorCode = " + + mErrorCode + + ", " + + "timeSinceCreatedMillis = " + + mTimeSinceCreatedMillis + + " }"; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + EditingEndedEvent that = (EditingEndedEvent) o; + return mFinalState == that.mFinalState + && mErrorCode == that.mErrorCode + && mTimeSinceCreatedMillis == that.mTimeSinceCreatedMillis; + } + + @Override + public int hashCode() { + return Objects.hash(mFinalState, mErrorCode, mTimeSinceCreatedMillis); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mFinalState); + dest.writeInt(mErrorCode); + dest.writeLong(mTimeSinceCreatedMillis); + dest.writeBundle(mMetricsBundle); + } + + @Override + public int describeContents() { + return 0; + } + + private EditingEndedEvent(@NonNull Parcel in) { + int finalState = in.readInt(); + int errorCode = in.readInt(); + long timeSinceCreatedMillis = in.readLong(); + Bundle metricsBundle = in.readBundle(); + + mFinalState = finalState; + mErrorCode = errorCode; + mTimeSinceCreatedMillis = timeSinceCreatedMillis; + mMetricsBundle = metricsBundle; + } + + public static final @NonNull Creator<EditingEndedEvent> CREATOR = + new Creator<>() { + @Override + public EditingEndedEvent[] newArray(int size) { + return new EditingEndedEvent[size]; + } + + @Override + public EditingEndedEvent createFromParcel(@NonNull Parcel in) { + return new EditingEndedEvent(in); + } + }; + + /** Builder for {@link EditingEndedEvent} */ + @FlaggedApi(FLAG_ADD_MEDIA_METRICS_EDITING) + public static final class Builder { + private final @FinalState int mFinalState; + private @ErrorCode int mErrorCode; + private long mTimeSinceCreatedMillis; + private Bundle mMetricsBundle; + + /** + * Creates a new Builder. + * + * @param finalState The state of the editing session when it ended. + */ + public Builder(@FinalState int finalState) { + mFinalState = finalState; + mErrorCode = ERROR_CODE_NONE; + mTimeSinceCreatedMillis = -1; + mMetricsBundle = new Bundle(); + } + + /** + * Sets the elapsed time since creating the editing session, in milliseconds. + * + * @param timeSinceCreatedMillis The elapsed time since creating the editing session, in + * milliseconds, or -1 if the value is unknown. + * @see #getTimeSinceCreatedMillis() + */ + public @NonNull Builder setTimeSinceCreatedMillis( + @IntRange(from = -1) long timeSinceCreatedMillis) { + mTimeSinceCreatedMillis = timeSinceCreatedMillis; + return this; + } + + /** Sets the error code for a {@linkplain #FINAL_STATE_ERROR failed} editing session. */ + public @NonNull Builder setErrorCode(@ErrorCode int value) { + mErrorCode = value; + return this; + } + + /** + * Sets metrics-related information that is not supported by dedicated methods. + * + * <p>Used for backwards compatibility by the metrics infrastructure. + */ + public @NonNull Builder setMetricsBundle(@NonNull Bundle metricsBundle) { + mMetricsBundle = metricsBundle; + return this; + } + + /** Builds an instance. */ + public @NonNull EditingEndedEvent build() { + return new EditingEndedEvent( + mFinalState, mErrorCode, mTimeSinceCreatedMillis, mMetricsBundle); + } + } +} diff --git a/media/java/android/media/metrics/EditingSession.java b/media/java/android/media/metrics/EditingSession.java index 2ddf623b1ed3..964e12cfcc05 100644 --- a/media/java/android/media/metrics/EditingSession.java +++ b/media/java/android/media/metrics/EditingSession.java @@ -16,6 +16,9 @@ package android.media.metrics; +import static com.android.media.editing.flags.Flags.FLAG_ADD_MEDIA_METRICS_EDITING; + +import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; @@ -24,7 +27,8 @@ import com.android.internal.util.AnnotationValidations; import java.util.Objects; /** - * An instances of this class represents a session of media editing. + * Represents a session of media editing, for example, transcoding between formats, transmuxing or + * applying trimming or audio/video effects to a stream. */ public final class EditingSession implements AutoCloseable { private final @NonNull String mId; @@ -40,6 +44,13 @@ public final class EditingSession implements AutoCloseable { mLogSessionId = new LogSessionId(mId); } + /** Reports that an editing operation ended. */ + @FlaggedApi(FLAG_ADD_MEDIA_METRICS_EDITING) + public void reportEditingEndedEvent(@NonNull EditingEndedEvent editingEndedEvent) { + mManager.reportEditingEndedEvent(mId, editingEndedEvent); + } + + /** Returns the identifier for logging this session. */ public @NonNull LogSessionId getSessionId() { return mLogSessionId; } diff --git a/media/java/android/media/metrics/IMediaMetricsManager.aidl b/media/java/android/media/metrics/IMediaMetricsManager.aidl index 51b1cc27c8a7..e07ca67296b3 100644 --- a/media/java/android/media/metrics/IMediaMetricsManager.aidl +++ b/media/java/android/media/metrics/IMediaMetricsManager.aidl @@ -16,6 +16,7 @@ package android.media.metrics; +import android.media.metrics.EditingEndedEvent; import android.media.metrics.NetworkEvent; import android.media.metrics.PlaybackErrorEvent; import android.media.metrics.PlaybackMetrics; @@ -24,7 +25,7 @@ import android.media.metrics.TrackChangeEvent; import android.os.PersistableBundle; /** - * Interface to the playback manager service. + * Interface to the media metrics manager service. * @hide */ interface IMediaMetricsManager { @@ -37,6 +38,8 @@ interface IMediaMetricsManager { void reportPlaybackStateEvent(in String sessionId, in PlaybackStateEvent event, int userId); void reportTrackChangeEvent(in String sessionId, in TrackChangeEvent event, int userId); + void reportEditingEndedEvent(in String sessionId, in EditingEndedEvent event, int userId); + String getTranscodingSessionId(int userId); String getEditingSessionId(int userId); String getBundleSessionId(int userId); diff --git a/media/java/android/media/metrics/MediaMetricsManager.java b/media/java/android/media/metrics/MediaMetricsManager.java index 0898874c2f65..622b0c158b8a 100644 --- a/media/java/android/media/metrics/MediaMetricsManager.java +++ b/media/java/android/media/metrics/MediaMetricsManager.java @@ -193,4 +193,18 @@ public final class MediaMetricsManager { throw e.rethrowFromSystemServer(); } } + + /** + * Reports the event of an editing session ending. + * + * @hide + */ + public void reportEditingEndedEvent( + @NonNull String sessionId, EditingEndedEvent editingEndedEvent) { + try { + mService.reportEditingEndedEvent(sessionId, editingEndedEvent, mUserId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } } diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl index e3dba03d6093..7b5853169923 100644 --- a/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl +++ b/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl @@ -61,7 +61,8 @@ oneway interface ITvInteractiveAppClient { void onSetTvRecordingInfo(in String recordingId, in TvRecordingInfo recordingInfo, int seq); void onRequestTvRecordingInfo(in String recordingId, int seq); void onRequestTvRecordingInfoList(in int type, int seq); - void onRequestSigning( - in String id, in String algorithm, in String alias, in byte[] data, int seq); + void onRequestSigning(in String id, in String algorithm, in String alias, in byte[] data, + int seq); + void onRequestCertificate(in String host, int port, int seq); void onAdRequest(in AdRequest request, int Seq); } diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl index 0f58b29247bb..1b9450b240ac 100644 --- a/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl +++ b/media/java/android/media/tv/interactive/ITvInteractiveAppManager.aidl @@ -58,6 +58,8 @@ interface ITvInteractiveAppManager { void sendAvailableSpeeds(in IBinder sessionToken, in float[] speeds, int userId); void sendSigningResult(in IBinder sessionToken, in String signingId, in byte[] result, int userId); + void sendCertificate(in IBinder sessionToken, in String host, int port, + in Bundle certBundle, int userId); void sendTvRecordingInfo(in IBinder sessionToken, in TvRecordingInfo recordingInfo, int userId); void sendTvRecordingInfoList(in IBinder sessionToken, in List<TvRecordingInfo> recordingInfoList, int userId); diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl index 06808c9ff915..3969315ab655 100644 --- a/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl +++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSession.aidl @@ -49,6 +49,7 @@ oneway interface ITvInteractiveAppSession { void sendTimeShiftMode(int mode); void sendAvailableSpeeds(in float[] speeds); void sendSigningResult(in String signingId, in byte[] result); + void sendCertificate(in String host, int port, in Bundle certBundle); void sendTvRecordingInfo(in TvRecordingInfo recordingInfo); void sendTvRecordingInfoList(in List<TvRecordingInfo> recordingInfoList); void notifyError(in String errMsg, in Bundle params); diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl index 416b8f12d5ea..cb89181fd714 100644 --- a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl +++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl @@ -61,5 +61,6 @@ oneway interface ITvInteractiveAppSessionCallback { void onRequestTvRecordingInfo(in String recordingId); void onRequestTvRecordingInfoList(in int type); void onRequestSigning(in String id, in String algorithm, in String alias, in byte[] data); + void onRequestCertificate(in String host, int port); void onAdRequest(in AdRequest request); } diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java index 77730aa46d0a..ec6c2bfab576 100644 --- a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java +++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionWrapper.java @@ -104,6 +104,7 @@ public class ITvInteractiveAppSessionWrapper private static final int DO_SEND_AVAILABLE_SPEEDS = 47; private static final int DO_SEND_SELECTED_TRACK_INFO = 48; private static final int DO_NOTIFY_VIDEO_FREEZE_UPDATED = 49; + private static final int DO_SEND_CERTIFICATE = 50; private final HandlerCaller mCaller; private Session mSessionImpl; @@ -369,6 +370,13 @@ public class ITvInteractiveAppSessionWrapper mSessionImpl.notifyVideoFreezeUpdated((Boolean) msg.obj); break; } + case DO_SEND_CERTIFICATE: { + SomeArgs args = (SomeArgs) msg.obj; + mSessionImpl.sendCertificate((String) args.arg1, (Integer) args.arg2, + (Bundle) args.arg3); + args.recycle(); + break; + } default: { Log.w(TAG, "Unhandled message code: " + msg.what); break; @@ -483,6 +491,12 @@ public class ITvInteractiveAppSessionWrapper } @Override + public void sendCertificate(@NonNull String host, int port, @NonNull Bundle certBundle) { + mCaller.executeOrSendMessage( + mCaller.obtainMessageOOO(DO_SEND_CERTIFICATE, host, port, certBundle)); + } + + @Override public void notifyError(@NonNull String errMsg, @NonNull Bundle params) { mCaller.executeOrSendMessage( mCaller.obtainMessageOO(DO_NOTIFY_ERROR, errMsg, params)); diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java index 8a340f6862bb..011744f94edb 100755 --- a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java +++ b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java @@ -34,6 +34,7 @@ import android.media.tv.TvInputManager; import android.media.tv.TvRecordingInfo; import android.media.tv.TvTrackInfo; import android.net.Uri; +import android.net.http.SslCertificate; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; @@ -656,6 +657,18 @@ public final class TvInteractiveAppManager { } @Override + public void onRequestCertificate(String host, int port, int seq) { + synchronized (mSessionCallbackRecordMap) { + SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); + if (record == null) { + Log.e(TAG, "Callback not found for seq " + seq); + return; + } + record.postRequestCertificate(host, port); + } + } + + @Override public void onSessionStateChanged(int state, int err, int seq) { synchronized (mSessionCallbackRecordMap) { SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); @@ -1328,6 +1341,19 @@ public final class TvInteractiveAppManager { } } + void sendCertificate(@NonNull String host, int port, @NonNull SslCertificate cert) { + if (mToken == null) { + Log.w(TAG, "The session has been already released"); + return; + } + try { + mService.sendCertificate(mToken, host, port, SslCertificate.saveState(cert), + 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"); @@ -2232,6 +2258,15 @@ public final class TvInteractiveAppManager { }); } + void postRequestCertificate(String host, int port) { + mHandler.post(new Runnable() { + @Override + public void run() { + mSessionCallback.onRequestCertificate(mSession, host, port); + } + }); + } + void postRequestTvRecordingInfo(String recordingId) { mHandler.post(new Runnable() { @Override @@ -2574,6 +2609,17 @@ public final class TvInteractiveAppManager { } /** + * This is called when the service requests a SSL certificate for client validation. + * + * @param session A {@link TvInteractiveAppService.Session} associated with this callback. + * @param host the host name of the SSL authentication server. + * @param port the port of the SSL authentication server. E.g., 443 + * @hide + */ + public void onRequestCertificate(Session session, String host, int port) { + } + + /** * This is called when {@link TvInteractiveAppService.Session#notifySessionStateChanged} is * called. * diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppService.java b/media/java/android/media/tv/interactive/TvInteractiveAppService.java index 5247a0ebe6e0..054b272d820f 100755 --- a/media/java/android/media/tv/interactive/TvInteractiveAppService.java +++ b/media/java/android/media/tv/interactive/TvInteractiveAppService.java @@ -46,6 +46,7 @@ import android.media.tv.TvTrackInfo; import android.media.tv.TvView; import android.media.tv.interactive.TvInteractiveAppView.TvInteractiveAppCallback; import android.net.Uri; +import android.net.http.SslCertificate; import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; @@ -734,6 +735,17 @@ public abstract class TvInteractiveAppService extends Service { } /** + * Receives the requested Certificate + * + * @param host the host name of the SSL authentication server. + * @param port the port of the SSL authentication server. E.g., 443 + * @param cert the SSL certificate received. + * @hide + */ + public void onCertificate(@NonNull String host, int port, @NonNull SslCertificate cert) { + } + + /** * Called when the application sends information of an error. * * @param errMsg the message of the error. @@ -1633,6 +1645,32 @@ public abstract class TvInteractiveAppService extends Service { } /** + * Requests a SSL certificate for client validation. + * + * @param host the host name of the SSL authentication server. + * @param port the port of the SSL authentication server. E.g., 443 + * @hide + */ + public void requestCertificate(@NonNull String host, int port) { + executeOrPostRunnableOnMainThread(new Runnable() { + @MainThread + @Override + public void run() { + try { + if (DEBUG) { + Log.d(TAG, "requestCertificate"); + } + if (mSessionCallback != null) { + mSessionCallback.onRequestCertificate(host, port); + } + } catch (RemoteException e) { + Log.w(TAG, "error in requestCertificate", e); + } + } + }); + } + + /** * Sends an advertisement request to be processed by the related TV input. * * @param request The advertisement request @@ -1725,6 +1763,11 @@ public abstract class TvInteractiveAppService extends Service { onSigningResult(signingId, result); } + void sendCertificate(String host, int port, Bundle certBundle) { + SslCertificate cert = SslCertificate.restoreState(certBundle); + onCertificate(host, port, cert); + } + void notifyError(String errMsg, Bundle params) { onError(errMsg, params); } diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppView.java b/media/java/android/media/tv/interactive/TvInteractiveAppView.java index 5bb61c261ae2..3b295742c244 100755 --- a/media/java/android/media/tv/interactive/TvInteractiveAppView.java +++ b/media/java/android/media/tv/interactive/TvInteractiveAppView.java @@ -34,6 +34,7 @@ import android.media.tv.interactive.TvInteractiveAppManager.Session; import android.media.tv.interactive.TvInteractiveAppManager.Session.FinishedInputEventCallback; import android.media.tv.interactive.TvInteractiveAppManager.SessionCallback; import android.net.Uri; +import android.net.http.SslCertificate; import android.os.Bundle; import android.os.Handler; import android.util.AttributeSet; @@ -756,6 +757,22 @@ public class TvInteractiveAppView extends ViewGroup { } /** + * Send the requested SSL certificate to the TV Interactive App + * @param host the host name of the SSL authentication server. + * @param port the port of the SSL authentication server. E.g., 443 + * @param cert the SSL certificate requested + * @hide + */ + public void sendCertificate(@NonNull String host, int port, @NonNull SslCertificate cert) { + if (DEBUG) { + Log.d(TAG, "sendCertificate"); + } + if (mSession != null) { + mSession.sendCertificate(host, port, cert); + } + } + + /** * Notifies the corresponding {@link TvInteractiveAppService} when there is an error. * * @param errMsg the message of the error. 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/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/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/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppRepository.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppRepository.kt index 81a8b324f70f..cea3d13e27ab 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppRepository.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppRepository.kt @@ -37,6 +37,7 @@ fun rememberAppRepository(): AppRepository = rememberContext(::AppRepositoryImpl interface AppRepository { fun loadLabel(app: ApplicationInfo): String + @Suppress("ABSTRACT_COMPOSABLE_DEFAULT_PARAMETER_VALUE") @Composable fun produceLabel(app: ApplicationInfo, isClonedAppPage: Boolean = false): State<String> { val context = LocalContext.current diff --git a/packages/SettingsLib/res/drawable/ic_external_display.xml b/packages/SettingsLib/res/drawable/ic_external_display.xml new file mode 100644 index 000000000000..de50de8c07c8 --- /dev/null +++ b/packages/SettingsLib/res/drawable/ic_external_display.xml @@ -0,0 +1,28 @@ +<!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="25dp" + android:viewportWidth="24" + android:viewportHeight="25"> + <group> + <clip-path + android:pathData="M0,0.307h24v24h-24z"/> + <path + android:pathData="M8,21.307V19.307H10V17.307H4C3.45,17.307 2.975,17.115 2.575,16.732C2.192,16.332 2,15.857 2,15.307V5.307C2,4.757 2.192,4.29 2.575,3.907C2.975,3.507 3.45,3.307 4,3.307H20C20.55,3.307 21.017,3.507 21.4,3.907C21.8,4.29 22,4.757 22,5.307V15.307C22,15.857 21.8,16.332 21.4,16.732C21.017,17.115 20.55,17.307 20,17.307H14V19.307H16V21.307H8ZM4,15.307H20V5.307H4V15.307ZM4,15.307V5.307V15.307Z" + android:fillColor="#E5E3D6"/> + </group> +</vector> diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HapClientProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HapClientProfile.java index cf4d6be9a042..0613676113f5 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HapClientProfile.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HapClientProfile.java @@ -99,6 +99,8 @@ public class HapClientProfile implements LocalBluetoothProfile { device.refresh(); } + // Check current list of CachedDevices to see if any are hearing aid devices. + mDeviceManager.updateHearingAidsDevices(); mIsProfileReady = true; mProfileManager.callServiceConnectedListeners(); } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java index 3a15b7108e3f..9fd174d4586c 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java @@ -15,8 +15,8 @@ */ package com.android.settingslib.bluetooth; -import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothHearingAid; +import android.bluetooth.BluetoothLeAudio; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothUuid; import android.bluetooth.le.ScanFilter; @@ -68,14 +68,9 @@ public class HearingAidDeviceManager { void initHearingAidDeviceIfNeeded(CachedBluetoothDevice newDevice, List<ScanFilter> leScanFilters) { - long hiSyncId = getHiSyncId(newDevice.getDevice()); - if (isValidHiSyncId(hiSyncId)) { - // Once hiSyncId is valid, assign hearing aid info - final HearingAidInfo.Builder infoBuilder = new HearingAidInfo.Builder() - .setAshaDeviceSide(getDeviceSide(newDevice.getDevice())) - .setAshaDeviceMode(getDeviceMode(newDevice.getDevice())) - .setHiSyncId(hiSyncId); - newDevice.setHearingAidInfo(infoBuilder.build()); + HearingAidInfo info = generateHearingAidInfo(newDevice); + if (info != null) { + newDevice.setHearingAidInfo(info); } else if (leScanFilters != null && !newDevice.isHearingAidDevice()) { // If the device is added with hearing aid scan filter during pairing, set an empty // hearing aid info to indicate it's a hearing aid device. The info will be updated @@ -94,38 +89,6 @@ public class HearingAidDeviceManager { } } - private long getHiSyncId(BluetoothDevice device) { - final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager(); - final HearingAidProfile profileProxy = profileManager.getHearingAidProfile(); - if (profileProxy == null) { - return BluetoothHearingAid.HI_SYNC_ID_INVALID; - } - - return profileProxy.getHiSyncId(device); - } - - private int getDeviceSide(BluetoothDevice device) { - final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager(); - final HearingAidProfile profileProxy = profileManager.getHearingAidProfile(); - if (profileProxy == null) { - Log.w(TAG, "HearingAidProfile is not supported and not ready to fetch device side"); - return HearingAidProfile.DeviceSide.SIDE_INVALID; - } - - return profileProxy.getDeviceSide(device); - } - - private int getDeviceMode(BluetoothDevice device) { - final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager(); - final HearingAidProfile profileProxy = profileManager.getHearingAidProfile(); - if (profileProxy == null) { - Log.w(TAG, "HearingAidProfile is not supported and not ready to fetch device mode"); - return HearingAidProfile.DeviceMode.MODE_INVALID; - } - - return profileProxy.getDeviceMode(device); - } - boolean setSubDeviceIfNeeded(CachedBluetoothDevice newDevice) { final long hiSyncId = newDevice.getHiSyncId(); if (isValidHiSyncId(hiSyncId)) { @@ -157,21 +120,17 @@ public class HearingAidDeviceManager { // To collect all HearingAid devices and call #onHiSyncIdChanged to group device by HiSyncId void updateHearingAidsDevices() { - final Set<Long> newSyncIdSet = new HashSet<Long>(); + final Set<Long> newSyncIdSet = new HashSet<>(); for (CachedBluetoothDevice cachedDevice : mCachedDevices) { // Do nothing if HiSyncId has been assigned - if (!isValidHiSyncId(cachedDevice.getHiSyncId())) { - final long newHiSyncId = getHiSyncId(cachedDevice.getDevice()); - // Do nothing if there is no HiSyncId on Bluetooth device - if (isValidHiSyncId(newHiSyncId)) { - // Once hiSyncId is valid, assign hearing aid info - final HearingAidInfo.Builder infoBuilder = new HearingAidInfo.Builder() - .setAshaDeviceSide(getDeviceSide(cachedDevice.getDevice())) - .setAshaDeviceMode(getDeviceMode(cachedDevice.getDevice())) - .setHiSyncId(newHiSyncId); - cachedDevice.setHearingAidInfo(infoBuilder.build()); - - newSyncIdSet.add(newHiSyncId); + if (isValidHiSyncId(cachedDevice.getHiSyncId())) { + continue; + } + HearingAidInfo info = generateHearingAidInfo(cachedDevice); + if (info != null) { + cachedDevice.setHearingAidInfo(info); + if (isValidHiSyncId(info.getHiSyncId())) { + newSyncIdSet.add(info.getHiSyncId()); } } } @@ -378,6 +337,54 @@ public class HearingAidDeviceManager { return null; } + private boolean isLeAudioHearingAid(CachedBluetoothDevice cachedDevice) { + List<LocalBluetoothProfile> profiles = cachedDevice.getProfiles(); + boolean supportLeAudio = profiles.stream().anyMatch(p -> p instanceof LeAudioProfile); + boolean supportHapClient = profiles.stream().anyMatch(p -> p instanceof HapClientProfile); + return supportLeAudio && supportHapClient; + } + + private boolean isAshaHearingAid(CachedBluetoothDevice cachedDevice) { + return cachedDevice.getProfiles().stream().anyMatch(p -> p instanceof HearingAidProfile); + } + + private HearingAidInfo generateHearingAidInfo(CachedBluetoothDevice cachedDevice) { + final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager(); + if (isAshaHearingAid(cachedDevice)) { + final HearingAidProfile asha = profileManager.getHearingAidProfile(); + if (asha == null) { + Log.w(TAG, "HearingAidProfile is not supported on this device"); + } else { + long hiSyncId = asha.getHiSyncId(cachedDevice.getDevice()); + if (isValidHiSyncId(hiSyncId)) { + final HearingAidInfo.Builder infoBuilder = new HearingAidInfo.Builder() + .setAshaDeviceSide(asha.getDeviceSide(cachedDevice.getDevice())) + .setAshaDeviceMode(asha.getDeviceMode(cachedDevice.getDevice())) + .setHiSyncId(hiSyncId); + return infoBuilder.build(); + } + } + } + if (isLeAudioHearingAid(cachedDevice)) { + final HapClientProfile hapClientProfile = profileManager.getHapClientProfile(); + final LeAudioProfile leAudioProfile = profileManager.getLeAudioProfile(); + if (hapClientProfile == null || leAudioProfile == null) { + Log.w(TAG, "HapClientProfile or LeAudioProfile is not supported on this device"); + } else { + int audioLocation = leAudioProfile.getAudioLocation(cachedDevice.getDevice()); + int hearingAidType = hapClientProfile.getHearingAidType(cachedDevice.getDevice()); + if (audioLocation != BluetoothLeAudio.AUDIO_LOCATION_INVALID + && hearingAidType != HapClientProfile.HearingAidType.TYPE_INVALID) { + final HearingAidInfo.Builder infoBuilder = new HearingAidInfo.Builder() + .setLeAudioLocation(audioLocation) + .setHapDeviceType(hearingAidType); + return infoBuilder.build(); + } + } + } + return null; + } + private void log(String msg) { if (DEBUG) { Log.d(TAG, msg); diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidProfile.java index 14fab16de3e6..f2450de60878 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidProfile.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidProfile.java @@ -109,7 +109,7 @@ public class HearingAidProfile implements LocalBluetoothProfile { device.refresh(); } - // Check current list of CachedDevices to see if any are Hearing Aid devices. + // Check current list of CachedDevices to see if any are hearing aid devices. mDeviceManager.updateHearingAidsDevices(); mIsProfileReady = true; mProfileManager.callServiceConnectedListeners(); diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java index 931a6f149b84..6be4336178eb 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LeAudioProfile.java @@ -83,6 +83,8 @@ public class LeAudioProfile implements LocalBluetoothProfile { device.refresh(); } + // Check current list of CachedDevices to see if any are hearing aid devices. + mDeviceManager.updateHearingAidsDevices(); mProfileManager.callServiceConnectedListeners(); mIsProfileReady = true; } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java index 934870507a20..1d2f7902b781 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java @@ -759,6 +759,20 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { } /** + * Update the LE Broadcast by calling {@link BluetoothLeBroadcast#updateBroadcast(int, + * BluetoothLeAudioContentMetadata)}, currently only updates programInfo. + */ + public void updateBroadcast() { + if (mServiceBroadcast == null) { + Log.d(TAG, "The BluetoothLeBroadcast is null when updating the broadcast."); + return; + } + String programInfo = getProgramInfo(); + mBluetoothLeAudioContentMetadata = mBuilder.setProgramInfo(programInfo).build(); + mServiceBroadcast.updateBroadcast(mBroadcastId, mBluetoothLeAudioContentMetadata); + } + + /** * Register Broadcast Callbacks to track its state and receivers * * @param executor Executor object for callback diff --git a/packages/SettingsLib/src/com/android/settingslib/media/DeviceIconUtil.java b/packages/SettingsLib/src/com/android/settingslib/media/DeviceIconUtil.java index cf224dc3be5f..3de49336f427 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/DeviceIconUtil.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/DeviceIconUtil.java @@ -71,15 +71,15 @@ public class DeviceIconUtil { new Device( AudioDeviceInfo.TYPE_HDMI, MediaRoute2Info.TYPE_HDMI, - mIsTv ? R.drawable.ic_tv : R.drawable.ic_headphone), + mIsTv ? R.drawable.ic_tv : R.drawable.ic_external_display), new Device( AudioDeviceInfo.TYPE_HDMI_ARC, MediaRoute2Info.TYPE_HDMI_ARC, - mIsTv ? R.drawable.ic_hdmi : R.drawable.ic_headphone), + mIsTv ? R.drawable.ic_hdmi : R.drawable.ic_external_display), new Device( AudioDeviceInfo.TYPE_HDMI_EARC, MediaRoute2Info.TYPE_HDMI_EARC, - mIsTv ? R.drawable.ic_hdmi : R.drawable.ic_headphone), + mIsTv ? R.drawable.ic_hdmi : R.drawable.ic_external_display), new Device( AudioDeviceInfo.TYPE_WIRED_HEADSET, MediaRoute2Info.TYPE_WIRED_HEADSET, diff --git a/packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java b/packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java index 8a122fcddcb3..aef09ac236f3 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/RouterInfoMediaManager.java @@ -26,11 +26,13 @@ import android.media.MediaRouter2Manager; import android.media.RouteDiscoveryPreference; import android.media.RouteListingPreference; import android.media.RoutingSessionInfo; +import android.os.Process; import android.text.TextUtils; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.media.flags.Flags; import com.android.settingslib.bluetooth.LocalBluetoothManager; import java.util.ArrayList; @@ -62,21 +64,33 @@ public final class RouterInfoMediaManager extends InfoMediaManager { refreshDevices(); }; - // TODO: b/192657812 - Create factory method in InfoMediaManager to return - // RouterInfoMediaManager or ManagerInfoMediaManager based on flag. + // TODO (b/321969740): Plumb target UserHandle between UMO and RouterInfoMediaManager. public RouterInfoMediaManager( Context context, String packageName, Notification notification, - LocalBluetoothManager localBluetoothManager) throws PackageNotAvailableException { + LocalBluetoothManager localBluetoothManager) + throws PackageNotAvailableException { super(context, packageName, notification, localBluetoothManager); - mRouter = MediaRouter2.getInstance(context, packageName); + MediaRouter2 router = null; - if (mRouter == null) { + if (Flags.enableCrossUserRoutingInMediaRouter2()) { + try { + router = MediaRouter2.getInstance(context, packageName, Process.myUserHandle()); + } catch (IllegalArgumentException ex) { + // Do nothing + } + } else { + router = MediaRouter2.getInstance(context, packageName); + } + if (router == null) { throw new PackageNotAvailableException( "Package name " + packageName + " does not exist."); } + // We have to defer initialization because mRouter is final. + mRouter = router; + mRouterManager = MediaRouter2Manager.getInstance(context); } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java index e7487e857464..aa5a2984e70c 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java @@ -15,6 +15,15 @@ */ package com.android.settingslib.bluetooth; +import static android.bluetooth.BluetoothHearingAid.HI_SYNC_ID_INVALID; +import static android.bluetooth.BluetoothLeAudio.AUDIO_LOCATION_FRONT_LEFT; +import static android.bluetooth.BluetoothLeAudio.AUDIO_LOCATION_INVALID; + +import static com.android.settingslib.bluetooth.HapClientProfile.HearingAidType.TYPE_BINAURAL; +import static com.android.settingslib.bluetooth.HapClientProfile.HearingAidType.TYPE_INVALID; +import static com.android.settingslib.bluetooth.HearingAidProfile.DeviceMode.MODE_BINAURAL; +import static com.android.settingslib.bluetooth.HearingAidProfile.DeviceSide.SIDE_RIGHT; + import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -32,7 +41,6 @@ import static org.mockito.Mockito.when; import android.bluetooth.BluetoothClass; import android.bluetooth.BluetoothDevice; -import android.bluetooth.BluetoothHearingAid; import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothUuid; import android.bluetooth.le.ScanFilter; @@ -92,6 +100,10 @@ public class HearingAidDeviceManagerTest { @Mock private HearingAidProfile mHearingAidProfile; @Mock + private LeAudioProfile mLeAudioProfile; + @Mock + private HapClientProfile mHapClientProfile; + @Mock private AudioProductStrategy mAudioStrategy; @Mock private BluetoothDevice mDevice1; @@ -123,6 +135,8 @@ public class HearingAidDeviceManagerTest { when(mLocalBluetoothManager.getEventManager()).thenReturn(mBluetoothEventManager); when(mLocalBluetoothManager.getProfileManager()).thenReturn(mLocalProfileManager); when(mLocalProfileManager.getHearingAidProfile()).thenReturn(mHearingAidProfile); + when(mLocalProfileManager.getLeAudioProfile()).thenReturn(mLeAudioProfile); + when(mLocalProfileManager.getHapClientProfile()).thenReturn(mHapClientProfile); when(mAudioStrategy.getAudioAttributesForLegacyStreamType( AudioManager.STREAM_MUSIC)) .thenReturn((new AudioAttributes.Builder()).build()); @@ -140,34 +154,43 @@ public class HearingAidDeviceManagerTest { } /** - * Test initHearingAidDeviceIfNeeded, set HearingAid's information, including HiSyncId, - * deviceSide, deviceMode. + * Test initHearingAidDeviceIfNeeded + * + * Conditions: + * 1) ASHA hearing aid + * 2) Valid HiSyncId + * Result: + * Set hearing aid info to the device. */ @Test - public void initHearingAidDeviceIfNeeded_validHiSyncId_setHearingAidInfo() { + public void initHearingAidDeviceIfNeeded_asha_validHiSyncId_setHearingAidInfo() { + when(mCachedDevice1.getProfiles()).thenReturn(List.of(mHearingAidProfile)); when(mHearingAidProfile.getHiSyncId(mDevice1)).thenReturn(HISYNCID1); - when(mHearingAidProfile.getDeviceMode(mDevice1)).thenReturn( - HearingAidProfile.DeviceMode.MODE_BINAURAL); - when(mHearingAidProfile.getDeviceSide(mDevice1)).thenReturn( - HearingAidProfile.DeviceSide.SIDE_RIGHT); + when(mHearingAidProfile.getDeviceMode(mDevice1)).thenReturn(MODE_BINAURAL); + when(mHearingAidProfile.getDeviceSide(mDevice1)).thenReturn(SIDE_RIGHT); assertThat(mCachedDevice1.getHiSyncId()).isNotEqualTo(HISYNCID1); mHearingAidDeviceManager.initHearingAidDeviceIfNeeded(mCachedDevice1, null); assertThat(mCachedDevice1.getHiSyncId()).isEqualTo(HISYNCID1); + assertThat(mCachedDevice1.getDeviceSide()).isEqualTo(HearingAidInfo.DeviceSide.SIDE_RIGHT); assertThat(mCachedDevice1.getDeviceMode()).isEqualTo( HearingAidInfo.DeviceMode.MODE_BINAURAL); - assertThat(mCachedDevice1.getDeviceSide()).isEqualTo( - HearingAidInfo.DeviceSide.SIDE_RIGHT); } /** - * Test initHearingAidDeviceIfNeeded, an invalid HiSyncId will not be assigned + * Test initHearingAidDeviceIfNeeded + * + * Conditions: + * 1) ASHA hearing aid + * 2) Invalid HiSyncId + * Result: + * Do not set hearing aid info to the device. */ @Test - public void initHearingAidDeviceIfNeeded_invalidHiSyncId_notToSetHearingAidInfo() { - when(mHearingAidProfile.getHiSyncId(mDevice1)).thenReturn( - BluetoothHearingAid.HI_SYNC_ID_INVALID); + public void initHearingAidDeviceIfNeeded_asha_invalidHiSyncId_notToSetHearingAidInfo() { + when(mCachedDevice1.getProfiles()).thenReturn(List.of(mHearingAidProfile)); + when(mHearingAidProfile.getHiSyncId(mDevice1)).thenReturn(HI_SYNC_ID_INVALID); mHearingAidDeviceManager.initHearingAidDeviceIfNeeded(mCachedDevice1, null); @@ -175,34 +198,89 @@ public class HearingAidDeviceManagerTest { } /** - * Test initHearingAidDeviceIfNeeded, an invalid HiSyncId and hearing aid scan filter, set an - * empty hearing aid info on the device. + * Test initHearingAidDeviceIfNeeded + * + * Conditions: + * 1) ASHA hearing aid + * 2) Invalid HiSyncId + * 3) ASHA uuid scan filter + * Result: + * Set an empty hearing aid info to the device. */ @Test - public void initHearingAidDeviceIfNeeded_hearingAidScanFilter_setHearingAidInfo() { - when(mHearingAidProfile.getHiSyncId(mDevice1)).thenReturn( - BluetoothHearingAid.HI_SYNC_ID_INVALID); + public void initHearingAidDeviceIfNeeded_asha_scanFilterNotNull_setEmptyHearingAidInfo() { + when(mCachedDevice1.getProfiles()).thenReturn(List.of(mHearingAidProfile)); + when(mHearingAidProfile.getHiSyncId(mDevice1)).thenReturn(HI_SYNC_ID_INVALID); final ScanFilter scanFilter = new ScanFilter.Builder() .setServiceData(BluetoothUuid.HEARING_AID, new byte[]{0}).build(); mHearingAidDeviceManager.initHearingAidDeviceIfNeeded(mCachedDevice1, List.of(scanFilter)); - assertThat(mCachedDevice1.isHearingAidDevice()).isTrue(); + verify(mCachedDevice1).setHearingAidInfo(new HearingAidInfo.Builder().build()); } /** - * Test initHearingAidDeviceIfNeeded, an invalid HiSyncId and random scan filter, not to set - * hearing aid info on the device. + * Test initHearingAidDeviceIfNeeded + * + * Conditions: + * 1) Asha hearing aid + * 2) Invalid HiSyncId + * 3) Random scan filter + * Result: + * Do not set hearing aid info to the device. */ @Test - public void initHearingAidDeviceIfNeeded_randomScanFilter_setHearingAidInfo() { - when(mHearingAidProfile.getHiSyncId(mDevice1)).thenReturn( - BluetoothHearingAid.HI_SYNC_ID_INVALID); + public void initHearingAidDeviceIfNeeded_asha_randomScanFilter_notToSetHearingAidInfo() { + when(mCachedDevice1.getProfiles()).thenReturn(List.of(mHearingAidProfile)); + when(mHearingAidProfile.getHiSyncId(mDevice1)).thenReturn(HI_SYNC_ID_INVALID); final ScanFilter scanFilter = new ScanFilter.Builder().build(); mHearingAidDeviceManager.initHearingAidDeviceIfNeeded(mCachedDevice1, List.of(scanFilter)); - assertThat(mCachedDevice1.isHearingAidDevice()).isFalse(); + verify(mCachedDevice1, never()).setHearingAidInfo(any(HearingAidInfo.class)); + } + + /** + * Test initHearingAidDeviceIfNeeded + * + * Conditions: + * 1) LeAudio hearing aid + * 2) Valid audio location and device type + * Result: + * Set hearing aid info to the device. + */ + @Test + public void initHearingAidDeviceIfNeeded_leAudio_validInfo_setHearingAidInfo() { + when(mCachedDevice1.getProfiles()).thenReturn(List.of(mLeAudioProfile, mHapClientProfile)); + when(mLeAudioProfile.getAudioLocation(mDevice1)).thenReturn(AUDIO_LOCATION_FRONT_LEFT); + when(mHapClientProfile.getHearingAidType(mDevice1)).thenReturn(TYPE_BINAURAL); + + mHearingAidDeviceManager.initHearingAidDeviceIfNeeded(mCachedDevice1, null); + + verify(mCachedDevice1).setHearingAidInfo(any(HearingAidInfo.class)); + assertThat(mCachedDevice1.getDeviceSide()).isEqualTo(HearingAidInfo.DeviceSide.SIDE_LEFT); + assertThat(mCachedDevice1.getDeviceMode()).isEqualTo( + HearingAidInfo.DeviceMode.MODE_BINAURAL); + } + + /** + * Test initHearingAidDeviceIfNeeded + * + * Conditions: + * 1) LeAudio hearing aid + * 2) Invalid audio location and device type + * Result: + * Do not set hearing aid info to the device. + */ + @Test + public void initHearingAidDeviceIfNeeded_leAudio_invalidInfo_notToSetHearingAidInfo() { + when(mCachedDevice1.getProfiles()).thenReturn(List.of(mLeAudioProfile, mHapClientProfile)); + when(mLeAudioProfile.getAudioLocation(mDevice1)).thenReturn(AUDIO_LOCATION_INVALID); + when(mHapClientProfile.getHearingAidType(mDevice1)).thenReturn(TYPE_INVALID); + + mHearingAidDeviceManager.initHearingAidDeviceIfNeeded(mCachedDevice1, null); + + verify(mCachedDevice1, never()).setHearingAidInfo(any(HearingAidInfo.class)); } /** @@ -234,13 +312,20 @@ public class HearingAidDeviceManagerTest { } /** - * Test updateHearingAidsDevices, to link two devices with the same HiSyncId. - * When first paired devices is connected and second paired device is disconnected, first - * paired device would be set as main device and second device will be removed from - * CachedDevices list. + * Test updateHearingAidsDevices + * + * Conditions: + * 1) Two ASHA hearing aids with the same HiSyncId + * 2) First paired devices is connected + * 3) Second paired device is disconnected + * Result: + * First paired device would be set as main device and second paired device will be set + * as sub device and removed from CachedDevices list. */ @Test - public void updateHearingAidsDevices_firstPairedDevicesConnected_verifySubDevice() { + public void updateHearingAidsDevices_asha_firstPairedDevicesConnected_verifySubDevice() { + when(mCachedDevice1.getProfiles()).thenReturn(List.of(mHearingAidProfile)); + when(mCachedDevice2.getProfiles()).thenReturn(List.of(mHearingAidProfile)); when(mHearingAidProfile.getHiSyncId(mDevice1)).thenReturn(HISYNCID1); when(mHearingAidProfile.getHiSyncId(mDevice2)).thenReturn(HISYNCID1); when(mCachedDevice1.isConnected()).thenReturn(true); @@ -257,13 +342,20 @@ public class HearingAidDeviceManagerTest { } /** - * Test updateHearingAidsDevices, to link two devices with the same HiSyncId. - * When second paired devices is connected and first paired device is disconnected, second - * paired device would be set as main device and first device will be removed from - * CachedDevices list. + * Test updateHearingAidsDevices + * + * Conditions: + * 1) Two ASHA hearing aids with the same HiSyncId + * 2) First paired devices is disconnected + * 3) Second paired device is connected + * Result: + * Second paired device would be set as main device and first paired device will be set + * as sub device and removed from CachedDevices list. */ @Test - public void updateHearingAidsDevices_secondPairedDeviceConnected_verifySubDevice() { + public void updateHearingAidsDevices_asha_secondPairedDeviceConnected_verifySubDevice() { + when(mCachedDevice1.getProfiles()).thenReturn(List.of(mHearingAidProfile)); + when(mCachedDevice2.getProfiles()).thenReturn(List.of(mHearingAidProfile)); when(mHearingAidProfile.getHiSyncId(mDevice1)).thenReturn(HISYNCID1); when(mHearingAidProfile.getHiSyncId(mDevice2)).thenReturn(HISYNCID1); when(mCachedDevice1.isConnected()).thenReturn(false); @@ -280,12 +372,20 @@ public class HearingAidDeviceManagerTest { } /** - * Test updateHearingAidsDevices, to link two devices with the same HiSyncId. - * When both devices are connected, to build up main and sub relationship and to remove sub - * device from CachedDevices list. + * Test updateHearingAidsDevices + * + * Conditions: + * 1) Two ASHA hearing aids with the same HiSyncId + * 2) First paired devices is connected + * 3) Second paired device is connected + * Result: + * First paired device would be set as main device and second paired device will be set + * as sub device and removed from CachedDevices list. */ @Test - public void updateHearingAidsDevices_BothConnected_verifySubDevice() { + public void updateHearingAidsDevices_asha_bothConnected_verifySubDevice() { + when(mCachedDevice1.getProfiles()).thenReturn(List.of(mHearingAidProfile)); + when(mCachedDevice2.getProfiles()).thenReturn(List.of(mHearingAidProfile)); when(mHearingAidProfile.getHiSyncId(mDevice1)).thenReturn(HISYNCID1); when(mHearingAidProfile.getHiSyncId(mDevice2)).thenReturn(HISYNCID1); when(mCachedDevice1.isConnected()).thenReturn(true); @@ -302,46 +402,64 @@ public class HearingAidDeviceManagerTest { } /** - * Test updateHearingAidsDevices, dispatch callback + * Test updateHearingAidsDevices + * + * Conditions: + * 1) Two ASHA hearing aids with the same HiSyncId + * Result: + * Dispatch device removed callback */ @Test - public void updateHearingAidsDevices_dispatchDeviceRemovedCallback() { + public void updateHearingAidsDevices_asha_dispatchDeviceRemovedCallback() { + when(mCachedDevice1.getProfiles()).thenReturn(List.of(mHearingAidProfile)); + when(mCachedDevice2.getProfiles()).thenReturn(List.of(mHearingAidProfile)); when(mHearingAidProfile.getHiSyncId(mDevice1)).thenReturn(HISYNCID1); when(mHearingAidProfile.getHiSyncId(mDevice2)).thenReturn(HISYNCID1); mCachedDeviceManager.mCachedDevices.add(mCachedDevice1); mCachedDeviceManager.mCachedDevices.add(mCachedDevice2); + mHearingAidDeviceManager.updateHearingAidsDevices(); verify(mBluetoothEventManager).dispatchDeviceRemoved(mCachedDevice1); } /** - * Test updateHearingAidsDevices, do nothing when HiSyncId is invalid + * Test updateHearingAidsDevices + * + * Conditions: + * 1) Two ASHA hearing aids with invalid HiSyncId + * Result: + * Do nothing */ @Test - public void updateHearingAidsDevices_invalidHiSyncId_doNothing() { - when(mHearingAidProfile.getHiSyncId(mDevice1)). - thenReturn(BluetoothHearingAid.HI_SYNC_ID_INVALID); - when(mHearingAidProfile.getHiSyncId(mDevice2)). - thenReturn(BluetoothHearingAid.HI_SYNC_ID_INVALID); + public void updateHearingAidsDevices_asha_invalidHiSyncId_doNothing() { + when(mCachedDevice1.getProfiles()).thenReturn(List.of(mHearingAidProfile)); + when(mCachedDevice2.getProfiles()).thenReturn(List.of(mHearingAidProfile)); + when(mHearingAidProfile.getHiSyncId(mDevice1)).thenReturn(HI_SYNC_ID_INVALID); + when(mHearingAidProfile.getHiSyncId(mDevice2)).thenReturn(HI_SYNC_ID_INVALID); mCachedDeviceManager.mCachedDevices.add(mCachedDevice1); mCachedDeviceManager.mCachedDevices.add(mCachedDevice2); + mHearingAidDeviceManager.updateHearingAidsDevices(); verify(mHearingAidDeviceManager, never()).onHiSyncIdChanged(anyLong()); } /** - * Test updateHearingAidsDevices, set HearingAid's information, including HiSyncId, deviceSide, - * deviceMode. + * Test updateHearingAidsDevices + * + * Conditions: + * 1) ASHA hearing aids + * 2) Valid HiSync Id + * Result: + * Set hearing aid info to the device. */ @Test - public void updateHearingAidsDevices_validHiSyncId_setHearingAidInfos() { + public void updateHearingAidsDevices_asha_validHiSyncId_setHearingAidInfo() { + when(mCachedDevice1.getProfiles()).thenReturn(List.of(mHearingAidProfile)); when(mHearingAidProfile.getHiSyncId(mDevice1)).thenReturn(HISYNCID1); - when(mHearingAidProfile.getDeviceMode(mDevice1)).thenReturn( - HearingAidProfile.DeviceMode.MODE_BINAURAL); - when(mHearingAidProfile.getDeviceSide(mDevice1)).thenReturn( - HearingAidProfile.DeviceSide.SIDE_RIGHT); + when(mHearingAidProfile.getDeviceMode(mDevice1)).thenReturn(MODE_BINAURAL); + when(mHearingAidProfile.getDeviceSide(mDevice1)).thenReturn(SIDE_RIGHT); mCachedDeviceManager.mCachedDevices.add(mCachedDevice1); mHearingAidDeviceManager.updateHearingAidsDevices(); @@ -355,6 +473,51 @@ public class HearingAidDeviceManagerTest { } /** + * Test updateHearingAidsDevices + * + * Conditions: + * 1) LeAudio hearing aid + * 2) Valid audio location and device type + * Result: + * Set hearing aid info to the device. + */ + @Test + public void updateHearingAidsDevices_leAudio_validInfo_setHearingAidInfo() { + when(mCachedDevice1.getProfiles()).thenReturn(List.of(mLeAudioProfile, mHapClientProfile)); + when(mLeAudioProfile.getAudioLocation(mDevice1)).thenReturn(AUDIO_LOCATION_FRONT_LEFT); + when(mHapClientProfile.getHearingAidType(mDevice1)).thenReturn(TYPE_BINAURAL); + mCachedDeviceManager.mCachedDevices.add(mCachedDevice1); + + mHearingAidDeviceManager.updateHearingAidsDevices(); + + verify(mCachedDevice1).setHearingAidInfo(any(HearingAidInfo.class)); + assertThat(mCachedDevice1.getDeviceSide()).isEqualTo(HearingAidInfo.DeviceSide.SIDE_LEFT); + assertThat(mCachedDevice1.getDeviceMode()).isEqualTo( + HearingAidInfo.DeviceMode.MODE_BINAURAL); + } + + /** + * Test updateHearingAidsDevices + * + * Conditions: + * 1) LeAudio hearing aid + * 2) Invalid audio location and device type + * Result: + * Do not set hearing aid info to the device. + */ + @Test + public void updateHearingAidsDevices_leAudio_invalidInfo_notToSetHearingAidInfo() { + when(mCachedDevice1.getProfiles()).thenReturn(List.of(mLeAudioProfile, mHapClientProfile)); + when(mLeAudioProfile.getAudioLocation(mDevice1)).thenReturn(AUDIO_LOCATION_INVALID); + when(mHapClientProfile.getHearingAidType(mDevice1)).thenReturn(TYPE_INVALID); + mCachedDeviceManager.mCachedDevices.add(mCachedDevice1); + + mHearingAidDeviceManager.updateHearingAidsDevices(); + + verify(mCachedDevice1, never()).setHearingAidInfo(any(HearingAidInfo.class)); + } + + /** * Test onProfileConnectionStateChangedIfProcessed. * When first hearing aid device is connected, to process it same as other generic devices. * No need to process it. diff --git a/packages/SettingsProvider/res/values/defaults.xml b/packages/SettingsProvider/res/values/defaults.xml index 89a8dd95d3c3..17d9f1b87fac 100644 --- a/packages/SettingsProvider/res/values/defaults.xml +++ b/packages/SettingsProvider/res/values/defaults.xml @@ -335,4 +335,7 @@ <!-- Default for Settings.BATTERY_CHARGING_STATE_ENFORCE_LEVEL. -1 means system internal default value is used. --> <integer name="def_battery_charging_state_enforce_level">-1</integer> + + <!-- Value to use as default scale for fonts --> + <item name="def_device_font_scale" format="float" type="dimen">1.0</item> </resources> diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java index 8ae50eb7ffad..8ad5f244b659 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java @@ -74,6 +74,7 @@ public class SecureSettings { Settings.Secure.TTS_DEFAULT_LOCALE, Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD, Settings.Secure.ACCESSIBILITY_BOUNCE_KEYS, + Settings.Secure.ACCESSIBILITY_SLOW_KEYS, Settings.Secure.ACCESSIBILITY_STICKY_KEYS, Settings.Secure.WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON, // moved to global Settings.Secure.WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY, // moved to global diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java index e7d7bb01e180..38ec931612f0 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java @@ -48,6 +48,7 @@ public class SystemSettings { Settings.System.WIFI_STATIC_DNS2, Settings.System.BLUETOOTH_DISCOVERABILITY, Settings.System.BLUETOOTH_DISCOVERABILITY_TIMEOUT, + Settings.System.DEFAULT_DEVICE_FONT_SCALE, Settings.System.FONT_SCALE, Settings.System.DIM_SCREEN, Settings.System.SCREEN_OFF_TIMEOUT, diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java index 285c8c969343..d854df38a9ef 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java @@ -120,6 +120,7 @@ public class SecureSettingsValidators { VALIDATORS.put(Secure.TTS_DEFAULT_LOCALE, TTS_LIST_VALIDATOR); VALIDATORS.put(Secure.SHOW_IME_WITH_HARD_KEYBOARD, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.ACCESSIBILITY_BOUNCE_KEYS, ANY_INTEGER_VALIDATOR); + VALIDATORS.put(Secure.ACCESSIBILITY_SLOW_KEYS, ANY_INTEGER_VALIDATOR); VALIDATORS.put(Secure.ACCESSIBILITY_STICKY_KEYS, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.WIFI_NETWORKS_AVAILABLE_NOTIFICATION_ON, BOOLEAN_VALIDATOR); VALIDATORS.put(Secure.WIFI_NETWORKS_AVAILABLE_REPEAT_DELAY, NON_NEGATIVE_INTEGER_VALIDATOR); diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SettingsValidators.java index a8a659ee1e5c..677c81ad9271 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SettingsValidators.java @@ -45,6 +45,9 @@ public class SettingsValidators { } }; + public static final Validator FONT_SCALE_VALIDATOR = new InclusiveFloatRangeValidator(0.25f, + 5.0f); + public static final Validator NON_NEGATIVE_INTEGER_VALIDATOR = new Validator() { @Override public boolean validate(@Nullable String value) { diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java index 572303a813bf..98941c7cc116 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java @@ -20,6 +20,7 @@ import static android.provider.settings.validators.SettingsValidators.ANY_INTEGE import static android.provider.settings.validators.SettingsValidators.ANY_STRING_VALIDATOR; import static android.provider.settings.validators.SettingsValidators.BOOLEAN_VALIDATOR; import static android.provider.settings.validators.SettingsValidators.COMPONENT_NAME_VALIDATOR; +import static android.provider.settings.validators.SettingsValidators.FONT_SCALE_VALIDATOR; import static android.provider.settings.validators.SettingsValidators.LENIENT_IP_ADDRESS_VALIDATOR; import static android.provider.settings.validators.SettingsValidators.NON_NEGATIVE_FLOAT_VALIDATOR; import static android.provider.settings.validators.SettingsValidators.NON_NEGATIVE_INTEGER_VALIDATOR; @@ -31,7 +32,6 @@ import android.compat.annotation.UnsupportedAppUsage; import android.content.ComponentName; import android.hardware.display.ColorDisplayManager; import android.os.BatteryManager; -import android.provider.Settings.Global; import android.provider.Settings.System; import android.util.ArrayMap; @@ -93,7 +93,8 @@ public class SystemSettingsValidators { return value == null || value.length() < MAX_LENGTH; } }); - VALIDATORS.put(System.FONT_SCALE, new InclusiveFloatRangeValidator(0.25f, 5.0f)); + VALIDATORS.put(System.DEFAULT_DEVICE_FONT_SCALE, FONT_SCALE_VALIDATOR); + VALIDATORS.put(System.FONT_SCALE, FONT_SCALE_VALIDATOR); VALIDATORS.put(System.DIM_SCREEN, BOOLEAN_VALIDATOR); VALIDATORS.put( System.DISPLAY_COLOR_MODE, diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java index 3a46f4e96ccb..febce97031bb 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java @@ -3812,7 +3812,7 @@ public class SettingsProvider extends ContentProvider { } private final class UpgradeController { - private static final int SETTINGS_VERSION = 224; + private static final int SETTINGS_VERSION = 225; private final int mUserId; @@ -6004,6 +6004,13 @@ public class SettingsProvider extends ContentProvider { currentVersion = 224; } + // Version 224: Update the default font scale depending on the + // R.dimen.def_device_font_scale configuration property. + if (currentVersion == 224) { + handleDefaultFontScale(getSystemSettingsLocked(userId)); + currentVersion = 225; + } + // vXXX: Add new settings above this point. if (currentVersion != newVersion) { @@ -6021,6 +6028,32 @@ public class SettingsProvider extends ContentProvider { return currentVersion; } + @SuppressWarnings("GuardedBy") + @GuardedBy("mLock") + private void handleDefaultFontScale(@NonNull SettingsState systemSettings) { + final float defaultFontScale = getContext().getResources() + .getFloat(R.dimen.def_device_font_scale); + // Persist the value for future use (e.g. Reset Settings option) + systemSettings.insertSettingLocked( + Settings.System.DEFAULT_DEVICE_FONT_SCALE, + String.valueOf(defaultFontScale), + /* tag= */ null, + /* makeDefault= */ false, + SettingsState.SYSTEM_PACKAGE_NAME); + // We verify if there is a pre existing value for font_scale. + final Setting existingFontScale = systemSettings.getSettingLocked( + Settings.System.FONT_SCALE); + if (existingFontScale == null || existingFontScale.isNull()) { + // Set the default value only if it didn't exist before + systemSettings.insertSettingLocked( + Settings.System.FONT_SCALE, + String.valueOf(defaultFontScale), + /* tag= */ null, + /* makeDefault= */ false, + SettingsState.SYSTEM_PACKAGE_NAME); + } + } + @GuardedBy("mLock") private void initGlobalSettingsDefaultValLocked(String key, boolean val) { initGlobalSettingsDefaultValLocked(key, val ? "1" : "0"); diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index d38454221f76..cdb4aea6ee79 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -561,7 +561,7 @@ <uses-permission android:name="android.permission.TEST_BIOMETRIC" /> <!-- Permission required for CTS test - android.server.biometrics --> - <uses-permission android:name="android.permission.MANAGE_BIOMETRIC_DIALOG" /> + <uses-permission android:name="android.permission.SET_BIOMETRIC_DIALOG_LOGO" /> <!-- Permission required for CTS test - android.server.biometrics --> <uses-permission android:name="android.permission.USE_BACKGROUND_FACE_AUTHENTICATION" /> @@ -902,6 +902,9 @@ <!-- Permission required for BinaryTransparencyService shell API and host test --> <uses-permission android:name="android.permission.GET_BACKGROUND_INSTALLED_PACKAGES" /> + <!-- Permissions required for CTS test - CtsPermissionUiTestCases --> + <uses-permission android:name="android.permission.MANAGE_ENHANCED_CONFIRMATION_STATES" /> + <application android:label="@string/app_label" android:theme="@android:style/Theme.DeviceDefault.DayNight" diff --git a/packages/SystemUI/aconfig/accessibility.aconfig b/packages/SystemUI/aconfig/accessibility.aconfig index 7ba889bc8fee..866aa8945525 100644 --- a/packages/SystemUI/aconfig/accessibility.aconfig +++ b/packages/SystemUI/aconfig/accessibility.aconfig @@ -17,6 +17,13 @@ flag { } flag { + name: "floating_menu_drag_to_edit" + namespace: "accessibility" + description: "adds a second drag button to allow the user edit the shortcut." + bug: "297583708" +} + +flag { name: "floating_menu_ime_displacement_animation" namespace: "accessibility" description: "Adds an animation for when the FAB is displaced by an IME becoming visible." diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 7eca04a5f85b..a2530d59e2e6 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -366,8 +366,8 @@ flag { } flag { - name: "enable_notif_linearlayout_optimized" - namespace: "systemui" - description: "Enables notification specific LinearLayout optimization" - bug: "316110233" + name: "keyguard_wm_state_refactor" + namespace: "systemui" + description: "Enables refactored logic for SysUI+WM unlock/occlusion code paths" + bug: "278086361" } 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 a22fecf3688d..4fdcf7579dc6 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 @@ -84,13 +84,14 @@ fun CommunalContainer( SceneTransitionLayout( state = sceneTransitionLayoutState, modifier = modifier.fillMaxSize(), - edgeDetector = FixedSizeEdgeDetector(ContainerDimensions.EdgeSwipeSize), + swipeSourceDetector = FixedSizeEdgeDetector(ContainerDimensions.EdgeSwipeSize), ) { scene( TransitionSceneKey.Blank, userActions = mapOf( - Swipe(SwipeDirection.Left, fromEdge = Edge.Right) to TransitionSceneKey.Communal + Swipe(SwipeDirection.Left, fromSource = Edge.Right) to + TransitionSceneKey.Communal ) ) { // This scene shows nothing only allowing for transitions to the communal scene. @@ -101,7 +102,7 @@ fun CommunalContainer( TransitionSceneKey.Communal, userActions = mapOf( - Swipe(SwipeDirection.Right, fromEdge = Edge.Left) to TransitionSceneKey.Blank + Swipe(SwipeDirection.Right, fromSource = Edge.Left) to TransitionSceneKey.Blank ), ) { CommunalScene(viewModel, modifier = modifier) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt index 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/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/keyguard/ui/composable/section/LockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt index 2a6bea75791c..be6f022d8d52 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/LockSection.kt @@ -33,6 +33,7 @@ import com.android.keyguard.LockIconView import com.android.keyguard.LockIconViewController import com.android.systemui.Flags.keyguardBottomAreaRefactor import com.android.systemui.biometrics.AuthController +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor import com.android.systemui.flags.FeatureFlagsClassic import com.android.systemui.flags.Flags @@ -47,10 +48,12 @@ import com.android.systemui.res.R import com.android.systemui.statusbar.VibratorHelper import dagger.Lazy import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope class LockSection @Inject constructor( + @Application private val applicationScope: CoroutineScope, private val windowManager: WindowManager, private val authController: AuthController, private val featureFlags: FeatureFlagsClassic, @@ -76,6 +79,7 @@ constructor( DeviceEntryIconView(context, null).apply { id = R.id.device_entry_icon_view DeviceEntryIconViewBinder.bind( + applicationScope, this, deviceEntryIconViewModel.get(), deviceEntryForegroundViewModel.get(), diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt index c35202cd830a..9f9e1f5bb56a 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt @@ -183,7 +183,7 @@ private fun UserAction.toTransitionUserAction(): SceneTransitionUserAction { is UserAction.Swipe -> Swipe( pointerCount = pointerCount, - fromEdge = + fromSource = when (this.fromEdge) { null -> null Edge.LEFT -> SceneTransitionEdge.Left diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/EdgeDetector.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/EdgeDetector.kt index 82d4239d7eb5..b0dc3a144533 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/EdgeDetector.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/EdgeDetector.kt @@ -23,24 +23,19 @@ import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp -interface EdgeDetector { - /** - * Return the [Edge] associated to [position] inside a layout of size [layoutSize], given - * [density] and [orientation]. - */ - fun edge( - layoutSize: IntSize, - position: IntOffset, - density: Density, - orientation: Orientation, - ): Edge? +/** The edge of a [SceneTransitionLayout]. */ +enum class Edge : SwipeSource { + Left, + Right, + Top, + Bottom, } val DefaultEdgeDetector = FixedSizeEdgeDetector(40.dp) -/** An [EdgeDetector] that detects edges assuming a fixed edge size of [size]. */ -class FixedSizeEdgeDetector(val size: Dp) : EdgeDetector { - override fun edge( +/** An [SwipeSourceDetector] that detects edges assuming a fixed edge size of [size]. */ +class FixedSizeEdgeDetector(val size: Dp) : SwipeSourceDetector { + override fun source( layoutSize: IntSize, position: IntOffset, density: Density, diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt index 90f46bd4dcaa..9d4b69c51690 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt @@ -44,7 +44,7 @@ sealed class Key(val debugName: String, val identity: Any) { class SceneKey( name: String, identity: Any = Object(), -) : Key(name, identity) { +) : Key(name, identity), UserActionResult { @VisibleForTesting // TODO(b/240432457): Make internal once PlatformComposeSceneTransitionLayoutTestsUtils can // access internal members. @@ -53,6 +53,10 @@ class SceneKey( /** The unique [ElementKey] identifying this scene's root element. */ val rootElementKey = ElementKey(name, identity) + // Implementation of [UserActionResult]. + override val toScene: SceneKey = this + override val distance: UserActionDistance? = null + override fun toString(): String { return "SceneKey(debugName=$debugName)" } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt index 38738782c889..8552aaf0ebff 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt @@ -64,8 +64,8 @@ import androidx.compose.ui.util.fastForEach @Stable internal fun Modifier.multiPointerDraggable( orientation: Orientation, - enabled: Boolean, - startDragImmediately: Boolean, + enabled: () -> Boolean, + startDragImmediately: () -> Boolean, onDragStarted: (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> Unit, onDragDelta: (delta: Float) -> Unit, onDragStopped: (velocity: Float) -> Unit, @@ -83,8 +83,8 @@ internal fun Modifier.multiPointerDraggable( private data class MultiPointerDraggableElement( private val orientation: Orientation, - private val enabled: Boolean, - private val startDragImmediately: Boolean, + private val enabled: () -> Boolean, + private val startDragImmediately: () -> Boolean, private val onDragStarted: (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> Unit, private val onDragDelta: (Float) -> Unit, @@ -110,10 +110,10 @@ private data class MultiPointerDraggableElement( } } -private class MultiPointerDraggableNode( +internal class MultiPointerDraggableNode( orientation: Orientation, - enabled: Boolean, - var startDragImmediately: Boolean, + enabled: () -> Boolean, + var startDragImmediately: () -> Boolean, var onDragStarted: (startedPosition: Offset, overSlop: Float, pointersDown: Int) -> Unit, var onDragDelta: (Float) -> Unit, var onDragStopped: (velocity: Float) -> Unit, @@ -122,7 +122,7 @@ private class MultiPointerDraggableNode( private val delegate = delegate(SuspendingPointerInputModifierNode(pointerInputHandler)) private val velocityTracker = VelocityTracker() - var enabled: Boolean = enabled + var enabled: () -> Boolean = enabled set(value) { // Reset the pointer input whenever enabled changed. if (value != field) { @@ -133,7 +133,7 @@ private class MultiPointerDraggableNode( var orientation: Orientation = orientation set(value) { - // Reset the pointer input whenever enabled orientation. + // Reset the pointer input whenever orientation changed. if (value != field) { field = value delegate.resetPointerInputHandler() @@ -149,7 +149,7 @@ private class MultiPointerDraggableNode( ) = delegate.onPointerEvent(pointerEvent, pass, bounds) private suspend fun PointerInputScope.pointerInput() { - if (!enabled) { + if (!enabled()) { return } @@ -163,8 +163,7 @@ private class MultiPointerDraggableNode( val onDragEnd: () -> Unit = { val maxFlingVelocity = currentValueOf(LocalViewConfiguration).maximumFlingVelocity.let { max -> - val maxF = max.toFloat() - Velocity(maxF, maxF) + Velocity(max, max) } val velocity = velocityTracker.calculateVelocity(maxFlingVelocity) @@ -183,7 +182,7 @@ private class MultiPointerDraggableNode( detectDragGestures( orientation = orientation, - startDragImmediately = { startDragImmediately }, + startDragImmediately = startDragImmediately, onDragStart = onDragStart, onDragEnd = onDragEnd, onDragCancel = onDragCancel, diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt index f67df54b088c..af51cee2a255 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt @@ -38,7 +38,7 @@ internal class Scene( val key: SceneKey, layoutImpl: SceneTransitionLayoutImpl, content: @Composable SceneScope.() -> Unit, - actions: Map<UserAction, SceneKey>, + actions: Map<UserAction, UserActionResult>, zIndex: Float, ) { internal val scope = SceneScopeImpl(layoutImpl, this) diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt index ff054786cf52..aed04f688628 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt @@ -28,6 +28,8 @@ import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.round import com.android.compose.nestedscroll.PriorityNestedScrollConnection @@ -75,23 +77,25 @@ internal class SceneGestureHandler( internal var currentSource: Any? = null - /** The [UserAction]s associated to the current swipe. */ - private var actionUpOrLeft: UserAction? = null - private var actionDownOrRight: UserAction? = null - private var actionUpOrLeftNoEdge: UserAction? = null - private var actionDownOrRightNoEdge: UserAction? = null - private var upOrLeftScene: SceneKey? = null - private var downOrRightScene: SceneKey? = null + /** The [Swipes] associated to the current gesture. */ + private var swipes: Swipes? = null + + /** The [UserActionResult] associated to up and down swipes. */ + private var upOrLeftResult: UserActionResult? = null + private var downOrRightResult: UserActionResult? = null internal fun onDragStarted(pointersDown: Int, startedPosition: Offset?, overSlop: Float) { if (isDrivingTransition) { // This [transition] was already driving the animation: simply take over it. // Stop animating and start from where the current offset. swipeTransition.cancelOffsetAnimation() - updateTargetScenes(swipeTransition._fromScene) + updateSwipesResults(swipeTransition._fromScene) return } + check(overSlop != 0f) { + "onDragStarted() called while isDrivingTransition=false overSlop=0f" + } val transitionState = layoutState.transitionState if (transitionState is TransitionState.Transition) { // TODO(b/290184746): Better handle interruptions here if state != idle. @@ -104,18 +108,25 @@ internal class SceneGestureHandler( } val fromScene = layoutImpl.scene(transitionState.currentScene) - setCurrentActions(fromScene, startedPosition, pointersDown) + updateSwipes(fromScene, startedPosition, pointersDown) val (targetScene, distance) = - findTargetSceneAndDistance(fromScene, overSlop, updateScenes = true) ?: return - + findTargetSceneAndDistance(fromScene, overSlop, updateSwipesResults = true) ?: return updateTransition(SwipeTransition(fromScene, targetScene, distance), force = true) } - private fun setCurrentActions(fromScene: Scene, startedPosition: Offset?, pointersDown: Int) { - val fromEdge = + private fun updateSwipes(fromScene: Scene, startedPosition: Offset?, pointersDown: Int) { + this.swipes = computeSwipes(fromScene, startedPosition, pointersDown) + } + + private fun computeSwipes( + fromScene: Scene, + startedPosition: Offset?, + pointersDown: Int + ): Swipes { + val fromSource = startedPosition?.let { position -> - layoutImpl.edgeDetector.edge( + layoutImpl.swipeSourceDetector.source( fromScene.targetSize, position.round(), layoutImpl.density, @@ -131,7 +142,7 @@ internal class SceneGestureHandler( Orientation.Vertical -> SwipeDirection.Up }, pointerCount = pointersDown, - fromEdge = fromEdge, + fromSource = fromSource, ) val downOrRight = @@ -142,33 +153,31 @@ internal class SceneGestureHandler( Orientation.Vertical -> SwipeDirection.Down }, pointerCount = pointersDown, - fromEdge = fromEdge, + fromSource = fromSource, ) - if (fromEdge == null) { - actionUpOrLeft = null - actionDownOrRight = null - actionUpOrLeftNoEdge = upOrLeft - actionDownOrRightNoEdge = downOrRight + return if (fromSource == null) { + Swipes( + upOrLeft = null, + downOrRight = null, + upOrLeftNoSource = upOrLeft, + downOrRightNoSource = downOrRight, + ) } else { - actionUpOrLeft = upOrLeft - actionDownOrRight = downOrRight - actionUpOrLeftNoEdge = upOrLeft.copy(fromEdge = null) - actionDownOrRightNoEdge = downOrRight.copy(fromEdge = null) + Swipes( + upOrLeft = upOrLeft, + downOrRight = downOrRight, + upOrLeftNoSource = upOrLeft.copy(fromSource = null), + downOrRightNoSource = downOrRight.copy(fromSource = null), + ) } } - /** - * Use the layout size in the swipe orientation for swipe distance. - * - * TODO(b/290184746): Also handle custom distances for transitions. With smaller distances, we - * will also have to make sure that we correctly handle overscroll. - */ - private fun Scene.getAbsoluteDistance(): Float { - return when (orientation) { - Orientation.Horizontal -> targetSize.width - Orientation.Vertical -> targetSize.height - }.toFloat() + private fun Scene.getAbsoluteDistance(distance: UserActionDistance?): Float { + val targetSize = this.targetSize + return with(distance ?: DefaultSwipeDistance) { + layoutImpl.density.absoluteDistance(targetSize, orientation) + } } internal fun onDrag(delta: Float) { @@ -183,7 +192,7 @@ internal class SceneGestureHandler( findTargetSceneAndDistance( fromScene, swipeTransition.dragOffset, - updateScenes = isNewFromScene, + updateSwipesResults = isNewFromScene, ) ?: run { onDragStopped(delta, true) @@ -200,9 +209,31 @@ internal class SceneGestureHandler( } } - private fun updateTargetScenes(fromScene: Scene) { - upOrLeftScene = fromScene.upOrLeft() - downOrRightScene = fromScene.downOrRight() + private fun updateSwipesResults(fromScene: Scene) { + val (upOrLeftResult, downOrRightResult) = + swipesResults( + fromScene, + this.swipes ?: error("updateSwipes() should be called before updateSwipesResults()") + ) + + this.upOrLeftResult = upOrLeftResult + this.downOrRightResult = downOrRightResult + } + + private fun swipesResults( + fromScene: Scene, + swipes: Swipes + ): Pair<UserActionResult?, UserActionResult?> { + val userActions = fromScene.userActions + fun sceneToSwipePair(swipe: Swipe?): UserActionResult? { + return userActions[swipe ?: return null] + } + + val upOrLeftResult = + sceneToSwipePair(swipes.upOrLeft) ?: sceneToSwipePair(swipes.upOrLeftNoSource) + val downOrRightResult = + sceneToSwipePair(swipes.downOrRight) ?: sceneToSwipePair(swipes.downOrRightNoSource) + return Pair(upOrLeftResult, downOrRightResult) } /** @@ -229,9 +260,9 @@ internal class SceneGestureHandler( // If the offset is past the distance then let's change fromScene so that the user can swipe // to the next screen or go back to the previous one. val offset = swipeTransition.dragOffset - return if (offset <= -absoluteDistance && upOrLeftScene == toScene.key) { + return if (offset <= -absoluteDistance && upOrLeftResult?.toScene == toScene.key) { Pair(toScene, absoluteDistance) - } else if (offset >= absoluteDistance && downOrRightScene == toScene.key) { + } else if (offset >= absoluteDistance && downOrRightResult?.toScene == toScene.key) { Pair(toScene, -absoluteDistance) } else { Pair(fromScene, 0f) @@ -244,31 +275,41 @@ internal class SceneGestureHandler( * @param fromScene the scene from which we look for the target * @param directionOffset signed float that indicates the direction. Positive is down or right * negative is up or left. - * @param updateScenes whether the target scenes should be updated to the current values held in - * the Scenes map. Usually we don't want to update them while doing a drag, because this could - * change the target scene (jump cutting) to a different scene, when some system state changed - * the targets the background. However, an update is needed any time we calculate the targets - * for a new fromScene. + * @param updateSwipesResults whether the target scenes should be updated to the current values + * held in the Scenes map. Usually we don't want to update them while doing a drag, because + * this could change the target scene (jump cutting) to a different scene, when some system + * state changed the targets the background. However, an update is needed any time we + * calculate the targets for a new fromScene. * @return null when there are no targets in either direction. If one direction is null and you * drag into the null direction this function will return the opposite direction, assuming * that the users intention is to start the drag into the other direction eventually. If * [directionOffset] is 0f and both direction are available, it will default to - * [upOrLeftScene]. + * [upOrLeftResult]. */ private inline fun findTargetSceneAndDistance( fromScene: Scene, directionOffset: Float, - updateScenes: Boolean, + updateSwipesResults: Boolean, ): Pair<Scene, Float>? { - if (updateScenes) updateTargetScenes(fromScene) - val absoluteDistance = fromScene.getAbsoluteDistance() + if (updateSwipesResults) updateSwipesResults(fromScene) // Compute the target scene depending on the current offset. return when { - upOrLeftScene == null && downOrRightScene == null -> null - (directionOffset < 0f && upOrLeftScene != null) || downOrRightScene == null -> - Pair(layoutImpl.scene(upOrLeftScene!!), -absoluteDistance) - else -> Pair(layoutImpl.scene(downOrRightScene!!), absoluteDistance) + upOrLeftResult == null && downOrRightResult == null -> null + (directionOffset < 0f && upOrLeftResult != null) || downOrRightResult == null -> + upOrLeftResult?.let { result -> + Pair( + layoutImpl.scene(result.toScene), + -fromScene.getAbsoluteDistance(result.distance) + ) + } + else -> + downOrRightResult?.let { result -> + Pair( + layoutImpl.scene(result.toScene), + fromScene.getAbsoluteDistance(result.distance) + ) + } } } @@ -280,24 +321,25 @@ internal class SceneGestureHandler( fromScene: Scene, directionOffset: Float, ): Pair<Scene, Float>? { - val absoluteDistance = fromScene.getAbsoluteDistance() return when { directionOffset > 0f -> - upOrLeftScene?.let { Pair(layoutImpl.scene(it), -absoluteDistance) } + upOrLeftResult?.let { result -> + Pair( + layoutImpl.scene(result.toScene), + -fromScene.getAbsoluteDistance(result.distance), + ) + } directionOffset < 0f -> - downOrRightScene?.let { Pair(layoutImpl.scene(it), absoluteDistance) } + downOrRightResult?.let { result -> + Pair( + layoutImpl.scene(result.toScene), + fromScene.getAbsoluteDistance(result.distance), + ) + } else -> null } } - private fun Scene.upOrLeft(): SceneKey? { - return userActions[actionUpOrLeft] ?: userActions[actionUpOrLeftNoEdge] - } - - private fun Scene.downOrRight(): SceneKey? { - return userActions[actionDownOrRight] ?: userActions[actionDownOrRightNoEdge] - } - internal fun onDragStopped(velocity: Float, canChangeScene: Boolean) { // The state was changed since the drag started; don't do anything. if (!isDrivingTransition) { @@ -515,6 +557,26 @@ internal class SceneGestureHandler( companion object { private const val TAG = "SceneGestureHandler" } + + private object DefaultSwipeDistance : UserActionDistance { + override fun Density.absoluteDistance( + fromSceneSize: IntSize, + orientation: Orientation, + ): Float { + return when (orientation) { + Orientation.Horizontal -> fromSceneSize.width + Orientation.Vertical -> fromSceneSize.height + }.toFloat() + } + } + + /** The [Swipe] associated to a given fromScene, startedPosition and pointersDown. */ + private class Swipes( + val upOrLeft: Swipe?, + val downOrRight: Swipe?, + val upOrLeftNoSource: Swipe?, + val downOrRightNoSource: Swipe?, + ) } private class SceneDraggableHandler( diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt index 80f8c1c9e987..7e0aa9c3e2b1 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt @@ -27,6 +27,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize /** * [SceneTransitionLayout] is a container that automatically animates its content whenever its state @@ -38,7 +42,8 @@ import androidx.compose.ui.platform.LocalDensity * UI code. * * @param state the state of this layout. - * @param edgeDetector the edge detector used to detect which edge a swipe is started from, if any. + * @param swipeSourceDetector the edge detector used to detect which edge a swipe is started from, + * if any. * @param transitionInterceptionThreshold used during a scene transition. For the scene to be * intercepted, the progress value must be above the threshold, and below (1 - threshold). * @param scenes the configuration of the different scenes of this layout. @@ -48,14 +53,14 @@ import androidx.compose.ui.platform.LocalDensity fun SceneTransitionLayout( state: SceneTransitionLayoutState, modifier: Modifier = Modifier, - edgeDetector: EdgeDetector = DefaultEdgeDetector, + swipeSourceDetector: SwipeSourceDetector = DefaultEdgeDetector, @FloatRange(from = 0.0, to = 0.5) transitionInterceptionThreshold: Float = 0f, scenes: SceneTransitionLayoutScope.() -> Unit, ) { SceneTransitionLayoutForTesting( state, modifier, - edgeDetector, + swipeSourceDetector, transitionInterceptionThreshold, onLayoutImpl = null, scenes, @@ -76,7 +81,8 @@ fun SceneTransitionLayout( * This is called when the user commits a transition to a new scene because of a [UserAction], for * instance by triggering back navigation or by swiping to a new scene. * @param transitions the definition of the transitions used to animate a change of scene. - * @param edgeDetector the edge detector used to detect which edge a swipe is started from, if any. + * @param swipeSourceDetector the source detector used to detect which source a swipe is started + * from, if any. * @param transitionInterceptionThreshold used during a scene transition. For the scene to be * intercepted, the progress value must be above the threshold, and below (1 - threshold). * @param scenes the configuration of the different scenes of this layout. @@ -87,7 +93,7 @@ fun SceneTransitionLayout( onChangeScene: (SceneKey) -> Unit, transitions: SceneTransitions, modifier: Modifier = Modifier, - edgeDetector: EdgeDetector = DefaultEdgeDetector, + swipeSourceDetector: SwipeSourceDetector = DefaultEdgeDetector, @FloatRange(from = 0.0, to = 0.5) transitionInterceptionThreshold: Float = 0f, scenes: SceneTransitionLayoutScope.() -> Unit, ) { @@ -95,7 +101,7 @@ fun SceneTransitionLayout( SceneTransitionLayout( state, modifier, - edgeDetector, + swipeSourceDetector, transitionInterceptionThreshold, scenes, ) @@ -113,7 +119,7 @@ interface SceneTransitionLayoutScope { */ fun scene( key: SceneKey, - userActions: Map<UserAction, SceneKey> = emptyMap(), + userActions: Map<UserAction, UserActionResult> = emptyMap(), content: @Composable SceneScope.() -> Unit, ) } @@ -335,7 +341,7 @@ data object Back : UserAction data class Swipe( val direction: SwipeDirection, val pointerCount: Int = 1, - val fromEdge: Edge? = null, + val fromSource: SwipeSource? = null, ) : UserAction { companion object { val Left = Swipe(SwipeDirection.Left) @@ -353,6 +359,95 @@ enum class SwipeDirection(val orientation: Orientation) { } /** + * The source of a Swipe. + * + * Important: This can be anything that can be returned by any [SwipeSourceDetector], but this must + * implement [equals] and [hashCode]. Note that those can be trivially implemented using data + * classes. + */ +interface SwipeSource { + // Require equals() and hashCode() to be implemented. + override fun equals(other: Any?): Boolean + + override fun hashCode(): Int +} + +interface SwipeSourceDetector { + /** + * Return the [SwipeSource] associated to [position] inside a layout of size [layoutSize], given + * [density] and [orientation]. + */ + fun source( + layoutSize: IntSize, + position: IntOffset, + density: Density, + orientation: Orientation, + ): SwipeSource? +} + +/** + * The result of performing a [UserAction]. + * + * Note: [UserActionResult] is implemented by [SceneKey], and you can also use [withDistance] to + * easily create a [UserActionResult] with a fixed distance: + * ``` + * SceneTransitionLayout(...) { + * scene( + * Scenes.Foo, + * userActions = + * mapOf( + * Swipe.Right to Scene.Bar, + * Swipe.Down to Scene.Doe withDistance 100.dp, + * ) + * ) + * ) { ... } + * } + * ``` + */ +interface UserActionResult { + /** The scene we should be transitioning to during the [UserAction]. */ + val toScene: SceneKey + + /** + * The distance the action takes to animate from 0% to 100%. + * + * If `null`, a default distance will be used that depends on the [UserAction] performed. + */ + val distance: UserActionDistance? +} + +interface UserActionDistance { + /** + * Return the **absolute** distance of the user action given the size of the scene we are + * animating from and the [orientation]. + */ + fun Density.absoluteDistance(fromSceneSize: IntSize, orientation: Orientation): Float +} + +/** + * A utility function to make it possible to define user actions with a distance using the syntax + * `Swipe.Up to Scene.foo withDistance 100.dp` + */ +infix fun Pair<UserAction, SceneKey>.withDistance( + distance: Dp +): Pair<UserAction, UserActionResult> { + val scene = second + val distance = FixedDistance(distance) + return first to + object : UserActionResult { + override val toScene: SceneKey = scene + override val distance: UserActionDistance = distance + } +} + +/** The user action has a fixed [absoluteDistance]. */ +private class FixedDistance(private val distance: Dp) : UserActionDistance { + override fun Density.absoluteDistance(fromSceneSize: IntSize, orientation: Orientation): Float { + return distance.toPx() + } +} + +/** * An internal version of [SceneTransitionLayout] to be used for tests. * * Important: You should use this only in tests and if you need to access the underlying @@ -362,7 +457,7 @@ enum class SwipeDirection(val orientation: Orientation) { internal fun SceneTransitionLayoutForTesting( state: SceneTransitionLayoutState, modifier: Modifier = Modifier, - edgeDetector: EdgeDetector = DefaultEdgeDetector, + swipeSourceDetector: SwipeSourceDetector = DefaultEdgeDetector, transitionInterceptionThreshold: Float = 0f, onLayoutImpl: ((SceneTransitionLayoutImpl) -> Unit)? = null, scenes: SceneTransitionLayoutScope.() -> Unit, @@ -373,7 +468,7 @@ internal fun SceneTransitionLayoutForTesting( SceneTransitionLayoutImpl( state = state as BaseSceneTransitionLayoutState, density = density, - edgeDetector = edgeDetector, + swipeSourceDetector = swipeSourceDetector, transitionInterceptionThreshold = transitionInterceptionThreshold, builder = scenes, coroutineScope = coroutineScope, @@ -394,7 +489,7 @@ internal fun SceneTransitionLayoutForTesting( } layoutImpl.density = density - layoutImpl.edgeDetector = edgeDetector + layoutImpl.swipeSourceDetector = swipeSourceDetector } layoutImpl.Content(modifier) diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt index 7cc9d2623e9c..8c5a4720e7fb 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt @@ -47,7 +47,7 @@ internal typealias MovableElementContent = internal class SceneTransitionLayoutImpl( internal val state: BaseSceneTransitionLayoutState, internal var density: Density, - internal var edgeDetector: EdgeDetector, + internal var swipeSourceDetector: SwipeSourceDetector, internal var transitionInterceptionThreshold: Float, builder: SceneTransitionLayoutScope.() -> Unit, private val coroutineScope: CoroutineScope, @@ -140,7 +140,7 @@ internal class SceneTransitionLayoutImpl( object : SceneTransitionLayoutScope { override fun scene( key: SceneKey, - userActions: Map<UserAction, SceneKey>, + userActions: Map<UserAction, UserActionResult>, content: @Composable SceneScope.() -> Unit, ) { scenesToRemove.remove(key) @@ -229,8 +229,10 @@ internal class SceneTransitionLayoutImpl( // Handle back events. // TODO(b/290184746): Make sure that this works with SystemUI once we use // SceneTransitionLayout in Flexiglass. - scene(state.transitionState.currentScene).userActions[Back]?.let { backScene -> - BackHandler { with(state) { coroutineScope.onChangeScene(backScene) } } + scene(state.transitionState.currentScene).userActions[Back]?.let { result -> + // TODO(b/290184746): Handle predictive back and use result.distance if + // specified. + BackHandler { with(state) { coroutineScope.onChangeScene(result.toScene) } } } Box { diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt index 0d3bc7d0cd85..b9c4ac0cd006 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt @@ -17,40 +17,98 @@ package com.android.compose.animation.scene import androidx.compose.foundation.gestures.Orientation +import androidx.compose.runtime.Stable import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.PointerEvent +import androidx.compose.ui.input.pointer.PointerEventPass +import androidx.compose.ui.node.DelegatingNode +import androidx.compose.ui.node.ModifierNodeElement +import androidx.compose.ui.node.PointerInputModifierNode +import androidx.compose.ui.unit.IntSize /** * Configures the swipeable behavior of a [SceneTransitionLayout] depending on the current state. */ +@Stable internal fun Modifier.swipeToScene(gestureHandler: SceneGestureHandler): Modifier { - /** Whether swipe should be enabled in the given [orientation]. */ - fun Scene.shouldEnableSwipes(orientation: Orientation): Boolean = - userActions.keys.any { it is Swipe && it.direction.orientation == orientation } - - val layoutImpl = gestureHandler.layoutImpl - val currentScene = layoutImpl.scene(layoutImpl.state.transitionState.currentScene) - val orientation = gestureHandler.orientation - val canSwipe = currentScene.shouldEnableSwipes(orientation) - val canOppositeSwipe = - currentScene.shouldEnableSwipes( - when (orientation) { - Orientation.Vertical -> Orientation.Horizontal - Orientation.Horizontal -> Orientation.Vertical - } + return this.then(SwipeToSceneElement(gestureHandler)) +} + +private data class SwipeToSceneElement( + val gestureHandler: SceneGestureHandler, +) : ModifierNodeElement<SwipeToSceneNode>() { + override fun create(): SwipeToSceneNode = SwipeToSceneNode(gestureHandler) + + override fun update(node: SwipeToSceneNode) { + node.gestureHandler = gestureHandler + } +} + +private class SwipeToSceneNode( + gestureHandler: SceneGestureHandler, +) : DelegatingNode(), PointerInputModifierNode { + private val delegate = + delegate( + MultiPointerDraggableNode( + orientation = gestureHandler.orientation, + enabled = ::enabled, + startDragImmediately = ::startDragImmediately, + onDragStarted = gestureHandler.draggable::onDragStarted, + onDragDelta = gestureHandler.draggable::onDelta, + onDragStopped = gestureHandler.draggable::onDragStopped, + ) ) - return multiPointerDraggable( - orientation = orientation, - enabled = gestureHandler.isDrivingTransition || canSwipe, - // Immediately start the drag if this our [transition] is currently animating to a scene + var gestureHandler: SceneGestureHandler = gestureHandler + set(value) { + if (value != field) { + field = value + + // Make sure to update the delegate orientation. Note that this will automatically + // reset the underlying pointer input handler, so previous gestures will be + // cancelled. + delegate.orientation = value.orientation + } + } + + override fun onPointerEvent( + pointerEvent: PointerEvent, + pass: PointerEventPass, + bounds: IntSize, + ) = delegate.onPointerEvent(pointerEvent, pass, bounds) + + override fun onCancelPointerInput() = delegate.onCancelPointerInput() + + private fun enabled(): Boolean { + return gestureHandler.isDrivingTransition || + currentScene().shouldEnableSwipes(gestureHandler.orientation) + } + + private fun currentScene(): Scene { + val layoutImpl = gestureHandler.layoutImpl + return layoutImpl.scene(layoutImpl.state.transitionState.currentScene) + } + + /** Whether swipe should be enabled in the given [orientation]. */ + private fun Scene.shouldEnableSwipes(orientation: Orientation): Boolean { + return userActions.keys.any { it is Swipe && it.direction.orientation == orientation } + } + + private fun startDragImmediately(): Boolean { + // Immediately start the drag if this our transition is currently animating to a scene // (i.e. the user released their input pointer after swiping in this orientation) and the // user can't swipe in the other direction. - startDragImmediately = - gestureHandler.isDrivingTransition && - gestureHandler.swipeTransition.isAnimatingOffset && - !canOppositeSwipe, - onDragStarted = gestureHandler.draggable::onDragStarted, - onDragDelta = gestureHandler.draggable::onDelta, - onDragStopped = gestureHandler.draggable::onDragStopped, - ) + return gestureHandler.isDrivingTransition && + gestureHandler.swipeTransition.isAnimatingOffset && + !canOppositeSwipe() + } + + private fun canOppositeSwipe(): Boolean { + val oppositeOrientation = + when (gestureHandler.orientation) { + Orientation.Vertical -> Orientation.Horizontal + Orientation.Horizontal -> Orientation.Vertical + } + return currentScene().shouldEnableSwipes(oppositeOrientation) + } } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt index dc8505c43889..a764a52723af 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt @@ -320,11 +320,3 @@ interface PropertyTransformationBuilder { anchorHeight: Boolean = true, ) } - -/** The edge of a [SceneTransitionLayout]. */ -enum class Edge { - Left, - Right, - Top, - Bottom, -} diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt b/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt index 2841bcf4e40c..ac11d3040d67 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt @@ -22,6 +22,7 @@ import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.unit.Velocity import com.android.compose.ui.util.SpaceVectorConverter +import kotlin.math.sign /** * This [NestedScrollConnection] waits for a child to scroll ([onPreScroll] or [onPostScroll]), and @@ -117,7 +118,12 @@ class PriorityNestedScrollConnection( return Velocity.Zero } - onPriorityStart(available = Offset.Zero) + // The offset passed to onPriorityStart() must be != 0f, so we create a small offset of 1px + // given the available velocity. + // TODO(b/291053278): Remove canStartPostFling() and instead make it possible to define the + // overscroll behavior on the Scene level. + val smallOffset = Offset(available.x.sign, available.y.sign) + onPriorityStart(available = smallOffset) // This is the last event of a scroll gesture. return onPriorityStop(available) diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/FixedSizeEdgeDetectorTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/FixedSizeEdgeDetectorTest.kt index a68282ae78f4..cceaf57ca82b 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/FixedSizeEdgeDetectorTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/FixedSizeEdgeDetectorTest.kt @@ -35,7 +35,7 @@ class FixedSizeEdgeDetectorTest { @Test fun horizontalEdges() { fun horizontalEdge(position: Int): Edge? = - detector.edge( + detector.source( layoutSize, position = IntOffset(position, 0), density, @@ -53,7 +53,7 @@ class FixedSizeEdgeDetectorTest { @Test fun verticalEdges() { fun verticalEdge(position: Int): Edge? = - detector.edge( + detector.source( layoutSize, position = IntOffset(0, position), density, diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt index 066a3e45fb3c..88363ad24d9a 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneGestureHandlerTest.kt @@ -77,7 +77,7 @@ class SceneGestureHandlerTest { userActions = mapOf( Swipe.Up to SceneB, - Swipe(SwipeDirection.Up, fromEdge = Edge.Bottom) to SceneA + Swipe(SwipeDirection.Up, fromSource = Edge.Bottom) to SceneA ), ) { Text("SceneC") @@ -90,7 +90,7 @@ class SceneGestureHandlerTest { SceneTransitionLayoutImpl( state = layoutState, density = Density(1f), - edgeDetector = DefaultEdgeDetector, + swipeSourceDetector = DefaultEdgeDetector, transitionInterceptionThreshold = transitionInterceptionThreshold, builder = scenesBuilder, coroutineScope = coroutineScope, @@ -192,16 +192,14 @@ class SceneGestureHandlerTest { @Test fun onDragStarted_shouldStartATransition() = runGestureTest { - draggable.onDragStarted() + draggable.onDragStarted(overSlop = down(0.1f)) assertTransition(currentScene = SceneA) } @Test fun afterSceneTransitionIsStarted_interceptDragEvents() = runGestureTest { - draggable.onDragStarted() + draggable.onDragStarted(overSlop = down(0.1f)) assertTransition(currentScene = SceneA) - - draggable.onDelta(pixels = down(0.1f)) assertThat(progress).isEqualTo(0.1f) draggable.onDelta(pixels = down(0.1f)) @@ -210,10 +208,7 @@ class SceneGestureHandlerTest { @Test fun onDragStoppedAfterDrag_velocityLowerThanThreshold_remainSameScene() = runGestureTest { - draggable.onDragStarted() - assertTransition(currentScene = SceneA) - - draggable.onDelta(pixels = down(0.1f)) + draggable.onDragStarted(overSlop = down(0.1f)) assertTransition(currentScene = SceneA) draggable.onDragStopped( @@ -228,10 +223,7 @@ class SceneGestureHandlerTest { @Test fun onDragStoppedAfterDrag_velocityAtLeastThreshold_goToNextScene() = runGestureTest { - draggable.onDragStarted() - assertTransition(currentScene = SceneA) - - draggable.onDelta(pixels = down(0.1f)) + draggable.onDragStarted(overSlop = down(0.1f)) assertTransition(currentScene = SceneA) draggable.onDragStopped(velocity = velocityThreshold) @@ -245,7 +237,7 @@ class SceneGestureHandlerTest { @Test fun onDragStoppedAfterStarted_returnToIdle() = runGestureTest { - draggable.onDragStarted() + draggable.onDragStarted(overSlop = down(0.1f)) assertTransition(currentScene = SceneA) draggable.onDragStopped(velocity = 0f) @@ -256,8 +248,7 @@ class SceneGestureHandlerTest { @Test fun onDragReversedDirection_changeToScene() = runGestureTest { // Drag A -> B with progress 0.6 - draggable.onDragStarted() - draggable.onDelta(up(0.6f)) + draggable.onDragStarted(overSlop = up(0.6f)) assertTransition( currentScene = SceneA, fromScene = SceneA, @@ -366,8 +357,7 @@ class SceneGestureHandlerTest { @Test fun onAccelaratedScroll_scrollToThirdScene() = runGestureTest { // Drag A -> B with progress 0.2 - draggable.onDragStarted() - draggable.onDelta(up(0.2f)) + draggable.onDragStarted(overSlop = up(0.2f)) assertTransition( currentScene = SceneA, fromScene = SceneA, @@ -401,9 +391,7 @@ class SceneGestureHandlerTest { @Test fun onAccelaratedScrollBothTargetsBecomeNull_settlesToIdle() = runGestureTest { - draggable.onDragStarted() - draggable.onDelta(up(0.2f)) - + draggable.onDragStarted(overSlop = up(0.2f)) draggable.onDelta(up(0.2f)) draggable.onDragStopped(velocity = -velocityThreshold) assertTransition(currentScene = SceneB, fromScene = SceneA, toScene = SceneB) @@ -459,16 +447,14 @@ class SceneGestureHandlerTest { draggable.onDragStopped(down(0.1f)) // now target changed to C for new drag that started before previous drag settled to Idle - draggable.onDragStarted(up(0.1f)) + draggable.onDragStarted(overSlop = 0f) + draggable.onDelta(up(0.1f)) assertTransition(fromScene = SceneA, toScene = SceneC, progress = 0.3f) } @Test fun startGestureDuringAnimatingOffset_shouldImmediatelyStopTheAnimation() = runGestureTest { - draggable.onDragStarted() - assertTransition(currentScene = SceneA) - - draggable.onDelta(pixels = down(0.1f)) + draggable.onDragStarted(overSlop = down(0.1f)) assertTransition(currentScene = SceneA) draggable.onDragStopped( @@ -759,10 +745,8 @@ class SceneGestureHandlerTest { @Test fun startNestedScrollWhileDragging() = runGestureTest { val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeAlways) - draggable.onDragStarted() + draggable.onDragStarted(overSlop = down(0.1f)) assertTransition(currentScene = SceneA) - - draggable.onDelta(down(0.1f)) assertThat(progress).isEqualTo(0.1f) // now we can intercept the scroll events diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt index 1ec3c8ba2301..940335853221 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt @@ -17,6 +17,7 @@ package com.android.compose.animation.scene import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable @@ -89,8 +90,8 @@ class SwipeToSceneTest { mapOf( Swipe.Down to TestScenes.SceneA, Swipe(SwipeDirection.Down, pointerCount = 2) to TestScenes.SceneB, - Swipe(SwipeDirection.Right, fromEdge = Edge.Left) to TestScenes.SceneB, - Swipe(SwipeDirection.Down, fromEdge = Edge.Top) to TestScenes.SceneB, + Swipe(SwipeDirection.Right, fromSource = Edge.Left) to TestScenes.SceneB, + Swipe(SwipeDirection.Down, fromSource = Edge.Top) to TestScenes.SceneB, ), ) { Box(Modifier.fillMaxSize()) @@ -349,4 +350,46 @@ class SwipeToSceneTest { assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java) assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC) } + + @Test + fun swipeDistance() { + // The draggable touch slop, i.e. the min px distance a touch pointer must move before it is + // detected as a drag event. + var touchSlop = 0f + + val layoutState = MutableSceneTransitionLayoutState(TestScenes.SceneA) + val verticalSwipeDistance = 50.dp + assertThat(verticalSwipeDistance).isNotEqualTo(LayoutHeight) + + rule.setContent { + touchSlop = LocalViewConfiguration.current.touchSlop + + SceneTransitionLayout( + state = layoutState, + modifier = Modifier.size(LayoutWidth, LayoutHeight) + ) { + scene( + TestScenes.SceneA, + userActions = + mapOf(Swipe.Down to TestScenes.SceneB withDistance verticalSwipeDistance), + ) { + Spacer(Modifier.fillMaxSize()) + } + scene(TestScenes.SceneB) { Spacer(Modifier.fillMaxSize()) } + } + } + + assertThat(layoutState.currentTransition).isNull() + + // Swipe by half of verticalSwipeDistance. + rule.onRoot().performTouchInput { + down(middleTop) + moveBy(Offset(0f, touchSlop + (verticalSwipeDistance / 2).toPx()), delayMillis = 1_000) + } + + // We should be at 50% + val transition = layoutState.currentTransition + assertThat(transition).isNotNull() + assertThat(transition!!.progress).isEqualTo(0.5f) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt index 030d41ddd8fb..c82688c2772a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt @@ -203,11 +203,17 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { whenever(deviceProvisionedController.isUserSetup(anyInt())).thenReturn(true) featureFlags = FakeFeatureFlags() - featureFlags.set(Flags.KEYGUARD_WM_STATE_REFACTOR, false) featureFlags.set(Flags.REFACTOR_KEYGUARD_DISMISS_INTENT, false) featureFlags.set(Flags.LOCKSCREEN_ENABLE_LANDSCAPE, false) - mSetFlagsRule.enableFlags(AConfigFlags.FLAG_REVAMPED_BOUNCER_MESSAGES) + mSetFlagsRule.enableFlags( + AConfigFlags.FLAG_REVAMPED_BOUNCER_MESSAGES, + ) + mSetFlagsRule.disableFlags( + FLAG_SIDEFPS_CONTROLLER_REFACTOR, + AConfigFlags.FLAG_KEYGUARD_WM_STATE_REFACTOR + ) + keyguardPasswordViewController = KeyguardPasswordViewController( keyguardPasswordView, @@ -238,7 +244,6 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { sceneInteractor.setTransitionState(sceneTransitionStateFlow) deviceEntryInteractor = kosmos.deviceEntryInteractor - mSetFlagsRule.disableFlags(FLAG_SIDEFPS_CONTROLLER_REFACTOR) underTest = KeyguardSecurityContainerController( view, 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/communal/data/repository/CommunalWidgetRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt index bb3429e72b35..c979ca63950a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt @@ -30,7 +30,6 @@ import com.android.systemui.communal.data.db.CommunalWidgetItem import com.android.systemui.communal.shared.CommunalWidgetHost import com.android.systemui.communal.shared.model.CommunalWidgetContentModel import com.android.systemui.communal.widgets.CommunalAppWidgetHost -import com.android.systemui.communal.widgets.WidgetConfigurator import com.android.systemui.communal.widgets.widgetConfiguratorFail import com.android.systemui.communal.widgets.widgetConfiguratorSuccess import com.android.systemui.coroutines.collectLastValue @@ -45,8 +44,7 @@ import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import java.util.Optional import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.flowOf -import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -62,24 +60,17 @@ import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidJUnit4::class) class CommunalWidgetRepositoryImplTest : SysuiTestCase() { - @Mock private lateinit var appWidgetManagerOptional: Optional<AppWidgetManager> - @Mock private lateinit var appWidgetManager: AppWidgetManager - @Mock private lateinit var appWidgetHost: CommunalAppWidgetHost - @Mock private lateinit var stopwatchProviderInfo: AppWidgetProviderInfo - @Mock private lateinit var providerInfoA: AppWidgetProviderInfo - @Mock private lateinit var communalWidgetHost: CommunalWidgetHost - @Mock private lateinit var communalWidgetDao: CommunalWidgetDao private lateinit var logBuffer: LogBuffer + private lateinit var fakeWidgets: MutableStateFlow<Map<CommunalItemRank, CommunalWidgetItem>> private val kosmos = testKosmos() - private val testDispatcher = kosmos.testDispatcher private val testScope = kosmos.testScope private val fakeAllowlist = @@ -94,7 +85,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { @Before fun setUp() { MockitoAnnotations.initMocks(this) - + fakeWidgets = MutableStateFlow(emptyMap()) logBuffer = logcatLogBuffer(name = "CommunalWidgetRepoImplTest") setAppWidgetIds(emptyList()) @@ -102,13 +93,11 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { overrideResource(R.array.config_communalWidgetAllowlist, fakeAllowlist.toTypedArray()) whenever(stopwatchProviderInfo.loadLabel(any())).thenReturn("Stopwatch") - whenever(communalWidgetDao.getWidgets()).thenReturn(flowOf(emptyMap())) - whenever(appWidgetManagerOptional.isPresent).thenReturn(true) - whenever(appWidgetManagerOptional.get()).thenReturn(appWidgetManager) + whenever(communalWidgetDao.getWidgets()).thenReturn(fakeWidgets) underTest = CommunalWidgetRepositoryImpl( - appWidgetManagerOptional, + Optional.of(appWidgetManager), appWidgetHost, testScope.backgroundScope, kosmos.testDispatcher, @@ -119,30 +108,16 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { } @Test - fun neverQueryDbForWidgets_whenHostIsInactive() = - testScope.runTest { - underTest.updateAppWidgetHostActive(false) - underTest.communalWidgets.launchIn(testScope.backgroundScope) - runCurrent() - - verify(communalWidgetDao, never()).getWidgets() - } - - @Test - fun communalWidgets_whenHostIsActive_queryWidgetsFromDb() = + fun communalWidgets_queryWidgetsFromDb() = testScope.runTest { - underTest.updateAppWidgetHostActive(true) - val communalItemRankEntry = CommunalItemRank(uid = 1L, rank = 1) val communalWidgetItemEntry = CommunalWidgetItem(uid = 1L, 1, "pk_name/cls_name", 1L) - whenever(communalWidgetDao.getWidgets()) - .thenReturn(flowOf(mapOf(communalItemRankEntry to communalWidgetItemEntry))) + fakeWidgets.value = mapOf(communalItemRankEntry to communalWidgetItemEntry) whenever(appWidgetManager.getAppWidgetInfo(anyInt())).thenReturn(providerInfoA) installedProviders(listOf(stopwatchProviderInfo)) val communalWidgets by collectLastValue(underTest.communalWidgets) - runCurrent() verify(communalWidgetDao).getWidgets() assertThat(communalWidgets) .containsExactly( @@ -157,8 +132,6 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { @Test fun addWidget_allocateId_bindWidget_andAddToDb() = testScope.runTest { - underTest.updateAppWidgetHostActive(true) - val provider = ComponentName("pkg_name", "cls_name") val id = 1 val priority = 1 @@ -176,8 +149,6 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { @Test fun addWidget_configurationFails_doNotAddWidgetToDb() = testScope.runTest { - underTest.updateAppWidgetHostActive(true) - val provider = ComponentName("pkg_name", "cls_name") val id = 1 val priority = 1 @@ -195,23 +166,13 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { @Test fun addWidget_configurationThrowsError_doNotAddWidgetToDb() = testScope.runTest { - underTest.updateAppWidgetHostActive(true) - val provider = ComponentName("pkg_name", "cls_name") val id = 1 val priority = 1 whenever(communalWidgetHost.getAppWidgetInfo(id)) .thenReturn(PROVIDER_INFO_REQUIRES_CONFIGURATION) whenever(communalWidgetHost.allocateIdAndBindWidget(provider)).thenReturn(id) - underTest.addWidget( - provider, - priority, - object : WidgetConfigurator { - override suspend fun configureWidget(appWidgetId: Int): Boolean { - throw IllegalStateException("some error") - } - } - ) + underTest.addWidget(provider, priority) { throw IllegalStateException("some error") } runCurrent() verify(communalWidgetHost).allocateIdAndBindWidget(provider) @@ -222,8 +183,6 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { @Test fun addWidget_configurationNotRequired_doesNotConfigure_addWidgetToDb() = testScope.runTest { - underTest.updateAppWidgetHostActive(true) - val provider = ComponentName("pkg_name", "cls_name") val id = 1 val priority = 1 @@ -241,8 +200,6 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { @Test fun deleteWidget_removeWidgetId_andDeleteFromDb() = testScope.runTest { - underTest.updateAppWidgetHostActive(true) - val id = 1 underTest.deleteWidget(id) runCurrent() @@ -254,8 +211,6 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { @Test fun reorderWidgets_queryDb() = testScope.runTest { - underTest.updateAppWidgetHostActive(true) - val widgetIdToPriorityMap = mapOf(104 to 1, 103 to 2, 101 to 3) underTest.updateWidgetOrder(widgetIdToPriorityMap) runCurrent() @@ -263,28 +218,6 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { verify(communalWidgetDao).updateWidgetOrder(widgetIdToPriorityMap) } - @Test - fun appWidgetHost_startListening() = - testScope.runTest { - verify(appWidgetHost, never()).startListening() - - underTest.updateAppWidgetHostActive(true) - - verify(appWidgetHost).startListening() - } - - @Test - fun appWidgetHost_stopListening() = - testScope.runTest { - underTest.updateAppWidgetHostActive(true) - - verify(appWidgetHost).startListening() - - underTest.updateAppWidgetHostActive(false) - - verify(appWidgetHost).stopListening() - } - private fun installedProviders(providers: List<AppWidgetProviderInfo>) { whenever(appWidgetManager.installedProviders).thenReturn(providers) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorCommunalDisabledTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorCommunalDisabledTest.kt index e8216735fb5d..6a3fc2a060eb 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorCommunalDisabledTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorCommunalDisabledTest.kt @@ -80,15 +80,4 @@ class CommunalInteractorCommunalDisabledTest : SysuiTestCase() { assertThat(isCommunalAvailable).isFalse() } - - @Test - fun updateAppWidgetHostActive_whenStorageUnlock_false() = - testScope.runTest { - assertThat(widgetRepository.isHostActive()).isFalse() - - keyguardRepository.setIsEncryptedOrLockdown(false) - runCurrent() - - assertThat(widgetRepository.isHostActive()).isFalse() - } } 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 1b7117f41bbb..a083e7cf22c7 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 @@ -172,20 +172,6 @@ class CommunalInteractorTest : SysuiTestCase() { } @Test - fun updateAppWidgetHostActive_uponStorageUnlockAsMainUser_true() = - testScope.runTest { - collectLastValue(underTest.isCommunalAvailable) - assertThat(widgetRepository.isHostActive()).isFalse() - - keyguardRepository.setIsEncryptedOrLockdown(false) - userRepository.setSelectedUserInfo(mainUser) - keyguardRepository.setKeyguardShowing(true) - runCurrent() - - assertThat(widgetRepository.isHostActive()).isTrue() - } - - @Test fun widget_tutorialCompletedAndWidgetsAvailable_showWidgetContent() = testScope.runTest { // Keyguard showing, and tutorial completed. diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt new file mode 100644 index 000000000000..112b0c797854 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.widgets + +import android.content.pm.UserInfo +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.communal.domain.interactor.communalInteractor +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.android.systemui.user.data.repository.fakeUserRepository +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.Mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class CommunalAppWidgetHostStartableTest : SysuiTestCase() { + private val kosmos = testKosmos() + + @Mock private lateinit var appWidgetHost: CommunalAppWidgetHost + + private lateinit var underTest: CommunalAppWidgetHostStartable + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + kosmos.fakeUserRepository.setUserInfos(listOf(MAIN_USER_INFO)) + + underTest = + CommunalAppWidgetHostStartable( + appWidgetHost, + kosmos.communalInteractor, + kosmos.applicationCoroutineScope, + kosmos.testDispatcher, + ) + } + + @Test + fun editModeShowingStartsAppWidgetHost() = + with(kosmos) { + testScope.runTest { + setCommunalAvailable(false) + communalInteractor.setEditModeOpen(true) + verify(appWidgetHost, never()).startListening() + + underTest.start() + runCurrent() + + verify(appWidgetHost).startListening() + verify(appWidgetHost, never()).stopListening() + + communalInteractor.setEditModeOpen(false) + runCurrent() + + verify(appWidgetHost).stopListening() + } + } + + @Test + fun communalShowingStartsAppWidgetHost() = + with(kosmos) { + testScope.runTest { + setCommunalAvailable(true) + communalInteractor.setEditModeOpen(false) + verify(appWidgetHost, never()).startListening() + + underTest.start() + runCurrent() + + verify(appWidgetHost).startListening() + verify(appWidgetHost, never()).stopListening() + + setCommunalAvailable(false) + runCurrent() + + verify(appWidgetHost).stopListening() + } + } + + @Test + fun communalAndEditModeNotShowingNeverStartListening() = + with(kosmos) { + testScope.runTest { + setCommunalAvailable(false) + communalInteractor.setEditModeOpen(false) + + underTest.start() + runCurrent() + + verify(appWidgetHost, never()).startListening() + verify(appWidgetHost, never()).stopListening() + } + } + + private suspend fun setCommunalAvailable(available: Boolean) = + with(kosmos) { + fakeKeyguardRepository.setIsEncryptedOrLockdown(!available) + fakeUserRepository.setSelectedUserInfo(MAIN_USER_INFO) + fakeKeyguardRepository.setKeyguardShowing(true) + } + + private companion object { + val MAIN_USER_INFO = UserInfo(0, "primary", UserInfo.FLAG_MAIN) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt index 6a14220e6a42..6808f5d643a9 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt @@ -38,6 +38,7 @@ import androidx.test.filters.SmallTest import com.android.internal.logging.InstanceId.fakeInstanceId import com.android.internal.logging.UiEventLogger import com.android.keyguard.KeyguardUpdateMonitor +import com.android.systemui.Flags as AConfigFlags import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.data.repository.FakeDisplayStateRepository import com.android.systemui.biometrics.data.repository.FakeFacePropertyRepository @@ -62,7 +63,6 @@ import com.android.systemui.display.data.repository.FakeDisplayRepository import com.android.systemui.display.data.repository.display import com.android.systemui.dump.DumpManager import com.android.systemui.flags.FakeFeatureFlags -import com.android.systemui.flags.Flags.KEYGUARD_WM_STATE_REFACTOR import com.android.systemui.keyguard.data.repository.BiometricType import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository import com.android.systemui.keyguard.data.repository.FakeCommandQueue @@ -194,7 +194,7 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { biometricSettingsRepository = FakeBiometricSettingsRepository() deviceEntryFingerprintAuthRepository = FakeDeviceEntryFingerprintAuthRepository() trustRepository = FakeTrustRepository() - featureFlags = FakeFeatureFlags().apply { set(KEYGUARD_WM_STATE_REFACTOR, false) } + featureFlags = FakeFeatureFlags() powerRepository = FakePowerRepository() powerInteractor = @@ -252,6 +252,10 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { .thenReturn(listOf(createFaceSensorProperties(supportsFaceDetection = true))) whenever(bypassController.bypassEnabled).thenReturn(true) underTest = createDeviceEntryFaceAuthRepositoryImpl(faceManager, bypassController) + + mSetFlagsRule.disableFlags( + AConfigFlags.FLAG_KEYGUARD_WM_STATE_REFACTOR, + ) } private fun createDeviceEntryFaceAuthRepositoryImpl( @@ -301,7 +305,6 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { faceAuthBuffer, keyguardTransitionInteractor, displayStateInteractor, - featureFlags, dumpManager, ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/AuthRippleInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/AuthRippleInteractorTest.kt new file mode 100644 index 000000000000..88ad3f37dacd --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/AuthRippleInteractorTest.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.deviceentry.domain.interactor + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository +import com.android.systemui.keyguard.shared.model.BiometricUnlockModel +import com.android.systemui.keyguard.shared.model.BiometricUnlockSource +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class AuthRippleInteractorTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val deviceEntrySourceInteractor = kosmos.deviceEntrySourceInteractor + private val fingerprintPropertyRepository = kosmos.fingerprintPropertyRepository + private val keyguardRepository = kosmos.fakeKeyguardRepository + private val underTest = kosmos.authRippleInteractor + + @Test + fun enteringDeviceFromDeviceEntryIcon_udfpsNotSupported_doesNotShowAuthRipple() = + testScope.runTest { + val showUnlockRipple by collectLastValue(underTest.showUnlockRipple) + fingerprintPropertyRepository.supportsRearFps() + keyguardRepository.setKeyguardDismissible(true) + runCurrent() + deviceEntrySourceInteractor.attemptEnterDeviceFromDeviceEntryIcon() + assertThat(showUnlockRipple).isNull() + } + + @Test + fun enteringDeviceFromDeviceEntryIcon_udfpsSupported_showsAuthRipple() = + testScope.runTest { + val showUnlockRipple by collectLastValue(underTest.showUnlockRipple) + fingerprintPropertyRepository.supportsUdfps() + keyguardRepository.setKeyguardDismissible(true) + runCurrent() + deviceEntrySourceInteractor.attemptEnterDeviceFromDeviceEntryIcon() + assertThat(showUnlockRipple).isEqualTo(BiometricUnlockSource.FINGERPRINT_SENSOR) + } + + @Test + fun faceUnlocked_showsAuthRipple() = + testScope.runTest { + val showUnlockRipple by collectLastValue(underTest.showUnlockRipple) + keyguardRepository.setBiometricUnlockSource(BiometricUnlockSource.FACE_SENSOR) + keyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK) + assertThat(showUnlockRipple).isEqualTo(BiometricUnlockSource.FACE_SENSOR) + } + + @Test + fun fingerprintUnlocked_showsAuthRipple() = + testScope.runTest { + val showUnlockRippleFromBiometricUnlock by collectLastValue(underTest.showUnlockRipple) + keyguardRepository.setBiometricUnlockSource(BiometricUnlockSource.FINGERPRINT_SENSOR) + keyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK) + assertThat(showUnlockRippleFromBiometricUnlock) + .isEqualTo(BiometricUnlockSource.FINGERPRINT_SENSOR) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntrySourceInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntrySourceInteractorTest.kt new file mode 100644 index 000000000000..d216fa0d0eff --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntrySourceInteractorTest.kt @@ -0,0 +1,77 @@ +/* + * 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.deviceentry.domain.interactor + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository +import com.android.systemui.keyguard.shared.model.BiometricUnlockModel +import com.android.systemui.keyguard.shared.model.BiometricUnlockSource +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class DeviceEntrySourceInteractorTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val keyguardRepository = kosmos.fakeKeyguardRepository + private val underTest = kosmos.deviceEntrySourceInteractor + + @Test + fun deviceEntryFromFaceUnlock() = + testScope.runTest { + val deviceEntryFromBiometricAuthentication by + collectLastValue(underTest.deviceEntryFromBiometricSource) + keyguardRepository.setBiometricUnlockSource(BiometricUnlockSource.FACE_SENSOR) + keyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK) + runCurrent() + assertThat(deviceEntryFromBiometricAuthentication) + .isEqualTo(BiometricUnlockSource.FACE_SENSOR) + } + + @Test + fun deviceEntryFromFingerprintUnlock() = runTest { + val deviceEntryFromBiometricAuthentication by + collectLastValue(underTest.deviceEntryFromBiometricSource) + keyguardRepository.setBiometricUnlockSource(BiometricUnlockSource.FINGERPRINT_SENSOR) + keyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK) + runCurrent() + assertThat(deviceEntryFromBiometricAuthentication) + .isEqualTo(BiometricUnlockSource.FINGERPRINT_SENSOR) + } + + @Test + fun noDeviceEntry() = runTest { + val deviceEntryFromBiometricAuthentication by + collectLastValue(underTest.deviceEntryFromBiometricSource) + keyguardRepository.setBiometricUnlockSource(BiometricUnlockSource.FINGERPRINT_SENSOR) + // doesn't dismiss keyguard: + keyguardRepository.setBiometricUnlockState(BiometricUnlockModel.ONLY_WAKE) + runCurrent() + assertThat(deviceEntryFromBiometricAuthentication).isNull() + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt index dc8b97abbfe8..78ae8b119c69 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt @@ -238,10 +238,10 @@ class KeyguardRepositoryImplTest : SysuiTestCase() { } @Test - fun isKeyguardUnlocked() = + fun isKeyguardDismissible() = testScope.runTest { whenever(keyguardStateController.isUnlocked).thenReturn(false) - val isKeyguardUnlocked by collectLastValue(underTest.isKeyguardUnlocked) + val isKeyguardUnlocked by collectLastValue(underTest.isKeyguardDismissible) runCurrent() assertThat(isKeyguardUnlocked).isFalse() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepositoryImplTest.kt index eb845b2b423c..d9f24b36dac2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepositoryImplTest.kt @@ -35,7 +35,6 @@ import com.android.systemui.common.data.repository.fakePackageChangeRepository import com.android.systemui.common.data.repository.packageChangeRepository import com.android.systemui.common.data.shared.model.PackageChangeModel import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.testKosmos import com.android.systemui.util.mockito.any @@ -44,6 +43,8 @@ import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test @@ -52,6 +53,7 @@ import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock import org.mockito.MockitoAnnotations +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) @TestableLooper.RunWithLooper @@ -82,7 +84,7 @@ class InstalledTilesComponentRepositoryImplTest : SysuiTestCase() { underTest = InstalledTilesComponentRepositoryImpl( context, - kosmos.testDispatcher, + testScope.backgroundScope, kosmos.packageChangeRepository ) } @@ -103,6 +105,7 @@ class InstalledTilesComponentRepositoryImplTest : SysuiTestCase() { .thenReturn(listOf(resolveInfo)) val componentNames by collectLastValue(underTest.getInstalledTilesComponents(userId)) + runCurrent() assertThat(componentNames).containsExactly(TEST_COMPONENT) } @@ -115,6 +118,8 @@ class InstalledTilesComponentRepositoryImplTest : SysuiTestCase() { ResolveInfo(TEST_COMPONENT, hasPermission = true, defaultEnabled = true) val componentNames by collectLastValue(underTest.getInstalledTilesComponents(userId)) + runCurrent() + assertThat(componentNames).isEmpty() whenever( @@ -126,6 +131,7 @@ class InstalledTilesComponentRepositoryImplTest : SysuiTestCase() { ) .thenReturn(listOf(resolveInfo)) kosmos.fakePackageChangeRepository.notifyChange(PackageChangeModel.Empty) + runCurrent() assertThat(componentNames).containsExactly(TEST_COMPONENT) } @@ -146,6 +152,8 @@ class InstalledTilesComponentRepositoryImplTest : SysuiTestCase() { .thenReturn(listOf(resolveInfo)) val componentNames by collectLastValue(underTest.getInstalledTilesComponents(userId)) + runCurrent() + assertThat(componentNames).isEmpty() } @@ -165,6 +173,8 @@ class InstalledTilesComponentRepositoryImplTest : SysuiTestCase() { .thenReturn(listOf(resolveInfo)) val componentNames by collectLastValue(underTest.getInstalledTilesComponents(userId)) + runCurrent() + assertThat(componentNames).isEmpty() } @@ -210,10 +220,31 @@ class InstalledTilesComponentRepositoryImplTest : SysuiTestCase() { .thenReturn(listOf(resolveInfo)) val componentNames by collectLastValue(underTest.getInstalledTilesComponents(userId)) + runCurrent() assertThat(componentNames).containsExactly(TEST_COMPONENT) } + @Test + fun loadComponentsForSameUserTwice_returnsSameFlow() = + testScope.runTest { + val flowForUser1 = underTest.getInstalledTilesComponents(1) + val flowForUser1TheSecondTime = underTest.getInstalledTilesComponents(1) + runCurrent() + + assertThat(flowForUser1TheSecondTime).isEqualTo(flowForUser1) + } + + @Test + fun loadComponentsForDifferentUsers_returnsDifferentFlow() = + testScope.runTest { + val flowForUser1 = underTest.getInstalledTilesComponents(1) + val flowForUser2 = underTest.getInstalledTilesComponents(2) + runCurrent() + + assertThat(flowForUser2).isNotEqualTo(flowForUser1) + } + companion object { private val INTENT = Intent(TileService.ACTION_QS_TILE) private val FLAGS = 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/util/kotlin/BooleanFlowOperatorsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/BooleanFlowOperatorsTest.kt new file mode 100644 index 000000000000..b4a0a37ec9ef --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/BooleanFlowOperatorsTest.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.util.kotlin + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.android.systemui.util.kotlin.BooleanFlowOperators.and +import com.android.systemui.util.kotlin.BooleanFlowOperators.not +import com.android.systemui.util.kotlin.BooleanFlowOperators.or +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class BooleanFlowOperatorsTest : SysuiTestCase() { + + val kosmos = testKosmos() + val testScope = kosmos.testScope + + @Test + fun and_allTrue_returnsTrue() = + testScope.runTest { + val result by collectLastValue(and(TRUE, TRUE)) + assertThat(result).isTrue() + } + + @Test + fun and_anyFalse_returnsFalse() = + testScope.runTest { + val result by collectLastValue(and(TRUE, FALSE, TRUE)) + assertThat(result).isFalse() + } + + @Test + fun and_allFalse_returnsFalse() = + testScope.runTest { + val result by collectLastValue(and(FALSE, FALSE, FALSE)) + assertThat(result).isFalse() + } + + @Test + fun or_allTrue_returnsTrue() = + testScope.runTest { + val result by collectLastValue(or(TRUE, TRUE)) + assertThat(result).isTrue() + } + + @Test + fun or_anyTrue_returnsTrue() = + testScope.runTest { + val result by collectLastValue(or(FALSE, TRUE, FALSE)) + assertThat(result).isTrue() + } + + @Test + fun or_allFalse_returnsFalse() = + testScope.runTest { + val result by collectLastValue(or(FALSE, FALSE, FALSE)) + assertThat(result).isFalse() + } + + @Test + fun not_true_returnsFalse() = + testScope.runTest { + val result by collectLastValue(not(TRUE)) + assertThat(result).isFalse() + } + + @Test + fun not_false_returnsFalse() = + testScope.runTest { + val result by collectLastValue(not(FALSE)) + assertThat(result).isTrue() + } + + private companion object { + val TRUE: Flow<Boolean> + get() = flowOf(true) + val FALSE: Flow<Boolean> + get() = flowOf(false) + } +} diff --git a/packages/SystemUI/res/values/ids.xml b/packages/SystemUI/res/values/ids.xml index 8ec5ccd7a080..2ab0813300e3 100644 --- a/packages/SystemUI/res/values/ids.xml +++ b/packages/SystemUI/res/values/ids.xml @@ -184,6 +184,7 @@ <item type="id" name="action_move_to_edge_and_hide"/> <item type="id" name="action_move_out_edge_and_show"/> <item type="id" name="action_remove_menu"/> + <item type="id" name="action_edit"/> <!-- rounded corner view id --> <item type="id" name="rounded_corner_top_left"/> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 9bc7681665f1..47ac96ca7960 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] --> @@ -2558,6 +2560,8 @@ <string name="accessibility_floating_button_action_remove_menu">Remove</string> <!-- Action in accessibility menu to toggle on/off the accessibility feature. [CHAR LIMIT=30]--> <string name="accessibility_floating_button_action_double_tap_to_toggle">toggle</string> + <!-- Action in accessibility menu to open the shortcut edit menu" [CHAR LIMIT=30]--> + <string name="accessibility_floating_button_action_edit">Edit</string> <!-- Device Controls strings --> <!-- Device Controls, Quick Settings tile title [CHAR LIMIT=30] --> 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 ecce22315c50..25d771308aea 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java @@ -19,9 +19,9 @@ package com.android.keyguard; import static android.app.StatusBarManager.SESSION_KEYGUARD; import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; +import static com.android.keyguard.KeyguardSecurityContainer.BOUNCER_DISMISSIBLE_KEYGUARD; import static com.android.keyguard.KeyguardSecurityContainer.BOUNCER_DISMISS_BIOMETRIC; import static com.android.keyguard.KeyguardSecurityContainer.BOUNCER_DISMISS_EXTENDED_ACCESS; -import static com.android.keyguard.KeyguardSecurityContainer.BOUNCER_DISMISSIBLE_KEYGUARD; import static com.android.keyguard.KeyguardSecurityContainer.BOUNCER_DISMISS_NONE_SECURITY; import static com.android.keyguard.KeyguardSecurityContainer.BOUNCER_DISMISS_PASSWORD; import static com.android.keyguard.KeyguardSecurityContainer.BOUNCER_DISMISS_SIM; @@ -84,6 +84,7 @@ import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInt import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; +import com.android.systemui.keyguard.KeyguardWmStateRefactor; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.log.SessionTracker; import com.android.systemui.plugins.ActivityStarter; @@ -100,8 +101,6 @@ import com.android.systemui.util.ViewController; import com.android.systemui.util.kotlin.JavaAdapter; import com.android.systemui.util.settings.GlobalSettings; -import dagger.Lazy; - import java.io.File; import java.util.Arrays; import java.util.Optional; @@ -109,6 +108,7 @@ import java.util.Optional; import javax.inject.Inject; import javax.inject.Provider; +import dagger.Lazy; import kotlinx.coroutines.Job; /** Controller for {@link KeyguardSecurityContainer} */ @@ -330,7 +330,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard } } - if (mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + if (KeyguardWmStateRefactor.isEnabled()) { mKeyguardTransitionInteractor.startDismissKeyguardTransition(); } } diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java index 3e8c6a76998a..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 @@ -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/keyguard/LockIconViewController.java b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java index d5dc85cd8715..8e9815085e31 100644 --- a/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java +++ b/packages/SystemUI/src/com/android/keyguard/LockIconViewController.java @@ -63,6 +63,7 @@ import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor; +import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; @@ -87,6 +88,8 @@ import java.util.function.Consumer; import javax.inject.Inject; +import kotlinx.coroutines.ExperimentalCoroutinesApi; + /** * Controls when to show the LockIcon affordance (lock/unlocked icon or circle) on lock screen. * @@ -717,6 +720,7 @@ public class LockIconViewController implements Dumpable { return mDownDetected; } + @ExperimentalCoroutinesApi @VisibleForTesting protected void onLongPress() { cancelTouches(); @@ -727,7 +731,8 @@ public class LockIconViewController implements Dumpable { // pre-emptively set to true to hide view mIsBouncerShowing = true; - if (mUdfpsSupported && mShowUnlockIcon && mAuthRippleController != null) { + if (!DeviceEntryUdfpsRefactor.isEnabled() + && mUdfpsSupported && mShowUnlockIcon && mAuthRippleController != null) { mAuthRippleController.showUnlockRipple(FINGERPRINT); } updateVisibility(); 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/accessibility/floatingmenu/DragToInteractAnimationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationController.java index 568b24dbd4f3..7fd72ec8ce93 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationController.java @@ -16,127 +16,138 @@ package com.android.systemui.accessibility.floatingmenu; +import static android.R.id.empty; + import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; +import android.util.ArrayMap; +import android.util.Pair; import android.view.MotionEvent; import androidx.annotation.NonNull; import androidx.dynamicanimation.animation.DynamicAnimation; import com.android.internal.annotations.VisibleForTesting; +import com.android.systemui.Flags; +import com.android.wm.shell.common.bubbles.DismissCircleView; import com.android.wm.shell.common.bubbles.DismissView; import com.android.wm.shell.common.magnetictarget.MagnetizedObject; +import java.util.Map; +import java.util.Objects; + /** * Controls the interaction between {@link MagnetizedObject} and * {@link MagnetizedObject.MagneticTarget}. */ class DragToInteractAnimationController { - private static final boolean ENABLE_FLING_TO_DISMISS_MENU = false; private static final float COMPLETELY_OPAQUE = 1.0f; private static final float COMPLETELY_TRANSPARENT = 0.0f; private static final float CIRCLE_VIEW_DEFAULT_SCALE = 1.0f; private static final float ANIMATING_MAX_ALPHA = 0.7f; + private final DragToInteractView mInteractView; private final DismissView mDismissView; private final MenuView mMenuView; - private final ValueAnimator mDismissAnimator; - private final MagnetizedObject<?> mMagnetizedObject; - private float mMinDismissSize; + + /** + * MagnetizedObject cannot differentiate between its MagnetizedTargets, + * so we need an object & an animator for every interactable. + */ + private final ArrayMap<Integer, Pair<MagnetizedObject<MenuView>, ValueAnimator>> mInteractMap; + + private float mMinInteractSize; private float mSizePercent; - DragToInteractAnimationController(DismissView dismissView, MenuView menuView) { - mDismissView = dismissView; - mDismissView.setPivotX(dismissView.getWidth() / 2.0f); - mDismissView.setPivotY(dismissView.getHeight() / 2.0f); + DragToInteractAnimationController(DragToInteractView interactView, MenuView menuView) { + mDismissView = null; + mInteractView = interactView; + mInteractView.setPivotX(interactView.getWidth() / 2.0f); + mInteractView.setPivotY(interactView.getHeight() / 2.0f); mMenuView = menuView; updateResources(); - mDismissAnimator = ValueAnimator.ofFloat(COMPLETELY_OPAQUE, COMPLETELY_TRANSPARENT); - mDismissAnimator.addUpdateListener(dismissAnimation -> { - final float animatedValue = (float) dismissAnimation.getAnimatedValue(); - final float scaleValue = Math.max(animatedValue, mSizePercent); - dismissView.getCircle().setScaleX(scaleValue); - dismissView.getCircle().setScaleY(scaleValue); - - menuView.setAlpha(Math.max(animatedValue, ANIMATING_MAX_ALPHA)); + mInteractMap = new ArrayMap<>(); + interactView.getInteractMap().forEach((viewId, pair) -> { + DismissCircleView circleView = pair.getFirst(); + createMagnetizedObjectAndAnimator(circleView); }); + } - mDismissAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(@NonNull Animator animation, boolean isReverse) { - super.onAnimationEnd(animation, isReverse); + DragToInteractAnimationController(DismissView dismissView, MenuView menuView) { + mDismissView = dismissView; + mInteractView = null; + mDismissView.setPivotX(dismissView.getWidth() / 2.0f); + mDismissView.setPivotY(dismissView.getHeight() / 2.0f); + mMenuView = menuView; - if (isReverse) { - mDismissView.getCircle().setScaleX(CIRCLE_VIEW_DEFAULT_SCALE); - mDismissView.getCircle().setScaleY(CIRCLE_VIEW_DEFAULT_SCALE); - mMenuView.setAlpha(COMPLETELY_OPAQUE); - } - } - }); + updateResources(); - mMagnetizedObject = - new MagnetizedObject<MenuView>(mMenuView.getContext(), mMenuView, - new MenuAnimationController.MenuPositionProperty( - DynamicAnimation.TRANSLATION_X), - new MenuAnimationController.MenuPositionProperty( - DynamicAnimation.TRANSLATION_Y)) { - @Override - public void getLocationOnScreen(MenuView underlyingObject, int[] loc) { - underlyingObject.getLocationOnScreen(loc); - } - - @Override - public float getHeight(MenuView underlyingObject) { - return underlyingObject.getHeight(); - } - - @Override - public float getWidth(MenuView underlyingObject) { - return underlyingObject.getWidth(); - } - }; - - final MagnetizedObject.MagneticTarget magneticTarget = new MagnetizedObject.MagneticTarget( - dismissView.getCircle(), (int) mMinDismissSize); - mMagnetizedObject.addTarget(magneticTarget); - mMagnetizedObject.setFlingToTargetEnabled(ENABLE_FLING_TO_DISMISS_MENU); + mInteractMap = new ArrayMap<>(); + createMagnetizedObjectAndAnimator(dismissView.getCircle()); } - void showDismissView(boolean show) { - if (show) { - mDismissView.show(); - } else { - mDismissView.hide(); + void showInteractView(boolean show) { + if (Flags.floatingMenuDragToEdit() && mInteractView != null) { + if (show) { + mInteractView.show(); + } else { + mInteractView.hide(); + } + } else if (mDismissView != null) { + if (show) { + mDismissView.show(); + } else { + mDismissView.hide(); + } } } void setMagnetListener(MagnetizedObject.MagnetListener magnetListener) { - mMagnetizedObject.setMagnetListener(magnetListener); + mInteractMap.forEach((viewId, pair) -> { + MagnetizedObject<?> magnetizedObject = pair.first; + magnetizedObject.setMagnetListener(magnetListener); + }); } @VisibleForTesting - MagnetizedObject.MagnetListener getMagnetListener() { - return mMagnetizedObject.getMagnetListener(); + MagnetizedObject.MagnetListener getMagnetListener(int id) { + return Objects.requireNonNull(mInteractMap.get(id)).first.getMagnetListener(); } void maybeConsumeDownMotionEvent(MotionEvent event) { - mMagnetizedObject.maybeConsumeMotionEvent(event); + mInteractMap.forEach((viewId, pair) -> { + MagnetizedObject<?> magnetizedObject = pair.first; + magnetizedObject.maybeConsumeMotionEvent(event); + }); + } + + private int maybeConsumeMotionEvent(MotionEvent event) { + for (Map.Entry<Integer, Pair<MagnetizedObject<MenuView>, ValueAnimator>> set: + mInteractMap.entrySet()) { + MagnetizedObject<MenuView> magnetizedObject = set.getValue().first; + if (magnetizedObject.maybeConsumeMotionEvent(event)) { + return set.getKey(); + } + } + return empty; } /** - * This used to pass {@link MotionEvent#ACTION_DOWN} to the magnetized object to check if it was - * within the magnetic field. It should be used in the {@link MenuListViewTouchHandler}. + * This used to pass {@link MotionEvent#ACTION_DOWN} to the magnetized objects + * to check if it was within a magnetic field. + * It should be used in the {@link MenuListViewTouchHandler}. * * @param event that move the magnetized object which is also the menu list view. - * @return true if the location of the motion events moves within the magnetic field of a - * target, but false if didn't set + * @return id of a target if the location of the motion events moves + * within the field of the target, otherwise it returns{@link android.R.id#empty}. + * <p> * {@link DragToInteractAnimationController#setMagnetListener(MagnetizedObject.MagnetListener)}. */ - boolean maybeConsumeMoveMotionEvent(MotionEvent event) { - return mMagnetizedObject.maybeConsumeMotionEvent(event); + int maybeConsumeMoveMotionEvent(MotionEvent event) { + return maybeConsumeMotionEvent(event); } /** @@ -144,31 +155,93 @@ class DragToInteractAnimationController { * within the magnetic field. It should be used in the {@link MenuListViewTouchHandler}. * * @param event that move the magnetized object which is also the menu list view. - * @return true if the location of the motion events moves within the magnetic field of a - * target, but false if didn't set + * @return id of a target if the location of the motion events moves + * within the field of the target, otherwise it returns{@link android.R.id#empty}. * {@link DragToInteractAnimationController#setMagnetListener(MagnetizedObject.MagnetListener)}. */ - boolean maybeConsumeUpMotionEvent(MotionEvent event) { - return mMagnetizedObject.maybeConsumeMotionEvent(event); + int maybeConsumeUpMotionEvent(MotionEvent event) { + return maybeConsumeMotionEvent(event); } - void animateDismissMenu(boolean scaleUp) { + void animateInteractMenu(int targetViewId, boolean scaleUp) { + Pair<MagnetizedObject<MenuView>, ValueAnimator> value = mInteractMap.get(targetViewId); + if (value == null) { + return; + } + ValueAnimator animator = value.second; if (scaleUp) { - mDismissAnimator.start(); + animator.start(); } else { - mDismissAnimator.reverse(); + animator.reverse(); } } void updateResources() { - final float maxDismissSize = mDismissView.getResources().getDimensionPixelSize( + final float maxInteractSize = mMenuView.getResources().getDimensionPixelSize( com.android.wm.shell.R.dimen.dismiss_circle_size); - mMinDismissSize = mDismissView.getResources().getDimensionPixelSize( + mMinInteractSize = mMenuView.getResources().getDimensionPixelSize( com.android.wm.shell.R.dimen.dismiss_circle_small); - mSizePercent = mMinDismissSize / maxDismissSize; + mSizePercent = mMinInteractSize / maxInteractSize; } - interface DismissCallback { - void onDismiss(); + /** + * Creates a magnetizedObject & valueAnimator pair for the provided circleView, + * and adds them to the interactMap. + * + * @param circleView circleView to create objects for. + */ + private void createMagnetizedObjectAndAnimator(DismissCircleView circleView) { + MagnetizedObject<MenuView> magnetizedObject = new MagnetizedObject<MenuView>( + mMenuView.getContext(), mMenuView, + new MenuAnimationController.MenuPositionProperty( + DynamicAnimation.TRANSLATION_X), + new MenuAnimationController.MenuPositionProperty( + DynamicAnimation.TRANSLATION_Y)) { + @Override + public void getLocationOnScreen(MenuView underlyingObject, @NonNull int[] loc) { + underlyingObject.getLocationOnScreen(loc); + } + + @Override + public float getHeight(MenuView underlyingObject) { + return underlyingObject.getHeight(); + } + + @Override + public float getWidth(MenuView underlyingObject) { + return underlyingObject.getWidth(); + } + }; + // Avoid unintended selection of an object / option + magnetizedObject.setFlingToTargetEnabled(false); + magnetizedObject.addTarget(new MagnetizedObject.MagneticTarget( + circleView, (int) mMinInteractSize)); + + final ValueAnimator animator = + ValueAnimator.ofFloat(COMPLETELY_OPAQUE, COMPLETELY_TRANSPARENT); + + animator.addUpdateListener(dismissAnimation -> { + final float animatedValue = (float) dismissAnimation.getAnimatedValue(); + final float scaleValue = Math.max(animatedValue, mSizePercent); + circleView.setScaleX(scaleValue); + circleView.setScaleY(scaleValue); + + mMenuView.setAlpha(Math.max(animatedValue, ANIMATING_MAX_ALPHA)); + }); + + animator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(@NonNull Animator animation, boolean isReverse) { + super.onAnimationEnd(animation, isReverse); + + if (isReverse) { + circleView.setScaleX(CIRCLE_VIEW_DEFAULT_SCALE); + circleView.setScaleY(CIRCLE_VIEW_DEFAULT_SCALE); + mMenuView.setAlpha(COMPLETELY_OPAQUE); + } + } + }); + + mInteractMap.put(circleView.getId(), new Pair<>(magnetizedObject, animator)); } } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DragToInteractView.kt b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DragToInteractView.kt new file mode 100644 index 000000000000..0ef3d200d1fa --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/DragToInteractView.kt @@ -0,0 +1,322 @@ +/* + * 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.floatingmenu + +import android.animation.ObjectAnimator +import android.content.Context +import android.graphics.Color +import android.graphics.drawable.GradientDrawable +import android.util.ArrayMap +import android.util.IntProperty +import android.util.Log +import android.view.Gravity +import android.view.View +import android.view.ViewGroup +import android.view.WindowInsets +import android.view.WindowManager +import android.widget.FrameLayout +import android.widget.LinearLayout +import android.widget.Space +import androidx.annotation.ColorRes +import androidx.annotation.DimenRes +import androidx.annotation.DrawableRes +import androidx.core.content.ContextCompat +import androidx.dynamicanimation.animation.DynamicAnimation +import androidx.dynamicanimation.animation.SpringForce.DAMPING_RATIO_LOW_BOUNCY +import androidx.dynamicanimation.animation.SpringForce.STIFFNESS_LOW +import com.android.wm.shell.R +import com.android.wm.shell.animation.PhysicsAnimator +import com.android.wm.shell.common.bubbles.DismissCircleView +import com.android.wm.shell.common.bubbles.DismissView + +/** + * View that handles interactions between DismissCircleView and BubbleStackView. + * + * @note [setup] method should be called after initialisation + */ +class DragToInteractView(context: Context) : FrameLayout(context) { + /** + * The configuration is used to provide module specific resource ids + * + * @see [setup] method + */ + data class Config( + /** dimen resource id of the dismiss target circle view size */ + @DimenRes val targetSizeResId: Int, + /** dimen resource id of the icon size in the dismiss target */ + @DimenRes val iconSizeResId: Int, + /** dimen resource id of the bottom margin for the dismiss target */ + @DimenRes var bottomMarginResId: Int, + /** dimen resource id of the height for dismiss area gradient */ + @DimenRes val floatingGradientHeightResId: Int, + /** color resource id of the dismiss area gradient color */ + @ColorRes val floatingGradientColorResId: Int, + /** drawable resource id of the dismiss target background */ + @DrawableRes val backgroundResId: Int, + /** drawable resource id of the icon for the dismiss target */ + @DrawableRes val iconResId: Int + ) + + companion object { + private const val SHOULD_SETUP = "The view isn't ready. Should be called after `setup`" + private val TAG = DragToInteractView::class.simpleName + } + + // START DragToInteractView modification + // We could technically access each DismissCircleView from their Animator, + // but the animators only store a weak reference to their targets. This is safer. + var interactMap = ArrayMap<Int, Pair<DismissCircleView, PhysicsAnimator<DismissCircleView>>>() + // END DragToInteractView modification + var isShowing = false + var config: Config? = null + + private val spring = PhysicsAnimator.SpringConfig(STIFFNESS_LOW, DAMPING_RATIO_LOW_BOUNCY) + private val INTERACT_SCRIM_FADE_MS = 200L + private var wm: WindowManager = + context.getSystemService(Context.WINDOW_SERVICE) as WindowManager + private var gradientDrawable: GradientDrawable? = null + + private val GRADIENT_ALPHA: IntProperty<GradientDrawable> = + object : IntProperty<GradientDrawable>("alpha") { + override fun setValue(d: GradientDrawable, percent: Int) { + d.alpha = percent + } + override fun get(d: GradientDrawable): Int { + return d.alpha + } + } + + init { + clipToPadding = false + clipChildren = false + visibility = View.INVISIBLE + + // START DragToInteractView modification + // Resources included within implementation as we aren't concerned with decoupling them. + setup( + Config( + targetSizeResId = R.dimen.dismiss_circle_size, + iconSizeResId = R.dimen.dismiss_target_x_size, + bottomMarginResId = R.dimen.floating_dismiss_bottom_margin, + floatingGradientHeightResId = R.dimen.floating_dismiss_gradient_height, + floatingGradientColorResId = android.R.color.system_neutral1_900, + backgroundResId = R.drawable.dismiss_circle_background, + iconResId = R.drawable.pip_ic_close_white + ) + ) + // END DragToInteractView modification + } + + /** + * Sets up view with the provided resource ids. + * + * Decouples resource dependency in order to be used externally (e.g. Launcher). Usually called + * with default params in module specific extension: + * + * @see [DismissView.setup] in DismissViewExt.kt + */ + fun setup(config: Config) { + this.config = config + + // Setup layout + layoutParams = + LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + resources.getDimensionPixelSize(config.floatingGradientHeightResId), + Gravity.BOTTOM + ) + updatePadding() + + // Setup gradient + gradientDrawable = createGradient(color = config.floatingGradientColorResId) + background = gradientDrawable + + // START DragToInteractView modification + + // Setup LinearLayout. Added to organize multiple circles. + val linearLayout = LinearLayout(context) + linearLayout.layoutParams = + LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + linearLayout.weightSum = 0f + addView(linearLayout) + + // Setup DismissCircleView. Code block replaced with repeatable functions + addSpace(linearLayout) + addCircle( + config, + com.android.systemui.res.R.id.action_remove_menu, + R.drawable.pip_ic_close_white, + linearLayout + ) + addCircle( + config, + com.android.systemui.res.R.id.action_edit, + com.android.systemui.res.R.drawable.ic_screenshot_edit, + linearLayout + ) + // END DragToInteractView modification + } + + /** Animates this view in. */ + fun show() { + if (isShowing) return + val gradientDrawable = checkExists(gradientDrawable) ?: return + isShowing = true + visibility = View.VISIBLE + val alphaAnim = + ObjectAnimator.ofInt(gradientDrawable, GRADIENT_ALPHA, gradientDrawable.alpha, 255) + alphaAnim.duration = INTERACT_SCRIM_FADE_MS + alphaAnim.start() + + // START DragToInteractView modification + interactMap.forEach { + val animator = it.value.second + animator.cancel() + animator.spring(DynamicAnimation.TRANSLATION_Y, 0f, spring).start() + } + // END DragToInteractView modification + } + + /** + * Animates this view out, as well as the circle that encircles the bubbles, if they were + * dragged into the target and encircled. + */ + fun hide() { + if (!isShowing) return + val gradientDrawable = checkExists(gradientDrawable) ?: return + isShowing = false + val alphaAnim = + ObjectAnimator.ofInt(gradientDrawable, GRADIENT_ALPHA, gradientDrawable.alpha, 0) + alphaAnim.duration = INTERACT_SCRIM_FADE_MS + alphaAnim.start() + + // START DragToInteractView modification + interactMap.forEach { + val animator = it.value.second + animator + .spring(DynamicAnimation.TRANSLATION_Y, height.toFloat(), spring) + .withEndActions({ visibility = View.INVISIBLE }) + .start() + } + // END DragToInteractView modification + } + + /** Cancels the animator for the dismiss target. */ + fun cancelAnimators() { + // START DragToInteractView modification + interactMap.forEach { + val animator = it.value.second + animator.cancel() + } + // END DragToInteractView modification + } + + fun updateResources() { + val config = checkExists(config) ?: return + updatePadding() + layoutParams.height = resources.getDimensionPixelSize(config.floatingGradientHeightResId) + val targetSize = resources.getDimensionPixelSize(config.targetSizeResId) + + // START DragToInteractView modification + interactMap.forEach { + val circle = it.value.first + circle.layoutParams.width = targetSize + circle.layoutParams.height = targetSize + circle.requestLayout() + } + // END DragToInteractView modification + } + + private fun createGradient(@ColorRes color: Int): GradientDrawable { + val gradientColor = ContextCompat.getColor(context, color) + val alpha = 0.7f * 255 + val gradientColorWithAlpha = + Color.argb( + alpha.toInt(), + Color.red(gradientColor), + Color.green(gradientColor), + Color.blue(gradientColor) + ) + val gd = + GradientDrawable( + GradientDrawable.Orientation.BOTTOM_TOP, + intArrayOf(gradientColorWithAlpha, Color.TRANSPARENT) + ) + gd.setDither(true) + gd.alpha = 0 + return gd + } + + private fun updatePadding() { + val config = checkExists(config) ?: return + val insets: WindowInsets = wm.currentWindowMetrics.windowInsets + val navInset = insets.getInsetsIgnoringVisibility(WindowInsets.Type.navigationBars()) + setPadding( + 0, + 0, + 0, + navInset.bottom + resources.getDimensionPixelSize(config.bottomMarginResId) + ) + } + + /** + * Checks if the value is set up and exists, if not logs an exception. Used for convenient + * logging in case `setup` wasn't called before + * + * @return value provided as argument + */ + private fun <T> checkExists(value: T?): T? { + if (value == null) Log.e(TAG, SHOULD_SETUP) + return value + } + + // START DragToInteractView modification + private fun addSpace(parent: LinearLayout) { + val space = Space(context) + // Spaces are weighted equally to space out circles evenly + space.layoutParams = + LinearLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + 1f + ) + parent.addView(space) + parent.weightSum = parent.weightSum + 1f + } + + private fun addCircle(config: Config, id: Int, iconResId: Int, parent: LinearLayout) { + val targetSize = resources.getDimensionPixelSize(config.targetSizeResId) + val circleLayoutParams = LinearLayout.LayoutParams(targetSize, targetSize, 0f) + circleLayoutParams.gravity = Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL + val circle = DismissCircleView(context) + circle.id = id + circle.setup(config.backgroundResId, iconResId, config.iconSizeResId) + circle.layoutParams = circleLayoutParams + + // Initial position with circle offscreen so it's animated up + circle.translationY = + resources.getDimensionPixelSize(config.floatingGradientHeightResId).toFloat() + + interactMap[circle.id] = Pair(circle, PhysicsAnimator.getInstance(circle)) + parent.addView(circle) + addSpace(parent) + } + // END DragToInteractView modification +} diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java index a2705584d76a..d3e85e092b3a 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationController.java @@ -37,7 +37,6 @@ import androidx.dynamicanimation.animation.SpringForce; import androidx.recyclerview.widget.RecyclerView; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.util.Preconditions; import com.android.systemui.Flags; import java.util.HashMap; @@ -73,7 +72,6 @@ class MenuAnimationController { private final ValueAnimator mFadeOutAnimator; private final Handler mHandler; private boolean mIsFadeEffectEnabled; - private DragToInteractAnimationController.DismissCallback mDismissCallback; private Runnable mSpringAnimationsEndAction; // Cache the animations state of {@link DynamicAnimation.TRANSLATION_X} and {@link @@ -170,11 +168,6 @@ class MenuAnimationController { mSpringAnimationsEndAction = runnable; } - void setDismissCallback( - DragToInteractAnimationController.DismissCallback dismissCallback) { - mDismissCallback = dismissCallback; - } - void moveToTopLeftPosition() { mMenuView.updateMenuMoveToTucked(/* isMoveToTucked= */ false); final Rect draggableBounds = mMenuView.getMenuDraggableBounds(); @@ -205,13 +198,6 @@ class MenuAnimationController { constrainPositionAndUpdate(position, /* writeToPosition = */ true); } - void removeMenu() { - Preconditions.checkArgument(mDismissCallback != null, - "The dismiss callback should be initialized first."); - - mDismissCallback.onDismiss(); - } - void flingMenuThenSpringToEdge(float x, float velocityX, float velocityY) { final boolean shouldMenuFlingLeft = isOnLeftSide() ? velocityX < ESCAPE_VELOCITY @@ -334,8 +320,6 @@ class MenuAnimationController { moveToEdgeAndHide(); return true; } - - fadeOutIfEnabled(); return false; } @@ -453,8 +437,6 @@ class MenuAnimationController { mMenuView.onBoundsInParentChanged((int) position.x, (int) position.y); constrainPositionAndUpdate(position, writeToPosition); - fadeOutIfEnabled(); - if (mSpringAnimationsEndAction != null) { mSpringAnimationsEndAction.run(); } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegate.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegate.java index 9c22a7738ad6..975a6020430d 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegate.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegate.java @@ -27,6 +27,7 @@ import androidx.annotation.NonNull; import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate; +import com.android.systemui.Flags; import com.android.systemui.res.R; /** @@ -35,15 +36,18 @@ import com.android.systemui.res.R; */ class MenuItemAccessibilityDelegate extends RecyclerViewAccessibilityDelegate.ItemDelegate { private final MenuAnimationController mAnimationController; + private final MenuViewLayer mMenuViewLayer; MenuItemAccessibilityDelegate(@NonNull RecyclerViewAccessibilityDelegate recyclerViewDelegate, - MenuAnimationController animationController) { + MenuAnimationController animationController, MenuViewLayer menuViewLayer) { super(recyclerViewDelegate); mAnimationController = animationController; + mMenuViewLayer = menuViewLayer; } @Override - public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCompat info) { + public void onInitializeAccessibilityNodeInfo( + @NonNull View host, @NonNull AccessibilityNodeInfoCompat info) { super.onInitializeAccessibilityNodeInfo(host, info); final Resources res = host.getResources(); @@ -90,6 +94,15 @@ class MenuItemAccessibilityDelegate extends RecyclerViewAccessibilityDelegate.It R.id.action_remove_menu, res.getString(R.string.accessibility_floating_button_action_remove_menu)); info.addAction(removeMenu); + + if (Flags.floatingMenuDragToEdit()) { + final AccessibilityNodeInfoCompat.AccessibilityActionCompat edit = + new AccessibilityNodeInfoCompat.AccessibilityActionCompat( + R.id.action_edit, + res.getString( + R.string.accessibility_floating_button_action_remove_menu)); + info.addAction(edit); + } } @Override @@ -132,8 +145,8 @@ class MenuItemAccessibilityDelegate extends RecyclerViewAccessibilityDelegate.It return true; } - if (action == R.id.action_remove_menu) { - mAnimationController.removeMenu(); + if (action == R.id.action_remove_menu || action == R.id.action_edit) { + mMenuViewLayer.dispatchAccessibilityAction(action); return true; } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandler.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandler.java index 52e7b91d373e..75191685b119 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandler.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandler.java @@ -16,6 +16,8 @@ package com.android.systemui.accessibility.floatingmenu; +import static android.R.id.empty; + import android.graphics.PointF; import android.view.MotionEvent; import android.view.VelocityTracker; @@ -78,10 +80,9 @@ class MenuListViewTouchHandler implements RecyclerView.OnItemTouchListener { mMenuAnimationController.onDraggingStart(); } - mDragToInteractAnimationController.showDismissView(/* show= */ true); - - if (!mDragToInteractAnimationController.maybeConsumeMoveMotionEvent( - motionEvent)) { + mDragToInteractAnimationController.showInteractView(/* show= */ true); + if (mDragToInteractAnimationController.maybeConsumeMoveMotionEvent(motionEvent) + == empty) { mMenuAnimationController.moveToPositionX(mMenuTranslationDown.x + dx); mMenuAnimationController.moveToPositionYIfNeeded( mMenuTranslationDown.y + dy); @@ -94,21 +95,19 @@ class MenuListViewTouchHandler implements RecyclerView.OnItemTouchListener { final float endX = mMenuTranslationDown.x + dx; mIsDragging = false; + mDragToInteractAnimationController.showInteractView(/* show= */ false); + if (mMenuAnimationController.maybeMoveToEdgeAndHide(endX)) { - mDragToInteractAnimationController.showDismissView(/* show= */ false); mMenuAnimationController.fadeOutIfEnabled(); - return true; } - if (!mDragToInteractAnimationController.maybeConsumeUpMotionEvent( - motionEvent)) { + if (mDragToInteractAnimationController.maybeConsumeUpMotionEvent(motionEvent) + == empty) { mVelocityTracker.computeCurrentVelocity(VELOCITY_UNIT_SECONDS); mMenuAnimationController.flingMenuThenSpringToEdge(endX, mVelocityTracker.getXVelocity(), mVelocityTracker.getYVelocity()); - mDragToInteractAnimationController.showDismissView(/* show= */ false); } - // Avoid triggering the listener of the item. return true; } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java index 76808cb7ab7b..334cc87144f9 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuView.java @@ -21,24 +21,28 @@ import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; import android.annotation.SuppressLint; import android.content.ComponentCallbacks; import android.content.Context; +import android.content.Intent; import android.content.res.Configuration; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.drawable.GradientDrawable; +import android.os.Bundle; +import android.os.UserHandle; +import android.provider.Settings; +import android.provider.SettingsStringUtil; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.widget.FrameLayout; import androidx.annotation.NonNull; -import androidx.core.view.AccessibilityDelegateCompat; import androidx.lifecycle.Observer; import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; -import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate; import com.android.internal.accessibility.dialog.AccessibilityTarget; import com.android.systemui.Flags; +import com.android.systemui.util.settings.SecureSettings; import java.util.ArrayList; import java.util.Collections; @@ -72,26 +76,20 @@ class MenuView extends FrameLayout implements private final MenuAnimationController mMenuAnimationController; private OnTargetFeaturesChangeListener mFeaturesChangeListener; private OnMoveToTuckedListener mMoveToTuckedListener; + private SecureSettings mSecureSettings; - MenuView(Context context, MenuViewModel menuViewModel, MenuViewAppearance menuViewAppearance) { + MenuView(Context context, MenuViewModel menuViewModel, MenuViewAppearance menuViewAppearance, + SecureSettings secureSettings) { super(context); mMenuViewModel = menuViewModel; mMenuViewAppearance = menuViewAppearance; + mSecureSettings = secureSettings; mMenuAnimationController = new MenuAnimationController(this, menuViewAppearance); mAdapter = new AccessibilityTargetAdapter(mTargetFeatures); mTargetFeaturesView = new RecyclerView(context); mTargetFeaturesView.setAdapter(mAdapter); mTargetFeaturesView.setLayoutManager(new LinearLayoutManager(context)); - mTargetFeaturesView.setAccessibilityDelegateCompat( - new RecyclerViewAccessibilityDelegate(mTargetFeaturesView) { - @NonNull - @Override - public AccessibilityDelegateCompat getItemDelegate() { - return new MenuItemAccessibilityDelegate(/* recyclerViewDelegate= */ this, - mMenuAnimationController); - } - }); setLayoutParams(new FrameLayout.LayoutParams(WRAP_CONTENT, WRAP_CONTENT)); // Avoid drawing out of bounds of the parent view setClipToOutline(true); @@ -278,6 +276,7 @@ class MenuView extends FrameLayout implements if (mFeaturesChangeListener != null) { mFeaturesChangeListener.onChange(newTargetFeatures); } + mMenuAnimationController.fadeOutIfEnabled(); } @@ -306,6 +305,10 @@ class MenuView extends FrameLayout implements return mMenuViewAppearance.getMenuPosition(); } + RecyclerView getTargetFeaturesView() { + return mTargetFeaturesView; + } + void persistPositionAndUpdateEdge(Position percentagePosition) { mMenuViewModel.updateMenuSavingPosition(percentagePosition); mMenuViewAppearance.setPercentagePosition(percentagePosition); @@ -424,6 +427,35 @@ class MenuView extends FrameLayout implements onPositionChanged(); } + void gotoEditScreen() { + if (!Flags.floatingMenuDragToEdit()) { + return; + } + mMenuAnimationController.flingMenuThenSpringToEdge( + getMenuPosition().x, 100f, 0f); + mContext.startActivity(getIntentForEditScreen()); + } + + Intent getIntentForEditScreen() { + List<String> targets = new SettingsStringUtil.ColonDelimitedSet.OfStrings( + mSecureSettings.getStringForUser( + Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, + UserHandle.USER_CURRENT)).stream().toList(); + + Intent intent = new Intent( + Settings.ACTION_ACCESSIBILITY_SHORTCUT_SETTINGS); + Bundle args = new Bundle(); + Bundle fragmentArgs = new Bundle(); + fragmentArgs.putStringArray("targets", targets.toArray(new String[0])); + args.putBundle(":settings:show_fragment_args", fragmentArgs); + // TODO: b/318748373 - The fragment should set its own title using the targets + args.putString( + ":settings:show_fragment_title", "Accessibility Shortcut"); + intent.replaceExtras(args); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP); + return intent; + } + private InstantInsetLayerDrawable getContainerViewInsetLayer() { return (InstantInsetLayerDrawable) getBackground(); } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java index 97999cc19dc8..bb5364d798da 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayer.java @@ -59,7 +59,10 @@ import android.widget.FrameLayout; import android.widget.TextView; import androidx.annotation.NonNull; +import androidx.core.view.AccessibilityDelegateCompat; import androidx.lifecycle.Observer; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate; import com.android.internal.accessibility.dialog.AccessibilityTarget; import com.android.internal.annotations.VisibleForTesting; @@ -94,6 +97,8 @@ class MenuViewLayer extends FrameLayout implements private final MenuListViewTouchHandler mMenuListViewTouchHandler; private final MenuMessageView mMessageView; private final DismissView mDismissView; + private final DragToInteractView mDragToInteractView; + private final MenuViewAppearance mMenuViewAppearance; private final MenuAnimationController mMenuAnimationController; private final AccessibilityManager mAccessibilityManager; @@ -178,7 +183,10 @@ class MenuViewLayer extends FrameLayout implements }; MenuViewLayer(@NonNull Context context, WindowManager windowManager, - AccessibilityManager accessibilityManager, IAccessibilityFloatingMenu floatingMenu, + AccessibilityManager accessibilityManager, + MenuViewModel menuViewModel, + MenuViewAppearance menuViewAppearance, MenuView menuView, + IAccessibilityFloatingMenu floatingMenu, SecureSettings secureSettings) { super(context); @@ -190,43 +198,52 @@ class MenuViewLayer extends FrameLayout implements mFloatingMenu = floatingMenu; mSecureSettings = secureSettings; - mMenuViewModel = new MenuViewModel(context, accessibilityManager, secureSettings); - mMenuViewAppearance = new MenuViewAppearance(context, windowManager); - mMenuView = new MenuView(context, mMenuViewModel, mMenuViewAppearance); + mMenuViewModel = menuViewModel; + mMenuViewAppearance = menuViewAppearance; + mMenuView = menuView; + RecyclerView targetFeaturesView = mMenuView.getTargetFeaturesView(); + targetFeaturesView.setAccessibilityDelegateCompat( + new RecyclerViewAccessibilityDelegate(targetFeaturesView) { + @NonNull + @Override + public AccessibilityDelegateCompat getItemDelegate() { + return new MenuItemAccessibilityDelegate(/* recyclerViewDelegate= */ this, + mMenuAnimationController, MenuViewLayer.this); + } + }); mMenuAnimationController = mMenuView.getMenuAnimationController(); - if (Flags.floatingMenuDragToHide()) { - mMenuAnimationController.setDismissCallback(this::hideMenuAndShowNotification); - } else { - mMenuAnimationController.setDismissCallback(this::hideMenuAndShowMessage); - } mMenuAnimationController.setSpringAnimationsEndAction(this::onSpringAnimationsEndAction); mDismissView = new DismissView(context); + mDragToInteractView = new DragToInteractView(context); DismissViewUtils.setup(mDismissView); + mDismissView.getCircle().setId(R.id.action_remove_menu); mNotificationFactory = new MenuNotificationFactory(context); mNotificationManager = context.getSystemService(NotificationManager.class); - mDragToInteractAnimationController = new DragToInteractAnimationController( - mDismissView, mMenuView); + + if (Flags.floatingMenuDragToEdit()) { + mDragToInteractAnimationController = new DragToInteractAnimationController( + mDragToInteractView, mMenuView); + } else { + mDragToInteractAnimationController = new DragToInteractAnimationController( + mDismissView, mMenuView); + } mDragToInteractAnimationController.setMagnetListener(new MagnetizedObject.MagnetListener() { @Override public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target) { - mDragToInteractAnimationController.animateDismissMenu(/* scaleUp= */ true); + mDragToInteractAnimationController.animateInteractMenu( + target.getTargetView().getId(), /* scaleUp= */ true); } @Override public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target, float velocityX, float velocityY, boolean wasFlungOut) { - mDragToInteractAnimationController.animateDismissMenu(/* scaleUp= */ false); + mDragToInteractAnimationController.animateInteractMenu( + target.getTargetView().getId(), /* scaleUp= */ false); } @Override public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) { - if (Flags.floatingMenuDragToHide()) { - hideMenuAndShowNotification(); - } else { - hideMenuAndShowMessage(); - } - mDismissView.hide(); - mDragToInteractAnimationController.animateDismissMenu(/* scaleUp= */ false); + dispatchAccessibilityAction(target.getTargetView().getId()); } }); @@ -262,7 +279,11 @@ class MenuViewLayer extends FrameLayout implements }); addView(mMenuView, LayerIndex.MENU_VIEW); - addView(mDismissView, LayerIndex.DISMISS_VIEW); + if (Flags.floatingMenuDragToEdit()) { + addView(mDragToInteractView, LayerIndex.DISMISS_VIEW); + } else { + addView(mDismissView, LayerIndex.DISMISS_VIEW); + } addView(mMessageView, LayerIndex.MESSAGE_VIEW); if (Flags.floatingMenuAnimatedTuck()) { @@ -272,6 +293,7 @@ class MenuViewLayer extends FrameLayout implements @Override public void onConfigurationChanged(@NonNull Configuration newConfig) { + mDragToInteractView.updateResources(); mDismissView.updateResources(); mDragToInteractAnimationController.updateResources(); } @@ -428,6 +450,23 @@ class MenuViewLayer extends FrameLayout implements } } + void dispatchAccessibilityAction(int id) { + if (id == R.id.action_remove_menu) { + if (Flags.floatingMenuDragToHide()) { + hideMenuAndShowNotification(); + } else { + hideMenuAndShowMessage(); + } + } else if (id == R.id.action_edit + && Flags.floatingMenuDragToEdit()) { + mMenuView.gotoEditScreen(); + } + mDismissView.hide(); + mDragToInteractView.hide(); + mDragToInteractAnimationController.animateInteractMenu( + id, /* scaleUp= */ false); + } + private CharSequence getMigrationMessage() { final Intent intent = new Intent(Settings.ACTION_ACCESSIBILITY_DETAILS_SETTINGS); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); @@ -475,7 +514,8 @@ class MenuViewLayer extends FrameLayout implements mEduTooltipView = Optional.empty(); } - private void hideMenuAndShowMessage() { + @VisibleForTesting + void hideMenuAndShowMessage() { final int delayTime = mAccessibilityManager.getRecommendedTimeoutMillis( SHOW_MESSAGE_DELAY_MS, AccessibilityManager.FLAG_CONTENT_TEXT @@ -485,7 +525,8 @@ class MenuViewLayer extends FrameLayout implements mMenuAnimationController.startShrinkAnimation(() -> mMenuView.setVisibility(GONE)); } - private void hideMenuAndShowNotification() { + @VisibleForTesting + void hideMenuAndShowNotification() { mMenuAnimationController.startShrinkAnimation(() -> mMenuView.setVisibility(GONE)); showNotification(); } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java index 1f549525256b..bc9d1ffd259b 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerController.java @@ -39,7 +39,16 @@ class MenuViewLayerController implements IAccessibilityFloatingMenu { MenuViewLayerController(Context context, WindowManager windowManager, AccessibilityManager accessibilityManager, SecureSettings secureSettings) { mWindowManager = windowManager; - mMenuViewLayer = new MenuViewLayer(context, windowManager, accessibilityManager, this, + + MenuViewModel menuViewModel = new MenuViewModel( + context, accessibilityManager, secureSettings); + MenuViewAppearance menuViewAppearance = new MenuViewAppearance(context, windowManager); + + mMenuViewLayer = new MenuViewLayer(context, windowManager, accessibilityManager, + menuViewModel, + menuViewAppearance, + new MenuView(context, menuViewModel, menuViewAppearance, secureSettings), + this, secureSettings); } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt index 86f372a94848..d2c62272e2ec 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthRippleController.kt @@ -25,6 +25,7 @@ import android.hardware.biometrics.BiometricFingerprintConstants import android.hardware.biometrics.BiometricSourceType import android.util.DisplayMetrics import androidx.annotation.VisibleForTesting +import androidx.lifecycle.repeatOnLifecycle import com.android.app.animation.Interpolators import com.android.keyguard.KeyguardUpdateMonitor import com.android.keyguard.KeyguardUpdateMonitorCallback @@ -35,8 +36,11 @@ 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.domain.interactor.AuthRippleInteractor import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor import com.android.systemui.keyguard.WakefulnessLifecycle +import com.android.systemui.keyguard.shared.model.BiometricUnlockSource +import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.log.core.LogLevel import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.res.R @@ -51,7 +55,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 +67,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, @@ -81,6 +83,7 @@ class AuthRippleController @Inject constructor( private val logger: KeyguardLogger, private val biometricUnlockController: BiometricUnlockController, private val lightRevealScrim: LightRevealScrim, + private val authRippleInteractor: AuthRippleInteractor, private val facePropertyRepository: FacePropertyRepository, rippleView: AuthRippleView? ) : @@ -103,6 +106,22 @@ class AuthRippleController @Inject constructor( init() } + init { + if (DeviceEntryUdfpsRefactor.isEnabled) { + rippleView?.repeatWhenAttached { + repeatOnLifecycle(androidx.lifecycle.Lifecycle.State.CREATED) { + authRippleInteractor.showUnlockRipple.collect { biometricUnlockSource -> + if (biometricUnlockSource == BiometricUnlockSource.FINGERPRINT_SENSOR) { + showUnlockRippleInternal(BiometricSourceType.FINGERPRINT) + } else { + showUnlockRippleInternal(BiometricSourceType.FACE) + } + } + } + } + } + } + @VisibleForTesting public override fun onViewAttached() { authController.addCallback(authControllerCallback) @@ -114,7 +133,9 @@ class AuthRippleController @Inject constructor( keyguardStateController.addCallback(this) wakefulnessLifecycle.addObserver(this) commandRegistry.registerCommand("auth-ripple") { AuthRippleCommand() } - biometricUnlockController.addListener(biometricModeListener) + if (!DeviceEntryUdfpsRefactor.isEnabled) { + biometricUnlockController.addListener(biometricModeListener) + } } private val biometricModeListener = @@ -122,8 +143,9 @@ class AuthRippleController @Inject constructor( override fun onBiometricUnlockedWithKeyguardDismissal( biometricSourceType: BiometricSourceType? ) { + DeviceEntryUdfpsRefactor.assertInLegacyMode() if (biometricSourceType != null) { - showUnlockRipple(biometricSourceType) + showUnlockRippleInternal(biometricSourceType) } else { logger.log(TAG, LogLevel.ERROR, @@ -146,7 +168,13 @@ class AuthRippleController @Inject constructor( notificationShadeWindowController.setForcePluginOpen(false, this) } - fun showUnlockRipple(biometricSourceType: BiometricSourceType) { + @Deprecated("Update authRippleInteractor.showUnlockRipple instead of calling this.") + fun showUnlockRipple(biometricSourceType: BiometricSourceType) { + DeviceEntryUdfpsRefactor.assertInLegacyMode() + showUnlockRippleInternal(biometricSourceType) + } + + private fun showUnlockRippleInternal(biometricSourceType: BiometricSourceType) { val keyguardNotShowing = !keyguardStateController.isShowing val unlockNotAllowed = !keyguardUpdateMonitor .isUnlockingWithBiometricAllowed(biometricSourceType) @@ -316,18 +344,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 = @@ -392,12 +408,12 @@ class AuthRippleController @Inject constructor( } "fingerprint" -> { pw.println("fingerprint ripple sensorLocation=$fingerprintSensorLocation") - showUnlockRipple(BiometricSourceType.FINGERPRINT) + showUnlockRippleInternal(BiometricSourceType.FINGERPRINT) } "face" -> { // note: only shows when about to proceed to the home screen pw.println("face ripple sensorLocation=$faceSensorLocation") - showUnlockRipple(BiometricSourceType.FACE) + showUnlockRippleInternal(BiometricSourceType.FACE) } "custom" -> { if (args.size != 3 || @@ -424,7 +440,7 @@ class AuthRippleController @Inject constructor( pw.println(" custom <x-location: int> <y-location: int>") } - fun invalidCommand(pw: PrintWriter) { + private fun invalidCommand(pw: PrintWriter) { pw.println("invalid command") help(pw) } 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/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/binder/SideFpsOverlayViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt index c36e0e21d021..80d37b4741e4 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/SideFpsOverlayViewBinder.kt @@ -121,11 +121,13 @@ constructor( if (it.isAttachedToWindow) { lottie = it.requireViewById<LottieAnimationView>(R.id.sidefps_animation) lottie?.pauseAnimation() + lottie?.removeAllLottieOnCompositionLoadedListener() windowManager.get().removeView(it) } } overlayView = layoutInflater.get().inflate(R.layout.sidefps_view, null, false) + val overlayViewModel = SideFpsOverlayViewModel( applicationContext, @@ -163,8 +165,10 @@ constructor( val lottie = it.requireViewById<LottieAnimationView>(R.id.sidefps_animation) lottie.addLottieOnCompositionLoadedListener { composition: LottieComposition -> - viewModel.setLottieBounds(composition.bounds) - overlayView.visibility = View.VISIBLE + if (overlayView.visibility != View.VISIBLE) { + viewModel.setLottieBounds(composition.bounds) + overlayView.visibility = View.VISIBLE + } } it.alpha = 0f val overlayShowAnimator = 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 e78a7a942993..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 @@ -259,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/communal/data/repository/CommunalWidgetRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt index 3287ed4d4991..f36547b01802 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt @@ -27,21 +27,17 @@ import com.android.systemui.communal.shared.model.CommunalWidgetContentModel import com.android.systemui.communal.widgets.CommunalAppWidgetHost import com.android.systemui.communal.widgets.WidgetConfigurator import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.Logger import com.android.systemui.log.dagger.CommunalLog +import com.android.systemui.util.kotlin.getValue import java.util.Optional import javax.inject.Inject import kotlin.coroutines.cancellation.CancellationException import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch @@ -67,18 +63,15 @@ interface CommunalWidgetRepository { * @param widgetIdToPriorityMap mapping of the widget ids to the priority of the widget. */ fun updateWidgetOrder(widgetIdToPriorityMap: Map<Int, Int>) {} - - /** Update whether the app widget host should be active. */ - fun updateAppWidgetHostActive(active: Boolean) } @SysUISingleton class CommunalWidgetRepositoryImpl @Inject constructor( - private val appWidgetManager: Optional<AppWidgetManager>, + appWidgetManagerOptional: Optional<AppWidgetManager>, private val appWidgetHost: CommunalAppWidgetHost, - @Application private val applicationScope: CoroutineScope, + @Background private val bgScope: CoroutineScope, @Background private val bgDispatcher: CoroutineDispatcher, private val communalWidgetHost: CommunalWidgetHost, private val communalWidgetDao: CommunalWidgetDao, @@ -90,41 +83,22 @@ constructor( private val logger = Logger(logBuffer, TAG) - override fun updateAppWidgetHostActive(active: Boolean) { - if (active == isHostActive.value) { - return - } - - if (active) { - appWidgetHost.startListening() - } else { - appWidgetHost.stopListening() - } - isHostActive.value = active - } - - private val isHostActive = MutableStateFlow(false) + private val appWidgetManager by appWidgetManagerOptional - @OptIn(ExperimentalCoroutinesApi::class) override val communalWidgets: Flow<List<CommunalWidgetContentModel>> = - isHostActive.flatMapLatest { isHostActive -> - if (!isHostActive || !appWidgetManager.isPresent) { - return@flatMapLatest flowOf(emptyList()) - } - communalWidgetDao - .getWidgets() - .map { it.map(::mapToContentModel) } - // As this reads from a database and triggers IPCs to AppWidgetManager, - // it should be executed in the background. - .flowOn(bgDispatcher) - } + communalWidgetDao + .getWidgets() + .map { it.mapNotNull(::mapToContentModel) } + // As this reads from a database and triggers IPCs to AppWidgetManager, + // it should be executed in the background. + .flowOn(bgDispatcher) override fun addWidget( provider: ComponentName, priority: Int, configurator: WidgetConfigurator? ) { - applicationScope.launch(bgDispatcher) { + bgScope.launch { val id = communalWidgetHost.allocateIdAndBindWidget(provider) if (id == null) { logger.e("Failed to allocate widget id to ${provider.flattenToString()}") @@ -170,7 +144,7 @@ constructor( } override fun deleteWidget(widgetId: Int) { - applicationScope.launch(bgDispatcher) { + bgScope.launch { communalWidgetDao.deleteWidgetById(widgetId) appWidgetHost.deleteAppWidgetId(widgetId) logger.i("Deleted widget with id $widgetId.") @@ -178,7 +152,7 @@ constructor( } override fun updateWidgetOrder(widgetIdToPriorityMap: Map<Int, Int>) { - applicationScope.launch(bgDispatcher) { + bgScope.launch { communalWidgetDao.updateWidgetOrder(widgetIdToPriorityMap) logger.i({ "Updated the order of widget list with ids: $str1." }) { str1 = widgetIdToPriorityMap.toString() @@ -189,11 +163,12 @@ constructor( @WorkerThread private fun mapToContentModel( entry: Map.Entry<CommunalItemRank, CommunalWidgetItem> - ): CommunalWidgetContentModel { + ): CommunalWidgetContentModel? { val (_, widgetId) = entry.value + val providerInfo = appWidgetManager?.getAppWidgetInfo(widgetId) ?: return null return CommunalWidgetContentModel( appWidgetId = widgetId, - providerInfo = appWidgetManager.get().getAppWidgetInfo(widgetId), + providerInfo = providerInfo, priority = entry.key.rank, ) } 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 c36f7fa22c82..28adb77f00e0 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 @@ -37,18 +37,22 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.smartspace.data.repository.SmartspaceRepository import com.android.systemui.user.data.repository.UserRepository +import com.android.systemui.util.kotlin.BooleanFlowOperators.and +import com.android.systemui.util.kotlin.BooleanFlowOperators.not +import com.android.systemui.util.kotlin.BooleanFlowOperators.or import javax.inject.Inject 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.flowOf import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn /** Encapsulates business-logic related to communal mode. */ @@ -68,6 +72,11 @@ constructor( private val appWidgetHost: CommunalAppWidgetHost, private val editWidgetsActivityStarter: EditWidgetsActivityStarter ) { + private val _editModeOpen = MutableStateFlow(false) + + /** Whether edit mode is currently open. */ + val editModeOpen: StateFlow<Boolean> = _editModeOpen.asStateFlow() + /** Whether communal features are enabled. */ val isCommunalEnabled: Boolean get() = communalRepository.isCommunalEnabled @@ -80,21 +89,17 @@ constructor( val isCommunalAvailable: StateFlow<Boolean> = flowOf(isCommunalEnabled) .flatMapLatest { enabled -> - if (enabled) - combine( - keyguardInteractor.isEncryptedOrLockdown, - userRepository.selectedUserInfo, - keyguardInteractor.isKeyguardVisible, - keyguardInteractor.isDreaming, - ) { isEncryptedOrLockdown, selectedUserInfo, isKeyguardVisible, isDreaming -> - !isEncryptedOrLockdown && - selectedUserInfo.isMain && - (isKeyguardVisible || isDreaming) - } - else flowOf(false) + if (enabled) { + val isMainUser = userRepository.selectedUserInfo.map { it.isMain } + and( + isMainUser, + not(keyguardInteractor.isEncryptedOrLockdown), + or(keyguardInteractor.isKeyguardVisible, keyguardInteractor.isDreaming), + ) + } else { + flowOf(false) + } } - .distinctUntilChanged() - .onEach { available -> widgetRepository.updateAppWidgetHostActive(available) } .stateIn( scope = applicationScope, started = SharingStarted.WhileSubscribed(), @@ -166,6 +171,10 @@ constructor( communalRepository.setDesiredScene(newScene) } + fun setEditModeOpen(isOpen: Boolean) { + _editModeOpen.value = isOpen + } + /** Show the widget editor Activity. */ fun showWidgetEditor() { editWidgetsActivityStarter.startActivity() diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt index 237a0c005af5..4b98f1ae4fe8 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt @@ -75,4 +75,7 @@ constructor( _reorderingWidgets.value = false uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_CANCEL) } + + /** Sets whether edit mode is currently open */ + fun setEditModeOpen(isOpen: Boolean) = communalInteractor.setEditModeOpen(isOpen) } diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt new file mode 100644 index 000000000000..586df32e6561 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.widgets + +import com.android.systemui.CoreStartable +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.util.kotlin.BooleanFlowOperators.or +import com.android.systemui.util.kotlin.pairwise +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.withContext + +@SysUISingleton +class CommunalAppWidgetHostStartable +@Inject +constructor( + private val appWidgetHost: CommunalAppWidgetHost, + private val communalInteractor: CommunalInteractor, + @Background private val bgScope: CoroutineScope, + @Main private val uiDispatcher: CoroutineDispatcher +) : CoreStartable { + override fun start() { + or(communalInteractor.isCommunalAvailable, communalInteractor.editModeOpen) + // Only trigger updates on state changes, ignoring the initial false value. + .pairwise(false) + .filter { (previous, new) -> previous != new } + .onEach { (_, shouldListen) -> updateAppWidgetHostActive(shouldListen) } + .launchIn(bgScope) + } + + private suspend fun updateAppWidgetHostActive(active: Boolean) = + // Always ensure this is called on the main/ui thread. + withContext(uiDispatcher) { + if (active) { + appWidgetHost.startListening() + } else { + appWidgetHost.stopListening() + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt index c7a14f9eefe1..a2575439e4b2 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt @@ -86,6 +86,8 @@ constructor( override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + communalViewModel.setEditModeOpen(true) + val windowInsetsController = window.decorView.windowInsetsController windowInsetsController?.hide(WindowInsets.Type.systemBars()) window.setDecorFitsSystemWindows(false) @@ -138,13 +140,16 @@ constructor( override fun onStart() { super.onStart() - uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_EDIT_MODE_SHOWN) } override fun onStop() { super.onStop() - uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_EDIT_MODE_GONE) } + + override fun onDestroy() { + super.onDestroy() + communalViewModel.setEditModeOpen(false) + } } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt index 8d82b552fc1e..95233f701bbb 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt @@ -25,6 +25,7 @@ import com.android.systemui.back.domain.interactor.BackActionInteractor import com.android.systemui.biometrics.BiometricNotificationService import com.android.systemui.clipboardoverlay.ClipboardListener import com.android.systemui.communal.log.CommunalLoggerStartable +import com.android.systemui.communal.widgets.CommunalAppWidgetHostStartable import com.android.systemui.controls.dagger.StartControlsStartableModule import com.android.systemui.dagger.qualifiers.PerUser import com.android.systemui.dreams.AssistantAttentionMonitor @@ -324,4 +325,11 @@ abstract class SystemUICoreStartableModule { @IntoMap @ClassKey(CommunalLoggerStartable::class) abstract fun bindCommunalLoggerStartable(impl: CommunalLoggerStartable): CoreStartable + + @Binds + @IntoMap + @ClassKey(CommunalAppWidgetHostStartable::class) + abstract fun bindCommunalAppWidgetHostStartable( + impl: CommunalAppWidgetHostStartable + ): CoreStartable } diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt index 7a70c4ac9fab..cf7d60140aee 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt @@ -40,8 +40,7 @@ import com.android.systemui.deviceentry.shared.model.FailedFaceAuthenticationSta import com.android.systemui.deviceentry.shared.model.HelpFaceAuthenticationStatus import com.android.systemui.deviceentry.shared.model.SuccessFaceAuthenticationStatus import com.android.systemui.dump.DumpManager -import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags +import com.android.systemui.keyguard.KeyguardWmStateRefactor import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository import com.android.systemui.keyguard.data.repository.BiometricType import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository @@ -63,10 +62,6 @@ import com.android.systemui.statusbar.phone.KeyguardBypassController import com.android.systemui.user.data.model.SelectionStatus import com.android.systemui.user.data.repository.UserRepository import com.google.errorprone.annotations.CompileTimeConstant -import java.io.PrintWriter -import java.util.Arrays -import java.util.stream.Collectors -import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -88,6 +83,10 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import java.io.PrintWriter +import java.util.Arrays +import java.util.stream.Collectors +import javax.inject.Inject /** * API to run face authentication and detection for device entry / on keyguard (as opposed to the @@ -165,7 +164,6 @@ constructor( @FaceAuthTableLog private val faceAuthLog: TableLogBuffer, private val keyguardTransitionInteractor: KeyguardTransitionInteractor, private val displayStateInteractor: DisplayStateInteractor, - private val featureFlags: FeatureFlags, dumpManager: DumpManager, ) : DeviceEntryFaceAuthRepository, Dumpable { private var authCancellationSignal: CancellationSignal? = null @@ -315,7 +313,7 @@ constructor( // or device starts going to sleep. merge( powerInteractor.isAsleep, - if (featureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + if (KeyguardWmStateRefactor.isEnabled) { keyguardTransitionInteractor.isInTransitionToState(KeyguardState.GONE) } else { keyguardRepository.keyguardDoneAnimationsFinished.map { true } diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepository.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepository.kt index 08e8c2d8271f..82834387f7b8 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepository.kt @@ -8,35 +8,26 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.keyguard.data.repository.KeyguardRepository -import com.android.systemui.keyguard.shared.model.BiometricUnlockModel -import com.android.systemui.keyguard.shared.model.BiometricUnlockSource import com.android.systemui.statusbar.phone.KeyguardBypassController import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.user.data.repository.UserRepository -import com.android.systemui.util.kotlin.sample import dagger.Binds import dagger.Module import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose -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.distinctUntilChanged -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.withContext /** Interface for classes that can access device-entry-related application state. */ interface DeviceEntryRepository { - /** Whether the device is immediately entering the device after a biometric unlock. */ - val enteringDeviceFromBiometricUnlock: Flow<BiometricUnlockSource> - /** * Whether the device is unlocked. * @@ -85,12 +76,6 @@ constructor( keyguardStateController: KeyguardStateController, keyguardRepository: KeyguardRepository, ) : DeviceEntryRepository { - override val enteringDeviceFromBiometricUnlock = - keyguardRepository.biometricUnlockState - .filter { BiometricUnlockModel.dismissesKeyguard(it) } - .sample( - keyguardRepository.biometricUnlockSource.filterNotNull(), - ) private val _isUnlocked = MutableStateFlow(false) diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/AuthRippleInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/AuthRippleInteractor.kt new file mode 100644 index 000000000000..337fe1ea7d98 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/AuthRippleInteractor.kt @@ -0,0 +1,55 @@ +/* + * 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.deviceentry.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.shared.model.BiometricUnlockSource +import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge + +/** Business logic for device entry auth ripple interactions. */ +@ExperimentalCoroutinesApi +@SysUISingleton +class AuthRippleInteractor +@Inject +constructor( + deviceEntrySourceInteractor: DeviceEntrySourceInteractor, + deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor, +) { + private val showUnlockRippleFromDeviceEntryIcon: Flow<BiometricUnlockSource> = + deviceEntryUdfpsInteractor.isUdfpsSupported.flatMapLatest { isUdfpsSupported -> + if (isUdfpsSupported) { + deviceEntrySourceInteractor.deviceEntryFromDeviceEntryIcon.map { + BiometricUnlockSource.FINGERPRINT_SENSOR + } + } else { + emptyFlow() + } + } + + private val showUnlockRippleFromBiometricUnlock: Flow<BiometricUnlockSource> = + deviceEntrySourceInteractor.deviceEntryFromBiometricSource + val showUnlockRipple: Flow<BiometricUnlockSource> = + merge( + showUnlockRippleFromDeviceEntryIcon, + showUnlockRippleFromBiometricUnlock, + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt index 649a9715ffea..782bce494d11 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt @@ -46,7 +46,7 @@ import kotlinx.coroutines.flow.onStart class DeviceEntryHapticsInteractor @Inject constructor( - deviceEntryInteractor: DeviceEntryInteractor, + deviceEntrySourceInteractor: DeviceEntrySourceInteractor, deviceEntryFingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor, deviceEntryBiometricAuthInteractor: DeviceEntryBiometricAuthInteractor, fingerprintPropertyRepository: FingerprintPropertyRepository, @@ -80,7 +80,7 @@ constructor( } val playSuccessHaptic: Flow<Unit> = - deviceEntryInteractor.enteringDeviceFromBiometricUnlock + deviceEntrySourceInteractor.deviceEntryFromBiometricSource .sample( combine( powerButtonSideFpsEnrolled, diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt index 09853578d3f1..73389cb1bdce 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractor.kt @@ -23,7 +23,6 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.deviceentry.data.repository.DeviceEntryFaceAuthRepository import com.android.systemui.deviceentry.data.repository.DeviceEntryRepository import com.android.systemui.keyguard.data.repository.TrustRepository -import com.android.systemui.keyguard.shared.model.BiometricUnlockSource import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.flag.SceneContainerFlags import com.android.systemui.scene.shared.model.SceneKey @@ -31,7 +30,6 @@ import com.android.systemui.scene.shared.model.SceneModel import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest @@ -55,7 +53,7 @@ class DeviceEntryInteractor @Inject constructor( @Application private val applicationScope: CoroutineScope, - repository: DeviceEntryRepository, + private val repository: DeviceEntryRepository, private val authenticationInteractor: AuthenticationInteractor, private val sceneInteractor: SceneInteractor, deviceEntryFaceAuthRepository: DeviceEntryFaceAuthRepository, @@ -63,9 +61,6 @@ constructor( flags: SceneContainerFlags, deviceUnlockedInteractor: DeviceUnlockedInteractor, ) { - val enteringDeviceFromBiometricUnlock: Flow<BiometricUnlockSource> = - repository.enteringDeviceFromBiometricUnlock - /** * Whether the device is unlocked. * diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntrySourceInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntrySourceInteractor.kt new file mode 100644 index 000000000000..d4f76a84c016 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntrySourceInteractor.kt @@ -0,0 +1,64 @@ +/* + * 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.deviceentry.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.keyguard.shared.model.BiometricUnlockModel +import com.android.systemui.keyguard.shared.model.BiometricUnlockSource +import com.android.systemui.util.kotlin.sample +import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.map + +/** + * Hosts application business logic related to the source of the user entering the device. Note: The + * source of the user entering the device isn't equivalent to the reason the device is unlocked. + * + * For example, the user successfully enters the device when they dismiss the lockscreen via a + * bypass biometric or, if the device is already unlocked, by triggering an affordance that + * dismisses the lockscreen. + */ +@ExperimentalCoroutinesApi +@SysUISingleton +class DeviceEntrySourceInteractor +@Inject +constructor( + keyguardInteractor: KeyguardInteractor, +) { + val deviceEntryFromBiometricSource: Flow<BiometricUnlockSource> = + keyguardInteractor.biometricUnlockState + .filter { BiometricUnlockModel.dismissesKeyguard(it) } + .sample( + keyguardInteractor.biometricUnlockSource.filterNotNull(), + ) + + private val attemptEnterDeviceFromDeviceEntryIcon: MutableSharedFlow<Unit> = MutableSharedFlow() + val deviceEntryFromDeviceEntryIcon: Flow<Unit> = + attemptEnterDeviceFromDeviceEntryIcon + .sample(keyguardInteractor.isKeyguardDismissible) + .filter { it } // only send events if the keyguard is dismissible + .map {} // map to Unit + + suspend fun attemptEnterDeviceFromDeviceEntryIcon() { + attemptEnterDeviceFromDeviceEntryIcon.emit(Unit) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt index d2883cce06c2..c69c9ef93761 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/Flags.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/Flags.kt @@ -220,19 +220,6 @@ object Flags { @JvmField val WALLPAPER_PICKER_PREVIEW_ANIMATION = releasedFlag("wallpaper_picker_preview_animation") - /** - * TODO(b/278086361): Tracking bug - * Complete rewrite of the interactions between System UI and Window Manager involving keyguard - * state. When enabled, calls to ActivityTaskManagerService from System UI will exclusively - * occur from [WmLockscreenVisibilityManager] rather than the legacy KeyguardViewMediator. - * - * This flag is under development; some types of unlock may not animate properly if you enable - * it. - */ - @JvmField - val KEYGUARD_WM_STATE_REFACTOR: UnreleasedFlag = - unreleasedFlag("keyguard_wm_state_refactor") - // 300 - power menu // TODO(b/254512600): Tracking Bug @JvmField val POWER_MENU_LITE = releasedFlag("power_menu_lite") diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java index e2ab20e29e2d..f10b87ee1cc5 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java @@ -77,7 +77,6 @@ import com.android.keyguard.mediator.ScreenOnCoordinator; import com.android.systemui.SystemUIApplication; import com.android.systemui.dagger.qualifiers.Application; import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.flags.Flags; import com.android.systemui.keyguard.ui.binder.KeyguardSurfaceBehindParamsApplier; import com.android.systemui.keyguard.ui.binder.KeyguardSurfaceBehindViewBinder; import com.android.systemui.keyguard.ui.binder.WindowManagerLockscreenVisibilityViewBinder; @@ -329,7 +328,7 @@ public class KeyguardService extends Service { mFlags = featureFlags; mPowerInteractor = powerInteractor; - if (mFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + if (KeyguardWmStateRefactor.isEnabled()) { WindowManagerLockscreenVisibilityViewBinder.bind( wmLockscreenVisibilityViewModel, wmLockscreenVisibilityManager, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt index 01ba0d214a97..53c81e537708 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardUnlockAnimationController.kt @@ -419,7 +419,7 @@ class KeyguardUnlockAnimationController @Inject constructor( */ fun canPerformInWindowLauncherAnimations(): Boolean { // TODO(b/278086361): Refactor in-window animations. - return !featureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR) && + return !KeyguardWmStateRefactor.isEnabled && isSupportedLauncherUnderneath() && // If the launcher is underneath, but we're about to launch an activity, don't do // the animations since they won't be visible. @@ -866,7 +866,7 @@ class KeyguardUnlockAnimationController @Inject constructor( } surfaceBehindRemoteAnimationTargets?.forEach { surfaceBehindRemoteAnimationTarget -> - if (!featureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + if (!KeyguardWmStateRefactor.isEnabled) { val surfaceHeight: Int = surfaceBehindRemoteAnimationTarget.screenSpaceBounds.height() @@ -1005,7 +1005,7 @@ class KeyguardUnlockAnimationController @Inject constructor( if (keyguardStateController.isShowing) { // Hide the keyguard, with no fade out since we animated it away during the unlock. - if (!featureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + if (!KeyguardWmStateRefactor.isEnabled) { keyguardViewController.hide( surfaceBehindRemoteAnimationStartTime, 0 /* fadeOutDuration */ diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java index 50caf17f71dd..8e3b19609142 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java @@ -139,7 +139,6 @@ import com.android.systemui.dagger.qualifiers.UiBackground; import com.android.systemui.dreams.DreamOverlayStateController; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.flags.Flags; import com.android.systemui.flags.SystemPropertiesHelper; import com.android.systemui.keyguard.dagger.KeyguardModule; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; @@ -175,8 +174,6 @@ import com.android.systemui.util.time.SystemClock; import com.android.systemui.wallpapers.data.repository.WallpaperRepository; import com.android.wm.shell.keyguard.KeyguardTransitions; -import dagger.Lazy; - import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -186,6 +183,7 @@ import java.util.Objects; import java.util.concurrent.Executor; import java.util.function.Consumer; +import dagger.Lazy; import kotlinx.coroutines.CoroutineDispatcher; /** @@ -1051,7 +1049,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, IRemoteAnimationFinishedCallback finishedCallback) { Trace.beginSection("mExitAnimationRunner.onAnimationStart#startKeyguardExitAnimation"); startKeyguardExitAnimation(transit, apps, wallpapers, nonApps, finishedCallback); - if (mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + if (KeyguardWmStateRefactor.isEnabled()) { mWmLockscreenVisibilityManager.get().onKeyguardGoingAwayRemoteAnimationStart( transit, apps, wallpapers, nonApps, finishedCallback); } @@ -1061,7 +1059,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, @Override // Binder interface public void onAnimationCancelled() { cancelKeyguardExitAnimation(); - if (mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + if (KeyguardWmStateRefactor.isEnabled()) { mWmLockscreenVisibilityManager.get().onKeyguardGoingAwayRemoteAnimationCancelled(); } } @@ -2757,7 +2755,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mUiBgExecutor.execute(() -> { Log.d(TAG, "updateActivityLockScreenState(" + showing + ", " + aodShowing + ")"); - if (mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + if (KeyguardWmStateRefactor.isEnabled()) { // Handled in WmLockscreenVisibilityManager if flag is enabled. return; } @@ -2811,7 +2809,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, setShowingLocked(true, hidingOrGoingAway /* force */); mHiding = false; - if (!mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + if (!KeyguardWmStateRefactor.isEnabled()) { // Handled directly in StatusBarKeyguardViewManager if enabled. mKeyguardViewControllerLazy.get().show(options); } @@ -2888,7 +2886,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, mKeyguardViewControllerLazy.get().setKeyguardGoingAwayState(true); // Handled in WmLockscreenVisibilityManager if flag is enabled. - if (!mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + if (!KeyguardWmStateRefactor.isEnabled()) { // Don't actually hide the Keyguard at the moment, wait for window manager // until it tells us it's safe to do so with startKeyguardExitAnimation. // Posting to mUiOffloadThread to ensure that calls to ActivityTaskManager @@ -2994,7 +2992,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, } else { Log.d(TAG, "Hiding keyguard while occluded. Just hide the keyguard view and exit."); - if (!mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + if (!KeyguardWmStateRefactor.isEnabled()) { mKeyguardViewControllerLazy.get().hide( mSystemClock.uptimeMillis() + mHideAnimation.getStartOffset(), mHideAnimation.getDuration()); @@ -3030,7 +3028,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, // If the flag is enabled, remote animation state is handled in // WmLockscreenVisibilityManager. if (finishedCallback != null - && !mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + && !KeyguardWmStateRefactor.isEnabled()) { // There will not execute animation, send a finish callback to ensure the remote // animation won't hang there. try { @@ -3056,7 +3054,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, new IRemoteAnimationFinishedCallback() { @Override public void onAnimationFinished() throws RemoteException { - if (!mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + if (!KeyguardWmStateRefactor.isEnabled()) { try { finishedCallback.onAnimationFinished(); } catch (RemoteException e) { @@ -3088,7 +3086,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, // it will dismiss the panel in that case. } else if (!mStatusBarStateController.leaveOpenOnKeyguardHide() && apps != null && apps.length > 0) { - if (!mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + if (!KeyguardWmStateRefactor.isEnabled()) { // Handled in WmLockscreenVisibilityManager. Other logic in this class will // short circuit when this is null. mSurfaceBehindRemoteAnimationFinishedCallback = finishedCallback; @@ -3112,7 +3110,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, createInteractionJankMonitorConf( CUJ_LOCKSCREEN_UNLOCK_ANIMATION, "RemoteAnimationDisabled")); - if (!mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + if (!KeyguardWmStateRefactor.isEnabled()) { // Handled directly in StatusBarKeyguardViewManager if enabled. mKeyguardViewControllerLazy.get().hide(startTime, fadeoutDuration); } @@ -3131,7 +3129,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, Slog.e(TAG, "Keyguard exit without a corresponding app to show."); try { - if (!mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + if (!KeyguardWmStateRefactor.isEnabled()) { finishedCallback.onAnimationFinished(); } } catch (RemoteException e) { @@ -3163,7 +3161,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, @Override public void onAnimationEnd(Animator animation) { try { - if (!mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + if (!KeyguardWmStateRefactor.isEnabled()) { finishedCallback.onAnimationFinished(); } } catch (RemoteException e) { @@ -3176,7 +3174,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, @Override public void onAnimationCancel(Animator animation) { try { - if (!mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + if (!KeyguardWmStateRefactor.isEnabled()) { finishedCallback.onAnimationFinished(); } } catch (RemoteException e) { @@ -3341,7 +3339,7 @@ public class KeyguardViewMediator implements CoreStartable, Dumpable, flags |= KEYGUARD_GOING_AWAY_FLAG_TO_LAUNCHER_CLEAR_SNAPSHOT; } - if (!mFeatureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + if (!KeyguardWmStateRefactor.isEnabled()) { // Handled in WmLockscreenVisibilityManager. mActivityTaskManagerService.keyguardGoingAway(flags); } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardWmStateRefactor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardWmStateRefactor.kt new file mode 100644 index 000000000000..ddccc5d9e96d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardWmStateRefactor.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard + +import com.android.systemui.Flags +import com.android.systemui.flags.FlagToken +import com.android.systemui.flags.RefactorFlagUtils + +/** Helper for reading or using the keyguard wm state refactor flag state. */ +@Suppress("NOTHING_TO_INLINE") +object KeyguardWmStateRefactor { + /** The aconfig flag name */ + const val FLAG_NAME = Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR + + /** A token used for dependency declaration */ + val token: FlagToken + get() = FlagToken(FLAG_NAME, isEnabled) + + /** Is the refactor enabled */ + @JvmStatic + inline val isEnabled + get() = Flags.keyguardWmStateRefactor() + + /** + * Called to ensure code is only run when the flag is enabled. This protects users from the + * unintended behaviors caused by accidentally running new logic, while also crashing on an eng + * build to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun isUnexpectedlyInLegacyMode() = + RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, FLAG_NAME) + + /** + * Called to ensure code is only run when the flag is disabled. This will throw an exception if + * the flag is enabled to ensure that the refactor author catches issues in testing. + */ + @JvmStatic + inline fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, FLAG_NAME) +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt index 14371949c9c8..1c6056c3b9d0 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt @@ -101,7 +101,7 @@ interface KeyguardRepository { * Whether the device is locked or unlocked right now. This is true when keyguard has been * dismissed or can be dismissed by a swipe */ - val isKeyguardUnlocked: StateFlow<Boolean> + val isKeyguardDismissible: StateFlow<Boolean> /** * Observable for the signal that keyguard is about to go away. @@ -388,7 +388,7 @@ constructor( } .distinctUntilChanged() - override val isKeyguardUnlocked: StateFlow<Boolean> = + override val isKeyguardDismissible: StateFlow<Boolean> = conflatedCallbackFlow { val callback = object : KeyguardStateController.Callback { @@ -396,7 +396,7 @@ constructor( trySendWithFailureLogging( keyguardStateController.isUnlocked, TAG, - "updated isKeyguardUnlocked due to onUnlockedChanged" + "updated isKeyguardDismissible due to onUnlockedChanged" ) } @@ -404,7 +404,7 @@ constructor( trySendWithFailureLogging( keyguardStateController.isUnlocked, TAG, - "updated isKeyguardUnlocked due to onKeyguardShowingChanged" + "updated isKeyguardDismissible due to onKeyguardShowingChanged" ) } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt index 8b2b45f1bf57..3965648bd224 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt @@ -23,7 +23,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags +import com.android.systemui.keyguard.KeyguardWmStateRefactor import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.KeyguardSurfaceBehindModel @@ -230,7 +230,7 @@ constructor( combine( startedKeyguardTransitionStep, keyguardInteractor.statusBarState, - keyguardInteractor.isKeyguardUnlocked, + keyguardInteractor.isKeyguardDismissible, ::Triple ), ::toQuad @@ -307,7 +307,7 @@ constructor( } private fun listenForLockscreenToGone() { - if (flags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + if (KeyguardWmStateRefactor.isEnabled) { return } @@ -324,7 +324,7 @@ constructor( } private fun listenForLockscreenToGoneDragging() { - if (flags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + if (KeyguardWmStateRefactor.isEnabled) { return } 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 33b6373d5876..acbd9fb4c407 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 @@ -23,7 +23,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.flags.FeatureFlags -import com.android.systemui.flags.Flags +import com.android.systemui.keyguard.KeyguardWmStateRefactor import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.KeyguardSurfaceBehindModel @@ -217,7 +217,7 @@ constructor( } private fun listenForPrimaryBouncerToGone() { - if (flags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + if (KeyguardWmStateRefactor.isEnabled) { // This is handled in KeyguardSecurityContainerController and // StatusBarKeyguardViewManager, which calls the transition interactor to kick off a // transition vs. listening to legacy state flags. 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 36bd905d23ac..22d11d08054e 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 @@ -32,6 +32,7 @@ import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.keyguard.data.repository.KeyguardRepository import com.android.systemui.keyguard.shared.model.BiometricUnlockModel +import com.android.systemui.keyguard.shared.model.BiometricUnlockSource import com.android.systemui.keyguard.shared.model.CameraLaunchSourceModel import com.android.systemui.keyguard.shared.model.DozeStateModel import com.android.systemui.keyguard.shared.model.DozeStateModel.Companion.isDozeOff @@ -162,8 +163,8 @@ constructor( /** Whether the keyguard is showing or not. */ val isKeyguardShowing: Flow<Boolean> = repository.isKeyguardShowing - /** Whether the keyguard is unlocked or not. */ - val isKeyguardUnlocked: Flow<Boolean> = repository.isKeyguardUnlocked + /** Whether the keyguard is dismissible or not. */ + val isKeyguardDismissible: Flow<Boolean> = repository.isKeyguardDismissible /** Whether the keyguard is occluded (covered by an activity). */ val isKeyguardOccluded: Flow<Boolean> = repository.isKeyguardOccluded @@ -194,6 +195,9 @@ constructor( /** Observable for the [StatusBarState] */ val statusBarState: Flow<StatusBarState> = repository.statusBarState + /** Source of the most recent biometric unlock, such as fingerprint or face. */ + val biometricUnlockSource: Flow<BiometricUnlockSource?> = repository.biometricUnlockSource + /** * Observable for [BiometricUnlockModel] when biometrics like face or any fingerprint (rear, * side, under display) is used to unlock the device. diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt index a02e8ac84a75..703bb879533c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt @@ -32,6 +32,7 @@ import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryIconViewModel import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.plugins.FalsingManager import com.android.systemui.statusbar.VibratorHelper +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch @@ -48,6 +49,7 @@ object DeviceEntryIconViewBinder { @SuppressLint("ClickableViewAccessibility") @JvmStatic fun bind( + applicationScope: CoroutineScope, view: DeviceEntryIconView, viewModel: DeviceEntryIconViewModel, fgViewModel: DeviceEntryForegroundViewModel, @@ -69,7 +71,7 @@ object DeviceEntryIconViewBinder { view, HapticFeedbackConstants.CONFIRM, ) - viewModel.onLongPress() + applicationScope.launch { viewModel.onLongPress() } } } 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 bc6c7cbf35fb..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 @@ -120,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 a1b3f270f642..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 @@ -31,6 +31,7 @@ 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 @@ -160,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) @@ -177,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)) { @@ -199,9 +195,7 @@ 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) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt index 0bf9ad02eb58..3fc9b4200f35 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySection.kt @@ -31,6 +31,7 @@ import com.android.keyguard.LockIconView import com.android.keyguard.LockIconViewController import com.android.systemui.Flags.keyguardBottomAreaRefactor import com.android.systemui.biometrics.AuthController +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags @@ -46,6 +47,7 @@ import com.android.systemui.shade.NotificationPanelView import com.android.systemui.statusbar.VibratorHelper import dagger.Lazy import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi /** Includes the device entry icon. */ @@ -53,6 +55,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi class DefaultDeviceEntrySection @Inject constructor( + @Application private val applicationScope: CoroutineScope, private val keyguardUpdateMonitor: KeyguardUpdateMonitor, private val authController: AuthController, private val windowManager: WindowManager, @@ -91,6 +94,7 @@ constructor( if (DeviceEntryUdfpsRefactor.isEnabled) { constraintLayout.findViewById<DeviceEntryIconView?>(deviceEntryIconViewId)?.let { DeviceEntryIconViewBinder.bind( + applicationScope, it, deviceEntryIconViewModel.get(), deviceEntryForegroundViewModel.get(), 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/DeviceEntryIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt index eacaa40de821..a3d54532411c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModel.kt @@ -20,6 +20,7 @@ import android.animation.FloatEvaluator import android.animation.IntEvaluator import com.android.keyguard.KeyguardViewController import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor +import com.android.systemui.deviceentry.domain.interactor.DeviceEntrySourceInteractor import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor import com.android.systemui.keyguard.domain.interactor.BurnInInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor @@ -56,6 +57,7 @@ constructor( private val sceneContainerFlags: SceneContainerFlags, private val keyguardViewController: Lazy<KeyguardViewController>, private val deviceEntryInteractor: DeviceEntryInteractor, + private val deviceEntrySourceInteractor: DeviceEntrySourceInteractor, ) { private val intEvaluator = IntEvaluator() private val floatEvaluator = FloatEvaluator() @@ -208,14 +210,13 @@ constructor( } } - fun onLongPress() { - // TODO (b/309804148): play auth ripple via an interactor - + suspend fun onLongPress() { if (sceneContainerFlags.isEnabled()) { deviceEntryInteractor.attemptDeviceEntry() } else { keyguardViewController.get().showPrimaryBouncer(/* scrim */ true) } + deviceEntrySourceInteractor.attemptEnterDeviceFromDeviceEntryIcon() } private fun DeviceEntryIconView.IconType.toAccessibilityHintType(): 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/BackPanelController.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt index fa03dc245745..93a6eeed3667 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/BackPanelController.kt @@ -34,6 +34,8 @@ import androidx.annotation.VisibleForTesting import androidx.core.os.postDelayed import androidx.core.view.isVisible import androidx.dynamicanimation.animation.DynamicAnimation +import com.android.internal.jank.Cuj.CUJ_BACK_PANEL_ARROW +import com.android.internal.jank.InteractionJankMonitor import com.android.internal.util.LatencyTracker import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.plugins.NavigationEdgeBackPlugin @@ -86,6 +88,7 @@ internal constructor( private val vibratorHelper: VibratorHelper, private val configurationController: ConfigurationController, private val latencyTracker: LatencyTracker, + private val interactionJankMonitor: InteractionJankMonitor, ) : ViewController<BackPanel>(BackPanel(context, latencyTracker)), NavigationEdgeBackPlugin { /** @@ -103,6 +106,7 @@ internal constructor( private val vibratorHelper: VibratorHelper, private val configurationController: ConfigurationController, private val latencyTracker: LatencyTracker, + private val interactionJankMonitor: InteractionJankMonitor, ) { /** Construct a [BackPanelController]. */ fun create(context: Context): BackPanelController { @@ -115,6 +119,7 @@ internal constructor( vibratorHelper, configurationController, latencyTracker, + interactionJankMonitor ) backPanelController.init() return backPanelController @@ -183,7 +188,7 @@ internal constructor( /* Arrow is animating in */ ENTRY, - /* could be entry, neutral, or stretched, releasing will commit back */ + /* releasing will commit back */ ACTIVE, /* releasing will cancel back */ @@ -366,6 +371,7 @@ internal constructor( // Receiving a CANCEL implies that something else intercepted // the gesture, i.e., the user did not cancel their gesture. // Therefore, disappear immediately, with minimum fanfare. + interactionJankMonitor.cancel(CUJ_BACK_PANEL_ARROW) updateArrowState(GestureState.GONE) velocityTracker = null } @@ -813,7 +819,7 @@ internal constructor( scale = when (currentState) { GestureState.ACTIVE, - GestureState.FLUNG, -> params.activeIndicator.scale + GestureState.FLUNG -> params.activeIndicator.scale GestureState.COMMITTED -> params.committedIndicator.scale else -> params.preThresholdIndicator.scale }, @@ -877,6 +883,16 @@ internal constructor( previousState = currentState currentState = newState + // First, update the jank tracker + when (currentState) { + GestureState.ENTRY -> { + interactionJankMonitor.cancel(CUJ_BACK_PANEL_ARROW) + interactionJankMonitor.begin(mView, CUJ_BACK_PANEL_ARROW) + } + GestureState.GONE -> interactionJankMonitor.end(CUJ_BACK_PANEL_ARROW) + else -> {} + } + when (currentState) { GestureState.CANCELLED -> { backCallback.cancelBack() 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/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/data/repository/InstalledTilesComponentRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepository.kt index bcd09bd877fd..dc39c97bc9ab 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/data/repository/InstalledTilesComponentRepository.kt @@ -25,6 +25,7 @@ import android.content.pm.PackageManager import android.content.pm.PackageManager.ResolveInfoFlags import android.os.UserHandle import android.service.quicksettings.TileService +import androidx.annotation.GuardedBy import com.android.systemui.common.data.repository.PackageChangeRepository import com.android.systemui.common.data.shared.model.PackageChangeModel import com.android.systemui.dagger.SysUISingleton @@ -32,12 +33,13 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.util.kotlin.isComponentActuallyEnabled import javax.inject.Inject -import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.shareIn interface InstalledTilesComponentRepository { @@ -49,33 +51,39 @@ class InstalledTilesComponentRepositoryImpl @Inject constructor( @Application private val applicationContext: Context, - @Background private val backgroundDispatcher: CoroutineDispatcher, + @Background private val backgroundScope: CoroutineScope, private val packageChangeRepository: PackageChangeRepository ) : InstalledTilesComponentRepository { - override fun getInstalledTilesComponents(userId: Int): Flow<Set<ComponentName>> { - /* - * In order to query [PackageManager] for different users, this implementation will call - * [Context.createContextAsUser] and retrieve the [PackageManager] from that context. - */ - val packageManager = - if (applicationContext.userId == userId) { - applicationContext.packageManager - } else { - applicationContext - .createContextAsUser( - UserHandle.of(userId), - /* flags */ 0, - ) - .packageManager + @GuardedBy("userMap") private val userMap = mutableMapOf<Int, Flow<Set<ComponentName>>>() + + override fun getInstalledTilesComponents(userId: Int): Flow<Set<ComponentName>> = + synchronized(userMap) { + userMap.getOrPut(userId) { + /* + * In order to query [PackageManager] for different users, this implementation will + * call [Context.createContextAsUser] and retrieve the [PackageManager] from that + * context. + */ + val packageManager = + if (applicationContext.userId == userId) { + applicationContext.packageManager + } else { + applicationContext + .createContextAsUser( + UserHandle.of(userId), + /* flags */ 0, + ) + .packageManager + } + packageChangeRepository + .packageChanged(UserHandle.of(userId)) + .onStart { emit(PackageChangeModel.Empty) } + .map { reloadComponents(userId, packageManager) } + .distinctUntilChanged() + .shareIn(backgroundScope, SharingStarted.WhileSubscribed(), replay = 1) } - return packageChangeRepository - .packageChanged(UserHandle.of(userId)) - .onStart { emit(PackageChangeModel.Empty) } - .map { reloadComponents(userId, packageManager) } - .distinctUntilChanged() - .flowOn(backgroundDispatcher) - } + } @WorkerThread private fun reloadComponents(userId: Int, packageManager: PackageManager): Set<ComponentName> { 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/tileimpl/QSIconViewImpl.java b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java index d04e4f52ac4d..53f287b81be9 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tileimpl/QSIconViewImpl.java @@ -33,11 +33,13 @@ import android.view.View; import android.widget.ImageView; import android.widget.ImageView.ScaleType; +import androidx.annotation.VisibleForTesting; + import com.android.settingslib.Utils; -import com.android.systemui.res.R; import com.android.systemui.plugins.qs.QSIconView; import com.android.systemui.plugins.qs.QSTile; import com.android.systemui.plugins.qs.QSTile.State; +import com.android.systemui.res.R; import java.util.Objects; @@ -52,7 +54,10 @@ public class QSIconViewImpl extends QSIconView { private boolean mDisabledByPolicy = false; private int mTint; @Nullable - private QSTile.Icon mLastIcon; + @VisibleForTesting + QSTile.Icon mLastIcon; + + private boolean mIconChangeScheduled; private ValueAnimator mColorAnimator = new ValueAnimator(); @@ -112,6 +117,7 @@ public class QSIconViewImpl extends QSIconView { } protected void updateIcon(ImageView iv, State state, boolean allowAnimations) { + mIconChangeScheduled = false; final QSTile.Icon icon = state.iconSupplier != null ? state.iconSupplier.get() : state.icon; if (!Objects.equals(icon, iv.getTag(R.id.qs_icon_tag))) { boolean shouldAnimate = allowAnimations && shouldAnimate(iv); @@ -167,7 +173,12 @@ public class QSIconViewImpl extends QSIconView { mState = state.state; mDisabledByPolicy = state.disabledByPolicy; if (mTint != 0 && allowAnimations && shouldAnimate(iv)) { - animateGrayScale(mTint, color, iv, () -> updateIcon(iv, state, allowAnimations)); + mIconChangeScheduled = true; + animateGrayScale(mTint, color, iv, () -> { + if (mIconChangeScheduled) { + updateIcon(iv, state, allowAnimations); + } + }); } else { setTint(iv, color); updateIcon(iv, state, allowAnimations); 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/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java index cc53aabfcd25..a9dd25bc403d 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java +++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java @@ -85,8 +85,8 @@ import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; -import com.android.systemui.flags.Flags; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; +import com.android.systemui.keyguard.KeyguardWmStateRefactor; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.keyguard.ui.view.InWindowLauncherUnlockAnimationManager; import com.android.systemui.model.SysUiState; @@ -616,7 +616,7 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis mDisplayTracker = displayTracker; mUnfoldTransitionProgressForwarder = unfoldTransitionProgressForwarder; - if (!featureFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + if (!KeyguardWmStateRefactor.isEnabled()) { mSysuiUnlockAnimationController = sysuiUnlockAnimationController; } else { mSysuiUnlockAnimationController = inWindowLauncherUnlockAnimationManager; diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index c0ceba3c1d4f..530c124f4f24 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -3576,11 +3576,6 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } @Override - public NotificationStackScrollLayoutController getNotificationStackScrollLayoutController() { - return mNotificationStackScrollLayoutController; - } - - @Override public void disableHeader(int state1, int state2, boolean animated) { mShadeHeaderController.disable(state1, state2, animated); } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeSurface.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeSurface.kt index e54286fcf992..4f970b3923aa 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeSurface.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeSurface.kt @@ -17,7 +17,6 @@ package com.android.systemui.shade import android.view.ViewPropertyAnimator import com.android.systemui.statusbar.GestureRecorder -import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController import com.android.systemui.statusbar.phone.CentralSurfaces import com.android.systemui.statusbar.policy.HeadsUpManager @@ -44,9 +43,6 @@ interface ShadeSurface : ShadeViewController { /** Animates the view from its current alpha to zero then runs the runnable. */ fun fadeOut(startDelayMs: Long, durationMs: Long, endAction: Runnable): ViewPropertyAnimator - /** Returns the NSSL controller. */ - val notificationStackScrollLayoutController: NotificationStackScrollLayoutController - /** Set whether the bouncer is showing. */ fun setBouncerShowing(bouncerShowing: Boolean) 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/ConversationNotifications.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt index 6e3b15da4423..c643238b7e30 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/ConversationNotifications.kt @@ -52,8 +52,8 @@ class ConversationNotificationProcessor @Inject constructor( entry: NotificationEntry, recoveredBuilder: Notification.Builder, logger: NotificationContentInflaterLogger - ) { - val messagingStyle = recoveredBuilder.style as? Notification.MessagingStyle ?: return + ): Notification.MessagingStyle? { + val messagingStyle = recoveredBuilder.style as? Notification.MessagingStyle ?: return null messagingStyle.conversationType = if (entry.ranking.channel.isImportantConversation) Notification.MessagingStyle.CONVERSATION_TYPE_IMPORTANT @@ -68,6 +68,7 @@ class ConversationNotificationProcessor @Inject constructor( } messagingStyle.unreadMessageCount = conversationNotificationManager.getUnreadCount(entry, recoveredBuilder) + return messagingStyle } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java index 73decfc326a4..639e23ae0765 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinator.java @@ -362,8 +362,12 @@ public class PreparationCoordinator implements Coordinator { } NotifInflater.Params getInflaterParams(NotifUiAdjustment adjustment, String reason) { - return new NotifInflater.Params(adjustment.isMinimized(), reason, - adjustment.isSnoozeEnabled()); + return new NotifInflater.Params( + adjustment.isMinimized(), + reason, + adjustment.isSnoozeEnabled(), + adjustment.isChildInGroup() + ); } private void abortInflation(NotificationEntry entry, String reason) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifInflater.kt index 4483599d6857..c0b187be42f3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifInflater.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifInflater.kt @@ -20,9 +20,9 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.render.NotifViewController /** - * Used by the [PreparationCoordinator]. When notifications are added or updated, the - * NotifInflater is asked to (re)inflated and prepare their views. This inflation occurs off the - * main thread. When the inflation is finished, NotifInflater will trigger its InflationCallback. + * Used by the [PreparationCoordinator]. When notifications are added or updated, the NotifInflater + * is asked to (re)inflated and prepare their views. This inflation occurs off the main thread. When + * the inflation is finished, NotifInflater will trigger its InflationCallback. */ interface NotifInflater { /** @@ -33,7 +33,7 @@ interface NotifInflater { fun rebindViews(entry: NotificationEntry, params: Params, callback: InflationCallback) /** - * Called to inflate the views of an entry. Views are not considered inflated until all of its + * Called to inflate the views of an entry. Views are not considered inflated until all of its * views are bound. Once all views are inflated, the InflationCallback is triggered. * * @param callback callback called after inflation finishes @@ -41,25 +41,24 @@ interface NotifInflater { fun inflateViews(entry: NotificationEntry, params: Params, callback: InflationCallback) /** - * Request to stop the inflation of an entry. For example, called when a notification is - * removed and no longer needs to be inflated. Returns whether anything may have been aborted. + * Request to stop the inflation of an entry. For example, called when a notification is removed + * and no longer needs to be inflated. Returns whether anything may have been aborted. */ fun abortInflation(entry: NotificationEntry): Boolean - /** - * Called to let the system remove the content views from the notification row. - */ + /** Called to let the system remove the content views from the notification row. */ fun releaseViews(entry: NotificationEntry) - /** - * Callback once all the views are inflated and bound for a given NotificationEntry. - */ + /** Callback once all the views are inflated and bound for a given NotificationEntry. */ interface InflationCallback { fun onInflationFinished(entry: NotificationEntry, controller: NotifViewController) } - /** - * A class holding parameters used when inflating the notification row - */ - class Params(val isLowPriority: Boolean, val reason: String, val showSnooze: Boolean) + /** A class holding parameters used when inflating the notification row */ + class Params( + val isLowPriority: Boolean, + val reason: String, + val showSnooze: Boolean, + val isChildInGroup: Boolean = false, + ) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustment.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustment.kt index ee0b00807e27..e1d2cdc65d5a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustment.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustment.kt @@ -20,6 +20,7 @@ import android.app.Notification import android.app.RemoteInput import android.graphics.drawable.Icon import android.text.TextUtils +import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation /** * An immutable object which contains minimal state extracted from an entry that represents state @@ -34,6 +35,7 @@ class NotifUiAdjustment internal constructor( val isSnoozeEnabled: Boolean, val isMinimized: Boolean, val needsRedaction: Boolean, + val isChildInGroup: Boolean, ) { companion object { @JvmStatic @@ -48,6 +50,11 @@ class NotifUiAdjustment internal constructor( oldAdjustment.needsRedaction != newAdjustment.needsRedaction -> true areDifferent(oldAdjustment.smartActions, newAdjustment.smartActions) -> true newAdjustment.smartReplies != oldAdjustment.smartReplies -> true + // TODO(b/217799515): Here we decide whether to re-inflate the row on every group-status + // change if we want to keep the single-line view, the following line should be: + // !oldAdjustment.isChildInGroup && newAdjustment.isChildInGroup -> true + AsyncHybridViewInflation.isEnabled && + oldAdjustment.isChildInGroup != newAdjustment.isChildInGroup -> true else -> false } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProvider.kt index 058545689c01..6f44c13a3e71 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProvider.kt @@ -29,6 +29,7 @@ import com.android.systemui.statusbar.NotificationLockscreenUserManager import com.android.systemui.statusbar.notification.collection.GroupEntry import com.android.systemui.statusbar.notification.collection.NotificationEntry import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider +import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager import com.android.systemui.util.ListenerSet import com.android.systemui.util.settings.SecureSettings import javax.inject.Inject @@ -43,7 +44,8 @@ class NotifUiAdjustmentProvider @Inject constructor( private val secureSettings: SecureSettings, private val lockscreenUserManager: NotificationLockscreenUserManager, private val sectionStyleProvider: SectionStyleProvider, - private val userTracker: UserTracker + private val userTracker: UserTracker, + private val groupMembershipManager: GroupMembershipManager, ) { private val dirtyListeners = ListenerSet<Runnable>() private var isSnoozeSettingsEnabled = false @@ -121,5 +123,6 @@ class NotifUiAdjustmentProvider @Inject constructor( isSnoozeEnabled = isSnoozeSettingsEnabled && !entry.isCanceled, isMinimized = isEntryMinimized(entry), needsRedaction = lockscreenUserManager.needsRedaction(entry), + isChildInGroup = groupMembershipManager.isChildInGroup(entry), ) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java index 80ef14bb4673..cd816aea452b 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/inflation/NotificationRowBinderImpl.java @@ -20,6 +20,7 @@ import static com.android.systemui.Flags.screenshareNotificationHiding; import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_CONTRACTED; import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_EXPANDED; import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_PUBLIC; +import static com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_SINGLE_LINE; import static java.util.Objects.requireNonNull; @@ -49,6 +50,7 @@ import com.android.systemui.statusbar.notification.row.RowContentBindParams; import com.android.systemui.statusbar.notification.row.RowContentBindStage; import com.android.systemui.statusbar.notification.row.RowInflaterTask; import com.android.systemui.statusbar.notification.row.dagger.ExpandableNotificationRowComponent; +import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation; import com.android.systemui.statusbar.notification.stack.NotificationListContainer; import javax.inject.Inject; @@ -127,6 +129,8 @@ public class NotificationRowBinderImpl implements NotificationRowBinder { @NonNull NotifInflater.Params params, NotificationRowContentBinder.InflationCallback callback) throws InflationException { + //TODO(b/217799515): Remove the entry parameter from getViewParentForNotification(), this + // function returns the NotificationStackScrollLayout regardless of the entry. ViewGroup parent = mListContainer.getViewParentForNotification(entry); if (entry.rowExists()) { @@ -174,6 +178,9 @@ public class NotificationRowBinderImpl implements NotificationRowBinder { params.markContentViewsFreeable(FLAG_CONTENT_VIEW_CONTRACTED); params.markContentViewsFreeable(FLAG_CONTENT_VIEW_EXPANDED); params.markContentViewsFreeable(FLAG_CONTENT_VIEW_PUBLIC); + if (AsyncHybridViewInflation.isEnabled()) { + params.markContentViewsFreeable(FLAG_CONTENT_VIEW_SINGLE_LINE); + } mRowContentBindStage.requestRebind(entry, null); } @@ -254,6 +261,16 @@ public class NotificationRowBinderImpl implements NotificationRowBinder { params.markContentViewsFreeable(FLAG_CONTENT_VIEW_PUBLIC); } + if (AsyncHybridViewInflation.isEnabled()) { + if (inflaterParams.isChildInGroup()) { + params.requireContentViews(FLAG_CONTENT_VIEW_SINGLE_LINE); + } else { + // TODO(b/217799515): here we decide whether to free the single-line view + // when the group status changes + params.markContentViewsFreeable(FLAG_CONTENT_VIEW_SINGLE_LINE); + } + } + params.rebindAllContentViews(); mLogger.logRequestingRebind(entry, inflaterParams); mRowContentBindStage.requestRebind(entry, en -> { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDiffer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDiffer.kt index 61e6f65b2bc2..8021d8f58ccc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDiffer.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/collection/render/ShadeViewDiffer.kt @@ -127,6 +127,9 @@ class ShadeViewDiffer( } } + /** + * Attach the Child Nodes to the parentNode using the structure from specMap + */ private fun attachChildren( parentNode: ShadeNode, specMap: Map<NodeController, NodeSpec> 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/row/BindStage.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BindStage.java index d626c18e46f5..8ae324fa4ef8 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BindStage.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/BindStage.java @@ -44,6 +44,7 @@ public abstract class BindStage<Params> extends BindRequester { /** * Execute the stage asynchronously. * + * @param entry the NotificationEntry to bind * @param row notification top-level view to bind views to * @param callback callback after stage finishes */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridConversationNotificationView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridConversationNotificationView.java index 43d99a0e03f2..6bc2b2f9e250 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridConversationNotificationView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridConversationNotificationView.java @@ -16,19 +16,27 @@ package com.android.systemui.statusbar.notification.row; +import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; +import android.content.res.ColorStateList; import android.graphics.drawable.Icon; import android.text.TextUtils; import android.util.AttributeSet; import android.view.View; +import android.view.ViewStub; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.TextView; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.widget.ConversationLayout; import com.android.systemui.res.R; import com.android.systemui.statusbar.notification.NotificationFadeAware; +import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation; +import com.android.systemui.statusbar.notification.row.ui.viewmodel.ConversationAvatar; +import com.android.systemui.statusbar.notification.row.ui.viewmodel.FacePile; +import com.android.systemui.statusbar.notification.row.ui.viewmodel.SingleIcon; /** * A hybrid view which may contain information about one ore more conversations. @@ -37,6 +45,7 @@ public class HybridConversationNotificationView extends HybridNotificationView { private ImageView mConversationIconView; private TextView mConversationSenderName; + private ViewStub mConversationFacePileStub; private View mConversationFacePile; private int mSingleAvatarSize; private int mFacePileSize; @@ -65,7 +74,16 @@ public class HybridConversationNotificationView extends HybridNotificationView { protected void onFinishInflate() { super.onFinishInflate(); mConversationIconView = requireViewById(com.android.internal.R.id.conversation_icon); - mConversationFacePile = requireViewById(com.android.internal.R.id.conversation_face_pile); + if (AsyncHybridViewInflation.isEnabled()) { + mConversationFacePileStub = + requireViewById(com.android.internal.R.id.conversation_face_pile); + } else { + // TODO(b/217799515): This usage is vague because mConversationFacePile represents both + // View and ViewStub at different stages of View inflation, should be removed when + // AsyncHybridViewInflation flag is removed + mConversationFacePile = + requireViewById(com.android.internal.R.id.conversation_face_pile); + } mConversationSenderName = requireViewById(R.id.conversation_notification_sender); applyTextColor(mConversationSenderName, mSecondaryTextColor); mFacePileSize = getResources() @@ -85,7 +103,8 @@ public class HybridConversationNotificationView extends HybridNotificationView { @Override public void bind(@Nullable CharSequence title, @Nullable CharSequence text, - @Nullable View contentView) { + @Nullable View contentView) { + AsyncHybridViewInflation.assertInLegacyMode(); if (!(contentView instanceof ConversationLayout)) { super.bind(title, text, contentView); return; @@ -137,6 +156,77 @@ public class HybridConversationNotificationView extends HybridNotificationView { super.bind(conversationTitle, conversationText, conversationLayout); } + /** + * Set the avatar using ConversationAvatar from SingleLineViewModel + * + * @param conversationAvatar the icon needed for a single-line conversation view, it should be + * either an instance of SingleIcon or FacePile + */ + public void setAvatar(@NonNull ConversationAvatar conversationAvatar) { + if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) return; + if (conversationAvatar instanceof SingleIcon) { + SingleIcon avatar = (SingleIcon) conversationAvatar; + if (mConversationFacePile != null) mConversationFacePile.setVisibility(GONE); + mConversationIconView.setVisibility(VISIBLE); + mConversationIconView.setImageDrawable(avatar.getIconDrawable()); + setSize(mConversationIconView, mSingleAvatarSize); + return; + } + + // If conversationAvatar is not a SingleIcon, it should be a FacePile. + // Bind the face pile with it. + FacePile facePileModel = (FacePile) conversationAvatar; + mConversationIconView.setVisibility(GONE); + // Inflate mConversationFacePile from ViewStub + if (mConversationFacePile == null) { + mConversationFacePile = mConversationFacePileStub.inflate(); + } + mConversationFacePile.setVisibility(VISIBLE); + + ImageView facePileBottomBg = mConversationFacePile.requireViewById( + com.android.internal.R.id.conversation_face_pile_bottom_background); + ImageView facePileBottom = mConversationFacePile.requireViewById( + com.android.internal.R.id.conversation_face_pile_bottom); + ImageView facePileTop = mConversationFacePile.requireViewById( + com.android.internal.R.id.conversation_face_pile_top); + + int bottomBackgroundColor = facePileModel.getBottomBackgroundColor(); + facePileBottomBg.setImageTintList(ColorStateList.valueOf(bottomBackgroundColor)); + + facePileBottom.setImageDrawable(facePileModel.getBottomIconDrawable()); + facePileTop.setImageDrawable(facePileModel.getTopIconDrawable()); + + setSize(mConversationFacePile, mFacePileSize); + setSize(facePileBottom, mFacePileAvatarSize); + setSize(facePileTop, mFacePileAvatarSize); + setSize(facePileBottomBg, mFacePileAvatarSize + 2 * mFacePileProtectionWidth); + + mTransformationHelper.addViewTransformingToSimilar(facePileTop); + mTransformationHelper.addViewTransformingToSimilar(facePileBottom); + mTransformationHelper.addViewTransformingToSimilar(facePileBottomBg); + + } + + /** + * bind the text views + */ + public void setText( + CharSequence titleText, + CharSequence contentText, + CharSequence conversationSenderName + ) { + if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) return; + if (conversationSenderName == null) { + mConversationSenderName.setVisibility(GONE); + } else { + mConversationSenderName.setVisibility(VISIBLE); + mConversationSenderName.setText(conversationSenderName); + } + // TODO (b/217799515): super.bind() doesn't use contentView, remove the contentView + // argument when the flag is removed + super.bind(/* title = */ titleText, /* text = */ contentText, /* contentView = */ null); + } + private static void setSize(View view, int size) { FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) view.getLayoutParams(); lp.width = size; @@ -153,4 +243,9 @@ public class HybridConversationNotificationView extends HybridNotificationView { super.setNotificationFaded(faded); NotificationFadeAware.setLayerTypeForFaded(mConversationFacePile, faded); } + + @VisibleForTesting + TextView getConversationSenderNameView() { + return mConversationSenderName; + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridGroupManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridGroupManager.java index ddd9bddc7375..09c034978977 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridGroupManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/HybridGroupManager.java @@ -32,6 +32,7 @@ import android.widget.TextView; import com.android.internal.widget.ConversationLayout; import com.android.systemui.res.R; +import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation; /** * A class managing hybrid groups that include {@link HybridNotificationView} and the notification @@ -41,6 +42,8 @@ public class HybridGroupManager { private final Context mContext; + private static final String TAG = "HybridGroupManager"; + private float mOverflowNumberSize; private int mOverflowNumberPadding; @@ -93,21 +96,34 @@ public class HybridGroupManager { public HybridNotificationView bindFromNotification(HybridNotificationView reusableView, View contentView, StatusBarNotification notification, ViewGroup parent) { + AsyncHybridViewInflation.assertInLegacyMode(); boolean isNewView = false; if (reusableView == null) { Trace.beginSection("HybridGroupManager#bindFromNotification"); reusableView = inflateHybridView(contentView, parent); isNewView = true; } - CharSequence titleText = resolveTitle(notification.getNotification()); - CharSequence contentText = resolveText(notification.getNotification()); - reusableView.bind(titleText, contentText, contentView); + + updateReusableView(reusableView, notification, contentView); if (isNewView) { Trace.endSection(); } return reusableView; } + /** + * Update the HybridNotificationView (single-line view)'s appearance + */ + public void updateReusableView(HybridNotificationView reusableView, + StatusBarNotification notification, View contentView) { + AsyncHybridViewInflation.assertInLegacyMode(); + final CharSequence titleText = resolveTitle(notification.getNotification()); + final CharSequence contentText = resolveText(notification.getNotification()); + if (reusableView != null) { + reusableView.bind(titleText, contentText, contentView); + } + } + @Nullable public static CharSequence resolveText(Notification notification) { CharSequence contentText = notification.extras.getCharSequence(Notification.EXTRA_TEXT); 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/NotificationContentInflater.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java index f186e665f773..913d5f6d3848 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflater.java @@ -20,6 +20,7 @@ import static com.android.internal.annotations.VisibleForTesting.Visibility.PACK import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_CONTRACTED; import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_EXPANDED; import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_HEADSUP; +import static com.android.systemui.statusbar.notification.row.NotificationContentView.VISIBLE_TYPE_SINGLELINE; import android.annotation.NonNull; import android.annotation.Nullable; @@ -41,15 +42,19 @@ import android.widget.RemoteViews; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.widget.ImageMessageConsumer; -import com.android.systemui.res.R; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.media.controls.util.MediaFeatureFlag; +import com.android.systemui.res.R; import com.android.systemui.statusbar.InflationTask; import com.android.systemui.statusbar.NotificationRemoteInputManager; import com.android.systemui.statusbar.notification.ConversationNotificationProcessor; import com.android.systemui.statusbar.notification.InflationException; import com.android.systemui.statusbar.notification.collection.NotificationEntry; +import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation; +import com.android.systemui.statusbar.notification.row.ui.viewbinder.SingleLineConversationViewBinder; +import com.android.systemui.statusbar.notification.row.ui.viewbinder.SingleLineViewBinder; +import com.android.systemui.statusbar.notification.row.ui.viewmodel.SingleLineViewModel; import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper; import com.android.systemui.statusbar.phone.CentralSurfaces; import com.android.systemui.statusbar.policy.InflatedSmartReplyState; @@ -135,7 +140,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder AsyncInflationTask task = new AsyncInflationTask( mBgExecutor, mInflateSynchronously, - contentToBind, + /* reInflateFlags = */ contentToBind, mRemoteViewCache, entry, mConversationProcessor, @@ -145,7 +150,7 @@ public class NotificationContentInflater implements NotificationRowContentBinder bindParams.usesIncreasedHeadsUpHeight, callback, mRemoteInputManager.getRemoteViewsOnClickHandler(), - mIsMediaInQS, + /* isMediaFlagEnabled = */ mIsMediaInQS, mSmartReplyStateInflater, mNotifLayoutInflaterFactoryProvider, mLogger); @@ -178,6 +183,29 @@ public class NotificationContentInflater implements NotificationRowContentBinder result = inflateSmartReplyViews(result, reInflateFlags, entry, row.getContext(), packageContext, row.getExistingSmartReplyState(), smartRepliesInflater, mLogger); + if (AsyncHybridViewInflation.isEnabled()) { + boolean isConversation = entry.getRanking().isConversation(); + Notification.MessagingStyle messagingStyle = null; + if (isConversation) { + messagingStyle = mConversationProcessor + .processNotification(entry, builder, mLogger); + } + result.mInflatedSingleLineViewModel = SingleLineViewInflater + .inflateSingleLineViewModel( + entry.getSbn().getNotification(), + messagingStyle, + builder, + row.getContext() + ); + result.mInflatedSingleLineViewHolder = + SingleLineViewInflater.inflateSingleLineViewHolder( + isConversation, + reInflateFlags, + entry, + row.getContext(), + mLogger + ); + } apply( mBgExecutor, @@ -255,6 +283,15 @@ public class NotificationContentInflater implements NotificationRowContentBinder mRemoteViewCache.removeCachedView(entry, FLAG_CONTENT_VIEW_PUBLIC); }); break; + case FLAG_CONTENT_VIEW_SINGLE_LINE: { + if (AsyncHybridViewInflation.isEnabled()) { + row.getPrivateLayout().performWhenContentInactive( + VISIBLE_TYPE_SINGLELINE, + () -> row.getPrivateLayout().setSingleLineView(null) + ); + } + break; + } default: break; } @@ -282,6 +319,10 @@ public class NotificationContentInflater implements NotificationRowContentBinder if ((contentViews & FLAG_CONTENT_VIEW_PUBLIC) != 0) { row.getPublicLayout().removeContentInactiveRunnable(VISIBLE_TYPE_CONTRACTED); } + if (AsyncHybridViewInflation.isEnabled() + && (contentViews & FLAG_CONTENT_VIEW_SINGLE_LINE) != 0) { + row.getPrivateLayout().removeContentInactiveRunnable(VISIBLE_TYPE_SINGLELINE); + } } private static InflationProgress inflateSmartReplyViews( @@ -772,6 +813,25 @@ public class NotificationContentInflater implements NotificationRowContentBinder } setRepliesAndActions = true; } + + if (AsyncHybridViewInflation.isEnabled() + && (reInflateFlags & FLAG_CONTENT_VIEW_SINGLE_LINE) != 0) { + HybridNotificationView viewHolder = result.mInflatedSingleLineViewHolder; + SingleLineViewModel viewModel = result.mInflatedSingleLineViewModel; + if (viewHolder != null && viewModel != null) { + if (viewModel.isConversation()) { + SingleLineConversationViewBinder.bind( + result.mInflatedSingleLineViewModel, + result.mInflatedSingleLineViewHolder + ); + } else { + SingleLineViewBinder.bind(result.mInflatedSingleLineViewModel, + result.mInflatedSingleLineViewHolder); + } + privateLayout.setSingleLineView(result.mInflatedSingleLineViewHolder); + } + } + if (setRepliesAndActions) { privateLayout.setInflatedSmartReplyState(result.inflatedSmartReplyState); } @@ -941,19 +1001,23 @@ public class NotificationContentInflater implements NotificationRowContentBinder // For all of our templates, we want it to be RTL packageContext = new RtlEnabledContext(packageContext); } - if (mEntry.getRanking().isConversation()) { - mConversationProcessor.processNotification(mEntry, recoveredBuilder, mLogger); + boolean isConversation = mEntry.getRanking().isConversation(); + Notification.MessagingStyle messagingStyle = null; + if (isConversation) { + messagingStyle = mConversationProcessor.processNotification( + mEntry, recoveredBuilder, mLogger); } InflationProgress inflationProgress = createRemoteViews(mReInflateFlags, recoveredBuilder, mIsLowPriority, mUsesIncreasedHeight, mUsesIncreasedHeadsUpHeight, packageContext, mRow, mNotifLayoutInflaterFactoryProvider, mLogger); + mLogger.logAsyncTaskProgress(mEntry, "getting existing smart reply state (on wrong thread!)"); InflatedSmartReplyState previousSmartReplyState = mRow.getExistingSmartReplyState(); mLogger.logAsyncTaskProgress(mEntry, "inflating smart reply views"); InflationProgress result = inflateSmartReplyViews( - inflationProgress, + /* result = */ inflationProgress, mReInflateFlags, mEntry, mContext, @@ -962,6 +1026,27 @@ public class NotificationContentInflater implements NotificationRowContentBinder mSmartRepliesInflater, mLogger); + if (AsyncHybridViewInflation.isEnabled()) { + // Inflate the single-line content view's ViewModel and ViewHolder from the + // background thread, the ViewHolder needs to be bind with ViewModel later from + // the main thread. + result.mInflatedSingleLineViewModel = SingleLineViewInflater + .inflateSingleLineViewModel( + mEntry.getSbn().getNotification(), + messagingStyle, + recoveredBuilder, + mContext + ); + result.mInflatedSingleLineViewHolder = + SingleLineViewInflater.inflateSingleLineViewHolder( + isConversation, + mReInflateFlags, + mEntry, + mContext, + mLogger + ); + } + mLogger.logAsyncTaskProgress(mEntry, "getting row image resolver (on wrong thread!)"); final NotificationInlineImageResolver imageResolver = mRow.getImageResolver(); @@ -1078,6 +1163,11 @@ public class NotificationContentInflater implements NotificationRowContentBinder private InflatedSmartReplyState inflatedSmartReplyState; private InflatedSmartReplyViewHolder expandedInflatedSmartReplies; private InflatedSmartReplyViewHolder headsUpInflatedSmartReplies; + + // ViewModel for SingleLineView, holds the UI State + SingleLineViewModel mInflatedSingleLineViewModel; + // Inflated SingleLineViewHolder, SingleLineView that lacks the UI State + HybridNotificationView mInflatedSingleLineViewHolder; } @VisibleForTesting diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterLogger.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterLogger.kt index 4f5455dc455f..ee9462c60674 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterLogger.kt @@ -26,6 +26,7 @@ import com.android.systemui.statusbar.notification.row.NotificationRowContentBin import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_EXPANDED import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_HEADS_UP import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_PUBLIC +import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_SINGLE_LINE import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag import javax.inject.Inject @@ -99,6 +100,26 @@ constructor(@NotifInflationLog private val buffer: LogBuffer) { ) } + fun logInflateSingleLine( + entry: NotificationEntry, + @InflationFlag inflationFlags: Int, + isConversation: Boolean + ) { + buffer.log( + TAG, + LogLevel.DEBUG, + { + str1 = entry.logKey + int1 = inflationFlags + bool1 = isConversation + }, + { + "inflateSingleLineView, inflationFlags: ${flagToString(int1)} for $str1, " + + "isConversation: $bool1" + } + ) + } + companion object { fun flagToString(@InflationFlag flag: Int): String { if (flag == 0) { @@ -121,6 +142,9 @@ constructor(@NotifInflationLog private val buffer: LogBuffer) { if (flag and FLAG_CONTENT_VIEW_PUBLIC != 0) { l.add("PUBLIC") } + if (flag and FLAG_CONTENT_VIEW_SINGLE_LINE != 0) { + l.add("SINGLE_LINE") + } return l.joinToString("|") } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java index a1718b9fbb02..402ea51bebb6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationContentView.java @@ -57,6 +57,7 @@ import com.android.systemui.statusbar.notification.NotificationUtils; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager; import com.android.systemui.statusbar.notification.people.PeopleNotificationIdentifier; +import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation; import com.android.systemui.statusbar.notification.row.wrapper.NotificationCustomViewWrapper; import com.android.systemui.statusbar.notification.row.wrapper.NotificationViewWrapper; import com.android.systemui.statusbar.policy.InflatedSmartReplyState; @@ -86,7 +87,7 @@ public class NotificationContentView extends FrameLayout implements Notification public static final int VISIBLE_TYPE_CONTRACTED = 0; public static final int VISIBLE_TYPE_EXPANDED = 1; public static final int VISIBLE_TYPE_HEADSUP = 2; - private static final int VISIBLE_TYPE_SINGLELINE = 3; + public static final int VISIBLE_TYPE_SINGLELINE = 3; /** * Used when there is no content on the view such as when we're a public layout but don't * need to show. @@ -98,6 +99,7 @@ public class NotificationContentView extends FrameLayout implements Notification private final Rect mClipBounds = new Rect(); private int mMinContractedHeight; + private int mMinSingleLineHeight; private View mContractedChild; private View mExpandedChild; private View mHeadsUpChild; @@ -234,6 +236,11 @@ public class NotificationContentView extends FrameLayout implements Notification public void reinflate() { mMinContractedHeight = getResources().getDimensionPixelSize( R.dimen.min_notification_layout_height); + if (AsyncHybridViewInflation.isEnabled()) { + //TODO: set the height with a more reasonable min single-line height + mMinSingleLineHeight = getResources().getDimensionPixelSize( + R.dimen.conversation_single_line_face_pile_size); + } } public void setHeights(int smallHeight, int headsUpMaxHeight, int maxHeight) { @@ -540,6 +547,28 @@ public class NotificationContentView extends FrameLayout implements Notification updateShownWrapper(mVisibleType); } + /** + * Sets the single-line view. Child may be null to remove the view. + * @param child single-line content view to set + */ + public void setSingleLineView(@Nullable HybridNotificationView child) { + if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) return; + if (mSingleLineView != null) { + mOnContentViewInactiveListeners.remove(mSingleLineView); + mSingleLineView.animate().cancel(); + removeView(mSingleLineView); + } + if (child == null) { + mSingleLineView = null; + if (mTransformationStartVisibleType == VISIBLE_TYPE_SINGLELINE) { + mTransformationStartVisibleType = VISIBLE_TYPE_NONE; + } + return; + } + addView(child); + mSingleLineView = child; + } + @Override public void onViewAdded(View child) { super.onViewAdded(child); @@ -809,7 +838,17 @@ public class NotificationContentView extends FrameLayout implements Notification return mContractedChild != null ? getViewHeight(VISIBLE_TYPE_CONTRACTED) : mMinContractedHeight; } else { - return mSingleLineView.getHeight(); + if (AsyncHybridViewInflation.isEnabled()) { + if (mSingleLineView != null) { + return getViewHeight(VISIBLE_TYPE_SINGLELINE); + } else { + Log.wtf(TAG, "getMinHeight: mSingleLineView == null"); + return mMinSingleLineHeight; + } + } else { + AsyncHybridViewInflation.assertInLegacyMode(); + return mSingleLineView.getHeight(); + } } } @@ -1264,19 +1303,30 @@ public class NotificationContentView extends FrameLayout implements Notification } private void updateSingleLineView() { - if (mIsChildInGroup) { + try { Trace.beginSection("NotifContentView#updateSingleLineView"); - boolean isNewView = mSingleLineView == null; - mSingleLineView = mHybridGroupManager.bindFromNotification( - mSingleLineView, mContractedChild, mNotificationEntry.getSbn(), this); - if (isNewView) { - updateViewVisibility(mVisibleType, VISIBLE_TYPE_SINGLELINE, - mSingleLineView, mSingleLineView); + if (AsyncHybridViewInflation.isEnabled()) { + return; + } + AsyncHybridViewInflation.assertInLegacyMode(); + if (mIsChildInGroup) { + boolean isNewView = mSingleLineView == null; + mSingleLineView = mHybridGroupManager.bindFromNotification( + /* reusableView = */ mSingleLineView, + /* contentView = */ mContractedChild, + /* notification = */ mNotificationEntry.getSbn(), + /* parent = */ this + ); + if (isNewView && mSingleLineView != null) { + updateViewVisibility(mVisibleType, VISIBLE_TYPE_SINGLELINE, + mSingleLineView, mSingleLineView); + } + } else if (mSingleLineView != null) { + removeView(mSingleLineView); + mSingleLineView = null; } + } finally { Trace.endSection(); - } else if (mSingleLineView != null) { - removeView(mSingleLineView); - mSingleLineView = null; } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java index d7b7aa210257..736140c44dfd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinder.java @@ -80,6 +80,7 @@ public interface NotificationRowContentBinder { FLAG_CONTENT_VIEW_EXPANDED, FLAG_CONTENT_VIEW_HEADS_UP, FLAG_CONTENT_VIEW_PUBLIC, + FLAG_CONTENT_VIEW_SINGLE_LINE, FLAG_CONTENT_VIEW_ALL}) @interface InflationFlag {} /** @@ -102,7 +103,12 @@ public interface NotificationRowContentBinder { */ int FLAG_CONTENT_VIEW_PUBLIC = 1 << 3; - int FLAG_CONTENT_VIEW_ALL = (1 << 4) - 1; + /** + * The single line notification view. Show when the notification is shown as a child in group. + */ + int FLAG_CONTENT_VIEW_SINGLE_LINE = 1 << 4; + + int FLAG_CONTENT_VIEW_ALL = (1 << 5) - 1; /** * Parameters for content view binding 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 46ddba4d4d8d..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,24 +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<>(); - 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/row/RowContentBindParams.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java index a52f638e7c26..1494c275d061 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindParams.java @@ -102,9 +102,9 @@ public final class RowContentBindParams { * @see InflationFlag */ public void markContentViewsFreeable(@InflationFlag int contentViews) { - @InflationFlag int existingContentViews = contentViews &= mContentViews; + @InflationFlag int existingFreeableContentViews = contentViews &= mContentViews; mContentViews &= ~contentViews; - mDirtyContentViews |= existingContentViews; + mDirtyContentViews |= existingFreeableContentViews; } public @InflationFlag int getContentViews() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java index b70da00ad517..f4f8374d0a9f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/RowContentBindStage.java @@ -63,7 +63,10 @@ public class RowContentBindStage extends BindStage<RowContentBindParams> { @InflationFlag int inflationFlags = params.getContentViews(); @InflationFlag int invalidatedFlags = params.getDirtyContentViews(); + // Rebind the content views which are needed now, and the corresponding old views are + // invalidated @InflationFlag int contentToBind = invalidatedFlags & inflationFlags; + // Unbind the content views that are not needed @InflationFlag int contentToUnbind = inflationFlags ^ FLAG_CONTENT_VIEW_ALL; // Bind/unbind with parameters diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt new file mode 100644 index 000000000000..d6118a0b3865 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt @@ -0,0 +1,390 @@ +/* + * 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 android.app.Notification +import android.app.Notification.MessagingStyle +import android.app.Person +import android.content.Context +import android.graphics.drawable.Icon +import android.util.Log +import android.view.LayoutInflater +import com.android.app.tracing.traceSection +import com.android.internal.R +import com.android.internal.widget.MessagingMessage +import com.android.internal.widget.PeopleHelper +import com.android.systemui.statusbar.notification.collection.NotificationEntry +import com.android.systemui.statusbar.notification.logKey +import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_SINGLE_LINE +import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation +import com.android.systemui.statusbar.notification.row.ui.viewmodel.ConversationAvatar +import com.android.systemui.statusbar.notification.row.ui.viewmodel.ConversationData +import com.android.systemui.statusbar.notification.row.ui.viewmodel.FacePile +import com.android.systemui.statusbar.notification.row.ui.viewmodel.SingleIcon +import com.android.systemui.statusbar.notification.row.ui.viewmodel.SingleLineViewModel + +/** The inflater of SingleLineViewModel and SingleLineViewHolder */ +internal object SingleLineViewInflater { + const val TAG = "SingleLineViewInflater" + + /** + * Inflate an instance of SingleLineViewModel. + * + * @param notification the notification to show + * @param messagingStyle the MessagingStyle information is only provided for conversation + * notification, not for legacy messaging notifications + * @param builder the recovered Notification Builder + * @param systemUiContext the context of Android System UI + * @return the inflated SingleLineViewModel + */ + @JvmStatic + fun inflateSingleLineViewModel( + notification: Notification, + messagingStyle: MessagingStyle?, + builder: Notification.Builder, + systemUiContext: Context, + ): SingleLineViewModel { + if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) { + return SingleLineViewModel(null, null, null) + } + peopleHelper.init(systemUiContext) + var titleText = HybridGroupManager.resolveTitle(notification) + var contentText = HybridGroupManager.resolveText(notification) + + if (messagingStyle == null) { + return SingleLineViewModel( + titleText = titleText, + contentText = contentText, + conversationData = null, + ) + } + + val isGroupConversation = messagingStyle.isGroupConversation + + val conversationTextData = messagingStyle.loadConversationTextData(systemUiContext) + if (conversationTextData?.conversationTitle?.isNotEmpty() == true) { + titleText = conversationTextData.conversationTitle + } + if (conversationTextData?.conversationText?.isNotEmpty() == true) { + contentText = conversationTextData.conversationText + } + + val conversationAvatar = + messagingStyle.loadConversationAvatar( + notification = notification, + isGroupConversation = isGroupConversation, + builder = builder, + systemUiContext = systemUiContext + ) + + val conversationData = + ConversationData( + // We don't show the sender's name for one-to-one conversation + conversationSenderName = + if (isGroupConversation) conversationTextData?.senderName else null, + avatar = conversationAvatar + ) + + return SingleLineViewModel( + titleText = titleText, + contentText = contentText, + conversationData = conversationData, + ) + } + + /** load conversation text data from the MessagingStyle of conversation notifications */ + private fun MessagingStyle.loadConversationTextData( + systemUiContext: Context + ): ConversationTextData? { + if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) { + return null + } + var conversationText: CharSequence? + + if (messages.isEmpty()) { + return null + } + + // load the conversation text + val lastMessage = messages[messages.lastIndex] + conversationText = lastMessage.text + if (conversationText == null && lastMessage.isImageMessage()) { + conversationText = findBackUpConversationText(lastMessage, systemUiContext) + } + + // load the sender's name to display + val name = lastMessage.senderPerson?.name + val senderName = + systemUiContext.resources.getString( + R.string.conversation_single_line_name_display, + name + ) + + // We need to find back-up values for those texts if they are needed and empty + return ConversationTextData( + conversationTitle = conversationTitle + ?: findBackUpConversationTitle(senderName, systemUiContext), + conversationText = conversationText, + senderName = senderName, + ) + } + + private fun MessagingStyle.Message.isImageMessage(): Boolean = MessagingMessage.hasImage(this) + + /** find a back-up conversation title when the conversation title is null. */ + private fun MessagingStyle.findBackUpConversationTitle( + senderName: CharSequence?, + systemUiContext: Context, + ): CharSequence { + if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) { + return "" + } + return if (isGroupConversation) { + systemUiContext.resources.getString(R.string.conversation_title_fallback_group_chat) + } else { + // Is one-to-one, let's try to use the last sender's name + // The last back-up is the value of resource: conversation_title_fallback_one_to_one + senderName + ?: systemUiContext.resources.getString( + R.string.conversation_title_fallback_one_to_one + ) + } + } + + /** + * find a back-up conversation text when the conversation has null text and is image message. + */ + private fun findBackUpConversationText( + message: MessagingStyle.Message, + context: Context, + ): CharSequence? { + if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) { + return null + } + // If the message is not an image message, just return empty, the back-up text for showing + // will be SingleLineViewModel.contentText + if (!message.isImageMessage()) return null + // If is image message, return a placeholder + return context.resources.getString(R.string.conversation_single_line_image_placeholder) + } + + /** + * The text data that we load from a conversation notification to show in the single-line views. + * + * Group conversation single-line view should be formatted as: + * [conversationTitle, senderName, conversationText] + * + * One-to-one single-line view should be formatted as: + * [conversationTitle (which is equal to the senderName), conversationText] + * + * @property conversationTitle the title of the conversation, not necessarily the title of the + * notification row. conversationTitle is non-null, though may be empty, in which case we need + * to show the notification title instead. + * @property conversationText the text content of the conversation, single-line will use the + * notification's text when conversationText is null + * @property senderName the sender's name to be shown in the row when needed. senderName can be + * null + */ + data class ConversationTextData( + val conversationTitle: CharSequence, + val conversationText: CharSequence?, + val senderName: CharSequence?, + ) + + private fun groupMessages( + messages: List<MessagingStyle.Message>, + historicMessages: List<MessagingStyle.Message>, + ): List<MutableList<MessagingStyle.Message>> { + if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) { + return listOf() + } + if (messages.isEmpty() && historicMessages.isEmpty()) return listOf() + var currentGroup: MutableList<MessagingStyle.Message>? = null + var currentSenderKey: CharSequence? = null + val groups = mutableListOf<MutableList<MessagingStyle.Message>>() + for (i in 0 until (historicMessages.size + messages.size)) { + val message = if (i < historicMessages.size) historicMessages[i] else messages[i] + + val sender = message.senderPerson + val senderKey = sender?.getKeyOrName() + val isNewGroup = (currentGroup == null) || senderKey != currentSenderKey + if (isNewGroup) { + currentGroup = mutableListOf() + groups.add(currentGroup) + currentSenderKey = senderKey + } + currentGroup?.add(message) + } + return groups + } + + private fun MessagingStyle.loadConversationAvatar( + builder: Notification.Builder, + notification: Notification, + isGroupConversation: Boolean, + systemUiContext: Context, + ): ConversationAvatar { + if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) { + return SingleIcon(null) + } + val userKey = user.getKeyOrName() + var conversationIcon: Icon? = null + var conversationText: CharSequence? = conversationTitle + + val groups = groupMessages(messages, historicMessages) + val uniqueNames = peopleHelper.mapUniqueNamesToPrefixWithGroupList(groups) + + if (!isGroupConversation) { + // Conversation is one-to-one, load the single icon + // Let's resolve the icon / text from the last sender + if (shortcutIcon != null) { + conversationIcon = shortcutIcon + } + + for (i in messages.lastIndex downTo 0) { + val message = messages[i] + val sender = message.senderPerson + val senderKey = sender?.getKeyOrName() + if ((sender != null && senderKey != userKey) || i == 0) { + if (conversationText.isNullOrEmpty()) { + // We use the senderName as header text if no conversation title is provided + // (This usually happens for most 1:1 conversations) + conversationText = sender?.name ?: "" + } + if (conversationIcon == null) { + var avatarIcon = sender?.icon + if (avatarIcon == null) { + avatarIcon = builder.getDefaultAvatar(name = conversationText) + } + conversationIcon = avatarIcon + } + break + } + } + } + + if (conversationIcon == null) { + conversationIcon = notification.getLargeIcon() + } + + // If is one-to-one or the conversation has an icon, return a single icon + if (!isGroupConversation || conversationIcon != null) { + return SingleIcon(conversationIcon?.loadDrawable(systemUiContext)) + } + + // Otherwise, let's find the two last conversations to build a face pile: + var secondLastIcon: Icon? = null + var lastIcon: Icon? = null + var lastKey: CharSequence? = null + + for (i in groups.lastIndex downTo 0) { + val message = groups[i][0] + val sender = message.senderPerson ?: user + val senderKey = sender.getKeyOrName() + val notUser = senderKey != userKey + val notIncluded = senderKey != lastKey + + if ((notUser && notIncluded) || (i == 0 && lastKey == null)) { + if (lastIcon == null) { + lastIcon = + sender.icon + ?: builder.getDefaultAvatar( + name = sender.name, + uniqueNames = uniqueNames + ) + lastKey = senderKey + } else { + secondLastIcon = + sender.icon + ?: builder.getDefaultAvatar( + name = sender.name, + uniqueNames = uniqueNames + ) + break + } + } + } + + if (lastIcon == null) { + lastIcon = builder.getDefaultAvatar(name = "") + } + + if (secondLastIcon == null) { + secondLastIcon = builder.getDefaultAvatar(name = "") + } + + return FacePile( + topIconDrawable = secondLastIcon.loadDrawable(systemUiContext), + bottomIconDrawable = lastIcon.loadDrawable(systemUiContext), + bottomBackgroundColor = builder.getBackgroundColor(/* isHeader = */ false), + ) + } + + @JvmStatic + fun inflateSingleLineViewHolder( + isConversation: Boolean, + reinflateFlags: Int, + entry: NotificationEntry, + context: Context, + logger: NotificationContentInflaterLogger, + ): HybridNotificationView? { + if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) return null + if (reinflateFlags and FLAG_CONTENT_VIEW_SINGLE_LINE == 0) { + return null + } + + logger.logInflateSingleLine(entry, reinflateFlags, isConversation) + logger.logAsyncTaskProgress(entry, "inflating single-line content view") + + var view: HybridNotificationView? = null + + traceSection("NotificationContentInflater#inflateSingleLineView") { + val inflater = LayoutInflater.from(context) + val layoutRes: Int = + if (isConversation) + com.android.systemui.res.R.layout.hybrid_conversation_notification + else com.android.systemui.res.R.layout.hybrid_notification + view = inflater.inflate(layoutRes, /* root = */ null) as HybridNotificationView + if (view == null) { + Log.wtf(TAG, "Single-line view inflation result is null for entry: ${entry.logKey}") + } + } + return view + } + + private fun Notification.Builder.getDefaultAvatar( + name: CharSequence?, + uniqueNames: PeopleHelper.NameToPrefixMap? = null + ): Icon { + val layoutColor = getSmallIconColor(/* isHeader = */ false) + if (!name.isNullOrEmpty()) { + val symbol = uniqueNames?.getPrefix(name) ?: "" + return peopleHelper.createAvatarSymbol( + /* name = */ name, + /* symbol = */ symbol, + /* layoutColor = */ layoutColor + ) + } + // If name is null, create default avatar with background color + // TODO(b/319829062): Investigate caching default icon for color + return peopleHelper.createAvatarSymbol(/* name = */ "", /* symbol = */ "", layoutColor) + } + + private fun Person.getKeyOrName(): CharSequence? = if (key == null) name else key + + private val peopleHelper = PeopleHelper() +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/SingleLineConversationViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/SingleLineConversationViewBinder.kt new file mode 100644 index 000000000000..69284bd7ef48 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/SingleLineConversationViewBinder.kt @@ -0,0 +1,40 @@ +/* + * 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.ui.viewbinder + +import com.android.systemui.statusbar.notification.row.HybridConversationNotificationView +import com.android.systemui.statusbar.notification.row.HybridNotificationView +import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation +import com.android.systemui.statusbar.notification.row.ui.viewmodel.SingleLineViewModel + +object SingleLineConversationViewBinder { + @JvmStatic + fun bind(viewModel: SingleLineViewModel, view: HybridNotificationView?) { + if (AsyncHybridViewInflation.isUnexpectedlyInLegacyMode()) return + if (view !is HybridConversationNotificationView || !viewModel.isConversation()) { + SingleLineViewBinder.bind(viewModel, view) + return + } + + viewModel.conversationData?.avatar?.let { view.setAvatar(it) } + view.setText( + viewModel.titleText, + viewModel.contentText, + viewModel.conversationData?.conversationSenderName + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/SingleLineViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/SingleLineViewBinder.kt new file mode 100644 index 000000000000..22e10c165521 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewbinder/SingleLineViewBinder.kt @@ -0,0 +1,34 @@ +/* + * 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.ui.viewbinder + +import com.android.systemui.statusbar.notification.row.HybridNotificationView +import com.android.systemui.statusbar.notification.row.ui.viewmodel.SingleLineViewModel + +object SingleLineViewBinder { + @JvmStatic + fun bind(viewModel: SingleLineViewModel?, view: HybridNotificationView?) { + // bind the title and content text views + view?.apply { + bind( + /* title = */ viewModel?.titleText, + /* text = */ viewModel?.contentText, + /* contentView = */ null + ) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/SingleLineViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/SingleLineViewModel.kt new file mode 100644 index 000000000000..d583fa5d97ed --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ui/viewmodel/SingleLineViewModel.kt @@ -0,0 +1,67 @@ +/* + * 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.ui.viewmodel + +import android.annotation.ColorInt +import android.graphics.drawable.Drawable + +/** + * ViewModel for SingleLine Notification View. + * + * @property titleText the text of notification view title + * @property contentText the text of view content + * @property conversationData the data that is needed specifically for conversation single-line + * views. Null conversationData shows that the notification is not conversation. Legacy + * MessagingStyle Notifications doesn't have this member. + */ +data class SingleLineViewModel( + var titleText: CharSequence?, + var contentText: CharSequence?, + var conversationData: ConversationData?, +) { + fun isConversation(): Boolean { + return conversationData != null + } +} + +/** + * @property conversationSenderName the name of sender to show in the single-line view. Only group + * conversation single-line views show the sender name. + * @property avatar the avatar to show for the conversation + */ +data class ConversationData( + val conversationSenderName: CharSequence?, + val avatar: ConversationAvatar, +) + +/** + * An avatar to show for a single-line conversation notification, it can be either a single icon or + * a face pile. + */ +sealed class ConversationAvatar + +data class SingleIcon(val iconDrawable: Drawable?) : ConversationAvatar() + +/** + * A kind of avatar to show for a group conversation notification view. It consists of two avatars + * of the last two senders. + */ +data class FacePile( + val topIconDrawable: Drawable?, + val bottomIconDrawable: Drawable?, + @ColorInt val bottomBackgroundColor: Int +) : ConversationAvatar() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java index 45b9c269b61c..abf6c27c68ac 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java @@ -1295,8 +1295,8 @@ public class NotificationChildrenContainer extends ViewGroup if (singleLineView != null) { minExpandHeight += singleLineView.getHeight(); } else { - Log.e(TAG, "getMinHeight: child " + child + " single line view is null", - new Exception()); + Log.e(TAG, "getMinHeight: child " + child.getEntry().getKey() + + " single line view is null", new Exception()); } visibleChildren++; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index 04db653282ac..dd04531b6b34 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -4917,30 +4917,12 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable public void removeContainerView(View v) { Assert.isMainThread(); removeView(v); - if (!FooterViewRefactor.isEnabled()) { - // A notification was removed, and we're not currently showing the empty shade view. - if (v instanceof ExpandableNotificationRow && !mController.isShowingEmptyShadeView()) { - mController.updateShowEmptyShadeView(); - updateFooter(); - mController.updateImportantForAccessibility(); - } - } - updateSpeedBumpIndex(); } public void addContainerView(View v) { Assert.isMainThread(); addView(v); - if (!FooterViewRefactor.isEnabled()) { - // A notification was added, and we're currently showing the empty shade view. - if (v instanceof ExpandableNotificationRow && mController.isShowingEmptyShadeView()) { - mController.updateShowEmptyShadeView(); - updateFooter(); - mController.updateImportantForAccessibility(); - } - } - updateSpeedBumpIndex(); } @@ -4948,14 +4930,6 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable Assert.isMainThread(); ensureRemovedFromTransientContainer(v); addView(v, index); - // A notification was added, and we're currently showing the empty shade view. - if (!FooterViewRefactor.isEnabled() && v instanceof ExpandableNotificationRow - && mController.isShowingEmptyShadeView()) { - mController.updateShowEmptyShadeView(); - updateFooter(); - mController.updateImportantForAccessibility(); - } - updateSpeedBumpIndex(); } 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 1143481863f5..49fde3984acc 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 @@ -2137,6 +2137,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { if (!FooterViewRefactor.isEnabled()) { updateShowEmptyShadeView(); + updateImportantForAccessibility(); } } } 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 f842e304ffdf..fe5bdd41a94f 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,9 +18,12 @@ package com.android.systemui.statusbar.notification.stack.ui.viewbinder import android.animation.Animator import android.animation.AnimatorListenerAdapter +import android.view.View +import android.view.WindowInsets import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.scene.shared.flag.SceneContainerFlags import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController @@ -30,6 +33,9 @@ import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificat import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.DisposableHandle +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch /** Binds the shared notification container to its view-model. */ @@ -65,6 +71,8 @@ object SharedNotificationContainerBinder { } } + val burnInParams = MutableStateFlow(BurnInParameters()) + /* * For animation sensitive coroutines, immediately run just like applicationScope does * instead of doing a post() to the main thread. This extra delay can cause visible jitter. @@ -122,7 +130,11 @@ object SharedNotificationContainerBinder { } } - launch { viewModel.translationY.collect { controller.setTranslationY(it) } } + launch { + burnInParams + .flatMapLatest { params -> viewModel.translationY(params) } + .collect { y -> controller.setTranslationY(y) } + } launch { viewModel.expansionAlpha.collect { controller.setMaxAlphaForExpansion(it) } @@ -137,11 +149,20 @@ object SharedNotificationContainerBinder { controller.setOnHeightChangedRunnable(Runnable { viewModel.notificationStackChanged() }) + view.setOnApplyWindowInsetsListener { v: View, insets: WindowInsets -> + val insetTypes = WindowInsets.Type.systemBars() or WindowInsets.Type.displayCutout() + burnInParams.update { current -> + current.copy(topInset = insets.getInsetsIgnoringVisibility(insetTypes).top) + } + insets + } + return object : DisposableHandle { override fun dispose() { disposableHandle.dispose() disposableHandleMainImmediate.dispose() controller.setOnHeightChangedRunnable(null) + view.setOnApplyWindowInsetsListener(null) } } } 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 99cd89b84c14..4617ce49f44a 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 @@ -28,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.AodBurnInViewModel +import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters import com.android.systemui.keyguard.ui.viewmodel.GlanceableHubToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.LockscreenToGlanceableHubTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.LockscreenToOccludedTransitionViewModel @@ -65,10 +67,11 @@ constructor( keyguardTransitionInteractor: KeyguardTransitionInteractor, private val shadeInteractor: ShadeInteractor, communalInteractor: CommunalInteractor, - occludedToLockscreenTransitionViewModel: OccludedToLockscreenTransitionViewModel, + private val occludedToLockscreenTransitionViewModel: OccludedToLockscreenTransitionViewModel, lockscreenToOccludedTransitionViewModel: LockscreenToOccludedTransitionViewModel, glanceableHubToLockscreenTransitionViewModel: GlanceableHubToLockscreenTransitionViewModel, - lockscreenToGlanceableHubTransitionViewModel: LockscreenToGlanceableHubTransitionViewModel + lockscreenToGlanceableHubTransitionViewModel: LockscreenToGlanceableHubTransitionViewModel, + private val aodBurnInViewModel: AodBurnInViewModel, ) { private val statesForConstrainedNotifications = setOf( @@ -313,20 +316,22 @@ constructor( * Under certain scenarios, such as swiping up on the lockscreen, the container will need to be * translated as the keyguard fades out. */ - val translationY: Flow<Float> = - combine( + fun translationY(params: BurnInParameters): Flow<Float> { + return combine( + aodBurnInViewModel.translationY(params).onStart { emit(0f) }, isOnLockscreenWithoutShade, merge( keyguardInteractor.keyguardTranslationY, occludedToLockscreenTransitionViewModel.lockscreenTranslationY, ) - ) { isOnLockscreenWithoutShade, translationY -> + ) { burnInY, isOnLockscreenWithoutShade, translationY -> if (isOnLockscreenWithoutShade) { - translationY + burnInY + translationY } else { 0f } } + } /** * When on keyguard, there is limited space to display notifications so calculate how many could 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 266c19c09941..7952511addfe 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -2587,8 +2587,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { // So if AOD is off or unsupported we need to trigger these updates at screen on // when the keyguard is occluded. mLockscreenUserManager.updatePublicMode(); - mShadeSurface.getNotificationStackScrollLayoutController() - .updateSensitivenessForOccludedWakeup(); + mStackScrollerController.updateSensitivenessForOccludedWakeup(); } if (mLaunchCameraWhenFinishedWaking) { mCameraLauncherLazy.get().launchCamera(mLastCameraLaunchSource, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java index 88347ab90606..4c83ca28b3cb 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java @@ -69,6 +69,7 @@ import com.android.systemui.dock.DockManager; import com.android.systemui.dreams.DreamOverlayStateController; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; +import com.android.systemui.keyguard.KeyguardWmStateRefactor; import com.android.systemui.keyguard.domain.interactor.KeyguardDismissActionInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.keyguard.domain.interactor.WindowManagerLockscreenVisibilityInteractor; @@ -474,7 +475,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb mIsDocked = mDockManager.isDocked(); } - if (mFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + if (KeyguardWmStateRefactor.isEnabled()) { // Show the keyguard views whenever we've told WM that the lockscreen is visible. mShadeViewController.postToView(() -> collectFlow( @@ -1428,7 +1429,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb executeAfterKeyguardGoneAction(); } - if (mFlags.isEnabled(Flags.KEYGUARD_WM_STATE_REFACTOR)) { + if (KeyguardWmStateRefactor.isEnabled()) { mKeyguardTransitionInteractor.startDismissKeyguardTransition(); } } 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/BooleanFlowOperators.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/BooleanFlowOperators.kt new file mode 100644 index 000000000000..693a835e25d2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/BooleanFlowOperators.kt @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.util.kotlin + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map + +object BooleanFlowOperators { + /** + * Logical AND operator for boolean flows. Will collect all flows and [combine] them to + * determine the result. + * + * Usage: + * ``` + * val result = and(flow1, flow2) + * ``` + */ + fun and(vararg flows: Flow<Boolean>): Flow<Boolean> = + combine(flows.asIterable()) { values -> values.all { it } } + + /** + * Logical NOT operator for a boolean flow. + * + * Usage: + * ``` + * val negatedFlow = not(flow) + * ``` + */ + fun not(flow: Flow<Boolean>) = flow.map { !it } + + /** + * Logical OR operator for a boolean flow. Will collect all flows and [combine] them to + * determine the result. + */ + fun or(vararg flows: Flow<Boolean>): Flow<Boolean> = + combine(flows.asIterable()) { values -> values.any { it } } +} diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/Dagger.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/Dagger.kt index c587f2edd601..5150389930a9 100644 --- a/packages/SystemUI/src/com/android/systemui/util/kotlin/Dagger.kt +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/Dagger.kt @@ -17,6 +17,7 @@ package com.android.systemui.util.kotlin import dagger.Lazy +import java.util.Optional import kotlin.reflect.KProperty /** @@ -30,3 +31,16 @@ import kotlin.reflect.KProperty * ``` */ operator fun <T> Lazy<T>.getValue(thisRef: Any?, property: KProperty<*>): T = get() + +/** + * Extension operator that allows developers to use [java.util.Optional] as a nullable property + * delegate: + * ```kotlin + * class MyClass @Inject constructor( + * optionalDependency: Optional<Foo>, + * ) { + * val dependency: Foo? by optionalDependency + * } + * ``` + */ +operator fun <T> Optional<T>.getValue(thisRef: Any?, property: KProperty<*>): T? = getOrNull() 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 d06457ba86ab..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())); @@ -2015,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/accessibility/floatingmenu/DragToInteractAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationControllerTest.java index 9bcab57bec87..90878169c201 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/DragToInteractAnimationControllerTest.java @@ -16,10 +16,12 @@ package com.android.systemui.accessibility.floatingmenu; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import android.annotation.NonNull; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.WindowManager; @@ -27,10 +29,12 @@ import android.view.accessibility.AccessibilityManager; import androidx.test.filters.SmallTest; +import com.android.systemui.Flags; import com.android.systemui.SysuiTestCase; +import com.android.systemui.accessibility.utils.TestUtils; import com.android.systemui.util.settings.SecureSettings; -import com.android.wm.shell.bubbles.DismissViewUtils; import com.android.wm.shell.common.bubbles.DismissView; +import com.android.wm.shell.common.magnetictarget.MagnetizedObject; import org.junit.Before; import org.junit.Rule; @@ -46,6 +50,7 @@ import org.mockito.junit.MockitoRule; @TestableLooper.RunWithLooper public class DragToInteractAnimationControllerTest extends SysuiTestCase { private DragToInteractAnimationController mDragToInteractAnimationController; + private DragToInteractView mInteractView; private DismissView mDismissView; @Rule @@ -57,29 +62,72 @@ public class DragToInteractAnimationControllerTest extends SysuiTestCase { @Before public void setUp() throws Exception { final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class); + final SecureSettings mockSecureSettings = TestUtils.mockSecureSettings(); final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext, mAccessibilityManager, - mock(SecureSettings.class)); + mockSecureSettings); final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext, stubWindowManager); - final MenuView stubMenuView = new MenuView(mContext, stubMenuViewModel, - stubMenuViewAppearance); + final MenuView stubMenuView = spy(new MenuView(mContext, stubMenuViewModel, + stubMenuViewAppearance, mockSecureSettings)); + mInteractView = spy(new DragToInteractView(mContext)); mDismissView = spy(new DismissView(mContext)); - DismissViewUtils.setup(mDismissView); - mDragToInteractAnimationController = new DragToInteractAnimationController( - mDismissView, stubMenuView); + + if (Flags.floatingMenuDragToEdit()) { + mDragToInteractAnimationController = new DragToInteractAnimationController( + mInteractView, stubMenuView); + } else { + mDragToInteractAnimationController = new DragToInteractAnimationController( + mDismissView, stubMenuView); + } + + mDragToInteractAnimationController.setMagnetListener(new MagnetizedObject.MagnetListener() { + @Override + public void onStuckToTarget(@NonNull MagnetizedObject.MagneticTarget target) { + + } + + @Override + public void onUnstuckFromTarget(@NonNull MagnetizedObject.MagneticTarget target, + float velX, float velY, boolean wasFlungOut) { + + } + + @Override + public void onReleasedInTarget(@NonNull MagnetizedObject.MagneticTarget target) { + + } + }); } @Test - public void showDismissView_success() { - mDragToInteractAnimationController.showDismissView(true); + @DisableFlags(Flags.FLAG_FLOATING_MENU_DRAG_TO_EDIT) + public void showDismissView_success_old() { + mDragToInteractAnimationController.showInteractView(true); verify(mDismissView).show(); } @Test - public void hideDismissView_success() { - mDragToInteractAnimationController.showDismissView(false); + @DisableFlags(Flags.FLAG_FLOATING_MENU_DRAG_TO_EDIT) + public void hideDismissView_success_old() { + mDragToInteractAnimationController.showInteractView(false); verify(mDismissView).hide(); } + + @Test + @EnableFlags(Flags.FLAG_FLOATING_MENU_DRAG_TO_EDIT) + public void showDismissView_success() { + mDragToInteractAnimationController.showInteractView(true); + + verify(mInteractView).show(); + } + + @Test + @EnableFlags(Flags.FLAG_FLOATING_MENU_DRAG_TO_EDIT) + public void hideDismissView_success() { + mDragToInteractAnimationController.showInteractView(false); + + verify(mInteractView).hide(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java index 215f93d1e163..e0df1e0e5586 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuAnimationControllerTest.java @@ -17,6 +17,7 @@ package com.android.systemui.accessibility.floatingmenu; import static com.google.common.truth.Truth.assertThat; + import static org.mockito.Mockito.any; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; @@ -42,6 +43,7 @@ import androidx.test.filters.SmallTest; import com.android.systemui.Flags; import com.android.systemui.Prefs; import com.android.systemui.SysuiTestCase; +import com.android.systemui.accessibility.utils.TestUtils; import com.android.systemui.util.settings.SecureSettings; import org.junit.After; @@ -79,10 +81,12 @@ public class MenuAnimationControllerTest extends SysuiTestCase { final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class); final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext, stubWindowManager); + final SecureSettings secureSettings = TestUtils.mockSecureSettings(); final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext, mAccessibilityManager, - mock(SecureSettings.class)); + secureSettings); - mMenuView = spy(new MenuView(mContext, stubMenuViewModel, stubMenuViewAppearance)); + mMenuView = spy(new MenuView(mContext, stubMenuViewModel, stubMenuViewAppearance, + secureSettings)); mViewPropertyAnimator = spy(mMenuView.animate()); doReturn(mViewPropertyAnimator).when(mMenuView).animate(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java index 9c8de302c5e1..c2ed7d4a9d3c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuItemAccessibilityDelegateTest.java @@ -22,10 +22,13 @@ import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTIO import static com.google.common.truth.Truth.assertThat; 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.graphics.Rect; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.WindowManager; @@ -37,7 +40,9 @@ import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate; import androidx.test.filters.SmallTest; +import com.android.systemui.Flags; import com.android.systemui.SysuiTestCase; +import com.android.systemui.accessibility.utils.TestUtils; import com.android.systemui.res.R; import com.android.systemui.util.settings.SecureSettings; @@ -49,6 +54,8 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; +import java.util.concurrent.atomic.AtomicBoolean; + /** Tests for {@link MenuItemAccessibilityDelegate}. */ @SmallTest @TestableLooper.RunWithLooper @@ -59,17 +66,16 @@ public class MenuItemAccessibilityDelegateTest extends SysuiTestCase { @Mock private AccessibilityManager mAccessibilityManager; - @Mock - private SecureSettings mSecureSettings; - @Mock - private DragToInteractAnimationController.DismissCallback mStubDismissCallback; - + private final SecureSettings mSecureSettings = TestUtils.mockSecureSettings(); private RecyclerView mStubListView; private MenuView mMenuView; + private MenuViewLayer mMenuViewLayer; private MenuItemAccessibilityDelegate mMenuItemAccessibilityDelegate; private MenuAnimationController mMenuAnimationController; private final Rect mDraggableBounds = new Rect(100, 200, 300, 400); + private final AtomicBoolean mEditReceived = new AtomicBoolean(false); + @Before public void setUp() { final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class); @@ -80,20 +86,28 @@ public class MenuItemAccessibilityDelegateTest extends SysuiTestCase { final int halfScreenHeight = stubWindowManager.getCurrentWindowMetrics().getBounds().height() / 2; - mMenuView = spy(new MenuView(mContext, stubMenuViewModel, stubMenuViewAppearance)); + mMenuView = spy(new MenuView(mContext, stubMenuViewModel, stubMenuViewAppearance, + mSecureSettings)); mMenuView.setTranslationY(halfScreenHeight); + mMenuViewLayer = spy(new MenuViewLayer( + mContext, stubWindowManager, mAccessibilityManager, + stubMenuViewModel, stubMenuViewAppearance, mMenuView, + mock(IAccessibilityFloatingMenu.class), mSecureSettings)); + doReturn(mDraggableBounds).when(mMenuView).getMenuDraggableBounds(); mStubListView = new RecyclerView(mContext); mMenuAnimationController = spy(new MenuAnimationController(mMenuView, stubMenuViewAppearance)); mMenuItemAccessibilityDelegate = new MenuItemAccessibilityDelegate(new RecyclerViewAccessibilityDelegate( - mStubListView), mMenuAnimationController); + mStubListView), mMenuAnimationController, mMenuViewLayer); + mEditReceived.set(false); } @Test - public void getAccessibilityActionList_matchSize() { + @DisableFlags(Flags.FLAG_FLOATING_MENU_DRAG_TO_EDIT) + public void getAccessibilityActionList_matchSize_withoutEdit() { final AccessibilityNodeInfoCompat info = new AccessibilityNodeInfoCompat(new AccessibilityNodeInfo()); @@ -103,6 +117,17 @@ public class MenuItemAccessibilityDelegateTest extends SysuiTestCase { } @Test + @EnableFlags(Flags.FLAG_FLOATING_MENU_DRAG_TO_EDIT) + public void getAccessibilityActionList_matchSize() { + final AccessibilityNodeInfoCompat info = + new AccessibilityNodeInfoCompat(new AccessibilityNodeInfo()); + + mMenuItemAccessibilityDelegate.onInitializeAccessibilityNodeInfo(mStubListView, info); + + assertThat(info.getActionList().size()).isEqualTo(7); + } + + @Test public void performMoveTopLeftAction_matchPosition() { final boolean moveTopLeftAction = mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView, @@ -169,13 +194,22 @@ public class MenuItemAccessibilityDelegateTest extends SysuiTestCase { @Test public void performRemoveMenuAction_success() { - mMenuAnimationController.setDismissCallback(mStubDismissCallback); final boolean removeMenuAction = mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView, R.id.action_remove_menu, null); assertThat(removeMenuAction).isTrue(); - verify(mMenuAnimationController).removeMenu(); + verify(mMenuViewLayer).dispatchAccessibilityAction(R.id.action_remove_menu); + } + + @Test + public void performEditAction_success() { + final boolean editAction = + mMenuItemAccessibilityDelegate.performAccessibilityAction(mStubListView, + R.id.action_edit, null); + + assertThat(editAction).isTrue(); + verify(mMenuViewLayer).dispatchAccessibilityAction(R.id.action_edit); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java index e1522f5f6751..9e8c6b3395e2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuListViewTouchHandlerTest.java @@ -16,6 +16,7 @@ package com.android.systemui.accessibility.floatingmenu; +import static android.R.id.empty; import static android.view.View.OVER_SCROLL_NEVER; import static com.google.common.truth.Truth.assertThat; @@ -27,6 +28,8 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.MotionEvent; @@ -38,10 +41,11 @@ import androidx.recyclerview.widget.RecyclerView; import androidx.test.filters.SmallTest; import com.android.internal.accessibility.dialog.AccessibilityTarget; +import com.android.systemui.Flags; import com.android.systemui.SysuiTestCase; import com.android.systemui.accessibility.MotionEventHelper; +import com.android.systemui.accessibility.utils.TestUtils; import com.android.systemui.util.settings.SecureSettings; -import com.android.wm.shell.bubbles.DismissViewUtils; import com.android.wm.shell.common.bubbles.DismissView; import org.junit.After; @@ -71,6 +75,7 @@ public class MenuListViewTouchHandlerTest extends SysuiTestCase { private DragToInteractAnimationController mDragToInteractAnimationController; private RecyclerView mStubListView; private DismissView mDismissView; + private DragToInteractView mInteractView; @Rule public MockitoRule mockito = MockitoJUnit.rule(); @@ -81,19 +86,28 @@ public class MenuListViewTouchHandlerTest extends SysuiTestCase { @Before public void setUp() throws Exception { final WindowManager windowManager = mContext.getSystemService(WindowManager.class); + final SecureSettings secureSettings = TestUtils.mockSecureSettings(); final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext, mAccessibilityManager, - mock(SecureSettings.class)); + secureSettings); final MenuViewAppearance stubMenuViewAppearance = new MenuViewAppearance(mContext, windowManager); - mStubMenuView = new MenuView(mContext, stubMenuViewModel, stubMenuViewAppearance); + mStubMenuView = new MenuView(mContext, stubMenuViewModel, stubMenuViewAppearance, + secureSettings); mStubMenuView.setTranslationX(0); mStubMenuView.setTranslationY(0); mMenuAnimationController = spy(new MenuAnimationController( mStubMenuView, stubMenuViewAppearance)); + mInteractView = spy(new DragToInteractView(mContext)); mDismissView = spy(new DismissView(mContext)); - DismissViewUtils.setup(mDismissView); - mDragToInteractAnimationController = - spy(new DragToInteractAnimationController(mDismissView, mStubMenuView)); + + if (Flags.floatingMenuDragToEdit()) { + mDragToInteractAnimationController = spy(new DragToInteractAnimationController( + mInteractView, mStubMenuView)); + } else { + mDragToInteractAnimationController = spy(new DragToInteractAnimationController( + mDismissView, mStubMenuView)); + } + mTouchHandler = new MenuListViewTouchHandler(mMenuAnimationController, mDragToInteractAnimationController); final AccessibilityTargetAdapter stubAdapter = new AccessibilityTargetAdapter(mStubTargets); @@ -115,7 +129,7 @@ public class MenuListViewTouchHandlerTest extends SysuiTestCase { @Test public void onActionMoveEvent_notConsumedEvent_shouldMoveToPosition() { - doReturn(false).when(mDragToInteractAnimationController).maybeConsumeMoveMotionEvent( + doReturn(empty).when(mDragToInteractAnimationController).maybeConsumeMoveMotionEvent( any(MotionEvent.class)); final int offset = 100; final MotionEvent stubDownEvent = @@ -136,6 +150,7 @@ public class MenuListViewTouchHandlerTest extends SysuiTestCase { } @Test + @DisableFlags(Flags.FLAG_FLOATING_MENU_DRAG_TO_EDIT) public void onActionMoveEvent_shouldShowDismissView() { final int offset = 100; final MotionEvent stubDownEvent = @@ -154,6 +169,25 @@ public class MenuListViewTouchHandlerTest extends SysuiTestCase { } @Test + @EnableFlags(Flags.FLAG_FLOATING_MENU_DRAG_TO_EDIT) + public void onActionMoveEvent_shouldShowInteractView() { + final int offset = 100; + final MotionEvent stubDownEvent = + mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 1, + MotionEvent.ACTION_DOWN, mStubMenuView.getTranslationX(), + mStubMenuView.getTranslationY()); + final MotionEvent stubMoveEvent = + mMotionEventHelper.obtainMotionEvent(/* downTime= */ 0, /* eventTime= */ 3, + MotionEvent.ACTION_MOVE, mStubMenuView.getTranslationX() + offset, + mStubMenuView.getTranslationY() + offset); + + mTouchHandler.onInterceptTouchEvent(mStubListView, stubDownEvent); + mTouchHandler.onInterceptTouchEvent(mStubListView, stubMoveEvent); + + verify(mInteractView).show(); + } + + @Test public void dragAndDrop_shouldFlingMenuThenSpringToEdge() { final int offset = 100; final MotionEvent stubDownEvent = diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java index bc9a0a5484ac..4a1bdbcc9b48 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewLayerTest.java @@ -30,6 +30,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.mock; @@ -72,6 +73,8 @@ import com.android.internal.messages.nano.SystemMessageProto; import com.android.systemui.Flags; import com.android.systemui.SysuiTestCase; import com.android.systemui.SysuiTestableContext; +import com.android.systemui.accessibility.utils.TestUtils; +import com.android.systemui.res.R; import com.android.systemui.util.settings.SecureSettings; import com.android.wm.shell.common.magnetictarget.MagnetizedObject; @@ -81,6 +84,7 @@ import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatcher; import org.mockito.Mock; import org.mockito.Spy; import org.mockito.junit.MockitoJUnit; @@ -122,18 +126,17 @@ public class MenuViewLayerTest extends SysuiTestCase { private SysuiTestableContext mSpyContext = getContext(); @Mock private IAccessibilityFloatingMenu mFloatingMenu; - - @Mock - private SecureSettings mSecureSettings; - @Mock private WindowManager mStubWindowManager; - @Mock private AccessibilityManager mStubAccessibilityManager; + private final SecureSettings mSecureSettings = TestUtils.mockSecureSettings(); private final NotificationManager mMockNotificationManager = mock(NotificationManager.class); + private final ArgumentMatcher<IntentFilter> mNotificationMatcher = + (arg) -> arg.hasAction(ACTION_UNDO) && arg.hasAction(ACTION_DELETE); + @Before public void setUp() throws Exception { mSpyContext.addMockSystemService(Context.NOTIFICATION_SERVICE, mMockNotificationManager); @@ -145,8 +148,16 @@ public class MenuViewLayerTest extends SysuiTestCase { new WindowMetrics(mDisplayBounds, fakeDisplayInsets(), /* density = */ 0.0f)); doReturn(mWindowMetrics).when(mStubWindowManager).getCurrentWindowMetrics(); - mMenuViewLayer = new MenuViewLayer(mSpyContext, mStubWindowManager, - mStubAccessibilityManager, mFloatingMenu, mSecureSettings); + MenuViewModel menuViewModel = new MenuViewModel( + mSpyContext, mStubAccessibilityManager, mSecureSettings); + MenuViewAppearance menuViewAppearance = new MenuViewAppearance( + mSpyContext, mStubWindowManager); + mMenuView = spy( + new MenuView(mSpyContext, menuViewModel, menuViewAppearance, mSecureSettings)); + + mMenuViewLayer = spy(new MenuViewLayer(mSpyContext, mStubWindowManager, + mStubAccessibilityManager, menuViewModel, menuViewAppearance, mMenuView, + mFloatingMenu, mSecureSettings)); mMenuView = (MenuView) mMenuViewLayer.getChildAt(LayerIndex.MENU_VIEW); mMenuAnimationController = mMenuView.getMenuAnimationController(); @@ -236,6 +247,27 @@ public class MenuViewLayerTest extends SysuiTestCase { } @Test + @EnableFlags(Flags.FLAG_FLOATING_MENU_DRAG_TO_EDIT) + public void onEditAction_gotoEditScreen_isCalled() { + mMenuViewLayer.dispatchAccessibilityAction(R.id.action_edit); + verify(mMenuView).gotoEditScreen(); + } + + @Test + @EnableFlags(Flags.FLAG_FLOATING_MENU_DRAG_TO_HIDE) + public void onDismissAction_hideMenuAndShowNotification() { + mMenuViewLayer.dispatchAccessibilityAction(R.id.action_remove_menu); + verify(mMenuViewLayer).hideMenuAndShowNotification(); + } + + @Test + @DisableFlags(Flags.FLAG_FLOATING_MENU_DRAG_TO_HIDE) + public void onDismissAction_hideMenuAndShowMessage() { + mMenuViewLayer.dispatchAccessibilityAction(R.id.action_remove_menu); + verify(mMenuViewLayer).hideMenuAndShowMessage(); + } + + @Test public void showingImeInsetsChange_notOverlapOnIme_menuKeepOriginalPosition() { final float menuTop = STATUS_BAR_HEIGHT + 100; mMenuAnimationController.moveAndPersistPosition(new PointF(0, menuTop)); @@ -307,19 +339,13 @@ public class MenuViewLayerTest extends SysuiTestCase { @Test @EnableFlags(Flags.FLAG_FLOATING_MENU_DRAG_TO_HIDE) public void onReleasedInTarget_hideMenuAndShowNotificationWithExpectedActions() { - dragMenuThenReleasedInTarget(); + dragMenuThenReleasedInTarget(R.id.action_remove_menu); verify(mMockNotificationManager).notify( eq(SystemMessageProto.SystemMessage.NOTE_A11Y_FLOATING_MENU_HIDDEN), any(Notification.class)); - ArgumentCaptor<IntentFilter> intentFilterCaptor = ArgumentCaptor.forClass( - IntentFilter.class); verify(mSpyContext).registerReceiver( - any(BroadcastReceiver.class), - intentFilterCaptor.capture(), - anyInt()); - assertThat(intentFilterCaptor.getValue().matchAction(ACTION_UNDO)).isTrue(); - assertThat(intentFilterCaptor.getValue().matchAction(ACTION_DELETE)).isTrue(); + any(BroadcastReceiver.class), argThat(mNotificationMatcher), anyInt()); } @Test @@ -327,10 +353,10 @@ public class MenuViewLayerTest extends SysuiTestCase { public void receiveActionUndo_dismissNotificationAndMenuVisible() { ArgumentCaptor<BroadcastReceiver> broadcastReceiverCaptor = ArgumentCaptor.forClass( BroadcastReceiver.class); - dragMenuThenReleasedInTarget(); + dragMenuThenReleasedInTarget(R.id.action_remove_menu); verify(mSpyContext).registerReceiver(broadcastReceiverCaptor.capture(), - any(IntentFilter.class), anyInt()); + argThat(mNotificationMatcher), anyInt()); broadcastReceiverCaptor.getValue().onReceive(mSpyContext, new Intent(ACTION_UNDO)); verify(mSpyContext).unregisterReceiver(broadcastReceiverCaptor.getValue()); @@ -344,10 +370,10 @@ public class MenuViewLayerTest extends SysuiTestCase { public void receiveActionDelete_dismissNotificationAndHideMenu() { ArgumentCaptor<BroadcastReceiver> broadcastReceiverCaptor = ArgumentCaptor.forClass( BroadcastReceiver.class); - dragMenuThenReleasedInTarget(); + dragMenuThenReleasedInTarget(R.id.action_remove_menu); verify(mSpyContext).registerReceiver(broadcastReceiverCaptor.capture(), - any(IntentFilter.class), anyInt()); + argThat(mNotificationMatcher), anyInt()); broadcastReceiverCaptor.getValue().onReceive(mSpyContext, new Intent(ACTION_DELETE)); verify(mSpyContext).unregisterReceiver(broadcastReceiverCaptor.getValue()); @@ -423,10 +449,12 @@ public class MenuViewLayerTest extends SysuiTestCase { }); } - private void dragMenuThenReleasedInTarget() { + private void dragMenuThenReleasedInTarget(int id) { MagnetizedObject.MagnetListener magnetListener = - mMenuViewLayer.getDragToInteractAnimationController().getMagnetListener(); + mMenuViewLayer.getDragToInteractAnimationController().getMagnetListener(id); + View view = mock(View.class); + when(view.getId()).thenReturn(id); magnetListener.onReleasedInTarget( - new MagnetizedObject.MagneticTarget(mock(View.class), 200)); + new MagnetizedObject.MagneticTarget(view, 200)); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java index 8da6cf98d76f..7c97f53d539d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/floatingmenu/MenuViewTest.java @@ -17,15 +17,19 @@ package com.android.systemui.accessibility.floatingmenu; import static android.app.UiModeManager.MODE_NIGHT_YES; + import static com.google.common.truth.Truth.assertThat; -import static org.mockito.Mockito.mock; + +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import android.app.UiModeManager; +import android.content.Intent; import android.graphics.Rect; import android.graphics.drawable.GradientDrawable; import android.platform.test.annotations.EnableFlags; +import android.provider.Settings; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.WindowManager; @@ -36,6 +40,8 @@ import androidx.test.filters.SmallTest; import com.android.systemui.Flags; import com.android.systemui.Prefs; import com.android.systemui.SysuiTestCase; +import com.android.systemui.SysuiTestableContext; +import com.android.systemui.accessibility.utils.TestUtils; import com.android.systemui.util.settings.SecureSettings; import org.junit.After; @@ -65,17 +71,23 @@ public class MenuViewTest extends SysuiTestCase { @Mock private AccessibilityManager mAccessibilityManager; + private SysuiTestableContext mSpyContext; + @Before public void setUp() throws Exception { mUiModeManager = mContext.getSystemService(UiModeManager.class); mNightMode = mUiModeManager.getNightMode(); mUiModeManager.setNightMode(MODE_NIGHT_YES); + + mSpyContext = spy(mContext); + final SecureSettings secureSettings = TestUtils.mockSecureSettings(); final MenuViewModel stubMenuViewModel = new MenuViewModel(mContext, mAccessibilityManager, - mock(SecureSettings.class)); + secureSettings); final WindowManager stubWindowManager = mContext.getSystemService(WindowManager.class); - mStubMenuViewAppearance = new MenuViewAppearance(mContext, stubWindowManager); - mMenuView = spy(new MenuView(mContext, stubMenuViewModel, mStubMenuViewAppearance)); - mLastPosition = Prefs.getString(mContext, + mStubMenuViewAppearance = new MenuViewAppearance(mSpyContext, stubWindowManager); + mMenuView = spy(new MenuView(mSpyContext, stubMenuViewModel, mStubMenuViewAppearance, + secureSettings)); + mLastPosition = Prefs.getString(mSpyContext, Prefs.Key.ACCESSIBILITY_FLOATING_MENU_POSITION, /* defaultValue= */ null); } @@ -154,6 +166,25 @@ public class MenuViewTest extends SysuiTestCase { assertThat(radiiAnimator.isStarted()).isTrue(); } + @Test + public void getIntentForEditScreen_validate() { + Intent intent = mMenuView.getIntentForEditScreen(); + String[] targets = intent.getBundleExtra( + ":settings:show_fragment_args").getStringArray("targets"); + + assertThat(intent.getAction()).isEqualTo(Settings.ACTION_ACCESSIBILITY_SHORTCUT_SETTINGS); + assertThat(targets).asList().containsExactlyElementsIn(TestUtils.TEST_BUTTON_TARGETS); + } + + @Test + @EnableFlags(Flags.FLAG_FLOATING_MENU_DRAG_TO_EDIT) + public void gotoEditScreen_sendsIntent() { + // Notably, this shouldn't crash the settings app, + // because the button target args are configured. + mMenuView.gotoEditScreen(); + verify(mSpyContext).startActivity(any()); + } + private InstantInsetLayerDrawable getMenuViewInsetLayer() { return (InstantInsetLayerDrawable) mMenuView.getBackground(); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/utils/TestUtils.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/utils/TestUtils.java index 10c8caa4fd27..8399fa85bfb1 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/utils/TestUtils.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/utils/TestUtils.java @@ -16,11 +16,27 @@ package com.android.systemui.accessibility.utils; +import static com.android.internal.accessibility.common.ShortcutConstants.SERVICES_SEPARATOR; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import android.content.ComponentName; import android.os.SystemClock; +import android.os.UserHandle; +import android.provider.Settings; + +import com.android.systemui.util.settings.SecureSettings; +import java.util.Set; +import java.util.StringJoiner; import java.util.function.BooleanSupplier; public class TestUtils { + private static final ComponentName TEST_COMPONENT_A = new ComponentName("pkg", "A"); + private static final ComponentName TEST_COMPONENT_B = new ComponentName("pkg", "B"); + public static final String[] TEST_BUTTON_TARGETS = { + TEST_COMPONENT_A.flattenToString(), TEST_COMPONENT_B.flattenToString()}; public static long DEFAULT_CONDITION_DURATION = 5_000; /** @@ -55,4 +71,28 @@ public class TestUtils { SystemClock.sleep(sleepMs); } } + + /** + * Returns a mock secure settings configured to return information needed for tests. + * Currently, this only includes button targets. + */ + public static SecureSettings mockSecureSettings() { + SecureSettings secureSettings = mock(SecureSettings.class); + + final String targets = getShortcutTargets( + Set.of(TEST_COMPONENT_A, TEST_COMPONENT_B)); + when(secureSettings.getStringForUser( + Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS, + UserHandle.USER_CURRENT)).thenReturn(targets); + + return secureSettings; + } + + private static String getShortcutTargets(Set<ComponentName> components) { + final StringJoiner stringJoiner = new StringJoiner(String.valueOf(SERVICES_SEPARATOR)); + for (ComponentName target : components) { + stringJoiner.add(target.flattenToString()); + } + return stringJoiner.toString(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt index a47e28801709..7c03d7899398 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthRippleControllerTest.kt @@ -27,12 +27,13 @@ import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession import com.android.keyguard.KeyguardUpdateMonitor import com.android.keyguard.KeyguardUpdateMonitorCallback import com.android.keyguard.logging.KeyguardLogger +import com.android.systemui.Flags import com.android.systemui.Flags.FLAG_LIGHT_REVEAL_MIGRATION import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.data.repository.FakeFacePropertyRepository -import com.android.systemui.log.logcatLogBuffer -import com.android.systemui.flags.FeatureFlags +import com.android.systemui.deviceentry.domain.interactor.AuthRippleInteractor import com.android.systemui.keyguard.WakefulnessLifecycle +import com.android.systemui.log.logcatLogBuffer import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.statusbar.LightRevealScrim import com.android.systemui.statusbar.NotificationShadeWindowController @@ -42,7 +43,7 @@ import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.leak.RotationUtils import com.android.systemui.util.mockito.any -import javax.inject.Provider +import kotlinx.coroutines.ExperimentalCoroutinesApi import org.junit.After import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue @@ -61,8 +62,10 @@ import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations import org.mockito.MockitoSession import org.mockito.quality.Strictness +import javax.inject.Provider +@ExperimentalCoroutinesApi @SmallTest @RunWith(AndroidTestingRunner::class) class AuthRippleControllerTest : SysuiTestCase() { @@ -74,6 +77,7 @@ class AuthRippleControllerTest : SysuiTestCase() { @Mock private lateinit var configurationController: ConfigurationController @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor @Mock private lateinit var authController: AuthController + @Mock private lateinit var authRippleInteractor: AuthRippleInteractor @Mock private lateinit var keyguardStateController: KeyguardStateController @Mock private lateinit var wakefulnessLifecycle: WakefulnessLifecycle @@ -88,8 +92,6 @@ class AuthRippleControllerTest : SysuiTestCase() { @Mock private lateinit var statusBarStateController: StatusBarStateController @Mock - private lateinit var featureFlags: FeatureFlags - @Mock private lateinit var lightRevealScrim: LightRevealScrim @Mock private lateinit var fpSensorProp: FingerprintSensorPropertiesInternal @@ -103,6 +105,7 @@ class AuthRippleControllerTest : SysuiTestCase() { @Before fun setUp() { + mSetFlagsRule.disableFlags(Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR) MockitoAnnotations.initMocks(this) staticMockSession = mockitoSession() .mockStatic(RotationUtils::class.java) @@ -128,6 +131,7 @@ class AuthRippleControllerTest : SysuiTestCase() { KeyguardLogger(logcatLogBuffer(AuthRippleController.TAG)), biometricUnlockController, lightRevealScrim, + authRippleInteractor, facePropertyRepository, rippleView, ) 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 8127bb1f0465..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 @@ -1226,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) @@ -1236,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) diff --git a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryHapticsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt index 0dfdeca60fcd..bdf0e06ce410 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryHapticsInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.deviceentry.data.repository +package com.android.systemui.deviceentry.domain.interactor import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -23,12 +23,13 @@ import com.android.systemui.biometrics.data.repository.fingerprintPropertyReposi 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.deviceEntryHapticsInteractor import com.android.systemui.deviceentry.shared.model.FailedFaceAuthenticationStatus import com.android.systemui.keyevent.data.repository.fakeKeyEventRepository import com.android.systemui.keyguard.data.repository.biometricSettingsRepository import com.android.systemui.keyguard.data.repository.deviceEntryFingerprintAuthRepository import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository +import com.android.systemui.keyguard.shared.model.BiometricUnlockModel import com.android.systemui.keyguard.shared.model.BiometricUnlockSource import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus import com.android.systemui.kosmos.testScope @@ -158,9 +159,10 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() { } private suspend fun enterDeviceFromBiometricUnlock() { - kosmos.fakeDeviceEntryRepository.enteringDeviceFromBiometricUnlock( + kosmos.fakeKeyguardRepository.setBiometricUnlockSource( BiometricUnlockSource.FINGERPRINT_SENSOR ) + kosmos.fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockModel.WAKE_AND_UNLOCK) } private fun fingerprintFailure() { 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 8a3a4342915b..1183964c39d2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/KeyguardViewMediatorTest.java @@ -25,6 +25,7 @@ import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STR import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT; import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN; import static com.android.systemui.Flags.FLAG_REFACTOR_GET_CURRENT_USER; +import static com.android.systemui.Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR; import static com.android.systemui.keyguard.KeyguardViewMediator.DELAYED_KEYGUARD_ACTION; import static com.android.systemui.keyguard.KeyguardViewMediator.KEYGUARD_LOCK_AFTER_DELAY_DEFAULT; import static com.android.systemui.keyguard.KeyguardViewMediator.REBOOT_MAINLINE_UPDATE; @@ -270,8 +271,8 @@ public class KeyguardViewMediatorTest extends SysuiTestCase { mSceneContainerFlags, mKosmos::getCommunalInteractor); mFeatureFlags = new FakeFeatureFlags(); - mFeatureFlags.set(Flags.KEYGUARD_WM_STATE_REFACTOR, false); mSetFlagsRule.enableFlags(FLAG_REFACTOR_GET_CURRENT_USER); + mSetFlagsRule.disableFlags(FLAG_KEYGUARD_WM_STATE_REFACTOR); DejankUtils.setImmediate(true); 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 4f3a63dd2829..e93ad0be3e85 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 @@ -21,6 +21,7 @@ import androidx.test.filters.SmallTest import com.android.keyguard.KeyguardSecurityModel import com.android.keyguard.KeyguardSecurityModel.SecurityMode.PIN import com.android.systemui.Flags.FLAG_COMMUNAL_HUB +import com.android.systemui.Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR import com.android.systemui.SysuiTestCase import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository @@ -29,7 +30,6 @@ 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.flags.FakeFeatureFlags -import com.android.systemui.flags.Flags import com.android.systemui.keyguard.data.repository.FakeCommandQueue import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository import com.android.systemui.keyguard.data.repository.FakeKeyguardSurfaceBehindRepository @@ -137,8 +137,8 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { whenever(keyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(PIN) - featureFlags = FakeFeatureFlags().apply { set(Flags.KEYGUARD_WM_STATE_REFACTOR, false) } mSetFlagsRule.enableFlags(FLAG_COMMUNAL_HUB) + featureFlags = FakeFeatureFlags() keyguardInteractor = createKeyguardInteractor() @@ -299,6 +299,10 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { powerInteractor = powerInteractor, ) .apply { start() } + + mSetFlagsRule.disableFlags( + FLAG_KEYGUARD_WM_STATE_REFACTOR, + ) } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt index c864704f6997..699284e29ce3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultDeviceEntrySectionTest.kt @@ -39,6 +39,7 @@ import com.android.systemui.shade.NotificationPanelView import com.android.systemui.statusbar.VibratorHelper import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -71,6 +72,7 @@ class DefaultDeviceEntrySectionTest : SysuiTestCase() { FakeFeatureFlagsClassic().apply { set(Flags.LOCKSCREEN_ENABLE_LANDSCAPE, false) } underTest = DefaultDeviceEntrySection( + TestScope().backgroundScope, keyguardUpdateMonitor, authController, windowManager, diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt index f93d52b2c35c..aa54565c2aa0 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/gestural/BackPanelControllerTest.kt @@ -28,6 +28,7 @@ import android.view.MotionEvent.ACTION_UP import android.view.ViewConfiguration import android.view.WindowManager import androidx.test.filters.SmallTest +import com.android.internal.jank.InteractionJankMonitor import com.android.internal.util.LatencyTracker import com.android.systemui.SysuiTestCase import com.android.systemui.plugins.NavigationEdgeBackPlugin @@ -40,8 +41,10 @@ import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.eq import org.mockito.Mock +import org.mockito.Mockito.anyInt import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations @SmallTest @@ -59,12 +62,16 @@ class BackPanelControllerTest : SysuiTestCase() { @Mock private lateinit var windowManager: WindowManager @Mock private lateinit var configurationController: ConfigurationController @Mock private lateinit var latencyTracker: LatencyTracker + @Mock private lateinit var interactionJankMonitor: InteractionJankMonitor @Mock private lateinit var layoutParams: WindowManager.LayoutParams @Mock private lateinit var backCallback: NavigationEdgeBackPlugin.BackCallback @Before fun setup() { MockitoAnnotations.initMocks(this) + `when`(interactionJankMonitor.begin(any(), anyInt())).thenReturn(true) + `when`(interactionJankMonitor.end(anyInt())).thenReturn(true) + `when`(interactionJankMonitor.cancel(anyInt())).thenReturn(true) mBackPanelController = BackPanelController( context, @@ -74,6 +81,7 @@ class BackPanelControllerTest : SysuiTestCase() { vibratorHelper, configurationController, latencyTracker, + interactionJankMonitor, ) mBackPanelController.setLayoutParams(layoutParams) mBackPanelController.setBackCallback(backCallback) diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest_311121830.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest_311121830.kt new file mode 100644 index 000000000000..e8aa8f0bdc5d --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tileimpl/QSIconViewImplTest_311121830.kt @@ -0,0 +1,94 @@ +/* + * 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.qs.tileimpl + +import android.animation.AnimatorTestRule +import android.content.Context +import android.service.quicksettings.Tile +import android.testing.AndroidTestingRunner +import android.testing.UiThreadTest +import android.view.ContextThemeWrapper +import android.view.View +import android.widget.ImageView +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.plugins.qs.QSTile +import com.android.systemui.res.R +import com.android.systemui.statusbar.connectivity.WifiIcons +import com.google.common.truth.Truth.assertThat +import kotlin.test.Test +import org.junit.Rule +import org.junit.runner.RunWith + +/** Test for regression b/311121830 */ +@RunWith(AndroidTestingRunner::class) +@UiThreadTest +@SmallTest +class QSIconViewImplTest_311121830 : SysuiTestCase() { + + @get:Rule val animatorRule = AnimatorTestRule() + + @Test + fun alwaysLastIcon() { + // Need to inflate with the correct theme so the colors can be retrieved and the animations + // are run + val iconView = + AnimateQSIconViewImpl( + ContextThemeWrapper(context, R.style.Theme_SystemUI_QuickSettings) + ) + + val initialState = + QSTile.State().apply { + state = Tile.STATE_INACTIVE + icon = QSTileImpl.ResourceIcon.get(R.drawable.ic_qs_no_internet_available) + } + val firstState = + QSTile.State().apply { + state = Tile.STATE_ACTIVE + icon = QSTileImpl.ResourceIcon.get(WifiIcons.WIFI_NO_INTERNET_ICONS[4]) + } + val secondState = + QSTile.State().apply { + state = Tile.STATE_ACTIVE + icon = QSTileImpl.ResourceIcon.get(WifiIcons.WIFI_FULL_ICONS[4]) + } + + // Start with the initial state + iconView.setIcon(initialState, /* allowAnimations= */ false) + + // Set the first state to animate, and advance time to half the time of the animation + iconView.setIcon(firstState, /* allowAnimations= */ true) + animatorRule.advanceTimeBy(QSIconViewImpl.QS_ANIM_LENGTH / 2) + + // Set the second state to animate (it shouldn't, because `State.state` is the same) and + // advance time to 2 animations length + iconView.setIcon(secondState, /* allowAnimations= */ true) + animatorRule.advanceTimeBy(QSIconViewImpl.QS_ANIM_LENGTH * 2) + + assertThat(iconView.mLastIcon).isEqualTo(secondState.icon) + } + + private class AnimateQSIconViewImpl(context: Context) : QSIconViewImpl(context) { + override fun createIcon(): View { + return object : ImageView(context) { + override fun isShown(): Boolean { + return true + } + } + } + } +} 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/recents/OverviewProxyServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt index 70a48f574949..e9f21329bfbc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt @@ -131,7 +131,10 @@ class OverviewProxyServiceTest : SysuiTestCase() { whenever(packageManager.resolveServiceAsUser(any(), anyInt(), anyInt())) .thenReturn(mock(ResolveInfo::class.java)) - featureFlags.set(Flags.KEYGUARD_WM_STATE_REFACTOR, false) + mSetFlagsRule.disableFlags( + com.android.systemui.Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR, + ) + subject = OverviewProxyService( context, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java index 58eec2e71d32..4519ba6d3590 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/coordinator/PreparationCoordinatorTest.java @@ -65,6 +65,7 @@ import com.android.systemui.statusbar.notification.collection.listbuilder.plugga import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifSectioner; import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener; import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider; +import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager; import com.android.systemui.statusbar.notification.collection.render.NotifViewBarn; import com.android.systemui.statusbar.notification.row.NotifInflationErrorManager; import com.android.systemui.util.settings.SecureSettings; @@ -111,6 +112,7 @@ public class PreparationCoordinatorTest extends SysuiTestCase { @Spy private FakeNotifInflater mNotifInflater = new FakeNotifInflater(); private final SectionStyleProvider mSectionStyleProvider = new SectionStyleProvider(); @Mock private UserTracker mUserTracker; + @Mock private GroupMembershipManager mGroupMembershipManager; private NotifUiAdjustmentProvider mAdjustmentProvider; @@ -127,7 +129,9 @@ public class PreparationCoordinatorTest extends SysuiTestCase { mSecureSettings, mLockscreenUserManager, mSectionStyleProvider, - mUserTracker); + mUserTracker, + mGroupMembershipManager + ); mEntry = getNotificationEntryBuilder().setParent(ROOT_ENTRY).build(); mInflationError = new Exception(TEST_MESSAGE); mErrorManager = new NotifInflationErrorManager(); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProviderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProviderTest.kt index f9f8d8a2cfc6..73c49c023dd5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProviderTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/collection/inflation/NotifUiAdjustmentProviderTest.kt @@ -17,6 +17,8 @@ package com.android.systemui.statusbar.notification.collection.inflation import android.database.ContentObserver import android.os.Handler +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import android.provider.Settings.Secure.SHOW_NOTIFICATION_SNOOZE import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper @@ -28,6 +30,8 @@ import com.android.systemui.statusbar.notification.collection.GroupEntry import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder import com.android.systemui.statusbar.notification.collection.listbuilder.NotifSection import com.android.systemui.statusbar.notification.collection.provider.SectionStyleProvider +import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager +import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.mock @@ -35,6 +39,8 @@ import com.android.systemui.util.mockito.withArgCaptor import com.android.systemui.util.settings.FakeSettings import com.android.systemui.util.settings.SecureSettings import com.google.common.truth.Truth.assertThat +import kotlin.test.assertFalse +import kotlin.test.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -55,6 +61,7 @@ class NotifUiAdjustmentProviderTest : SysuiTestCase() { private val uri = FakeSettings().getUriFor(SHOW_NOTIFICATION_SNOOZE) private val dirtyListener: Runnable = mock() private val userTracker: UserTracker = mock() + private val groupMembershipManager: GroupMembershipManager = mock() private val section = NotifSection(mock(), 0) private val entry = NotificationEntryBuilder() @@ -69,7 +76,8 @@ class NotifUiAdjustmentProviderTest : SysuiTestCase() { secureSettings, lockscreenUserManager, sectionStyleProvider, - userTracker + userTracker, + groupMembershipManager, ) @Before @@ -127,4 +135,42 @@ class NotifUiAdjustmentProviderTest : SysuiTestCase() { assertThat(withSnoozing.isSnoozeEnabled).isTrue() assertThat(withSnoozing).isNotEqualTo(original) } + + @Test + @EnableFlags(AsyncHybridViewInflation.FLAG_NAME) + fun changeIsChildInGroup_asyncHybirdFlagEnabled_needReInflation() { + // Given: an Entry that is not child in group + // AsyncHybridViewInflation flag is enabled + whenever(groupMembershipManager.isChildInGroup(entry)).thenReturn(false) + val oldAdjustment = adjustmentProvider.calculateAdjustment(entry) + assertThat(oldAdjustment.isChildInGroup).isFalse() + + // When: the Entry becomes a group child + whenever(groupMembershipManager.isChildInGroup(entry)).thenReturn(true) + val newAdjustment = adjustmentProvider.calculateAdjustment(entry) + assertThat(newAdjustment.isChildInGroup).isTrue() + assertThat(newAdjustment).isNotEqualTo(oldAdjustment) + + // Then: need re-inflation + assertTrue(NotifUiAdjustment.needReinflate(oldAdjustment, newAdjustment)) + } + + @Test + @DisableFlags(AsyncHybridViewInflation.FLAG_NAME) + fun changeIsChildInGroup_asyncHybirdFlagDisabled_noNeedForReInflation() { + // Given: an Entry that is not child in group + // AsyncHybridViewInflation flag is disabled + whenever(groupMembershipManager.isChildInGroup(entry)).thenReturn(false) + val oldAdjustment = adjustmentProvider.calculateAdjustment(entry) + assertThat(oldAdjustment.isChildInGroup).isFalse() + + // When: the Entry becomes a group child + whenever(groupMembershipManager.isChildInGroup(entry)).thenReturn(true) + val newAdjustment = adjustmentProvider.calculateAdjustment(entry) + assertThat(newAdjustment.isChildInGroup).isTrue() + assertThat(newAdjustment).isNotEqualTo(oldAdjustment) + + // Then: need no re-inflation + assertFalse(NotifUiAdjustment.needReinflate(oldAdjustment, newAdjustment)) + } } 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/NotificationContentInflaterTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java index b0996ad48d0a..a0d10759ba56 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationContentInflaterTest.java @@ -88,6 +88,8 @@ public class NotificationContentInflaterTest extends SysuiTestCase { private Notification.Builder mBuilder; private ExpandableNotificationRow mRow; + private NotificationTestHelper mHelper; + @Mock private NotifRemoteViewCache mCache; @Mock private ConversationNotificationProcessor mConversationNotificationProcessor; @Mock private InflatedSmartReplyState mInflatedSmartReplyState; @@ -119,11 +121,11 @@ public class NotificationContentInflaterTest extends SysuiTestCase { .setContentTitle("Title") .setContentText("Text") .setStyle(new Notification.BigTextStyle().bigText("big text")); - NotificationTestHelper helper = new NotificationTestHelper( + mHelper = new NotificationTestHelper( mContext, mDependency, TestableLooper.get(this)); - ExpandableNotificationRow row = helper.createRow(mBuilder.build()); + ExpandableNotificationRow row = mHelper.createRow(mBuilder.build()); mRow = spy(row); when(mNotifLayoutInflaterFactoryProvider.provide(any(), any())) .thenReturn(mNotifLayoutInflaterFactory); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineConversationViewBinderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineConversationViewBinderTest.kt new file mode 100644 index 000000000000..1c959af6ec3f --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineConversationViewBinderTest.kt @@ -0,0 +1,117 @@ +/* + * 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 android.app.Notification +import android.app.Person +import android.platform.test.annotations.EnableFlags +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import androidx.test.filters.SmallTest +import com.android.internal.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_SINGLE_LINE +import com.android.systemui.statusbar.notification.row.SingleLineViewInflater.inflateSingleLineViewHolder +import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation +import com.android.systemui.statusbar.notification.row.ui.viewbinder.SingleLineConversationViewBinder +import com.android.systemui.util.mockito.mock +import kotlin.test.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper +class SingleLineConversationViewBinderTest : SysuiTestCase() { + private lateinit var notificationBuilder: Notification.Builder + private lateinit var helper: NotificationTestHelper + + @Before + fun setUp() { + allowTestableLooperAsMainThread() + helper = NotificationTestHelper(context, mDependency, TestableLooper.get(this)) + notificationBuilder = Notification.Builder(context, CHANNEL_ID) + notificationBuilder + .setSmallIcon(R.drawable.ic_corp_icon) + .setContentTitle(CONTENT_TITLE) + .setContentText(CONTENT_TEXT) + } + + @Test + @EnableFlags(AsyncHybridViewInflation.FLAG_NAME) + fun bindGroupConversationSingleLineView() { + // GIVEN a row with a group conversation notification + val user = + Person.Builder() + // .setIcon(Icon.createWithResource(mContext, + // R.drawable.ic_account_circle)) + .setName(USER_NAME) + .build() + val style = + Notification.MessagingStyle(user) + .addMessage(MESSAGE_TEXT, System.currentTimeMillis(), user) + .addMessage( + "How about lunch?", + System.currentTimeMillis(), + Person.Builder().setName("user2").build() + ) + .setGroupConversation(true) + notificationBuilder.setStyle(style).setShortcutId(SHORTCUT_ID) + val notification = notificationBuilder.build() + val row = helper.createRow(notification) + + val viewHolder = + inflateSingleLineViewHolder( + isConversation = true, + reinflateFlags = FLAG_CONTENT_VIEW_SINGLE_LINE, + entry = row.entry, + context = context, + logger = mock() + ) + as HybridConversationNotificationView + val viewModel = + SingleLineViewInflater.inflateSingleLineViewModel( + notification = notification, + messagingStyle = style, + builder = notificationBuilder, + systemUiContext = context, + ) + // WHEN: binds the viewHolder + SingleLineConversationViewBinder.bind( + viewModel, + viewHolder, + ) + + // THEN: the single-line conversation view should be bind with view model's corresponding + // fields + assertEquals(viewModel.titleText, viewHolder.titleView.text) + assertEquals(viewModel.contentText, viewHolder.textView.text) + assertEquals( + viewModel.conversationData?.conversationSenderName, + viewHolder.conversationSenderNameView.text + ) + } + + private companion object { + const val CHANNEL_ID = "CHANNEL_ID" + const val CONTENT_TITLE = "CONTENT_TITLE" + const val CONTENT_TEXT = "CONTENT_TEXT" + const val USER_NAME = "USER_NAME" + const val MESSAGE_TEXT = "MESSAGE_TEXT" + const val SHORTCUT_ID = "Shortcut" + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewBinderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewBinderTest.kt new file mode 100644 index 000000000000..f0fc349777b2 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewBinderTest.kt @@ -0,0 +1,91 @@ +/* + * 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 android.app.Notification +import android.platform.test.annotations.EnableFlags +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import androidx.test.filters.SmallTest +import com.android.internal.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.FLAG_CONTENT_VIEW_SINGLE_LINE +import com.android.systemui.statusbar.notification.row.SingleLineViewInflater.inflateSingleLineViewHolder +import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation +import com.android.systemui.statusbar.notification.row.ui.viewbinder.SingleLineViewBinder +import com.android.systemui.util.mockito.mock +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper +class SingleLineViewBinderTest : SysuiTestCase() { + private lateinit var notificationBuilder: Notification.Builder + private lateinit var helper: NotificationTestHelper + + @Before + fun setUp() { + allowTestableLooperAsMainThread() + helper = NotificationTestHelper(mContext, mDependency, TestableLooper.get(this)) + notificationBuilder = Notification.Builder(mContext, CHANNEL_ID) + notificationBuilder + .setSmallIcon(R.drawable.ic_corp_icon) + .setContentTitle(CONTENT_TITLE) + .setContentText(CONTENT_TEXT) + } + + @Test + @EnableFlags(AsyncHybridViewInflation.FLAG_NAME) + fun bindNonConversationSingleLineView() { + // GIVEN: a row with bigText style notification + val style = Notification.BigTextStyle().bigText(CONTENT_TEXT) + notificationBuilder.setStyle(style) + val notification = notificationBuilder.build() + val row: ExpandableNotificationRow = helper.createRow(notification) + + val viewHolder = + inflateSingleLineViewHolder( + isConversation = false, + reinflateFlags = FLAG_CONTENT_VIEW_SINGLE_LINE, + entry = row.entry, + context = context, + logger = mock() + ) + val viewModel = + SingleLineViewInflater.inflateSingleLineViewModel( + notification = notification, + messagingStyle = null, + builder = notificationBuilder, + systemUiContext = context, + ) + + // WHEN: binds the viewHolder + SingleLineViewBinder.bind(viewModel, viewHolder) + + // THEN: the single-line view should be bind with viewModel's title and content text + Assert.assertEquals(viewModel.titleText, viewHolder?.titleView?.text) + Assert.assertEquals(viewModel.contentText, viewHolder?.textView?.text) + } + + private companion object { + const val CHANNEL_ID = "CHANNEL_ID" + const val CONTENT_TITLE = "A Cool New Feature" + const val CONTENT_TEXT = "Checkout out new feature!" + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflaterTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflaterTest.kt new file mode 100644 index 000000000000..b67153a842ac --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflaterTest.kt @@ -0,0 +1,463 @@ +/* + * 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 android.app.Notification +import android.app.Person +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.PorterDuff +import android.graphics.PorterDuffXfermode +import android.graphics.drawable.Drawable +import android.graphics.drawable.Icon +import android.platform.test.annotations.EnableFlags +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import androidx.core.graphics.drawable.toBitmap +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.res.R +import com.android.systemui.statusbar.notification.row.shared.AsyncHybridViewInflation +import com.android.systemui.statusbar.notification.row.ui.viewmodel.ConversationAvatar +import com.android.systemui.statusbar.notification.row.ui.viewmodel.FacePile +import com.android.systemui.statusbar.notification.row.ui.viewmodel.SingleIcon +import com.android.systemui.statusbar.notification.row.ui.viewmodel.SingleLineViewModel +import kotlin.test.assertEquals +import kotlin.test.assertIs +import kotlin.test.assertIsNot +import kotlin.test.assertNull +import kotlin.test.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper +@EnableFlags(AsyncHybridViewInflation.FLAG_NAME) +class SingleLineViewInflaterTest : SysuiTestCase() { + private lateinit var helper: NotificationTestHelper + // Non-group MessagingStyles only have firstSender + private lateinit var firstSender: Person + private lateinit var lastSender: Person + private lateinit var firstSenderIcon: Icon + private lateinit var lastSenderIcon: Icon + private var firstSenderIconDrawable: Drawable? = null + private var lastSenderIconDrawable: Drawable? = null + private val currentUser: Person? = null + + private companion object { + const val FIRST_SENDER_NAME = "First Sender" + const val LAST_SENDER_NAME = "Second Sender" + const val LAST_MESSAGE = "How about lunch?" + + const val CONVERSATION_TITLE = "The Sender Family" + const val CONTENT_TITLE = "A Cool Group" + const val CONTENT_TEXT = "This is an amazing group chat" + + const val SHORTCUT_ID = "Shortcut" + } + + @Before + fun setUp() { + helper = NotificationTestHelper(mContext, mDependency, TestableLooper.get(this)) + firstSenderIcon = Icon.createWithBitmap(getBitmap(context, R.drawable.ic_person)) + firstSenderIconDrawable = firstSenderIcon.loadDrawable(context) + lastSenderIcon = + Icon.createWithBitmap( + getBitmap(context, com.android.internal.R.drawable.ic_account_circle) + ) + lastSenderIconDrawable = lastSenderIcon.loadDrawable(context) + firstSender = Person.Builder().setName(FIRST_SENDER_NAME).setIcon(firstSenderIcon).build() + lastSender = Person.Builder().setName(LAST_SENDER_NAME).setIcon(lastSenderIcon).build() + } + + @Test + fun createViewModelForNonConversationSingleLineView() { + // Given: a non-conversation notification + val notificationType = NonMessaging() + val notification = getNotification(NonMessaging()) + + // When: inflate the SingleLineViewModel + val singleLineViewModel = notification.makeSingleLineViewModel(notificationType) + + // Then: the inflated SingleLineViewModel should be as expected + // conversationData: null, because it's not a conversation notification + assertEquals(SingleLineViewModel(CONTENT_TITLE, CONTENT_TEXT, null), singleLineViewModel) + } + + @Test + fun createViewModelForNonGroupConversationNotification() { + // Given: a non-group conversation notification + val notificationType = OneToOneConversation() + val notification = getNotification(notificationType) + + // When: inflate the SingleLineViewModel + val singleLineViewModel = notification.makeSingleLineViewModel(notificationType) + + // Then: the inflated SingleLineViewModel should be as expected + // titleText: Notification.ConversationTitle + // contentText: the last message text + // conversationSenderName: null, because it's not a group conversation + // conversationData.avatar: a single icon of the last sender + assertEquals(CONVERSATION_TITLE, singleLineViewModel.titleText) + assertEquals(LAST_MESSAGE, singleLineViewModel.contentText) + assertNull( + singleLineViewModel.conversationData?.conversationSenderName, + "Sender name should be null for one-on-one conversation" + ) + assertTrue { + singleLineViewModel.conversationData + ?.avatar + ?.equalsTo(SingleIcon(firstSenderIcon.loadDrawable(context))) == true + } + } + + @Test + fun createViewModelForNonGroupLegacyMessagingStyleNotification() { + // Given: a non-group legacy messaging style notification + val notificationType = LegacyMessaging() + val notification = getNotification(notificationType) + + // When: inflate the SingleLineViewModel + val singleLineViewModel = notification.makeSingleLineViewModel(notificationType) + + // Then: the inflated SingleLineViewModel should be as expected + // titleText: CONVERSATION_TITLE: SENDER_NAME + // contentText: the last message text + // conversationData: null, because it's not a conversation notification + assertEquals("$CONVERSATION_TITLE: $FIRST_SENDER_NAME", singleLineViewModel.titleText) + assertEquals(LAST_MESSAGE, singleLineViewModel.contentText) + assertNull( + singleLineViewModel.conversationData, + "conversationData should be null for legacy messaging conversation" + ) + } + + @Test + fun createViewModelForGroupLegacyMessagingStyleNotification() { + // Given: a non-group legacy messaging style notification + val notificationType = LegacyMessagingGroup() + val notification = getNotification(notificationType) + + // When: inflate the SingleLineViewModel + val singleLineViewModel = notification.makeSingleLineViewModel(notificationType) + + // Then: the inflated SingleLineViewModel should be as expected + // titleText: CONVERSATION_TITLE: LAST_SENDER_NAME + // contentText: the last message text + // conversationData: null, because it's not a conversation notification + assertEquals("$CONVERSATION_TITLE: $LAST_SENDER_NAME", singleLineViewModel.titleText) + assertEquals(LAST_MESSAGE, singleLineViewModel.contentText) + assertNull( + singleLineViewModel.conversationData, + "conversationData should be null for legacy messaging conversation" + ) + } + + @Test + fun createViewModelForNonGroupConversationNotificationWithShortcutIcon() { + // Given: a non-group conversation notification with a shortcut icon + val shortcutIcon = + Icon.createWithResource(context, com.android.internal.R.drawable.ic_account_circle) + val notificationType = OneToOneConversation(shortcutIcon = shortcutIcon) + val notification = getNotification(notificationType) + + // When: inflate the SingleLineViewModel + val singleLineViewModel = notification.makeSingleLineViewModel(notificationType) + + // Then: the inflated SingleLineViewModel should be expected + // titleText: Notification.ConversationTitle + // contentText: the last message text + // conversationSenderName: null, because it's not a group conversation + // conversationData.avatar: a single icon of the shortcut icon + assertEquals(CONVERSATION_TITLE, singleLineViewModel.titleText) + assertEquals(LAST_MESSAGE, singleLineViewModel.contentText) + assertNull( + singleLineViewModel.conversationData?.conversationSenderName, + "Sender name should be null for one-on-one conversation" + ) + assertTrue { + singleLineViewModel.conversationData + ?.avatar + ?.equalsTo(SingleIcon(shortcutIcon.loadDrawable(context))) == true + } + } + + @Test + fun createViewModelForGroupConversationNotificationWithLargeIcon() { + // Given: a group conversation notification with a large icon + val largeIcon = + Icon.createWithResource(context, com.android.internal.R.drawable.ic_account_circle) + val notificationType = GroupConversation(largeIcon = largeIcon) + val notification = getNotification(notificationType) + + // When: inflate the SingleLineViewModel + val singleLineViewModel = notification.makeSingleLineViewModel(notificationType) + + // Then: the inflated SingleLineViewModel should be expected + // titleText: Notification.ConversationTitle + // contentText: the last message text + // conversationSenderName: the last non-user sender's name + // conversationData.avatar: a single icon + assertEquals(CONVERSATION_TITLE, singleLineViewModel.titleText) + assertEquals(LAST_MESSAGE, singleLineViewModel.contentText) + assertEquals( + context.resources.getString( + com.android.internal.R.string.conversation_single_line_name_display, + LAST_SENDER_NAME + ), + singleLineViewModel.conversationData?.conversationSenderName + ) + assertTrue { + singleLineViewModel.conversationData + ?.avatar + ?.equalsTo(SingleIcon(largeIcon.loadDrawable(context))) == true + } + } + + @Test + fun createViewModelForGroupConversationWithNoIcon() { + // Given: a group conversation notification + val notificationType = GroupConversation() + val notification = getNotification(notificationType) + + // When: inflate the SingleLineViewModel + val singleLineViewModel = notification.makeSingleLineViewModel(notificationType) + + // Then: the inflated SingleLineViewModel should be expected + // titleText: Notification.ConversationTitle + // contentText: the last message text + // conversationSenderName: the last non-user sender's name + // conversationData.avatar: a face-pile consists the last sender's icon + assertEquals(CONVERSATION_TITLE, singleLineViewModel.titleText) + assertEquals(LAST_MESSAGE, singleLineViewModel.contentText) + assertEquals( + context.resources.getString( + com.android.internal.R.string.conversation_single_line_name_display, + LAST_SENDER_NAME + ), + singleLineViewModel.conversationData?.conversationSenderName + ) + + val backgroundColor = + Notification.Builder.recoverBuilder(context, notification) + .getBackgroundColor(/* isHeader = */ false) + assertTrue { + singleLineViewModel.conversationData + ?.avatar + ?.equalsTo( + FacePile( + firstSenderIconDrawable, + lastSenderIconDrawable, + backgroundColor, + ) + ) == true + } + } + + sealed class NotificationType(val largeIcon: Icon? = null) + + class NonMessaging(largeIcon: Icon? = null) : NotificationType(largeIcon) + + class LegacyMessaging(largeIcon: Icon? = null) : NotificationType(largeIcon) + + class LegacyMessagingGroup(largeIcon: Icon? = null) : NotificationType(largeIcon) + + class OneToOneConversation(largeIcon: Icon? = null, val shortcutIcon: Icon? = null) : + NotificationType(largeIcon) + + class GroupConversation(largeIcon: Icon? = null) : NotificationType(largeIcon) + + private fun getNotification(type: NotificationType): Notification { + val notificationBuilder: Notification.Builder = + Notification.Builder(mContext, "channelId") + .setSmallIcon(R.drawable.ic_person) + .setContentTitle(CONTENT_TITLE) + .setContentText(CONTENT_TEXT) + .setLargeIcon(type.largeIcon) + + val user = Person.Builder().setName("User").build() + + val buildMessagingStyle = + Notification.MessagingStyle(user) + .setConversationTitle(CONVERSATION_TITLE) + .addMessage("Hi", 0, currentUser) + + return when (type) { + is NonMessaging -> + notificationBuilder + .setStyle(Notification.BigTextStyle().bigText("Big Text")) + .build() + is LegacyMessaging -> { + buildMessagingStyle + .addMessage("What's up?", 0, firstSender) + .addMessage("Not much", 0, currentUser) + .addMessage(LAST_MESSAGE, 0, firstSender) + + val notification = notificationBuilder.setStyle(buildMessagingStyle).build() + + assertNull(notification.shortcutId) + notification + } + is LegacyMessagingGroup -> { + buildMessagingStyle + .addMessage("What's up?", 0, firstSender) + .addMessage("Check out my new hover board!", 0, lastSender) + .setGroupConversation(true) + .addMessage(LAST_MESSAGE, 0, lastSender) + + val notification = notificationBuilder.setStyle(buildMessagingStyle).build() + + assertNull(notification.shortcutId) + notification + } + is OneToOneConversation -> { + buildMessagingStyle + .addMessage("What's up?", 0, firstSender) + .addMessage("Not much", 0, currentUser) + .addMessage(LAST_MESSAGE, 0, firstSender) + .setShortcutIcon(type.shortcutIcon) + notificationBuilder.setShortcutId(SHORTCUT_ID).setStyle(buildMessagingStyle).build() + } + is GroupConversation -> { + buildMessagingStyle + .addMessage("What's up?", 0, firstSender) + .addMessage("Check out my new hover board!", 0, lastSender) + .setGroupConversation(true) + .addMessage(LAST_MESSAGE, 0, lastSender) + notificationBuilder.setShortcutId(SHORTCUT_ID).setStyle(buildMessagingStyle).build() + } + } + } + + private fun Notification.makeSingleLineViewModel(type: NotificationType): SingleLineViewModel { + val builder = Notification.Builder.recoverBuilder(context, this) + + // Validate the recovered builder has the right type of style + val expectMessagingStyle = + when (type) { + is LegacyMessaging, + is LegacyMessagingGroup, + is OneToOneConversation, + is GroupConversation -> true + else -> false + } + if (expectMessagingStyle) { + assertIs<Notification.MessagingStyle>( + builder.style, + "Notification style should be MessagingStyle" + ) + } else { + assertIsNot<Notification.MessagingStyle>( + builder.style, + message = "Notification style should not be MessagingStyle" + ) + } + + // Inflate the SingleLineViewModel + // Mock the behavior of NotificationContentInflater.doInBackground + val messagingStyle = builder.getMessagingStyle() + val isConversation = type is OneToOneConversation || type is GroupConversation + return SingleLineViewInflater.inflateSingleLineViewModel( + this, + if (isConversation) messagingStyle else null, + builder, + context + ) + } + + private fun Notification.Builder.getMessagingStyle(): Notification.MessagingStyle? { + return style as? Notification.MessagingStyle + } + + private fun getBitmap(context: Context, resId: Int): Bitmap { + val largeIconDimension = + context.resources.getDimension(R.dimen.conversation_single_line_avatar_size) + val d = context.resources.getDrawable(resId) + val b = + Bitmap.createBitmap( + largeIconDimension.toInt(), + largeIconDimension.toInt(), + Bitmap.Config.ARGB_8888 + ) + val c = Canvas(b) + val paint = Paint() + c.drawCircle( + largeIconDimension / 2, + largeIconDimension / 2, + largeIconDimension.coerceAtMost(largeIconDimension) / 2, + paint + ) + d.setBounds(0, 0, largeIconDimension.toInt(), largeIconDimension.toInt()) + paint.setXfermode(PorterDuffXfermode(PorterDuff.Mode.SRC_IN)) + c.saveLayer(0F, 0F, largeIconDimension, largeIconDimension, paint, Canvas.ALL_SAVE_FLAG) + d.draw(c) + c.restore() + return b + } + + fun ConversationAvatar.equalsTo(other: ConversationAvatar?): Boolean = + when { + this === other -> true + this is SingleIcon && other is SingleIcon -> equalsTo(other) + this is FacePile && other is FacePile -> equalsTo(other) + else -> false + } + + private fun SingleIcon.equalsTo(other: SingleIcon): Boolean = + iconDrawable?.equalsTo(other.iconDrawable) == true + + private fun FacePile.equalsTo(other: FacePile): Boolean = + when { + bottomBackgroundColor != other.bottomBackgroundColor -> false + topIconDrawable?.equalsTo(other.topIconDrawable) != true -> false + bottomIconDrawable?.equalsTo(other.bottomIconDrawable) != true -> false + else -> true + } + + fun Drawable.equalsTo(other: Drawable?): Boolean = + when { + this === other -> true + this.pixelsEqualTo(other) -> true + else -> false + } + + private fun <T : Drawable> T.pixelsEqualTo(t: T?) = + toBitmap().pixelsEqualTo(t?.toBitmap(), false) + + private fun Bitmap.pixelsEqualTo(otherBitmap: Bitmap?, shouldRecycle: Boolean = false) = + otherBitmap?.let { other -> + if (width == other.width && height == other.height) { + val res = toPixels().contentEquals(other.toPixels()) + if (shouldRecycle) { + doRecycle().also { otherBitmap.doRecycle() } + } + res + } else false + } + ?: kotlin.run { false } + + private fun Bitmap.toPixels() = + IntArray(width * height).apply { getPixels(this, 0, width, 0, 0, width, height) } + + fun Bitmap.doRecycle() { + if (!isRecycled) recycle() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java index 1ab4c32c7d08..dbe63f290407 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java @@ -860,9 +860,6 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { when(mNotificationStackScrollLayout.onKeyguard()).thenReturn(true); mController.getNotifStackController().setNotifStats(NotifStats.getEmpty()); - // WHEN: call updateImportantForAccessibility - mController.updateImportantForAccessibility(); - // THEN: mNotificationStackScrollLayout should not be important for A11y verify(mNotificationStackScrollLayout) .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_NO); @@ -884,9 +881,6 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { /* hasClearableSilentNotifs = */ false) ); - // WHEN: call updateImportantForAccessibility - mController.updateImportantForAccessibility(); - // THEN: mNotificationStackScrollLayout should be important for A11y verify(mNotificationStackScrollLayout) .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); @@ -908,9 +902,6 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { /* hasClearableSilentNotifs = */ false) ); - // WHEN: call updateImportantForAccessibility - mController.updateImportantForAccessibility(); - // THEN: mNotificationStackScrollLayout should be important for A11y verify(mNotificationStackScrollLayout) .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); @@ -925,9 +916,6 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { when(mNotificationStackScrollLayout.onKeyguard()).thenReturn(false); mController.getNotifStackController().setNotifStats(NotifStats.getEmpty()); - // WHEN: call updateImportantForAccessibility - mController.updateImportantForAccessibility(); - // THEN: mNotificationStackScrollLayout should be important for A11y verify(mNotificationStackScrollLayout) .setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES); 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 06298b78ae57..32c727c70172 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 @@ -38,6 +38,9 @@ import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.StatusBarState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel +import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters +import com.android.systemui.keyguard.ui.viewmodel.aodBurnInViewModel import com.android.systemui.keyguard.ui.viewmodel.keyguardRootViewModel import com.android.systemui.kosmos.testScope import com.android.systemui.res.R @@ -45,6 +48,7 @@ import com.android.systemui.shade.data.repository.shadeRepository 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.any import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -55,15 +59,22 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.Mockito.mock @SmallTest @RunWith(AndroidJUnit4::class) class SharedNotificationContainerViewModelTest : SysuiTestCase() { + val aodBurnInViewModel = mock(AodBurnInViewModel::class.java) + lateinit var translationYFlow: MutableStateFlow<Float> val kosmos = testKosmos().apply { fakeFeatureFlagsClassic.apply { set(Flags.FULL_SCREEN_USER_SWITCHER, false) } } + + init { + kosmos.aodBurnInViewModel = aodBurnInViewModel + } val testScope = kosmos.testScope val configurationRepository = kosmos.fakeConfigurationRepository val keyguardRepository = kosmos.fakeKeyguardRepository @@ -75,11 +86,14 @@ class SharedNotificationContainerViewModelTest : SysuiTestCase() { val sharedNotificationContainerInteractor = kosmos.sharedNotificationContainerInteractor val largeScreenHeaderHelper = kosmos.mockLargeScreenHeaderHelper - val underTest = kosmos.sharedNotificationContainerViewModel + lateinit var underTest: SharedNotificationContainerViewModel @Before fun setUp() { overrideResource(R.bool.config_use_split_notification_shade, false) + translationYFlow = MutableStateFlow(0f) + whenever(aodBurnInViewModel.translationY(any())).thenReturn(translationYFlow) + underTest = kosmos.sharedNotificationContainerViewModel } @Test @@ -579,9 +593,21 @@ class SharedNotificationContainerViewModelTest : SysuiTestCase() { } @Test + fun translationYUpdatesOnKeyguardForBurnIn() = + testScope.runTest { + val translationY by collectLastValue(underTest.translationY(BurnInParameters())) + + showLockscreen() + assertThat(translationY).isEqualTo(0) + + translationYFlow.value = 150f + assertThat(translationY).isEqualTo(150f) + } + + @Test fun translationYUpdatesOnKeyguard() = testScope.runTest { - val translationY by collectLastValue(underTest.translationY) + val translationY by collectLastValue(underTest.translationY(BurnInParameters())) configurationRepository.setDimensionPixelSize( R.dimen.keyguard_translate_distance_on_swipe_up, @@ -601,7 +627,7 @@ class SharedNotificationContainerViewModelTest : SysuiTestCase() { @Test fun translationYDoesNotUpdateWhenShadeIsExpanded() = testScope.runTest { - val translationY by collectLastValue(underTest.translationY) + val translationY by collectLastValue(underTest.translationY(BurnInParameters())) configurationRepository.setDimensionPixelSize( R.dimen.keyguard_translate_distance_on_swipe_up, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java index 8dde9359bdfc..cb4531567e86 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java @@ -182,8 +182,10 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { when(mBouncerViewDelegate.getBackCallback()).thenReturn(mBouncerViewDelegateBackCallback); mFeatureFlags = new FakeFeatureFlags(); mFeatureFlags.set(Flags.REFACTOR_KEYGUARD_DISMISS_INTENT, false); - mFeatureFlags.set(Flags.KEYGUARD_WM_STATE_REFACTOR, false); - mSetFlagsRule.disableFlags(com.android.systemui.Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR); + mSetFlagsRule.disableFlags( + com.android.systemui.Flags.FLAG_DEVICE_ENTRY_UDFPS_REFACTOR, + com.android.systemui.Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR + ); when(mNotificationShadeWindowController.getWindowRootView()) .thenReturn(mNotificationShadeWindowView); 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/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/communal/data/repository/FakeCommunalWidgetRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt index e25e8c099c21..bc7e7af245a6 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt @@ -39,13 +39,4 @@ class FakeCommunalWidgetRepository(private val coroutineScope: CoroutineScope) : private fun onConfigured(id: Int, providerInfo: AppWidgetProviderInfo, priority: Int) { _communalWidgets.value += listOf(CommunalWidgetContentModel(id, providerInfo, priority)) } - - private var isHostActive = false - override fun updateAppWidgetHostActive(active: Boolean) { - isHostActive = active - } - - fun isHostActive(): Boolean { - return isHostActive - } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FakeDeviceEntryRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FakeDeviceEntryRepository.kt index 6436a382eb7f..77caeaa6da4d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FakeDeviceEntryRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/data/repository/FakeDeviceEntryRepository.kt @@ -16,25 +16,16 @@ package com.android.systemui.deviceentry.data.repository import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.keyguard.shared.model.BiometricUnlockSource import dagger.Binds import dagger.Module import javax.inject.Inject -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow /** Fake implementation of [DeviceEntryRepository] */ @SysUISingleton class FakeDeviceEntryRepository @Inject constructor() : DeviceEntryRepository { - private val _enteringDeviceFromBiometricUnlock: MutableSharedFlow<BiometricUnlockSource> = - MutableSharedFlow() - override val enteringDeviceFromBiometricUnlock: Flow<BiometricUnlockSource> = - _enteringDeviceFromBiometricUnlock.asSharedFlow() - private var isLockscreenEnabled = true private val _isBypassEnabled = MutableStateFlow(false) @@ -62,10 +53,6 @@ class FakeDeviceEntryRepository @Inject constructor() : DeviceEntryRepository { fun setBypassEnabled(isBypassEnabled: Boolean) { _isBypassEnabled.value = isBypassEnabled } - - suspend fun enteringDeviceFromBiometricUnlock(sourceType: BiometricUnlockSource) { - _enteringDeviceFromBiometricUnlock.emit(sourceType) - } } @Module diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/AuthRippleInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/AuthRippleInteractorKosmos.kt new file mode 100644 index 000000000000..3070cf4c06ad --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/AuthRippleInteractorKosmos.kt @@ -0,0 +1,29 @@ +/* + * 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.deviceentry.domain.interactor + +import com.android.systemui.kosmos.Kosmos +import kotlinx.coroutines.ExperimentalCoroutinesApi + +@OptIn(ExperimentalCoroutinesApi::class) +val Kosmos.authRippleInteractor by + Kosmos.Fixture { + AuthRippleInteractor( + deviceEntrySourceInteractor = deviceEntrySourceInteractor, + deviceEntryUdfpsInteractor = deviceEntryUdfpsInteractor, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt index de58ae5e9452..878e38594fe1 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt @@ -30,7 +30,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi val Kosmos.deviceEntryHapticsInteractor by Kosmos.Fixture { DeviceEntryHapticsInteractor( - deviceEntryInteractor = deviceEntryInteractor, + deviceEntrySourceInteractor = deviceEntrySourceInteractor, deviceEntryFingerprintAuthInteractor = deviceEntryFingerprintAuthInteractor, deviceEntryBiometricAuthInteractor = deviceEntryBiometricAuthInteractor, fingerprintPropertyRepository = fingerprintPropertyRepository, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorKosmos.kt index 8dcdd3a9425c..0d1a31f9605e 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryInteractorKosmos.kt @@ -14,8 +14,6 @@ * limitations under the License. */ -@file:OptIn(ExperimentalCoroutinesApi::class) - package com.android.systemui.deviceentry.domain.interactor import com.android.systemui.authentication.domain.interactor.authenticationInteractor @@ -28,6 +26,7 @@ import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.flag.sceneContainerFlags import kotlinx.coroutines.ExperimentalCoroutinesApi +@ExperimentalCoroutinesApi val Kosmos.deviceEntryInteractor by Kosmos.Fixture { DeviceEntryInteractor( diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntrySourceInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntrySourceInteractorKosmos.kt new file mode 100644 index 000000000000..0b9ec92af2b5 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntrySourceInteractorKosmos.kt @@ -0,0 +1,29 @@ +/* + * 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.deviceentry.domain.interactor + +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor +import com.android.systemui.kosmos.Kosmos +import kotlinx.coroutines.ExperimentalCoroutinesApi + +@ExperimentalCoroutinesApi +val Kosmos.deviceEntrySourceInteractor by + Kosmos.Fixture { + DeviceEntrySourceInteractor( + keyguardInteractor = keyguardInteractor, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt index 5766f7a9028c..793e2d7efcda 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt @@ -65,7 +65,7 @@ class FakeKeyguardRepository @Inject constructor() : KeyguardRepository { override val isKeyguardShowing: Flow<Boolean> = _isKeyguardShowing private val _isKeyguardUnlocked = MutableStateFlow(false) - override val isKeyguardUnlocked: StateFlow<Boolean> = _isKeyguardUnlocked.asStateFlow() + override val isKeyguardDismissible: StateFlow<Boolean> = _isKeyguardUnlocked.asStateFlow() private val _isKeyguardOccluded = MutableStateFlow(false) override val isKeyguardOccluded: Flow<Boolean> = _isKeyguardOccluded @@ -165,7 +165,7 @@ class FakeKeyguardRepository @Inject constructor() : KeyguardRepository { _isKeyguardOccluded.value = isOccluded } - fun setKeyguardUnlocked(isUnlocked: Boolean) { + fun setKeyguardDismissible(isUnlocked: Boolean) { _isKeyguardUnlocked.value = isUnlocked } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelKosmos.kt index 35cfa89e56ed..a8f45b0974c4 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModelKosmos.kt @@ -26,7 +26,7 @@ import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import kotlinx.coroutines.ExperimentalCoroutinesApi -val Kosmos.aodBurnInViewModel by Fixture { +var Kosmos.aodBurnInViewModel by Fixture { AodBurnInViewModel( burnInInteractor = burnInInteractor, configurationInteractor = configurationInteractor, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelKosmos.kt index 5ceefde32d2a..73fd9991945c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryIconViewModelKosmos.kt @@ -17,6 +17,7 @@ package com.android.systemui.keyguard.ui.viewmodel import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor +import com.android.systemui.deviceentry.domain.interactor.deviceEntrySourceInteractor import com.android.systemui.deviceentry.domain.interactor.deviceEntryUdfpsInteractor import com.android.systemui.keyguard.domain.interactor.burnInInteractor import com.android.systemui.keyguard.domain.interactor.keyguardInteractor @@ -27,6 +28,7 @@ import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.scene.shared.flag.sceneContainerFlags import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.statusbar.phone.statusBarKeyguardViewManager +import kotlinx.coroutines.ExperimentalCoroutinesApi val Kosmos.fakeDeviceEntryIconViewModelTransition by Fixture { FakeDeviceEntryIconTransition() } @@ -34,6 +36,7 @@ val Kosmos.deviceEntryIconViewModelTransitionsMock by Fixture { setOf<DeviceEntryIconTransition>(fakeDeviceEntryIconViewModelTransition) } +@ExperimentalCoroutinesApi val Kosmos.deviceEntryIconViewModel by Fixture { DeviceEntryIconViewModel( transitions = deviceEntryIconViewModelTransitionsMock, @@ -46,5 +49,6 @@ val Kosmos.deviceEntryIconViewModel by Fixture { sceneContainerFlags = sceneContainerFlags, keyguardViewController = { statusBarKeyguardViewManager }, deviceEntryInteractor = deviceEntryInteractor, + deviceEntrySourceInteractor = deviceEntrySourceInteractor, ) } 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 db4050905200..7c398cd45f90 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 @@ -19,6 +19,7 @@ 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.aodBurnInViewModel import com.android.systemui.keyguard.ui.viewmodel.glanceableHubToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.lockscreenToGlanceableHubTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.lockscreenToOccludedTransitionViewModel @@ -40,6 +41,7 @@ val Kosmos.sharedNotificationContainerViewModel by Fixture { occludedToLockscreenTransitionViewModel = occludedToLockscreenTransitionViewModel, lockscreenToOccludedTransitionViewModel = lockscreenToOccludedTransitionViewModel, glanceableHubToLockscreenTransitionViewModel = glanceableHubToLockscreenTransitionViewModel, - lockscreenToGlanceableHubTransitionViewModel = lockscreenToGlanceableHubTransitionViewModel + lockscreenToGlanceableHubTransitionViewModel = lockscreenToGlanceableHubTransitionViewModel, + aodBurnInViewModel = aodBurnInViewModel, ) } diff --git a/ravenwood/framework-minus-apex-ravenwood-policies.txt b/ravenwood/framework-minus-apex-ravenwood-policies.txt index f5e4af50fc29..16f99e9289db 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 @@ -129,6 +138,9 @@ class com.android.modules.utils.TypedXmlSerializer stubclass class android.net.Uri stubclass class android.net.UriCodec stubclass +# Telephony +class android.telephony.PinResult stubclass + # Just enough to support mocking, no further functionality class android.content.Context stub method <init> ()V stub 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..eaf01a32592e 100644 --- a/ravenwood/ravenwood-annotation-allowed-classes.txt +++ b/ravenwood/ravenwood-annotation-allowed-classes.txt @@ -1,6 +1,10 @@ # Only classes listed here can use the Ravenwood annotations. +com.android.internal.display.BrightnessSynchronizer 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 @@ -28,6 +32,7 @@ android.util.LruCache android.util.MonthDisplayHelper android.util.RecurrenceRule android.util.RotationUtils +android.util.Singleton android.util.Slog android.util.SparseDoubleArray android.util.SparseSetArray @@ -47,6 +52,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,11 +71,14 @@ 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 +android.os.UserManager android.os.WorkSource android.content.ClipData @@ -83,16 +92,22 @@ android.content.Intent android.content.IntentFilter android.content.UriMatcher -android.content.pm.PackageInfo +android.content.pm.ActivityInfo android.content.pm.ApplicationInfo -android.content.pm.PackageItemInfo android.content.pm.ComponentInfo -android.content.pm.ActivityInfo -android.content.pm.ServiceInfo +android.content.pm.PackageInfo +android.content.pm.PackageItemInfo +android.content.pm.PackageManager$Flags +android.content.pm.PackageManager$PackageInfoFlags +android.content.pm.PackageManager$ApplicationInfoFlags +android.content.pm.PackageManager$ComponentInfoFlags +android.content.pm.PackageManager$ResolveInfoFlags android.content.pm.PathPermission android.content.pm.ProviderInfo android.content.pm.ResolveInfo +android.content.pm.ServiceInfo android.content.pm.Signature +android.content.pm.UserInfo android.database.AbstractCursor android.database.CharArrayBuffer @@ -126,6 +141,12 @@ android.graphics.RectF android.content.ContentProvider +android.metrics.LogMaker + +android.view.Display$HdrCapabilities +android.view.Display$Mode +android.view.DisplayInfo + com.android.server.LocalServices com.android.server.power.stats.BatteryStatsImpl diff --git a/services/backup/flags.aconfig b/services/backup/flags.aconfig index 4022e3378954..1416c888f790 100644 --- a/services/backup/flags.aconfig +++ b/services/backup/flags.aconfig @@ -11,7 +11,7 @@ flag { flag { name: "enable_metrics_system_backup_agents" - namespace: "backup" + namespace: "onboarding" description: "Enable SystemBackupAgent to collect B&R agent metrics by passing an instance of " "the logger to each BackupHelper." bug: "296844513" diff --git a/services/companion/TEST_MAPPING b/services/companion/TEST_MAPPING index 37c47baa813b..ae6d59129adb 100644 --- a/services/companion/TEST_MAPPING +++ b/services/companion/TEST_MAPPING @@ -9,5 +9,10 @@ { "name": "CtsCompanionDeviceManagerNoCompanionServicesTestCases" } + ], + "postsubmit": [ + { + "name": "CtsCompanionDeviceManagerMultiProcessTestCases" + } ] } diff --git a/services/companion/java/com/android/server/companion/virtual/InputController.java b/services/companion/java/com/android/server/companion/virtual/InputController.java index 3b9d92dc3d02..8962bf02ff2e 100644 --- a/services/companion/java/com/android/server/companion/virtual/InputController.java +++ b/services/companion/java/com/android/server/companion/virtual/InputController.java @@ -163,7 +163,7 @@ class InputController { createDeviceInternal(InputDeviceDescriptor.TYPE_MOUSE, deviceName, vendorId, productId, deviceToken, displayId, phys, () -> mNativeWrapper.openUinputMouse(deviceName, vendorId, productId, phys)); - mInputManagerInternal.setVirtualMousePointerDisplayId(displayId); + setVirtualMousePointerDisplayId(displayId); } void createTouchscreen(@NonNull String deviceName, int vendorId, int productId, @@ -235,8 +235,7 @@ class InputController { // id if there's another mouse (choose the most recent). The inputDeviceDescriptor must be // removed from the mInputDeviceDescriptors instance variable prior to this point. if (inputDeviceDescriptor.isMouse()) { - if (mInputManagerInternal.getVirtualMousePointerDisplayId() - == inputDeviceDescriptor.getDisplayId()) { + if (getVirtualMousePointerDisplayId() == inputDeviceDescriptor.getDisplayId()) { updateActivePointerDisplayIdLocked(); } } @@ -271,6 +270,7 @@ class InputController { mWindowManager.setDisplayImePolicy(displayId, policy); } + // TODO(b/293587049): Remove after pointer icon refactor is complete. @GuardedBy("mLock") private void updateActivePointerDisplayIdLocked() { InputDeviceDescriptor mostRecentlyCreatedMouse = null; @@ -285,11 +285,11 @@ class InputController { } } if (mostRecentlyCreatedMouse != null) { - mInputManagerInternal.setVirtualMousePointerDisplayId( + setVirtualMousePointerDisplayId( mostRecentlyCreatedMouse.getDisplayId()); } else { // All mice have been unregistered - mInputManagerInternal.setVirtualMousePointerDisplayId(Display.INVALID_DISPLAY); + setVirtualMousePointerDisplayId(Display.INVALID_DISPLAY); } } @@ -349,10 +349,8 @@ class InputController { if (inputDeviceDescriptor == null) { return false; } - if (inputDeviceDescriptor.getDisplayId() - != mInputManagerInternal.getVirtualMousePointerDisplayId()) { - mInputManagerInternal.setVirtualMousePointerDisplayId( - inputDeviceDescriptor.getDisplayId()); + if (inputDeviceDescriptor.getDisplayId() != getVirtualMousePointerDisplayId()) { + setVirtualMousePointerDisplayId(inputDeviceDescriptor.getDisplayId()); } return mNativeWrapper.writeButtonEvent(inputDeviceDescriptor.getNativePointer(), event.getButtonCode(), event.getAction(), event.getEventTimeNanos()); @@ -380,10 +378,8 @@ class InputController { if (inputDeviceDescriptor == null) { return false; } - if (inputDeviceDescriptor.getDisplayId() - != mInputManagerInternal.getVirtualMousePointerDisplayId()) { - mInputManagerInternal.setVirtualMousePointerDisplayId( - inputDeviceDescriptor.getDisplayId()); + if (inputDeviceDescriptor.getDisplayId() != getVirtualMousePointerDisplayId()) { + setVirtualMousePointerDisplayId(inputDeviceDescriptor.getDisplayId()); } return mNativeWrapper.writeRelativeEvent(inputDeviceDescriptor.getNativePointer(), event.getRelativeX(), event.getRelativeY(), event.getEventTimeNanos()); @@ -397,10 +393,8 @@ class InputController { if (inputDeviceDescriptor == null) { return false; } - if (inputDeviceDescriptor.getDisplayId() - != mInputManagerInternal.getVirtualMousePointerDisplayId()) { - mInputManagerInternal.setVirtualMousePointerDisplayId( - inputDeviceDescriptor.getDisplayId()); + if (inputDeviceDescriptor.getDisplayId() != getVirtualMousePointerDisplayId()) { + setVirtualMousePointerDisplayId(inputDeviceDescriptor.getDisplayId()); } return mNativeWrapper.writeScrollEvent(inputDeviceDescriptor.getNativePointer(), event.getXAxisMovement(), event.getYAxisMovement(), event.getEventTimeNanos()); @@ -415,12 +409,11 @@ class InputController { throw new IllegalArgumentException( "Could not get cursor position for input device for given token"); } - if (inputDeviceDescriptor.getDisplayId() - != mInputManagerInternal.getVirtualMousePointerDisplayId()) { - mInputManagerInternal.setVirtualMousePointerDisplayId( - inputDeviceDescriptor.getDisplayId()); + if (inputDeviceDescriptor.getDisplayId() != getVirtualMousePointerDisplayId()) { + setVirtualMousePointerDisplayId(inputDeviceDescriptor.getDisplayId()); } - return LocalServices.getService(InputManagerInternal.class).getCursorPosition(); + return LocalServices.getService(InputManagerInternal.class).getCursorPosition( + inputDeviceDescriptor.getDisplayId()); } } @@ -847,4 +840,22 @@ class InputController { /** Returns true if the calling thread is a valid thread for device creation. */ boolean isValidThread(); } + + // TODO(b/293587049): Remove after pointer icon refactor is complete. + private void setVirtualMousePointerDisplayId(int displayId) { + if (com.android.input.flags.Flags.enablePointerChoreographer()) { + // We no longer need to set the pointer display when pointer choreographer is enabled. + return; + } + mInputManagerInternal.setVirtualMousePointerDisplayId(displayId); + } + + // TODO(b/293587049): Remove after pointer icon refactor is complete. + private int getVirtualMousePointerDisplayId() { + if (com.android.input.flags.Flags.enablePointerChoreographer()) { + // We no longer need to get the pointer display when pointer choreographer is enabled. + return Display.INVALID_DISPLAY; + } + return mInputManagerInternal.getVirtualMousePointerDisplayId(); + } } diff --git a/services/core/java/com/android/server/am/AppBatteryExemptionTracker.java b/services/core/java/com/android/server/am/AppBatteryExemptionTracker.java index b07d9a6b258c..9c2e69be7685 100644 --- a/services/core/java/com/android/server/am/AppBatteryExemptionTracker.java +++ b/services/core/java/com/android/server/am/AppBatteryExemptionTracker.java @@ -520,7 +520,7 @@ final class AppBatteryExemptionTracker /** * Default value to {@link #mTrackerEnabled}. */ - static final boolean DEFAULT_BG_BATTERY_EXEMPTION_ENABLED = true; + static final boolean DEFAULT_BG_BATTERY_EXEMPTION_ENABLED = false; AppBatteryExemptionPolicy(@NonNull Injector injector, @NonNull AppBatteryExemptionTracker tracker) { diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java index dc14c7aaa0b9..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", 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/audio/AudioDeviceInventory.java b/services/core/java/com/android/server/audio/AudioDeviceInventory.java index 57b19cda7c12..690c37a9349a 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceInventory.java +++ b/services/core/java/com/android/server/audio/AudioDeviceInventory.java @@ -914,28 +914,27 @@ public class AudioDeviceInventory { di.mDeviceCodecFormat = codec; mConnectedDevices.replace(key, di); codecChange = true; - } - final int res = mAudioSystem.handleDeviceConfigChange( - btInfo.mAudioSystemDevice, address, BtHelper.getName(btDevice), codec); - - if (res != AudioSystem.AUDIO_STATUS_OK) { - AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( - "APM handleDeviceConfigChange failed for A2DP device addr=" - + address + " codec=" - + AudioSystem.audioFormatToString(codec)) - .printLog(TAG)); - - // force A2DP device disconnection in case of error so that AudioService - // state is consistent with audio policy manager state - setBluetoothActiveDevice(new AudioDeviceBroker.BtDeviceInfo(btInfo, - BluetoothProfile.STATE_DISCONNECTED)); - } else { - AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( - "APM handleDeviceConfigChange success for A2DP device addr=" - + address - + " codec=" + AudioSystem.audioFormatToString(codec)) - .printLog(TAG)); - + final int res = mAudioSystem.handleDeviceConfigChange( + btInfo.mAudioSystemDevice, address, + BtHelper.getName(btDevice), codec); + if (res != AudioSystem.AUDIO_STATUS_OK) { + AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( + "APM handleDeviceConfigChange failed for A2DP device addr=" + + address + " codec=" + + AudioSystem.audioFormatToString(codec)) + .printLog(TAG)); + + // force A2DP device disconnection in case of error so that AudioService + // state is consistent with audio policy manager state + setBluetoothActiveDevice(new AudioDeviceBroker.BtDeviceInfo(btInfo, + BluetoothProfile.STATE_DISCONNECTED)); + } else { + AudioService.sDeviceLogger.enqueue(new EventLogger.StringEvent( + "APM handleDeviceConfigChange success for A2DP device addr=" + + address + + " codec=" + AudioSystem.audioFormatToString(codec)) + .printLog(TAG)); + } } } if (!codecChange) { diff --git a/services/core/java/com/android/server/audio/SoundDoseHelper.java b/services/core/java/com/android/server/audio/SoundDoseHelper.java index a30cdc47a461..9610034caf01 100644 --- a/services/core/java/com/android/server/audio/SoundDoseHelper.java +++ b/services/core/java/com/android/server/audio/SoundDoseHelper.java @@ -1203,7 +1203,7 @@ public class SoundDoseHelper { @GuardedBy("mCsdStateLock") private void sanitizeDoseRecords_l() { if (mDoseRecords.size() > MAX_NUMBER_OF_CACHED_RECORDS) { - int nrToRemove = MAX_NUMBER_OF_CACHED_RECORDS - mDoseRecords.size(); + int nrToRemove = mDoseRecords.size() - MAX_NUMBER_OF_CACHED_RECORDS; Log.w(TAG, "Removing " + nrToRemove + " records from the total of " + mDoseRecords.size()); // Remove older elements to fit into persisted settings max length diff --git a/services/core/java/com/android/server/biometrics/AuthService.java b/services/core/java/com/android/server/biometrics/AuthService.java index 8fd2ee2bdc33..3f3540e2868c 100644 --- a/services/core/java/com/android/server/biometrics/AuthService.java +++ b/services/core/java/com/android/server/biometrics/AuthService.java @@ -20,7 +20,7 @@ package com.android.server.biometrics; // TODO(b/141025588): Create separate internal and external permissions for AuthService. // TODO(b/141025588): Get rid of the USE_FINGERPRINT permission. -import static android.Manifest.permission.MANAGE_BIOMETRIC_DIALOG; +import static android.Manifest.permission.SET_BIOMETRIC_DIALOG_LOGO; import static android.Manifest.permission.TEST_BIOMETRIC; import static android.Manifest.permission.USE_BIOMETRIC; import static android.Manifest.permission.USE_BIOMETRIC_INTERNAL; @@ -305,7 +305,7 @@ public class AuthService extends SystemService { if (promptInfo.containsPrivateApiConfigurations()) { checkInternalPermission(); } - if (promptInfo.containsManageBioApiConfigurations()) { + if (promptInfo.containsSetLogoApiConfigurations()) { checkManageBiometricPermission(); } @@ -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 @@ -989,8 +997,8 @@ public class AuthService extends SystemService { } private void checkManageBiometricPermission() { - getContext().enforceCallingOrSelfPermission(MANAGE_BIOMETRIC_DIALOG, - "Must have MANAGE_BIOMETRIC_DIALOG permission"); + getContext().enforceCallingOrSelfPermission(SET_BIOMETRIC_DIALOG_LOGO, + "Must have SET_BIOMETRIC_DIALOG_LOGO permission"); } private void checkPermission() { 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/input/InputManagerInternal.java b/services/core/java/com/android/server/input/InputManagerInternal.java index 380106ba486d..b963a4b614e8 100644 --- a/services/core/java/com/android/server/input/InputManagerInternal.java +++ b/services/core/java/com/android/server/input/InputManagerInternal.java @@ -87,12 +87,16 @@ public abstract class InputManagerInternal { * connected, the caller may be blocked for an arbitrary period of time. * * @return true if the pointer displayId was set successfully, or false if it fails. + * + * @deprecated TODO(b/293587049): Not needed - remove after Pointer Icon Refactor is complete. */ public abstract boolean setVirtualMousePointerDisplayId(int pointerDisplayId); /** * Gets the display id that the MouseCursorController is being forced to target. Returns * {@link android.view.Display#INVALID_DISPLAY} if there is no override + * + * @deprecated TODO(b/293587049): Not needed - remove after Pointer Icon Refactor is complete. */ public abstract int getVirtualMousePointerDisplayId(); @@ -101,7 +105,7 @@ public abstract class InputManagerInternal { * * Returns NaN-s as the coordinates if the cursor is not available. */ - public abstract PointF getCursorPosition(); + public abstract PointF getCursorPosition(int displayId); /** * Enables or disables pointer acceleration for mouse movements. diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index 67c23fc4db12..687def05b1d7 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -1448,6 +1448,10 @@ public class InputManagerService extends IInputManager.Stub } private boolean setVirtualMousePointerDisplayIdBlocking(int overrideDisplayId) { + if (com.android.input.flags.Flags.enablePointerChoreographer()) { + throw new IllegalStateException( + "This must not be used when PointerChoreographer is enabled"); + } final boolean isRemovingOverride = overrideDisplayId == Display.INVALID_DISPLAY; // Take care to not make calls to window manager while holding internal locks. @@ -1486,6 +1490,10 @@ public class InputManagerService extends IInputManager.Stub } private int getVirtualMousePointerDisplayId() { + if (com.android.input.flags.Flags.enablePointerChoreographer()) { + throw new IllegalStateException( + "This must not be used when PointerChoreographer is enabled"); + } synchronized (mAdditionalDisplayInputPropertiesLock) { return mOverriddenPointerDisplayId; } @@ -3332,8 +3340,8 @@ public class InputManagerService extends IInputManager.Stub } @Override - public PointF getCursorPosition() { - final float[] p = mNative.getMouseCursorPosition(); + public PointF getCursorPosition(int displayId) { + final float[] p = mNative.getMouseCursorPosition(displayId); if (p == null || p.length != 2) { throw new IllegalStateException("Failed to get mouse cursor position"); } @@ -3614,6 +3622,13 @@ public class InputManagerService extends IInputManager.Stub } /** + * Sets Accessibility slow keys threshold in milliseconds. + */ + public void setAccessibilitySlowKeysThreshold(int thresholdTimeMs) { + mNative.setAccessibilitySlowKeysThreshold(thresholdTimeMs); + } + + /** * Sets whether Accessibility sticky keys is enabled. */ public void setAccessibilityStickyKeysEnabled(boolean enabled) { diff --git a/services/core/java/com/android/server/input/InputSettingsObserver.java b/services/core/java/com/android/server/input/InputSettingsObserver.java index 572d844d752d..165dfe445751 100644 --- a/services/core/java/com/android/server/input/InputSettingsObserver.java +++ b/services/core/java/com/android/server/input/InputSettingsObserver.java @@ -89,6 +89,8 @@ class InputSettingsObserver extends ContentObserver { (reason) -> updateShowRotaryInput()), Map.entry(Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_BOUNCE_KEYS), (reason) -> updateAccessibilityBounceKeys()), + Map.entry(Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_SLOW_KEYS), + (reason) -> updateAccessibilitySlowKeys()), Map.entry(Settings.Secure.getUriFor(Settings.Secure.ACCESSIBILITY_STICKY_KEYS), (reason) -> updateAccessibilityStickyKeys())); } @@ -228,6 +230,11 @@ class InputSettingsObserver extends ContentObserver { InputSettings.getAccessibilityBounceKeysThreshold(mContext)); } + private void updateAccessibilitySlowKeys() { + mService.setAccessibilitySlowKeysThreshold( + InputSettings.getAccessibilitySlowKeysThreshold(mContext)); + } + private void updateAccessibilityStickyKeys() { mService.setAccessibilityStickyKeysEnabled( InputSettings.isAccessibilityStickyKeysEnabled(mContext)); diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java index 8aec8ca7a23f..bc8207835a6e 100644 --- a/services/core/java/com/android/server/input/NativeInputManagerService.java +++ b/services/core/java/com/android/server/input/NativeInputManagerService.java @@ -233,14 +233,15 @@ interface NativeInputManagerService { void setStylusButtonMotionEventsEnabled(boolean enabled); /** - * Get the current position of the mouse cursor. + * Get the current position of the mouse cursor on the given display. * - * If the mouse cursor is not currently shown, the coordinate values will be NaN-s. + * If the mouse cursor is not currently shown, the coordinate values will be NaN-s. Use + * {@link android.view.Display#INVALID_DISPLAY} to get the position of the default mouse cursor. * * NOTE: This will grab the PointerController's lock, so we must be careful about calling this * from the InputReader or Display threads, which may result in a deadlock. */ - float[] getMouseCursorPosition(); + float[] getMouseCursorPosition(int displayId); /** Set whether showing a pointer icon for styluses is enabled. */ void setStylusPointerIconEnabled(boolean enabled); @@ -257,6 +258,11 @@ interface NativeInputManagerService { void setAccessibilityBounceKeysThreshold(int thresholdTimeMs); /** + * Notify if Accessibility slow keys threshold is changed from InputSettings. + */ + void setAccessibilitySlowKeysThreshold(int thresholdTimeMs); + + /** * Notify if Accessibility sticky keys is enabled/disabled from InputSettings. */ void setAccessibilityStickyKeysEnabled(boolean enabled); @@ -514,7 +520,7 @@ interface NativeInputManagerService { public native void setStylusButtonMotionEventsEnabled(boolean enabled); @Override - public native float[] getMouseCursorPosition(); + public native float[] getMouseCursorPosition(int displayId); @Override public native void setStylusPointerIconEnabled(boolean enabled); @@ -526,6 +532,9 @@ interface NativeInputManagerService { public native void setAccessibilityBounceKeysThreshold(int thresholdTimeMs); @Override + public native void setAccessibilitySlowKeysThreshold(int thresholdTimeMs); + + @Override public native void setAccessibilityStickyKeysEnabled(boolean enabled); } } diff --git a/services/core/java/com/android/server/inputmethod/ClientController.java b/services/core/java/com/android/server/inputmethod/ClientController.java index 21b952bb7760..ece236a5f18c 100644 --- a/services/core/java/com/android/server/inputmethod/ClientController.java +++ b/services/core/java/com/android/server/inputmethod/ClientController.java @@ -21,8 +21,6 @@ import android.content.pm.PackageManagerInternal; import android.os.IBinder; import android.os.RemoteException; import android.util.ArrayMap; -import android.util.SparseArray; -import android.view.inputmethod.InputBinding; import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; @@ -156,48 +154,4 @@ final class ClientController { return InputMethodUtils.checkIfPackageBelongsToUid( mPackageManagerInternal, cs.mUid, packageName); } - - static final class ClientState { - final IInputMethodClientInvoker mClient; - final IRemoteInputConnection mFallbackInputConnection; - final int mUid; - final int mPid; - final int mSelfReportedDisplayId; - final InputBinding mBinding; - final IBinder.DeathRecipient mClientDeathRecipient; - - @GuardedBy("ImfLock.class") - boolean mSessionRequested; - - @GuardedBy("ImfLock.class") - boolean mSessionRequestedForAccessibility; - - @GuardedBy("ImfLock.class") - InputMethodManagerService.SessionState mCurSession; - - @GuardedBy("ImfLock.class") - SparseArray<InputMethodManagerService.AccessibilitySessionState> mAccessibilitySessions = - new SparseArray<>(); - - @Override - public String toString() { - return "ClientState{" + Integer.toHexString( - System.identityHashCode(this)) + " mUid=" + mUid - + " mPid=" + mPid + " mSelfReportedDisplayId=" + mSelfReportedDisplayId + "}"; - } - - ClientState(IInputMethodClientInvoker client, - IRemoteInputConnection fallbackInputConnection, - int uid, int pid, int selfReportedDisplayId, - IBinder.DeathRecipient clientDeathRecipient) { - mClient = client; - mFallbackInputConnection = fallbackInputConnection; - mUid = uid; - mPid = pid; - mSelfReportedDisplayId = selfReportedDisplayId; - mBinding = new InputBinding(null /*conn*/, mFallbackInputConnection.asBinder(), mUid, - mPid); - mClientDeathRecipient = clientDeathRecipient; - } - } } diff --git a/services/core/java/com/android/server/inputmethod/ClientState.java b/services/core/java/com/android/server/inputmethod/ClientState.java new file mode 100644 index 000000000000..e98a5a73ab90 --- /dev/null +++ b/services/core/java/com/android/server/inputmethod/ClientState.java @@ -0,0 +1,68 @@ +/* + * 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.os.IBinder; +import android.util.SparseArray; +import android.view.inputmethod.InputBinding; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.inputmethod.IRemoteInputConnection; + +final class ClientState { + final IInputMethodClientInvoker mClient; + final IRemoteInputConnection mFallbackInputConnection; + final int mUid; + final int mPid; + final int mSelfReportedDisplayId; + final InputBinding mBinding; + final IBinder.DeathRecipient mClientDeathRecipient; + + @GuardedBy("ImfLock.class") + boolean mSessionRequested; + + @GuardedBy("ImfLock.class") + boolean mSessionRequestedForAccessibility; + + @GuardedBy("ImfLock.class") + InputMethodManagerService.SessionState mCurSession; + + @GuardedBy("ImfLock.class") + SparseArray<InputMethodManagerService.AccessibilitySessionState> mAccessibilitySessions = + new SparseArray<>(); + + @Override + public String toString() { + return "ClientState{" + Integer.toHexString( + System.identityHashCode(this)) + " mUid=" + mUid + + " mPid=" + mPid + " mSelfReportedDisplayId=" + mSelfReportedDisplayId + "}"; + } + + ClientState(IInputMethodClientInvoker client, + IRemoteInputConnection fallbackInputConnection, + int uid, int pid, int selfReportedDisplayId, + IBinder.DeathRecipient clientDeathRecipient) { + mClient = client; + mFallbackInputConnection = fallbackInputConnection; + mUid = uid; + mPid = pid; + mSelfReportedDisplayId = selfReportedDisplayId; + mBinding = new InputBinding(null /*conn*/, mFallbackInputConnection.asBinder(), mUid, + mPid); + mClientDeathRecipient = clientDeathRecipient; + } +} diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 50340d241347..5def4288253f 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -48,7 +48,6 @@ import static android.view.Display.INVALID_DISPLAY; import static android.view.WindowManager.DISPLAY_IME_POLICY_HIDE; import static android.view.WindowManager.DISPLAY_IME_POLICY_LOCAL; -import static com.android.server.inputmethod.ClientController.ClientState; import static com.android.server.inputmethod.ImeVisibilityStateComputer.ImeTargetWindowState; import static com.android.server.inputmethod.ImeVisibilityStateComputer.ImeVisibilityResult; import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_HIDE_IME; @@ -116,7 +115,6 @@ import android.util.PrintWriterPrinter; import android.util.Printer; import android.util.Slog; import android.util.SparseArray; -import android.util.SparseBooleanArray; import android.util.proto.ProtoOutputStream; import android.view.InputChannel; import android.view.InputDevice; @@ -282,8 +280,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @NonNull private InputMethodSettings mSettings; final SettingsObserver mSettingsObserver; - private final SparseBooleanArray mLoggedDeniedGetInputMethodWindowVisibleHeightForUid = - new SparseBooleanArray(0); final WindowManagerInternal mWindowManagerInternal; private final ActivityManagerInternal mActivityManagerInternal; final PackageManagerInternal mPackageManagerInternal; @@ -1355,13 +1351,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub clearPackageChangeState(); } - @Override - public void onUidRemoved(int uid) { - synchronized (ImfLock.class) { - mLoggedDeniedGetInputMethodWindowVisibleHeightForUid.delete(uid); - } - } - private void clearPackageChangeState() { // No need to lock them because we access these fields only on getRegisteredHandler(). mChangedPackages.clear(); @@ -2176,7 +2165,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub /** * Hide the IME if the removed user is the current user. */ - private void onClientRemoved(ClientController.ClientState client) { + private void onClientRemoved(ClientState client) { synchronized (ImfLock.class) { clearClientSessionLocked(client); clearClientSessionForAccessibilityLocked(client); @@ -4277,10 +4266,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub synchronized (ImfLock.class) { if (!canInteractWithImeLocked(callingUid, client, "getInputMethodWindowVisibleHeight", null /* statsToken */)) { - if (!mLoggedDeniedGetInputMethodWindowVisibleHeightForUid.get(callingUid)) { - EventLog.writeEvent(0x534e4554, "204906124", callingUid, ""); - mLoggedDeniedGetInputMethodWindowVisibleHeightForUid.put(callingUid, true); - } return 0; } // This should probably use the caller's display id, but because this is unsupported diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java index 2cd3ab1ddbbb..1d516e2931d7 100644 --- a/services/core/java/com/android/server/media/MediaSessionService.java +++ b/services/core/java/com/android/server/media/MediaSessionService.java @@ -287,10 +287,14 @@ public class MediaSessionService extends SystemService implements Monitor { } user.mPriorityStack.onSessionActiveStateChanged(record); } - setForegroundServiceAllowance( - record, - /* allowRunningInForeground= */ record.isActive() - && (playbackState == null || playbackState.isActive())); + boolean allowRunningInForeground = record.isActive() + && (playbackState == null || playbackState.isActive()); + + Log.d(TAG, "onSessionActiveStateChanged: " + + "record=" + record + + "playbackState=" + playbackState + + "allowRunningInForeground=" + allowRunningInForeground); + setForegroundServiceAllowance(record, allowRunningInForeground); mHandler.postSessionsChanged(record); } } @@ -388,10 +392,12 @@ public class MediaSessionService extends SystemService implements Monitor { } user.mPriorityStack.onPlaybackStateChanged(record, shouldUpdatePriority); if (playbackState != null) { - setForegroundServiceAllowance( - record, - /* allowRunningInForeground= */ playbackState.isActive() - && record.isActive()); + boolean allowRunningInForeground = playbackState.isActive() && record.isActive(); + Log.d(TAG, "onSessionPlaybackStateChanged: " + + "record=" + record + + "playbackState=" + playbackState + + "allowRunningInForeground=" + allowRunningInForeground); + setForegroundServiceAllowance(record, allowRunningInForeground); } } } @@ -556,6 +562,8 @@ public class MediaSessionService extends SystemService implements Monitor { } session.close(); + + Log.d(TAG, "destroySessionLocked: record=" + session); setForegroundServiceAllowance(session, /* allowRunningInForeground= */ false); mHandler.postSessionsChanged(session); } diff --git a/services/core/java/com/android/server/media/metrics/MediaMetricsManagerService.java b/services/core/java/com/android/server/media/metrics/MediaMetricsManagerService.java index df612e63927f..bbe6d3a0c8fa 100644 --- a/services/core/java/com/android/server/media/metrics/MediaMetricsManagerService.java +++ b/services/core/java/com/android/server/media/metrics/MediaMetricsManagerService.java @@ -20,6 +20,7 @@ import android.content.Context; import android.content.pm.PackageManager; import android.media.MediaMetrics; import android.media.metrics.BundleSession; +import android.media.metrics.EditingEndedEvent; import android.media.metrics.IMediaMetricsManager; import android.media.metrics.NetworkEvent; import android.media.metrics.PlaybackErrorEvent; @@ -346,6 +347,24 @@ public final class MediaMetricsManagerService extends SystemService { StatsLog.write(statsEvent); } + @Override + public void reportEditingEndedEvent(String sessionId, EditingEndedEvent event, int userId) { + int level = loggingLevel(); + if (level == LOGGING_LEVEL_BLOCKED) { + return; + } + StatsEvent statsEvent = + StatsEvent.newBuilder() + .setAtomId(798) + .writeString(sessionId) + .writeInt(event.getFinalState()) + .writeInt(event.getErrorCode()) + .writeLong(event.getTimeSinceCreatedMillis()) + .usePooledBuffer() + .build(); + StatsLog.write(statsEvent); + } + private int loggingLevel() { synchronized (mLock) { int uid = Binder.getCallingUid(); diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java index c067fa068b12..923be56dd545 100644 --- a/services/core/java/com/android/server/notification/ZenModeHelper.java +++ b/services/core/java/com/android/server/notification/ZenModeHelper.java @@ -1183,7 +1183,7 @@ public class ZenModeHelper { != newPolicy.getPriorityConversationSenders()) { userModifiedFields |= ZenPolicy.FIELD_CONVERSATIONS; } - if (oldPolicy.getPriorityChannels() != newPolicy.getPriorityChannels()) { + if (oldPolicy.getPriorityChannelsAllowed() != newPolicy.getPriorityChannelsAllowed()) { userModifiedFields |= ZenPolicy.FIELD_ALLOW_CHANNELS; } if (oldPolicy.getPriorityCategoryReminders() diff --git a/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java b/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java index 1660c3ef952a..8452c0e61a81 100644 --- a/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java +++ b/services/core/java/com/android/server/os/BugreportManagerServiceImpl.java @@ -152,14 +152,13 @@ class BugreportManagerServiceImpl extends IDumpstate.Stub { @RequiresPermission(value = android.Manifest.permission.INTERACT_ACROSS_USERS, conditional = true) void ensureCallerPreviouslyGeneratedFile( - Context context, Pair<Integer, String> callingInfo, int userId, - String bugreportFile, boolean forceUpdateMapping) { + Context context, PackageManager packageManager, Pair<Integer, String> callingInfo, + int userId, String bugreportFile, boolean forceUpdateMapping) { synchronized (mLock) { if (onboardingBugreportV2Enabled()) { final int uidForUser = Binder.withCleanCallingIdentity(() -> { try { - return context.getPackageManager() - .getPackageUidAsUser(callingInfo.second, userId); + return packageManager.getPackageUidAsUser(callingInfo.second, userId); } catch (PackageManager.NameNotFoundException exception) { throwInvalidBugreportFileForCallerException( bugreportFile, callingInfo.second); @@ -441,8 +440,8 @@ class BugreportManagerServiceImpl extends IDumpstate.Stub { Slogf.i(TAG, "Retrieving bugreport for %s / %d", callingPackage, callingUid); try { mBugreportFileManager.ensureCallerPreviouslyGeneratedFile( - mContext, new Pair<>(callingUid, callingPackage), userId, bugreportFile, - /* forceUpdateMapping= */ false); + mContext, mContext.getPackageManager(), new Pair<>(callingUid, callingPackage), + userId, bugreportFile, /* forceUpdateMapping= */ false); } catch (IllegalArgumentException e) { Slog.e(TAG, e.getMessage()); reportError(listener, IDumpstateListener.BUGREPORT_ERROR_NO_BUGREPORT_TO_RETRIEVE); diff --git a/services/core/java/com/android/server/pm/BACKGROUND_INSTALL_OWNERS b/services/core/java/com/android/server/pm/BACKGROUND_INSTALL_OWNERS new file mode 100644 index 000000000000..baa41a55c519 --- /dev/null +++ b/services/core/java/com/android/server/pm/BACKGROUND_INSTALL_OWNERS @@ -0,0 +1,2 @@ +georgechan@google.com +wenhaowang@google.com
\ No newline at end of file diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java index f311034a4dd2..ada79aed9d16 100644 --- a/services/core/java/com/android/server/pm/InstallPackageHelper.java +++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java @@ -4217,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/OWNERS b/services/core/java/com/android/server/pm/OWNERS index 84324f2524fc..c8bc56ce7dcd 100644 --- a/services/core/java/com/android/server/pm/OWNERS +++ b/services/core/java/com/android/server/pm/OWNERS @@ -51,3 +51,5 @@ per-file ShortcutRequestPinProcessor.java = omakoto@google.com, yamasani@google. per-file ShortcutService.java = omakoto@google.com, yamasani@google.com, sunnygoyal@google.com, mett@google.com, pinyaoting@google.com per-file ShortcutUser.java = omakoto@google.com, yamasani@google.com, sunnygoyal@google.com, mett@google.com, pinyaoting@google.com +# background install control service +per-file BackgroundInstall* = file:BACKGROUND_INSTALL_OWNERS
\ No newline at end of file diff --git a/services/core/java/com/android/server/pm/PreferredComponent.java b/services/core/java/com/android/server/pm/PreferredComponent.java index 18caafdaa56a..f3b146464864 100644 --- a/services/core/java/com/android/server/pm/PreferredComponent.java +++ b/services/core/java/com/android/server/pm/PreferredComponent.java @@ -28,7 +28,8 @@ import com.android.internal.util.XmlUtils; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; import com.android.server.LocalServices; -import com.android.server.pm.pkg.PackageUserState; +import com.android.server.pm.pkg.PackageStateInternal; +import com.android.server.pm.pkg.PackageUserStateInternal; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -218,11 +219,15 @@ public class PreferredComponent { continue; } - // Avoid showing the disambiguation dialog if the package which is installed with - // reason INSTALL_REASON_DEVICE_SETUP. - final PackageUserState pkgUserState = - pmi.getPackageStateInternal(ai.packageName).getUserStates().get(userId); - if (pkgUserState != null && pkgUserState.getInstallReason() + // Avoid showing the disambiguation dialog if the package is not installed or + // installed with reason INSTALL_REASON_DEVICE_SETUP. + final PackageStateInternal ps = pmi.getPackageStateInternal(ai.packageName); + if (ps == null) { + continue; + } + final PackageUserStateInternal pkgUserState = ps.getUserStates().get(userId); + if (pkgUserState == null + || pkgUserState.getInstallReason() == PackageManager.INSTALL_REASON_DEVICE_SETUP) { continue; } diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index a6598d602d01..c0596bb10823 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -295,8 +295,6 @@ public class UserManagerService extends IUserManager.Stub { private static final int USER_VERSION = 11; - private static final int MAX_USER_STRING_LENGTH = 500; - private static final long EPOCH_PLUS_30_YEARS = 30L * 365 * 24 * 60 * 60 * 1000L; // ms static final int WRITE_USER_MSG = 1; @@ -4692,16 +4690,18 @@ public class UserManagerService extends IUserManager.Stub { if (userData.persistSeedData) { if (userData.seedAccountName != null) { serializer.attribute(null, ATTR_SEED_ACCOUNT_NAME, - truncateString(userData.seedAccountName)); + truncateString(userData.seedAccountName, + UserManager.MAX_ACCOUNT_STRING_LENGTH)); } if (userData.seedAccountType != null) { serializer.attribute(null, ATTR_SEED_ACCOUNT_TYPE, - truncateString(userData.seedAccountType)); + truncateString(userData.seedAccountType, + UserManager.MAX_ACCOUNT_STRING_LENGTH)); } } if (userInfo.name != null) { serializer.startTag(null, TAG_NAME); - serializer.text(truncateString(userInfo.name)); + serializer.text(truncateString(userInfo.name, UserManager.MAX_USER_NAME_LENGTH)); serializer.endTag(null, TAG_NAME); } synchronized (mRestrictionsLock) { @@ -4765,11 +4765,11 @@ public class UserManagerService extends IUserManager.Stub { serializer.endDocument(); } - private String truncateString(String original) { - if (original == null || original.length() <= MAX_USER_STRING_LENGTH) { + private String truncateString(String original, int limit) { + if (original == null || original.length() <= limit) { return original; } - return original.substring(0, MAX_USER_STRING_LENGTH); + return original.substring(0, limit); } /* @@ -5236,7 +5236,7 @@ public class UserManagerService extends IUserManager.Stub { @UserIdInt int parentId, boolean preCreate, @Nullable String[] disallowedPackages, @NonNull TimingsTraceAndSlog t, @Nullable Object token) throws UserManager.CheckedUserOperationException { - String truncatedName = truncateString(name); + String truncatedName = truncateString(name, UserManager.MAX_USER_NAME_LENGTH); final UserTypeDetails userTypeDetails = mUserTypes.get(userType); if (userTypeDetails == null) { throwCheckedUserOperationException( @@ -6821,9 +6821,14 @@ public class UserManagerService extends IUserManager.Stub { Slog.e(LOG_TAG, "No such user for settings seed data u=" + userId); return; } - userData.seedAccountName = truncateString(accountName); - userData.seedAccountType = truncateString(accountType); - userData.seedAccountOptions = accountOptions; + userData.seedAccountName = truncateString(accountName, + UserManager.MAX_ACCOUNT_STRING_LENGTH); + userData.seedAccountType = truncateString(accountType, + UserManager.MAX_ACCOUNT_STRING_LENGTH); + if (accountOptions != null && accountOptions.isBundleContentsWithinLengthLimit( + UserManager.MAX_ACCOUNT_OPTIONS_LENGTH)) { + userData.seedAccountOptions = accountOptions; + } userData.persistSeedData = persist; } if (persist) { diff --git a/services/core/java/com/android/server/pm/VerifyingSession.java b/services/core/java/com/android/server/pm/VerifyingSession.java index f0ff85df13d1..dd2b409c7100 100644 --- a/services/core/java/com/android/server/pm/VerifyingSession.java +++ b/services/core/java/com/android/server/pm/VerifyingSession.java @@ -357,7 +357,8 @@ final class VerifyingSession { verifierUser = UserHandle.of(mPm.mUserManager.getCurrentUserId()); } // TODO(b/300965895): Remove when inconsistencies loading classpaths from apex for - // user > 1 are fixed. + // user > 1 are fixed. Tests should cover verifiers from apex classpaths run on + // primary user, secondary user and work profile. if (pkgLite.isSdkLibrary) { verifierUser = UserHandle.SYSTEM; } diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 0abf304c34ee..1fdcc64a90c8 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -2185,6 +2185,10 @@ public class PhoneWindowManager implements WindowManagerPolicy { TalkbackShortcutController getTalkbackShortcutController() { return new TalkbackShortcutController(mContext); } + + WindowWakeUpPolicy getWindowWakeUpPolicy() { + return new WindowWakeUpPolicy(mContext); + } } /** {@inheritDoc} */ @@ -2433,7 +2437,7 @@ public class PhoneWindowManager implements WindowManagerPolicy { com.android.internal.R.integer.config_keyguardDrawnTimeout); mKeyguardDelegate = injector.getKeyguardServiceDelegate(); mTalkbackShortcutController = injector.getTalkbackShortcutController(); - mWindowWakeUpPolicy = new WindowWakeUpPolicy(mContext); + mWindowWakeUpPolicy = injector.getWindowWakeUpPolicy(); initKeyCombinationRules(); initSingleKeyGestureRules(injector.getLooper()); mButtonOverridePermissionChecker = injector.getButtonOverridePermissionChecker(); diff --git a/services/core/java/com/android/server/selinux/QuotaLimiter.java b/services/core/java/com/android/server/selinux/QuotaLimiter.java new file mode 100644 index 000000000000..e89ddfd2627c --- /dev/null +++ b/services/core/java/com/android/server/selinux/QuotaLimiter.java @@ -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.server.selinux; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.os.Clock; + +import java.time.Duration; +import java.time.Instant; + +/** + * A QuotaLimiter allows to define a maximum number of Atom pushes within a specific time window. + * + * <p>The limiter divides the time line in windows of a fixed size. Every time a new permit is + * requested, the limiter checks whether the previous request was in the same time window as the + * current one. If the two windows are the same, it grants a permit only if the number of permits + * granted within the window does not exceed the quota. If the two windows are different, it resets + * the quota. + */ +public class QuotaLimiter { + + private final Clock mClock; + private final Duration mWindowSize; + private final int mMaxPermits; + + private long mCurrentWindow = 0; + private int mPermitsGranted = 0; + + @VisibleForTesting + QuotaLimiter(Clock clock, Duration windowSize, int maxPermits) { + mClock = clock; + mWindowSize = windowSize; + mMaxPermits = maxPermits; + } + + public QuotaLimiter(Duration windowSize, int maxPermits) { + this(Clock.SYSTEM_CLOCK, windowSize, maxPermits); + } + + public QuotaLimiter(int maxPermitsPerDay) { + this(Clock.SYSTEM_CLOCK, Duration.ofDays(1), maxPermitsPerDay); + } + + /** + * Acquires a permit if there is one available in the current time window. + * + * @return true if a permit was acquired. + */ + boolean acquire() { + long nowWindow = + Duration.between(Instant.EPOCH, Instant.ofEpochMilli(mClock.currentTimeMillis())) + .dividedBy(mWindowSize); + if (nowWindow > mCurrentWindow) { + mCurrentWindow = nowWindow; + mPermitsGranted = 0; + } + + if (mPermitsGranted < mMaxPermits) { + mPermitsGranted++; + return true; + } + + return false; + } +} diff --git a/services/core/java/com/android/server/selinux/RateLimiter.java b/services/core/java/com/android/server/selinux/RateLimiter.java new file mode 100644 index 000000000000..599b8409cbc3 --- /dev/null +++ b/services/core/java/com/android/server/selinux/RateLimiter.java @@ -0,0 +1,85 @@ +/* + * 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.selinux; + +import android.os.SystemClock; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.os.Clock; + +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; + +/** + * Rate limiter to ensure Atoms are pushed only within the allowed QPS window. This class is not + * thread-safe. + * + * <p>The rate limiter is smoothed, meaning that a rate limiter allowing X permits per second (or X + * QPS) will grant permits at a ratio of one every 1/X seconds. + */ +public final class RateLimiter { + + private Instant mNextPermit = Instant.EPOCH; + + private final Clock mClock; + private final Duration mWindow; + + @VisibleForTesting + RateLimiter(Clock clock, Duration window) { + mClock = clock; + // Truncating because the system clock does not support units smaller than milliseconds. + mWindow = window; + } + + /** + * Create a rate limiter generating one permit every {@code window} of time, using the {@link + * Clock.SYSTEM_CLOCK}. + */ + public RateLimiter(Duration window) { + this(Clock.SYSTEM_CLOCK, window); + } + + /** + * Acquire a permit if allowed by the rate limiter. If not, wait until a permit becomes + * available. + */ + public void acquire() { + Instant now = Instant.ofEpochMilli(mClock.currentTimeMillis()); + + if (mNextPermit.isAfter(now)) { // Sleep until we can acquire. + SystemClock.sleep(ChronoUnit.MILLIS.between(now, mNextPermit)); + mNextPermit = mNextPermit.plus(mWindow); + } else { + mNextPermit = now.plus(mWindow); + } + } + + /** + * Try to acquire a permit if allowed by the rate limiter. Non-blocking. + * + * @return true if a permit was acquired. Otherwise, return false. + */ + public boolean tryAcquire() { + final Instant now = Instant.ofEpochMilli(mClock.currentTimeMillis()); + + if (mNextPermit.isAfter(now)) { + return false; + } + mNextPermit = now.plus(mWindow); + return true; + } +} diff --git a/services/core/java/com/android/server/selinux/SelinuxAuditLogBuilder.java b/services/core/java/com/android/server/selinux/SelinuxAuditLogBuilder.java new file mode 100644 index 000000000000..8d8d5960038e --- /dev/null +++ b/services/core/java/com/android/server/selinux/SelinuxAuditLogBuilder.java @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.server.selinux; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.stream.Stream; + +/** Builder for SelinuxAuditLogs. */ +class SelinuxAuditLogBuilder { + + // Currently logs collection is hardcoded for the sdk_sandbox_audit. + private static final String SDK_SANDBOX_AUDIT = "sdk_sandbox_audit"; + static final Matcher SCONTEXT_MATCHER = + Pattern.compile( + "u:r:(?<stype>" + + SDK_SANDBOX_AUDIT + + "):s0(:c)?(?<scategories>((,c)?\\d+)+)*") + .matcher(""); + + static final Matcher TCONTEXT_MATCHER = + Pattern.compile("u:object_r:(?<ttype>\\w+):s0(:c)?(?<tcategories>((,c)?\\d+)+)*") + .matcher(""); + + static final Matcher PATH_MATCHER = + Pattern.compile("\"(?<path>/\\w+(/\\w+)?)(/\\w+)*\"").matcher(""); + + private Iterator<String> mTokens; + private final SelinuxAuditLog mAuditLog = new SelinuxAuditLog(); + + void reset(String denialString) { + mTokens = + Arrays.asList( + Optional.ofNullable(denialString) + .map(s -> s.split("\\s+|=")) + .orElse(new String[0])) + .iterator(); + mAuditLog.reset(); + } + + SelinuxAuditLog build() { + while (mTokens.hasNext()) { + final String token = mTokens.next(); + + switch (token) { + case "granted": + mAuditLog.mGranted = true; + break; + case "denied": + mAuditLog.mGranted = false; + break; + case "{": + Stream.Builder<String> permissionsStream = Stream.builder(); + boolean closed = false; + while (!closed && mTokens.hasNext()) { + String permission = mTokens.next(); + if ("}".equals(permission)) { + closed = true; + } else { + permissionsStream.add(permission); + } + } + if (!closed) { + return null; + } + mAuditLog.mPermissions = permissionsStream.build().toArray(String[]::new); + break; + case "scontext": + if (!nextTokenMatches(SCONTEXT_MATCHER)) { + return null; + } + mAuditLog.mSType = SCONTEXT_MATCHER.group("stype"); + mAuditLog.mSCategories = toCategories(SCONTEXT_MATCHER.group("scategories")); + break; + case "tcontext": + if (!nextTokenMatches(TCONTEXT_MATCHER)) { + return null; + } + mAuditLog.mTType = TCONTEXT_MATCHER.group("ttype"); + mAuditLog.mTCategories = toCategories(TCONTEXT_MATCHER.group("tcategories")); + break; + case "tclass": + if (!mTokens.hasNext()) { + return null; + } + mAuditLog.mTClass = mTokens.next(); + break; + case "path": + if (nextTokenMatches(PATH_MATCHER)) { + mAuditLog.mPath = PATH_MATCHER.group("path"); + } + break; + case "permissive": + if (!mTokens.hasNext()) { + return null; + } + mAuditLog.mPermissive = "1".equals(mTokens.next()); + break; + default: + break; + } + } + return mAuditLog; + } + + boolean nextTokenMatches(Matcher matcher) { + return mTokens.hasNext() && matcher.reset(mTokens.next()).matches(); + } + + static int[] toCategories(String categories) { + return categories == null + ? null + : Arrays.stream(categories.split(",c")).mapToInt(Integer::parseInt).toArray(); + } + + static class SelinuxAuditLog { + boolean mGranted = false; + String[] mPermissions = null; + String mSType = null; + int[] mSCategories = null; + String mTType = null; + int[] mTCategories = null; + String mTClass = null; + String mPath = null; + boolean mPermissive = false; + + private void reset() { + mGranted = false; + mPermissions = null; + mSType = null; + mSCategories = null; + mTType = null; + mTCategories = null; + mTClass = null; + mPath = null; + mPermissive = false; + } + } +} diff --git a/services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java b/services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java new file mode 100644 index 000000000000..0219645bee38 --- /dev/null +++ b/services/core/java/com/android/server/selinux/SelinuxAuditLogsCollector.java @@ -0,0 +1,144 @@ +/* + * 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.selinux; + +import android.util.EventLog; +import android.util.EventLog.Event; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.util.FrameworkStatsLog; +import com.android.server.selinux.SelinuxAuditLogBuilder.SelinuxAuditLog; + +import java.io.IOException; +import java.time.Instant; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** Class in charge of collecting SELinux audit logs and push the SELinux atoms. */ +class SelinuxAuditLogsCollector { + + private static final String TAG = "SelinuxAuditLogs"; + + private static final String SELINUX_PATTERN = "^.*\\bavc:\\s+(?<denial>.*)$"; + + @VisibleForTesting + static final Matcher SELINUX_MATCHER = Pattern.compile(SELINUX_PATTERN).matcher(""); + + private final RateLimiter mRateLimiter; + private final QuotaLimiter mQuotaLimiter; + + @VisibleForTesting Instant mLastWrite = Instant.MIN; + + final AtomicBoolean mStopRequested = new AtomicBoolean(false); + + SelinuxAuditLogsCollector(RateLimiter rateLimiter, QuotaLimiter quotaLimiter) { + mRateLimiter = rateLimiter; + mQuotaLimiter = quotaLimiter; + } + + /** + * Collect and push SELinux audit logs for the provided {@code tagCode}. + * + * @return true if the job was completed. If the job was interrupted, return false. + */ + boolean collect(int tagCode) { + Queue<Event> logLines = new ArrayDeque<>(); + Instant latestTimestamp = collectLogLines(tagCode, logLines); + + boolean quotaExceeded = writeAuditLogs(logLines); + if (quotaExceeded) { + Log.w(TAG, "Too many SELinux logs in the queue, I am giving up."); + mLastWrite = latestTimestamp; // next run we will ignore all these logs. + logLines.clear(); + } + + return logLines.isEmpty(); + } + + private Instant collectLogLines(int tagCode, Queue<Event> logLines) { + List<Event> events = new ArrayList<>(); + try { + EventLog.readEvents(new int[] {tagCode}, events); + } catch (IOException e) { + Log.e(TAG, "Error reading event logs", e); + } + + Instant latestTimestamp = mLastWrite; + for (Event event : events) { + Instant eventTime = Instant.ofEpochSecond(0, event.getTimeNanos()); + if (eventTime.isAfter(latestTimestamp)) { + latestTimestamp = eventTime; + } + if (eventTime.isBefore(mLastWrite)) { + continue; + } + Object eventData = event.getData(); + if (!(eventData instanceof String)) { + continue; + } + logLines.add(event); + } + return latestTimestamp; + } + + private boolean writeAuditLogs(Queue<Event> logLines) { + final SelinuxAuditLogBuilder auditLogBuilder = new SelinuxAuditLogBuilder(); + + while (!mStopRequested.get() && !logLines.isEmpty()) { + Event event = logLines.poll(); + String logLine = (String) event.getData(); + Instant logTime = Instant.ofEpochSecond(0, event.getTimeNanos()); + if (!SELINUX_MATCHER.reset(logLine).matches()) { + continue; + } + + auditLogBuilder.reset(SELINUX_MATCHER.group("denial")); + final SelinuxAuditLog auditLog = auditLogBuilder.build(); + if (auditLog == null) { + continue; + } + + if (!mQuotaLimiter.acquire()) { + return true; + } + mRateLimiter.acquire(); + + FrameworkStatsLog.write( + FrameworkStatsLog.SELINUX_AUDIT_LOG, + auditLog.mGranted, + auditLog.mPermissions, + auditLog.mSType, + auditLog.mSCategories, + auditLog.mTType, + auditLog.mTCategories, + auditLog.mTClass, + auditLog.mPath, + auditLog.mPermissive); + + if (logTime.isAfter(mLastWrite)) { + mLastWrite = logTime; + } + } + + return false; + } +} diff --git a/services/core/java/com/android/server/selinux/SelinuxAuditLogsService.java b/services/core/java/com/android/server/selinux/SelinuxAuditLogsService.java new file mode 100644 index 000000000000..8a661bcc13af --- /dev/null +++ b/services/core/java/com/android/server/selinux/SelinuxAuditLogsService.java @@ -0,0 +1,132 @@ +/* + * 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.selinux; + +import static com.android.sdksandbox.flags.Flags.selinuxSdkSandboxAudit; + +import android.app.job.JobInfo; +import android.app.job.JobParameters; +import android.app.job.JobScheduler; +import android.app.job.JobService; +import android.content.ComponentName; +import android.content.Context; +import android.util.EventLog; +import android.util.Log; + +import java.time.Duration; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Scheduled jobs related to logging of SELinux denials and audits. The job runs daily on idle + * devices. + */ +public class SelinuxAuditLogsService extends JobService { + + private static final String TAG = "SelinuxAuditLogs"; + private static final String SELINUX_AUDIT_NAMESPACE = "SelinuxAuditLogsNamespace"; + + static final int AUDITD_TAG_CODE = EventLog.getTagCode("auditd"); + + private static final int SELINUX_AUDIT_JOB_ID = 25327386; + private static final JobInfo SELINUX_AUDIT_JOB = + new JobInfo.Builder( + SELINUX_AUDIT_JOB_ID, + new ComponentName("android", SelinuxAuditLogsService.class.getName())) + .setPeriodic(TimeUnit.DAYS.toMillis(1)) + .setRequiresDeviceIdle(true) + .setRequiresCharging(true) + .setRequiresBatteryNotLow(true) + .build(); + + private static final ExecutorService EXECUTOR_SERVICE = Executors.newSingleThreadExecutor(); + private static final AtomicReference<Boolean> IS_RUNNING = new AtomicReference<>(false); + + // Audit logging is subject to both rate and quota limiting. We can only push one atom every 10 + // milliseconds, and no more than 50K atoms can be pushed each day. + private static final SelinuxAuditLogsCollector AUDIT_LOGS_COLLECTOR = + new SelinuxAuditLogsCollector( + new RateLimiter(/* window= */ Duration.ofMillis(10)), + new QuotaLimiter(/* maxPermitsPerDay= */ 50000)); + + /** Schedule jobs with the {@link JobScheduler}. */ + public static void schedule(Context context) { + if (!selinuxSdkSandboxAudit()) { + Log.d(TAG, "SelinuxAuditLogsService not enabled"); + return; + } + + if (AUDITD_TAG_CODE == -1) { + Log.e(TAG, "auditd is not a registered tag on this system"); + return; + } + + if (context.getSystemService(JobScheduler.class) + .forNamespace(SELINUX_AUDIT_NAMESPACE) + .schedule(SELINUX_AUDIT_JOB) + == JobScheduler.RESULT_FAILURE) { + Log.e(TAG, "SelinuxAuditLogsService could not be started."); + } + } + + @Override + public boolean onStartJob(JobParameters params) { + if (params.getJobId() != SELINUX_AUDIT_JOB_ID) { + Log.e(TAG, "The job id does not match the expected selinux job id."); + return false; + } + + AUDIT_LOGS_COLLECTOR.mStopRequested.set(false); + IS_RUNNING.set(true); + EXECUTOR_SERVICE.execute(new LogsCollectorJob(this, params)); + + return true; // the job is running + } + + @Override + public boolean onStopJob(JobParameters params) { + if (params.getJobId() != SELINUX_AUDIT_JOB_ID) { + return false; + } + + AUDIT_LOGS_COLLECTOR.mStopRequested.set(true); + return IS_RUNNING.get(); + } + + private static class LogsCollectorJob implements Runnable { + private final JobService mAuditLogService; + private final JobParameters mParams; + + LogsCollectorJob(JobService auditLogService, JobParameters params) { + mAuditLogService = auditLogService; + mParams = params; + } + + @Override + public void run() { + IS_RUNNING.updateAndGet( + isRunning -> { + boolean done = AUDIT_LOGS_COLLECTOR.collect(AUDITD_TAG_CODE); + if (done) { + mAuditLogService.jobFinished(mParams, /* wantsReschedule= */ false); + } + return !done; + }); + } + } +} 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 d7b8495929a2..b6d0ca19d484 100644 --- a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java +++ b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java @@ -2390,6 +2390,33 @@ public class TvInteractiveAppManagerService extends SystemService { } @Override + public void sendCertificate(IBinder sessionToken, String host, int port, + Bundle certBundle, int userId) { + if (DEBUG) { + Slogf.d(TAG, "sendCertificate(host=%s port=%d cert=%s)", host, port, + certBundle); + } + final int callingUid = Binder.getCallingUid(); + final int resolvedUserId = resolveCallingUserId(Binder.getCallingPid(), callingUid, + userId, "sendCertificate"); + SessionState sessionState = null; + final long identity = Binder.clearCallingIdentity(); + try { + synchronized (mLock) { + try { + sessionState = getSessionStateLocked(sessionToken, callingUid, + resolvedUserId); + getSessionLocked(sessionState).sendCertificate(host, port, certBundle); + } catch (RemoteException | SessionNotFoundException e) { + Slogf.e(TAG, "error in sendCertificate", 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); @@ -4125,6 +4152,24 @@ public class TvInteractiveAppManagerService extends SystemService { } @Override + public void onRequestCertificate(String host, int port) { + synchronized (mLock) { + if (DEBUG) { + Slogf.d(TAG, "onRequestCertificate"); + } + if (mSessionState.mSession == null || mSessionState.mClient == null) { + return; + } + try { + mSessionState.mClient.onRequestCertificate(host, port, mSessionState.mSeq); + } catch (RemoteException e) { + Slogf.e(TAG, "error in onRequestCertificate", e); + } + } + } + + + @Override public void onAdRequest(AdRequest request) { synchronized (mLock) { if (DEBUG) { diff --git a/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java b/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java index 19fd9a90518d..9e1b5d238d48 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperDisplayHelper.java @@ -96,7 +96,8 @@ class WallpaperDisplayHelper { } if (populateOrientationPairs) { int orientation = WallpaperManager.getOrientation(displaySize); - float newSurface = displaySize.x * displaySize.y * metric.getDensity(); + float newSurface = displaySize.x * displaySize.y + / (metric.getDensity() * metric.getDensity()); if (surface <= 0) { surface = newSurface; firstOrientation = orientation; 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/AccessibilityController.java b/services/core/java/com/android/server/wm/AccessibilityController.java index 2d584c428d4c..f2d9bf810b53 100644 --- a/services/core/java/com/android/server/wm/AccessibilityController.java +++ b/services/core/java/com/android/server/wm/AccessibilityController.java @@ -462,17 +462,16 @@ final class AccessibilityController { } } - // TODO(b/318327737): Remove parameter 't' when removing flag DRAW_IN_WM_LOCK. - void drawMagnifiedRegionBorderIfNeeded(int displayId, SurfaceControl.Transaction t) { + void drawMagnifiedRegionBorderIfNeeded(int displayId) { if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK)) { mAccessibilityTracing.logTrace( TAG + ".drawMagnifiedRegionBorderIfNeeded", FLAGS_MAGNIFICATION_CALLBACK, - "displayId=" + displayId + "; transaction={" + t + "}"); + "displayId=" + displayId); } final DisplayMagnifier displayMagnifier = mDisplayMagnifiers.get(displayId); if (displayMagnifier != null) { - displayMagnifier.drawMagnifiedRegionBorderIfNeeded(t); + displayMagnifier.drawMagnifiedRegionBorderIfNeeded(); } // Not relevant for the window observer. } @@ -870,12 +869,12 @@ final class AccessibilityController { .sendToTarget(); } - void drawMagnifiedRegionBorderIfNeeded(SurfaceControl.Transaction t) { + void drawMagnifiedRegionBorderIfNeeded() { if (mAccessibilityTracing.isTracingEnabled(FLAGS_MAGNIFICATION_CALLBACK)) { mAccessibilityTracing.logTrace(LOG_TAG + ".drawMagnifiedRegionBorderIfNeeded", - FLAGS_MAGNIFICATION_CALLBACK, "transition={" + t + "}"); + FLAGS_MAGNIFICATION_CALLBACK); } - mMagnifedViewport.drawWindowIfNeeded(t); + mMagnifedViewport.drawWindowIfNeeded(); } void dump(PrintWriter pw, String prefix) { @@ -1121,14 +1120,6 @@ final class AccessibilityController { } void setMagnifiedRegionBorderShown(boolean shown, boolean animate) { - if (ViewportWindow.DRAW_IN_WM_LOCK) { - if (shown) { - mFullRedrawNeeded = true; - mOldMagnificationRegion.set(0, 0, 0, 0); - } - mWindow.setShown(shown, animate); - return; - } if (mWindow.setShown(shown, animate)) { mFullRedrawNeeded = true; // Clear the old region, so recomputeBounds will refresh the current region. @@ -1151,12 +1142,8 @@ final class AccessibilityController { return mMagnificationSpec; } - void drawWindowIfNeeded(SurfaceControl.Transaction t) { + void drawWindowIfNeeded() { recomputeBounds(); - if (ViewportWindow.DRAW_IN_WM_LOCK) { - mWindow.drawOrRemoveIfNeeded(t); - return; - } mWindow.postDrawIfNeeded(); } @@ -1187,8 +1174,6 @@ final class AccessibilityController { private final class ViewportWindow implements Runnable { private static final String SURFACE_TITLE = "Magnification Overlay"; - // TODO(b/318327737): Remove if it is stable. - static final boolean DRAW_IN_WM_LOCK = !Flags.drawMagnifierBorderOutsideWmlock(); private final Region mBounds = new Region(); private final Rect mDirtyRect = new Rect(); @@ -1328,14 +1313,14 @@ final class AccessibilityController { @Override public void run() { - drawOrRemoveIfNeeded(mTransaction); + drawOrRemoveIfNeeded(); } /** * This method must only be called by animation handler directly to make sure * thread safe and there is no lock held outside. */ - private void drawOrRemoveIfNeeded(SurfaceControl.Transaction t) { + private void drawOrRemoveIfNeeded() { // Drawing variables (alpha, dirty rect, and bounds) access is synchronized // using WindowManagerGlobalLock. Grab copies of these values before // drawing on the canvas so that drawing can be performed outside of the lock. @@ -1343,7 +1328,7 @@ final class AccessibilityController { Rect drawingRect = null; Region drawingBounds = null; synchronized (mService.mGlobalLock) { - if (!DRAW_IN_WM_LOCK && mBlastBufferQueue.mNativeObject == 0) { + if (mBlastBufferQueue.mNativeObject == 0) { // Complete removal since releaseSurface has been called. if (mSurface.isValid()) { mTransaction.remove(mSurfaceControl).apply(); @@ -1388,16 +1373,8 @@ final class AccessibilityController { mPaint.setAlpha(alpha); canvas.drawPath(drawingBounds.getBoundaryPath(), mPaint); mSurface.unlockCanvasAndPost(canvas); - if (DRAW_IN_WM_LOCK) { - t.show(mSurfaceControl); - return; - } showSurface = true; } else { - if (DRAW_IN_WM_LOCK) { - t.hide(mSurfaceControl); - return; - } showSurface = false; } @@ -1413,11 +1390,6 @@ final class AccessibilityController { @GuardedBy("mService.mGlobalLock") void releaseSurface() { mBlastBufferQueue.destroy(); - if (DRAW_IN_WM_LOCK) { - mService.mTransactionFactory.get().remove(mSurfaceControl).apply(); - mSurface.release(); - return; - } // Post to perform cleanup on the thread which handles mSurface. mService.mAnimationHandler.post(this); } diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 036f7b6841c2..d9fa01e64a68 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() { @@ -2631,10 +2635,20 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A if (snapshot == null) { return false; } - if (!snapshot.getTopActivityComponent().equals(mActivityComponent)) { - // Obsoleted snapshot. - return false; - } + return isSnapshotComponentCompatible(snapshot) && isSnapshotOrientationCompatible(snapshot); + } + + /** + * Returns {@code true} if the top activity component of task snapshot equals to this activity. + */ + boolean isSnapshotComponentCompatible(@NonNull TaskSnapshot snapshot) { + return snapshot.getTopActivityComponent().equals(mActivityComponent); + } + + /** + * Returns {@code true} if the orientation of task snapshot is compatible with this activity. + */ + boolean isSnapshotOrientationCompatible(@NonNull TaskSnapshot snapshot) { final int rotation = mDisplayContent.rotationForActivityInDifferentOrientation(this); final int currentRotation = task.getWindowConfiguration().getRotation(); final int targetRotation = rotation != ROTATION_UNDEFINED @@ -9276,6 +9290,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, @@ -9525,6 +9592,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); diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java index a4d15e07a3ed..83ccbdc1a4d1 100644 --- a/services/core/java/com/android/server/wm/BackNavigationController.java +++ b/services/core/java/com/android/server/wm/BackNavigationController.java @@ -103,10 +103,6 @@ class BackNavigationController { static final boolean sPredictBackEnable = SystemProperties.getBoolean("persist.wm.debug.predictive_back", true); - static boolean isScreenshotEnabled() { - return SystemProperties.getInt("persist.wm.debug.predictive_back_screenshot", 0) != 0; - } - // Notify focus window changed void onFocusChanged(WindowState newFocus) { mNavigationMonitor.onFocusWindowChanged(newFocus); @@ -310,9 +306,11 @@ class BackNavigationController { // keyguard locked and activities are unable to show when locked. backType = BackNavigationInfo.TYPE_CALLBACK; } + } else if (currentTask.mAtmService.getLockTaskController().isTaskLocked(currentTask)) { + // Do not predict if current task is in task locked. + backType = BackNavigationInfo.TYPE_CALLBACK; } else { - // TODO(208789724): Create single source of truth for this, maybe in - // RootWindowContainer + // Check back-to-home or cross-task prevTask = currentTask.mRootWindowContainer.getTask(t -> { if (t.showToCurrentUser() && !t.mChildren.isEmpty()) { final ActivityRecord ar = t.getTopNonFinishingActivity(); @@ -958,6 +956,18 @@ class BackNavigationController { return; } + // Start fixed rotation for previous activity before create animation. + if (openingActivities.length == 1) { + final ActivityRecord next = openingActivities[0]; + final DisplayContent dc = next.mDisplayContent; + dc.rotateInDifferentOrientationIfNeeded(next); + if (next.hasFixedRotationTransform()) { + // Set the record so we can recognize it to continue to update display + // orientation if the previous activity becomes the top later. + dc.setFixedRotationLaunchingApp(next, + next.getWindowConfiguration().getRotation()); + } + } mOpenAnimAdaptor = new BackWindowAnimationAdaptorWrapper(true, mSwitchType, open); if (!mOpenAnimAdaptor.isValid()) { Slog.w(TAG, "compose animations fail, skip"); @@ -1623,16 +1633,6 @@ class BackNavigationController { } activity.mLaunchTaskBehind = true; - // Handle fixed rotation launching app. - final DisplayContent dc = activity.mDisplayContent; - dc.rotateInDifferentOrientationIfNeeded(activity); - if (activity.hasFixedRotationTransform()) { - // Set the record so we can recognize it to continue to update display - // orientation if the previous activity becomes the top later. - dc.setFixedRotationLaunchingApp(activity, - activity.getWindowConfiguration().getRotation()); - } - ProtoLog.d(WM_DEBUG_BACK_PREVIEW, "Setting Activity.mLauncherTaskBehind to true. Activity=%s", activity); activity.mTaskSupervisor.mStoppingActivities.remove(activity); @@ -1700,21 +1700,38 @@ class BackNavigationController { static TaskSnapshot getSnapshot(@NonNull WindowContainer w, ActivityRecord[] visibleOpenActivities) { + TaskSnapshot snapshot = null; if (w.asTask() != null) { final Task task = w.asTask(); - return task.mRootWindowContainer.mWindowManager.mTaskSnapshotController.getSnapshot( + snapshot = task.mRootWindowContainer.mWindowManager.mTaskSnapshotController.getSnapshot( task.mTaskId, task.mUserId, false /* restoreFromDisk */, false /* isLowResolution */); - } - - if (w.asActivityRecord() != null) { + } else if (w.asActivityRecord() != null) { final ActivityRecord ar = w.asActivityRecord(); - return ar.mWmService.mSnapshotController.mActivitySnapshotController + snapshot = ar.mWmService.mSnapshotController.mActivitySnapshotController .getSnapshot(visibleOpenActivities); } - return null; + + return isSnapshotCompatible(snapshot, visibleOpenActivities) ? snapshot : null; } + static boolean isSnapshotCompatible(@NonNull TaskSnapshot snapshot, + @NonNull ActivityRecord[] visibleOpenActivities) { + if (snapshot == null) { + return false; + } + boolean oneComponentMatch = false; + for (int i = visibleOpenActivities.length - 1; i >= 0; --i) { + final ActivityRecord ar = visibleOpenActivities[i]; + if (!ar.isSnapshotOrientationCompatible(snapshot)) { + return false; + } + oneComponentMatch |= ar.isSnapshotComponentCompatible(snapshot); + } + return oneComponentMatch; + } + + void setWindowManager(WindowManagerService wm) { mWindowManagerService = wm; mAnimationHandler = new AnimationHandler(wm); diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java index 9ac4a5c4ad5c..4681396affa5 100644 --- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java +++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java @@ -717,31 +717,6 @@ public class BackgroundActivityStartController { boolean callerCanAllow = resultForCaller.allows() && !state.callerExplicitOptOut(); boolean realCallerCanAllow = resultForRealCaller.allows() && !state.realCallerExplicitOptOut(); - if (callerCanAllow && realCallerCanAllow) { - // Both caller and real caller allow with system defined behavior - if (state.mBalAllowedByPiCreatorWithHardening.allowsBackgroundActivityStarts()) { - // Will be allowed even with BAL hardening. - if (DEBUG_ACTIVITY_STARTS) { - Slog.d(TAG, "Activity start allowed by caller. " - + state.dump()); - } - return allowBasedOnCaller(state); - } - if (state.mBalAllowedByPiCreator.allowsBackgroundActivityStarts()) { - Slog.wtf(TAG, - "With Android 15 BAL hardening this activity start may be blocked" - + " if the PI creator upgrades target_sdk to 35+" - + " AND the PI sender upgrades target_sdk to 34+! " - + state.dump()); - showBalRiskToast(); - return allowBasedOnCaller(state); - } - Slog.wtf(TAG, - "Without Android 15 BAL hardening this activity start would be allowed" - + " (missing opt in by PI creator or sender)! " - + state.dump()); - return abortLaunch(state); - } if (callerCanAllow) { // Allowed before V by creator if (state.mBalAllowedByPiCreatorWithHardening.allowsBackgroundActivityStarts()) { @@ -753,35 +728,29 @@ public class BackgroundActivityStartController { return allowBasedOnCaller(state); } if (state.mBalAllowedByPiCreator.allowsBackgroundActivityStarts()) { - Slog.wtf(TAG, - "With Android 15 BAL hardening this activity start may be blocked" + Slog.wtf(TAG, "With Android 15 BAL hardening this activity start may be blocked" + " if the PI creator upgrades target_sdk to 35+! " + " (missing opt in by PI creator)! " + state.dump()); showBalRiskToast(); return allowBasedOnCaller(state); } - Slog.wtf(TAG, - "Without Android 15 BAL hardening this activity start would be allowed" - + " (missing opt in by PI creator)! " - + state.dump()); - return abortLaunch(state); } if (realCallerCanAllow) { // Allowed before U by sender if (state.mBalAllowedByPiSender.allowsBackgroundActivityStarts()) { - Slog.wtf(TAG, - "With Android 14 BAL hardening this activity start will be blocked" + Slog.wtf(TAG, "With Android 14 BAL hardening this activity start will be blocked" + " if the PI sender upgrades target_sdk to 34+! " + " (missing opt in by PI sender)! " + state.dump()); showBalRiskToast(); return allowBasedOnRealCaller(state); } - Slog.wtf(TAG, "Without Android 14 BAL hardening this activity start would be allowed" - + " (missing opt in by PI sender)! " - + state.dump()); - return abortLaunch(state); + } + // caller or real caller could start the activity, but would need to explicitly opt in + if (callerCanAllow || realCallerCanAllow) { + Slog.wtf(TAG, "Without BAL hardening this activity start would be allowed " + + state.dump()); } // neither the caller not the realCaller can allow or have explicitly opted out return abortLaunch(state); diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java index f2796895d639..0e2d3d151db0 100644 --- a/services/core/java/com/android/server/wm/LetterboxUiController.java +++ b/services/core/java/com/android/server/wm/LetterboxUiController.java @@ -1424,7 +1424,7 @@ final class LetterboxUiController { @VisibleForTesting boolean shouldShowLetterboxUi(WindowState mainWindow) { - if (mIsRelaunchingAfterRequestedOrientationChanged || !isSurfaceReadyToShow(mainWindow)) { + if (mIsRelaunchingAfterRequestedOrientationChanged) { return mLastShouldShowLetterboxUi; } @@ -1442,13 +1442,6 @@ final class LetterboxUiController { } @VisibleForTesting - boolean isSurfaceReadyToShow(WindowState mainWindow) { - return mainWindow.isDrawn() // Regular case - // Waiting for relayoutWindow to call preserveSurface - || mainWindow.isDragResizeChanged(); - } - - @VisibleForTesting boolean isSurfaceVisible(WindowState mainWindow) { return mainWindow.isOnScreen() && (mActivityRecord.isVisible() || mActivityRecord.isVisibleRequested()); diff --git a/services/core/java/com/android/server/wm/Session.java b/services/core/java/com/android/server/wm/Session.java index f10a733040ed..083872a03edd 100644 --- a/services/core/java/com/android/server/wm/Session.java +++ b/services/core/java/com/android/server/wm/Session.java @@ -669,7 +669,7 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient { } @Override - public Bundle sendWallpaperCommand(IBinder window, String action, int x, int y, + public void sendWallpaperCommand(IBinder window, String action, int x, int y, int z, Bundle extras, boolean sync) { synchronized (mService.mGlobalLock) { final long ident = Binder.clearCallingIdentity(); @@ -680,10 +680,9 @@ class Session extends IWindowSession.Stub implements IBinder.DeathRecipient { if (mCanAlwaysUpdateWallpaper || windowState == wallpaperController.getWallpaperTarget() || windowState == wallpaperController.getPrevWallpaperTarget()) { - return wallpaperController.sendWindowWallpaperCommandUnchecked( + wallpaperController.sendWindowWallpaperCommandUnchecked( windowState, action, x, y, z, extras, sync); } - return null; } finally { Binder.restoreCallingIdentity(ident); } diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index 0c6b174b2408..b2b547e7d9e5 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -45,7 +45,6 @@ import static android.view.WindowManager.TRANSIT_FLAG_OPEN_BEHIND; import static android.view.WindowManager.TRANSIT_NONE; import static android.view.WindowManager.TRANSIT_OPEN; -import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_BACK_PREVIEW; import static com.android.internal.protolog.ProtoLogGroup.WM_DEBUG_STATES; import static com.android.server.wm.ActivityRecord.State.PAUSED; import static com.android.server.wm.ActivityRecord.State.PAUSING; @@ -89,7 +88,6 @@ import android.content.pm.PackageManager; import android.content.res.Configuration; import android.graphics.Point; import android.graphics.Rect; -import android.hardware.HardwareBuffer; import android.os.IBinder; import android.os.UserHandle; import android.util.DisplayMetrics; @@ -99,7 +97,6 @@ import android.view.DisplayInfo; import android.view.RemoteAnimationTarget; import android.view.SurfaceControl; import android.window.ITaskFragmentOrganizer; -import android.window.ScreenCapture; import android.window.TaskFragmentAnimationParams; import android.window.TaskFragmentInfo; import android.window.TaskFragmentOrganizerToken; @@ -113,7 +110,6 @@ import com.android.window.flags.Flags; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; import java.util.Set; import java.util.function.Consumer; @@ -403,10 +399,6 @@ class TaskFragment extends WindowContainer<WindowContainer> { /** For calculating app bounds, i.e. the area without the nav bar and display cutout. */ private final Rect mTmpNonDecorBounds = new Rect(); - //TODO(b/207481538) Remove once the infrastructure to support per-activity screenshot is - // implemented - HashMap<String, ScreenCapture.ScreenshotHardwareBuffer> mBackScreenshots = new HashMap<>(); - private final EnsureActivitiesVisibleHelper mEnsureActivitiesVisibleHelper = new EnsureActivitiesVisibleHelper(this); @@ -2092,17 +2084,6 @@ class TaskFragment extends WindowContainer<WindowContainer> { super.addChild(child, index); if (isAddingActivity && task != null) { - // TODO(b/207481538): temporary per-activity screenshoting - if (r != null && BackNavigationController.isScreenshotEnabled()) { - ProtoLog.v(WM_DEBUG_BACK_PREVIEW, "Screenshotting Activity %s", - r.mActivityComponent.flattenToString()); - Rect outBounds = r.getBounds(); - ScreenCapture.ScreenshotHardwareBuffer backBuffer = ScreenCapture.captureLayers( - r.mSurfaceControl, - new Rect(0, 0, outBounds.width(), outBounds.height()), - 1f); - mBackScreenshots.put(r.mActivityComponent.flattenToString(), backBuffer); - } addingActivity.inHistory = true; task.onDescendantActivityAdded(taskHadActivity, activityType, addingActivity); } @@ -2905,19 +2886,6 @@ class TaskFragment extends WindowContainer<WindowContainer> { return !mCreatedByOrganizer || mIsRemovalRequested; } - @Nullable - HardwareBuffer getSnapshotForActivityRecord(@Nullable ActivityRecord r) { - if (!BackNavigationController.isScreenshotEnabled()) { - return null; - } - if (r != null && r.mActivityComponent != null) { - ScreenCapture.ScreenshotHardwareBuffer backBuffer = - mBackScreenshots.get(r.mActivityComponent.flattenToString()); - return backBuffer != null ? backBuffer.getHardwareBuffer() : null; - } - return null; - } - @Override void removeChild(WindowContainer child) { removeChild(child, true /* removeSelfIfPossible */); @@ -2926,13 +2894,6 @@ class TaskFragment extends WindowContainer<WindowContainer> { void removeChild(WindowContainer child, boolean removeSelfIfPossible) { super.removeChild(child); final ActivityRecord r = child.asActivityRecord(); - if (BackNavigationController.isScreenshotEnabled()) { - //TODO(b/207481538) Remove once the infrastructure to support per-activity screenshot is - // implemented - if (r != null) { - mBackScreenshots.remove(r.mActivityComponent.flattenToString()); - } - } final WindowProcessController hostProcess = getOrganizerProcessIfDifferent(r); if (hostProcess != null) { hostProcess.removeEmbeddedActivity(r); diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java index 59e3350d5c13..d7b4a399514d 100644 --- a/services/core/java/com/android/server/wm/TransitionController.java +++ b/services/core/java/com/android/server/wm/TransitionController.java @@ -496,6 +496,9 @@ class TransitionController { if (mCollectingTransition != null && mCollectingTransition.isInTransientHide(task)) { return true; } + for (int i = mWaitingTransitions.size() - 1; i >= 0; --i) { + if (mWaitingTransitions.get(i).isInTransientHide(task)) return true; + } for (int i = mPlayingTransitions.size() - 1; i >= 0; --i) { if (mPlayingTransitions.get(i).isInTransientHide(task)) return true; } @@ -506,6 +509,9 @@ class TransitionController { if (mCollectingTransition != null && mCollectingTransition.isTransientVisible(task)) { return true; } + for (int i = mWaitingTransitions.size() - 1; i >= 0; --i) { + if (mWaitingTransitions.get(i).isTransientVisible(task)) return true; + } for (int i = mPlayingTransitions.size() - 1; i >= 0; --i) { if (mPlayingTransitions.get(i).isTransientVisible(task)) return true; } diff --git a/services/core/java/com/android/server/wm/WallpaperController.java b/services/core/java/com/android/server/wm/WallpaperController.java index 0fc62a758c5e..399815b70a06 100644 --- a/services/core/java/com/android/server/wm/WallpaperController.java +++ b/services/core/java/com/android/server/wm/WallpaperController.java @@ -641,11 +641,10 @@ class WallpaperController { } } - Bundle sendWindowWallpaperCommandUnchecked( + void sendWindowWallpaperCommandUnchecked( WindowState window, String action, int x, int y, int z, Bundle extras, boolean sync) { sendWindowWallpaperCommand(action, x, y, z, extras, sync); - return null; } private void sendWindowWallpaperCommand( diff --git a/services/core/java/com/android/server/wm/WindowAnimator.java b/services/core/java/com/android/server/wm/WindowAnimator.java index 750fd509e50f..b43a4540bbde 100644 --- a/services/core/java/com/android/server/wm/WindowAnimator.java +++ b/services/core/java/com/android/server/wm/WindowAnimator.java @@ -146,10 +146,11 @@ public class WindowAnimator { for (int i = 0; i < numDisplays; i++) { final DisplayContent dc = root.getChildAt(i); - dc.checkAppWindowsReadyToShow(); + if (!useShellTransition) { + dc.checkAppWindowsReadyToShow(); + } if (accessibilityController.hasCallbacks()) { - accessibilityController.drawMagnifiedRegionBorderIfNeeded(dc.mDisplayId, - mTransaction); + accessibilityController.drawMagnifiedRegionBorderIfNeeded(dc.mDisplayId); } if (dc.isAnimating(animationFlags, ANIMATION_TYPE_ALL)) { diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 56f2bc3d3e3b..24e50c54aa61 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -169,6 +169,7 @@ import static com.android.server.wm.WindowStateProto.PENDING_SEAMLESS_ROTATION; import static com.android.server.wm.WindowStateProto.REMOVED; import static com.android.server.wm.WindowStateProto.REMOVE_ON_EXIT; import static com.android.server.wm.WindowStateProto.REQUESTED_HEIGHT; +import static com.android.server.wm.WindowStateProto.REQUESTED_VISIBLE_TYPES; import static com.android.server.wm.WindowStateProto.REQUESTED_WIDTH; import static com.android.server.wm.WindowStateProto.STACK_ID; import static com.android.server.wm.WindowStateProto.SURFACE_INSETS; @@ -3988,6 +3989,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP proto.write(FORCE_SEAMLESS_ROTATION, mForceSeamlesslyRotate); proto.write(HAS_COMPAT_SCALE, hasCompatScale()); proto.write(GLOBAL_SCALE, mGlobalScale); + proto.write(REQUESTED_VISIBLE_TYPES, mRequestedVisibleTypes); for (Rect r : mKeepClearAreas) { r.dumpDebug(proto, KEEP_CLEAR_AREAS); } @@ -5187,6 +5189,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 8bc41af8af62..cbc301b87295 100644 --- a/services/core/jni/com_android_server_input_InputManagerService.cpp +++ b/services/core/jni/com_android_server_input_InputManagerService.cpp @@ -306,7 +306,7 @@ public: void setMotionClassifierEnabled(bool enabled); std::optional<std::string> getBluetoothAddress(int32_t deviceId); void setStylusButtonMotionEventsEnabled(bool enabled); - FloatPoint getMouseCursorPosition(); + FloatPoint getMouseCursorPosition(int32_t displayId); void setStylusPointerIconEnabled(bool enabled); /* --- InputReaderPolicyInterface implementation --- */ @@ -1784,10 +1784,12 @@ void NativeInputManager::setStylusButtonMotionEventsEnabled(bool enabled) { InputReaderConfiguration::Change::STYLUS_BUTTON_REPORTING); } -FloatPoint NativeInputManager::getMouseCursorPosition() { +FloatPoint NativeInputManager::getMouseCursorPosition(int32_t displayId) { if (ENABLE_POINTER_CHOREOGRAPHER) { - return mInputManager->getChoreographer().getMouseCursorPosition(ADISPLAY_ID_NONE); + return mInputManager->getChoreographer().getMouseCursorPosition(displayId); } + // To maintain the status-quo, the displayId parameter (used when PointerChoreographer is + // enabled) is ignored in the old pipeline. std::scoped_lock _l(mLock); const auto pc = mLocked.legacyPointerController.lock(); if (!pc) return {AMOTION_EVENT_INVALID_CURSOR_POSITION, AMOTION_EVENT_INVALID_CURSOR_POSITION}; @@ -2751,9 +2753,10 @@ static void nativeSetStylusButtonMotionEventsEnabled(JNIEnv* env, jobject native im->setStylusButtonMotionEventsEnabled(enabled); } -static jfloatArray nativeGetMouseCursorPosition(JNIEnv* env, jobject nativeImplObj) { +static jfloatArray nativeGetMouseCursorPosition(JNIEnv* env, jobject nativeImplObj, + jint displayId) { NativeInputManager* im = getNativeInputManager(env, nativeImplObj); - const auto p = im->getMouseCursorPosition(); + const auto p = im->getMouseCursorPosition(displayId); const std::array<float, 2> arr = {{p.x, p.y}}; jfloatArray outArr = env->NewFloatArray(2); env->SetFloatArrayRegion(outArr, 0, arr.size(), arr.data()); @@ -2775,6 +2778,15 @@ static void nativeSetAccessibilityBounceKeysThreshold(JNIEnv* env, jobject nativ } } +static void nativeSetAccessibilitySlowKeysThreshold(JNIEnv* env, jobject nativeImplObj, + jint thresholdTimeMs) { + NativeInputManager* im = getNativeInputManager(env, nativeImplObj); + if (ENABLE_INPUT_FILTER_RUST) { + im->getInputManager()->getInputFilter().setAccessibilitySlowKeysThreshold( + static_cast<nsecs_t>(thresholdTimeMs) * 1000000); + } +} + static void nativeSetAccessibilityStickyKeysEnabled(JNIEnv* env, jobject nativeImplObj, jboolean enabled) { NativeInputManager* im = getNativeInputManager(env, nativeImplObj); @@ -2883,10 +2895,12 @@ static const JNINativeMethod gInputManagerMethods[] = { {"getBluetoothAddress", "(I)Ljava/lang/String;", (void*)nativeGetBluetoothAddress}, {"setStylusButtonMotionEventsEnabled", "(Z)V", (void*)nativeSetStylusButtonMotionEventsEnabled}, - {"getMouseCursorPosition", "()[F", (void*)nativeGetMouseCursorPosition}, + {"getMouseCursorPosition", "(I)[F", (void*)nativeGetMouseCursorPosition}, {"setStylusPointerIconEnabled", "(Z)V", (void*)nativeSetStylusPointerIconEnabled}, {"setAccessibilityBounceKeysThreshold", "(I)V", (void*)nativeSetAccessibilityBounceKeysThreshold}, + {"setAccessibilitySlowKeysThreshold", "(I)V", + (void*)nativeSetAccessibilitySlowKeysThreshold}, {"setAccessibilityStickyKeysEnabled", "(Z)V", (void*)nativeSetAccessibilityStickyKeysEnabled}, }; diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index 86ad49458c48..2b8bcc77281a 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -203,6 +203,7 @@ import com.android.server.security.FileIntegrityService; import com.android.server.security.KeyAttestationApplicationIdProviderService; import com.android.server.security.KeyChainSystemService; import com.android.server.security.rkp.RemoteProvisioningService; +import com.android.server.selinux.SelinuxAuditLogsService; import com.android.server.sensorprivacy.SensorPrivacyService; import com.android.server.sensors.SensorService; import com.android.server.signedconfig.SignedConfigService; @@ -433,6 +434,9 @@ public final class SystemServer implements Dumpable { private static final String ROLE_SERVICE_CLASS = "com.android.role.RoleService"; private static final String GAME_MANAGER_SERVICE_CLASS = "com.android.server.app.GameManagerService$Lifecycle"; + private static final String ENHANCED_CONFIRMATION_SERVICE_CLASS = + "com.android.ecm.EnhancedConfirmationService"; + private static final String UWB_APEX_SERVICE_JAR_PATH = "/apex/com.android.uwb/javalib/service-uwb.jar"; private static final String UWB_SERVICE_CLASS = "com.android.server.uwb.UwbService"; @@ -1592,6 +1596,12 @@ public final class SystemServer implements Dumpable { mSystemServiceManager.startService(DropBoxManagerService.class); t.traceEnd(); + if (android.permission.flags.Flags.enhancedConfirmationModeApisEnabled()) { + t.traceBegin("StartEnhancedConfirmationService"); + mSystemServiceManager.startService(ENHANCED_CONFIRMATION_SERVICE_CLASS); + t.traceEnd(); + } + // Grants default permissions and defines roles t.traceBegin("StartRoleManagerService"); LocalManagerRegistry.addManager(RoleServicePlatformHelper.class, @@ -2609,6 +2619,14 @@ public final class SystemServer implements Dumpable { t.traceEnd(); } + t.traceBegin("StartSelinuxAuditLogsService"); + try { + SelinuxAuditLogsService.schedule(context); + } catch (Throwable e) { + reportWtf("starting SelinuxAuditLogsService", e); + } + t.traceEnd(); + // LauncherAppsService uses ShortcutService. t.traceBegin("StartShortcutServiceLifecycle"); mSystemServiceManager.startService(ShortcutService.Lifecycle.class); 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..3284cf19db43 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) } } @@ -127,7 +129,10 @@ class DevicePermissionPolicy : SchemePolicy() { val packageState = newState.externalState.packageStates[packageName] ?: return val androidPackage = packageState.androidPackage ?: return val appId = packageState.appId - val appIdPermissionFlags = newState.userStates[userId]!!.appIdDevicePermissionFlags + // The user may happen removed due to DeletePackageHelper.removeUnusedPackagesLPw() calling + // deletePackageX() asynchronously. + val userState = newState.userStates[userId] ?: return + val devicePermissionFlags = userState.appIdDevicePermissionFlags[appId] ?: return androidPackage.requestedPermissions.forEach { permissionName -> val isRequestedByOtherPackages = anyPackageInAppId(appId) { @@ -137,7 +142,7 @@ class DevicePermissionPolicy : SchemePolicy() { if (isRequestedByOtherPackages) { return@forEach } - appIdPermissionFlags[appId]?.forEachIndexed { _, deviceId, _ -> + devicePermissionFlags.forEachIndexed { _, deviceId, _ -> setPermissionFlags(appId, deviceId, userId, permissionName, 0) } } @@ -245,6 +250,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/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ClientControllerTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ClientControllerTest.java index 30afa72e0f03..b9f1ea06aebe 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ClientControllerTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ClientControllerTest.java @@ -16,13 +16,14 @@ package com.android.server.inputmethod; import static com.android.server.inputmethod.ClientController.ClientControllerCallback; -import static com.android.server.inputmethod.ClientController.ClientState; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.assertThrows; import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.anyLong; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -53,6 +54,7 @@ public final class ClientControllerTest { private static final int ANY_DISPLAY_ID = Display.DEFAULT_DISPLAY; private static final int ANY_CALLER_UID = 1; private static final int ANY_CALLER_PID = 1; + private static final String SOME_PACKAGE_NAME = "some.package"; @Rule public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder() @@ -81,7 +83,8 @@ public final class ClientControllerTest { } @Test - // TODO(b/314150112): Enable host side mode for this test once b/315544364 is fixed. + // TODO(b/314150112): Enable host side mode for this test once Ravenwood is enabled for + // inputmethod server classes. @IgnoreUnderRavenwood(blockedBy = {InputBinding.class, IInputMethodClientInvoker.class}) public void testAddClient_cannotAddTheSameClientTwice() { var invoker = IInputMethodClientInvoker.create(mClient, mHandler); @@ -103,7 +106,8 @@ public final class ClientControllerTest { } @Test - // TODO(b/314150112): Enable host side mode for this test once b/315544364 is fixed. + // TODO(b/314150112): Enable host side mode for this test once Ravenwood is enabled for + // inputmethod server classes. @IgnoreUnderRavenwood(blockedBy = {InputBinding.class, IInputMethodClientInvoker.class}) public void testAddClient() throws Exception { synchronized (ImfLock.class) { @@ -117,7 +121,8 @@ public final class ClientControllerTest { } @Test - // TODO(b/314150112): Enable host side mode for this test once b/315544364 is fixed. + // TODO(b/314150112): Enable host side mode for this test once Ravenwood is enabled for + // inputmethod server classes. @IgnoreUnderRavenwood(blockedBy = {InputBinding.class, IInputMethodClientInvoker.class}) public void testRemoveClient() { var callback = new TestClientControllerCallback(); @@ -137,6 +142,36 @@ public final class ClientControllerTest { assertThat(removed).isSameInstanceAs(added); } + @Test + // TODO(b/314150112): Enable host side mode for this test once Ravenwood is enabled for + // inputmethod server classes and updated to newer Mockito with static mock support (mock + // InputMethodUtils#checkIfPackageBelongsToUid instead of PackageManagerInternal#isSameApp) + @IgnoreUnderRavenwood(blockedBy = {InputMethodUtils.class}) + public void testVerifyClientAndPackageMatch() { + when(mMockPackageManagerInternal.isSameApp(eq(SOME_PACKAGE_NAME), /* flags= */ + anyLong(), eq(ANY_CALLER_UID), /* userId= */ anyInt())).thenReturn(true); + + synchronized (ImfLock.class) { + var invoker = IInputMethodClientInvoker.create(mClient, mHandler); + mController.addClient(invoker, mConnection, ANY_DISPLAY_ID, ANY_CALLER_UID, + ANY_CALLER_PID); + assertThat( + mController.verifyClientAndPackageMatch(mClient, SOME_PACKAGE_NAME)).isTrue(); + } + } + + @Test + public void testVerifyClientAndPackageMatch_unknownClient() { + synchronized (ImfLock.class) { + assertThrows(IllegalArgumentException.class, + () -> { + synchronized (ImfLock.class) { + mController.verifyClientAndPackageMatch(mClient, SOME_PACKAGE_NAME); + } + }); + } + } + private static class TestClientControllerCallback implements ClientControllerCallback { private final CountDownLatch mLatch = new CountDownLatch(1); diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java index 438bea458c47..1c71a6287c79 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/DefaultImeVisibilityApplierTest.java @@ -22,7 +22,6 @@ import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VI import static com.android.internal.inputmethod.SoftInputShowHideReason.HIDE_SOFT_INPUT; import static com.android.internal.inputmethod.SoftInputShowHideReason.HIDE_SWITCH_USER; import static com.android.internal.inputmethod.SoftInputShowHideReason.SHOW_SOFT_INPUT; -import static com.android.server.inputmethod.ClientController.ClientState; import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_HIDE_IME; import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_HIDE_IME_EXPLICIT; import static com.android.server.inputmethod.ImeVisibilityStateComputer.STATE_HIDE_IME_NOT_ALWAYS; diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java index 93a2eefba5b1..28471b37d2a0 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/FlexibilityControllerTest.java @@ -180,6 +180,7 @@ public class FlexibilityControllerTest { JobSchedulerService.sElapsedRealtimeClock = Clock.fixed(Instant.ofEpochMilli(FROZEN_TIME), ZoneOffset.UTC); // Initialize real objects. + doReturn(Long.MAX_VALUE).when(mPrefetchController).getNextEstimatedLaunchTimeLocked(any()); ArgumentCaptor<BroadcastReceiver> receiverCaptor = ArgumentCaptor.forClass(BroadcastReceiver.class); mFlexibilityController = new FlexibilityController(mJobSchedulerService, diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java index 5bec903e6414..656bc71eebca 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/UserManagerServiceTest.java @@ -556,7 +556,7 @@ public final class UserManagerServiceTest { @Test public void testCreateUserWithLongName_TruncatesName() { UserInfo user = mUms.createUserWithThrow(generateLongString(), USER_TYPE_FULL_SECONDARY, 0); - assertThat(user.name.length()).isEqualTo(500); + assertThat(user.name.length()).isEqualTo(UserManager.MAX_USER_NAME_LENGTH); UserInfo user1 = mUms.createUserWithThrow("Test", USER_TYPE_FULL_SECONDARY, 0); assertThat(user1.name.length()).isEqualTo(4); } diff --git a/services/tests/mockingservicestests/src/com/android/server/selinux/RateLimiterTest.java b/services/tests/mockingservicestests/src/com/android/server/selinux/RateLimiterTest.java new file mode 100644 index 000000000000..01c7fbe5bfe9 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/selinux/RateLimiterTest.java @@ -0,0 +1,113 @@ +/* + * 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.selinux; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.internal.os.Clock; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; + +@RunWith(AndroidJUnit4.class) +public class RateLimiterTest { + + private final MockClock mMockClock = new MockClock(); + + @Test + public void testRateLimiter_1QPS() { + RateLimiter rateLimiter = new RateLimiter(mMockClock, Duration.ofSeconds(1)); + + // First acquire is granted. + assertThat(rateLimiter.tryAcquire()).isTrue(); + // Next acquire is negated because it's too soon. + assertThat(rateLimiter.tryAcquire()).isFalse(); + // Wait >=1 seconds. + mMockClock.currentTimeMillis += Duration.ofSeconds(1).toMillis(); + assertThat(rateLimiter.tryAcquire()).isTrue(); + } + + @Test + public void testRateLimiter_3QPS() { + RateLimiter rateLimiter = + new RateLimiter( + mMockClock, + Duration.ofSeconds(1).dividedBy(3).truncatedTo(ChronoUnit.MILLIS)); + + assertThat(rateLimiter.tryAcquire()).isTrue(); + mMockClock.currentTimeMillis += Duration.ofSeconds(1).dividedBy(2).toMillis(); + assertThat(rateLimiter.tryAcquire()).isTrue(); + mMockClock.currentTimeMillis += Duration.ofSeconds(1).dividedBy(3).toMillis(); + assertThat(rateLimiter.tryAcquire()).isTrue(); + mMockClock.currentTimeMillis += Duration.ofSeconds(1).dividedBy(4).toMillis(); + assertThat(rateLimiter.tryAcquire()).isFalse(); + } + + @Test + public void testRateLimiter_infiniteQPS() { + RateLimiter rateLimiter = new RateLimiter(mMockClock, Duration.ofMillis(0)); + + // so many permits. + assertThat(rateLimiter.tryAcquire()).isTrue(); + assertThat(rateLimiter.tryAcquire()).isTrue(); + assertThat(rateLimiter.tryAcquire()).isTrue(); + assertThat(rateLimiter.tryAcquire()).isTrue(); + assertThat(rateLimiter.tryAcquire()).isTrue(); + + mMockClock.currentTimeMillis += Duration.ofSeconds(10).toMillis(); + // still so many permits. + assertThat(rateLimiter.tryAcquire()).isTrue(); + assertThat(rateLimiter.tryAcquire()).isTrue(); + assertThat(rateLimiter.tryAcquire()).isTrue(); + assertThat(rateLimiter.tryAcquire()).isTrue(); + assertThat(rateLimiter.tryAcquire()).isTrue(); + + mMockClock.currentTimeMillis += Duration.ofDays(-10).toMillis(); + // only going backwards in time you will stop the permits. + assertThat(rateLimiter.tryAcquire()).isFalse(); + assertThat(rateLimiter.tryAcquire()).isFalse(); + assertThat(rateLimiter.tryAcquire()).isFalse(); + } + + @Test + public void testRateLimiter_negativeQPS() { + RateLimiter rateLimiter = new RateLimiter(mMockClock, Duration.ofMillis(-10)); + + // Negative QPS is effectively turning of the rate limiter. + assertThat(rateLimiter.tryAcquire()).isTrue(); + assertThat(rateLimiter.tryAcquire()).isTrue(); + assertThat(rateLimiter.tryAcquire()).isTrue(); + mMockClock.currentTimeMillis += Duration.ofSeconds(1000).toMillis(); + assertThat(rateLimiter.tryAcquire()).isTrue(); + assertThat(rateLimiter.tryAcquire()).isTrue(); + assertThat(rateLimiter.tryAcquire()).isTrue(); + } + + private static final class MockClock extends Clock { + + public long currentTimeMillis = 0; + + @Override + public long currentTimeMillis() { + return currentTimeMillis; + } + } +} diff --git a/services/tests/mockingservicestests/src/com/android/server/selinux/SelinuxAuditLogsBuilderTest.java b/services/tests/mockingservicestests/src/com/android/server/selinux/SelinuxAuditLogsBuilderTest.java new file mode 100644 index 000000000000..b36c9bdaf456 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/selinux/SelinuxAuditLogsBuilderTest.java @@ -0,0 +1,202 @@ +/* + * 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.selinux; + +import static com.android.server.selinux.SelinuxAuditLogBuilder.PATH_MATCHER; +import static com.android.server.selinux.SelinuxAuditLogBuilder.SCONTEXT_MATCHER; +import static com.android.server.selinux.SelinuxAuditLogBuilder.TCONTEXT_MATCHER; +import static com.android.server.selinux.SelinuxAuditLogBuilder.toCategories; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.server.selinux.SelinuxAuditLogBuilder.SelinuxAuditLog; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class SelinuxAuditLogsBuilderTest { + + private final SelinuxAuditLogBuilder mAuditLogBuilder = new SelinuxAuditLogBuilder(); + + @Test + public void testMatcher_scontext() { + assertThat(SCONTEXT_MATCHER.reset("u:r:sdk_sandbox_audit:s0").matches()).isTrue(); + assertThat(SCONTEXT_MATCHER.group("stype")).isEqualTo("sdk_sandbox_audit"); + assertThat(SCONTEXT_MATCHER.group("scategories")).isNull(); + + assertThat(SCONTEXT_MATCHER.reset("u:r:sdk_sandbox_audit:s0:c123,c456").matches()).isTrue(); + assertThat(SCONTEXT_MATCHER.group("stype")).isEqualTo("sdk_sandbox_audit"); + assertThat(toCategories(SCONTEXT_MATCHER.group("scategories"))) + .isEqualTo(new int[] {123, 456}); + + assertThat(SCONTEXT_MATCHER.reset("u:r:not_sdk_sandbox:s0").matches()).isFalse(); + assertThat(SCONTEXT_MATCHER.reset("u:object_r:sdk_sandbox_audit:s0").matches()).isFalse(); + assertThat(SCONTEXT_MATCHER.reset("u:r:sdk_sandbox_audit:s0:p123").matches()).isFalse(); + } + + @Test + public void testMatcher_tcontext() { + assertThat(TCONTEXT_MATCHER.reset("u:object_r:target_type:s0").matches()).isTrue(); + assertThat(TCONTEXT_MATCHER.group("ttype")).isEqualTo("target_type"); + assertThat(TCONTEXT_MATCHER.group("tcategories")).isNull(); + + assertThat(TCONTEXT_MATCHER.reset("u:object_r:target_type2:s0:c666").matches()).isTrue(); + assertThat(TCONTEXT_MATCHER.group("ttype")).isEqualTo("target_type2"); + assertThat(toCategories(TCONTEXT_MATCHER.group("tcategories"))).isEqualTo(new int[] {666}); + + assertThat(TCONTEXT_MATCHER.reset("u:r:target_type:s0").matches()).isFalse(); + assertThat(TCONTEXT_MATCHER.reset("u:r:sdk_sandbox_audit:s0:x456").matches()).isFalse(); + } + + @Test + public void testMatcher_path() { + assertThat(PATH_MATCHER.reset("\"/data\"").matches()).isTrue(); + assertThat(PATH_MATCHER.group("path")).isEqualTo("/data"); + assertThat(PATH_MATCHER.reset("\"/data/local\"").matches()).isTrue(); + assertThat(PATH_MATCHER.group("path")).isEqualTo("/data/local"); + assertThat(PATH_MATCHER.reset("\"/data/local/tmp\"").matches()).isTrue(); + assertThat(PATH_MATCHER.group("path")).isEqualTo("/data/local"); + + assertThat(PATH_MATCHER.reset("\"/data/local").matches()).isFalse(); + assertThat(PATH_MATCHER.reset("\"_data_local\"").matches()).isFalse(); + } + + @Test + public void testSelinuxAuditLogsBuilder_noOptionals() { + mAuditLogBuilder.reset( + "granted { p } scontext=u:r:sdk_sandbox_audit:s0 tcontext=u:object_r:t:s0" + + " tclass=c"); + assertAuditLog( + mAuditLogBuilder.build(), true, new String[] {"p"}, "sdk_sandbox_audit", "t", "c"); + + mAuditLogBuilder.reset( + "tclass=c2 granted { p2 } tcontext=u:object_r:t2:s0" + + " scontext=u:r:sdk_sandbox_audit:s0"); + assertAuditLog( + mAuditLogBuilder.build(), + true, + new String[] {"p2"}, + "sdk_sandbox_audit", + "t2", + "c2"); + } + + @Test + public void testSelinuxAuditLogsBuilder_withCategories() { + mAuditLogBuilder.reset( + "granted { p } scontext=u:r:sdk_sandbox_audit:s0:c123" + + " tcontext=u:object_r:t:s0:c456,c666 tclass=c"); + assertAuditLog( + mAuditLogBuilder.build(), + true, + new String[] {"p"}, + "sdk_sandbox_audit", + new int[] {123}, + "t", + new int[] {456, 666}, + "c", + null, + false); + } + + @Test + public void testSelinuxAuditLogsBuilder_withPath() { + mAuditLogBuilder.reset( + "granted { p } scontext=u:r:sdk_sandbox_audit:s0 path=\"/very/long/path\"" + + " tcontext=u:object_r:t:s0 tclass=c"); + assertAuditLog( + mAuditLogBuilder.build(), + true, + new String[] {"p"}, + "sdk_sandbox_audit", + null, + "t", + null, + "c", + "/very/long", + false); + } + + @Test + public void testSelinuxAuditLogsBuilder_withPermissive() { + mAuditLogBuilder.reset( + "granted { p } scontext=u:r:sdk_sandbox_audit:s0 permissive=0" + + " tcontext=u:object_r:t:s0 tclass=c"); + assertAuditLog( + mAuditLogBuilder.build(), + true, + new String[] {"p"}, + "sdk_sandbox_audit", + null, + "t", + null, + "c", + null, + false); + + mAuditLogBuilder.reset( + "granted { p } scontext=u:r:sdk_sandbox_audit:s0 tcontext=u:object_r:t:s0 tclass=c" + + " permissive=1"); + assertAuditLog( + mAuditLogBuilder.build(), + true, + new String[] {"p"}, + "sdk_sandbox_audit", + null, + "t", + null, + "c", + null, + true); + } + + private void assertAuditLog( + SelinuxAuditLog auditLog, + boolean granted, + String[] permissions, + String sType, + String tType, + String tClass) { + assertAuditLog( + auditLog, granted, permissions, sType, null, tType, null, tClass, null, false); + } + + private void assertAuditLog( + SelinuxAuditLog auditLog, + boolean granted, + String[] permissions, + String sType, + int[] sCategories, + String tType, + int[] tCategories, + String tClass, + String path, + boolean permissive) { + assertThat(auditLog).isNotNull(); + assertThat(auditLog.mGranted).isEqualTo(granted); + assertThat(auditLog.mPermissions).isEqualTo(permissions); + assertThat(auditLog.mSType).isEqualTo(sType); + assertThat(auditLog.mSCategories).isEqualTo(sCategories); + assertThat(auditLog.mTType).isEqualTo(tType); + assertThat(auditLog.mTCategories).isEqualTo(tCategories); + assertThat(auditLog.mTClass).isEqualTo(tClass); + assertThat(auditLog.mPath).isEqualTo(path); + assertThat(auditLog.mPermissive).isEqualTo(permissive); + } +} diff --git a/services/tests/mockingservicestests/src/com/android/server/selinux/SelinuxAuditLogsCollectorTest.java b/services/tests/mockingservicestests/src/com/android/server/selinux/SelinuxAuditLogsCollectorTest.java new file mode 100644 index 000000000000..9758ea596335 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/selinux/SelinuxAuditLogsCollectorTest.java @@ -0,0 +1,644 @@ +/* + * 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.selinux; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; + +import android.util.EventLog; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.internal.os.Clock; +import com.android.internal.util.FrameworkStatsLog; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.MockitoSession; + +import java.time.Duration; +import java.time.Instant; +import java.util.Arrays; +import java.util.stream.Collectors; + +@RunWith(AndroidJUnit4.class) +public class SelinuxAuditLogsCollectorTest { + + // Fake tag to use for testing + private static final int ANSWER_TAG = 42; + + private final MockClock mClock = new MockClock(); + + private final SelinuxAuditLogsCollector mSelinuxAutidLogsCollector = + // Ignore rate limiting for tests + new SelinuxAuditLogsCollector( + new RateLimiter(mClock, /* window= */ Duration.ofMillis(0)), + new QuotaLimiter( + mClock, /* windowSize= */ Duration.ofHours(1), /* maxPermits= */ 5)); + + private MockitoSession mMockitoSession; + + @Before + public void setUp() { + // move the clock forward for the limiters. + mClock.currentTimeMillis += Duration.ofHours(1).toMillis(); + // Ignore what was written in the event logs by previous tests. + mSelinuxAutidLogsCollector.mLastWrite = Instant.now(); + + mMockitoSession = + mockitoSession().initMocks(this).mockStatic(FrameworkStatsLog.class).startMocking(); + } + + @After + public void tearDown() { + mMockitoSession.finishMocking(); + } + + @Test + public void testWriteSdkSandboxAuditLogs() { + writeTestLog("granted", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm1", "sdk_sandbox_audit", "ttype1", "tclass1"); + + boolean done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG); + + assertThat(done).isTrue(); + verify( + () -> + FrameworkStatsLog.write( + FrameworkStatsLog.SELINUX_AUDIT_LOG, + true, + new String[] {"perm"}, + "sdk_sandbox_audit", + null, + "ttype", + null, + "tclass", + null, + false)); + verify( + () -> + FrameworkStatsLog.write( + FrameworkStatsLog.SELINUX_AUDIT_LOG, + false, + new String[] {"perm1"}, + "sdk_sandbox_audit", + null, + "ttype1", + null, + "tclass1", + null, + false)); + } + + @Test + public void testWriteSdkSandboxAuditLogs_multiplePerms() { + writeTestLog("denied", "perm1 perm2", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm3 perm4", "sdk_sandbox_audit", "ttype", "tclass"); + + boolean done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG); + + assertThat(done).isTrue(); + verify( + () -> + FrameworkStatsLog.write( + FrameworkStatsLog.SELINUX_AUDIT_LOG, + false, + new String[] {"perm1", "perm2"}, + "sdk_sandbox_audit", + null, + "ttype", + null, + "tclass", + null, + false)); + verify( + () -> + FrameworkStatsLog.write( + FrameworkStatsLog.SELINUX_AUDIT_LOG, + false, + new String[] {"perm3", "perm4"}, + "sdk_sandbox_audit", + null, + "ttype", + null, + "tclass", + null, + false)); + } + + @Test + public void testWriteSdkSandboxAuditLogs_withPaths() { + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass", "/good/path"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass", "/very/long/path"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass", "/short_path"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass", "not_a_path"); + + boolean done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG); + + assertThat(done).isTrue(); + verify( + () -> + FrameworkStatsLog.write( + FrameworkStatsLog.SELINUX_AUDIT_LOG, + false, + new String[] {"perm"}, + "sdk_sandbox_audit", + null, + "ttype", + null, + "tclass", + "/good/path", + false)); + verify( + () -> + FrameworkStatsLog.write( + FrameworkStatsLog.SELINUX_AUDIT_LOG, + false, + new String[] {"perm"}, + "sdk_sandbox_audit", + null, + "ttype", + null, + "tclass", + "/very/long", + false)); + verify( + () -> + FrameworkStatsLog.write( + FrameworkStatsLog.SELINUX_AUDIT_LOG, + false, + new String[] {"perm"}, + "sdk_sandbox_audit", + null, + "ttype", + null, + "tclass", + "/short_path", + false)); + verify( + () -> + FrameworkStatsLog.write( + FrameworkStatsLog.SELINUX_AUDIT_LOG, + false, + new String[] {"perm"}, + "sdk_sandbox_audit", + null, + "ttype", + null, + "tclass", + null, + false)); + } + + @Test + public void testWriteSdkSandboxAuditLogs_withCategories() { + writeTestLog( + "denied", "perm", "sdk_sandbox_audit", new int[] {123}, "ttype", null, "tclass"); + writeTestLog( + "denied", + "perm", + "sdk_sandbox_audit", + new int[] {123, 456}, + "ttype", + null, + "tclass"); + writeTestLog( + "denied", "perm", "sdk_sandbox_audit", null, "ttype", new int[] {666}, "tclass"); + writeTestLog( + "denied", + "perm", + "sdk_sandbox_audit", + new int[] {123, 456}, + "ttype", + new int[] {666, 777}, + "tclass"); + + boolean done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG); + + assertThat(done).isTrue(); + verify( + () -> + FrameworkStatsLog.write( + FrameworkStatsLog.SELINUX_AUDIT_LOG, + false, + new String[] {"perm"}, + "sdk_sandbox_audit", + new int[] {123}, + "ttype", + null, + "tclass", + null, + false)); + verify( + () -> + FrameworkStatsLog.write( + FrameworkStatsLog.SELINUX_AUDIT_LOG, + false, + new String[] {"perm"}, + "sdk_sandbox_audit", + new int[] {123, 456}, + "ttype", + null, + "tclass", + null, + false)); + verify( + () -> + FrameworkStatsLog.write( + FrameworkStatsLog.SELINUX_AUDIT_LOG, + false, + new String[] {"perm"}, + "sdk_sandbox_audit", + null, + "ttype", + new int[] {666}, + "tclass", + null, + false)); + verify( + () -> + FrameworkStatsLog.write( + FrameworkStatsLog.SELINUX_AUDIT_LOG, + false, + new String[] {"perm"}, + "sdk_sandbox_audit", + new int[] {123, 456}, + "ttype", + new int[] {666, 777}, + "tclass", + null, + false)); + } + + @Test + public void testWriteSdkSandboxAuditLogs_withPathAndCategories() { + writeTestLog( + "denied", + "perm", + "sdk_sandbox_audit", + new int[] {123}, + "ttype", + new int[] {666}, + "tclass", + "/a/path"); + + boolean done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG); + + assertThat(done).isTrue(); + verify( + () -> + FrameworkStatsLog.write( + FrameworkStatsLog.SELINUX_AUDIT_LOG, + false, + new String[] {"perm"}, + "sdk_sandbox_audit", + new int[] {123}, + "ttype", + new int[] {666}, + "tclass", + "/a/path", + false)); + } + + @Test + public void testWriteSdkSandboxAuditLogs_permissive() { + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass", true); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass", false); + + boolean done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG); + + assertThat(done).isTrue(); + verify( + () -> + FrameworkStatsLog.write( + FrameworkStatsLog.SELINUX_AUDIT_LOG, + false, + new String[] {"perm"}, + "sdk_sandbox_audit", + null, + "ttype", + null, + "tclass", + null, + false), + times(2)); + verify( + () -> + FrameworkStatsLog.write( + FrameworkStatsLog.SELINUX_AUDIT_LOG, + false, + new String[] {"perm"}, + "sdk_sandbox_audit", + null, + "ttype", + null, + "tclass", + null, + true)); + } + + @Test + public void testNotWriteAuditLogs_notSdkSandbox() { + writeTestLog("denied", "perm", "stype", "ttype", "tclass"); + + boolean done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG); + + assertThat(done).isTrue(); + verify( + () -> + FrameworkStatsLog.write( + anyInt(), + anyBoolean(), + any(), + anyString(), + any(), + anyString(), + any(), + anyString(), + any(), + anyBoolean()), + never()); + } + + @Test + public void testWriteSdkSandboxAuditLogs_upToQuota() { + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + // These are not pushed. + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + + boolean done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG); + + assertThat(done).isTrue(); + verify( + () -> + FrameworkStatsLog.write( + anyInt(), + anyBoolean(), + any(), + anyString(), + any(), + anyString(), + any(), + anyString(), + any(), + anyBoolean()), + times(5)); + } + + @Test + public void testWriteSdkSandboxAuditLogs_resetQuota() { + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + + boolean done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG); + assertThat(done).isTrue(); + verify( + () -> + FrameworkStatsLog.write( + anyInt(), + anyBoolean(), + any(), + anyString(), + any(), + anyString(), + any(), + anyString(), + any(), + anyBoolean()), + times(5)); + + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + // move the clock forward to reset the quota limiter. + mClock.currentTimeMillis += Duration.ofHours(1).toMillis(); + done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG); + assertThat(done).isTrue(); + verify( + () -> + FrameworkStatsLog.write( + anyInt(), + anyBoolean(), + any(), + anyString(), + any(), + anyString(), + any(), + anyString(), + any(), + anyBoolean()), + times(10)); + } + + @Test + public void testNotWriteAuditLogs_stopRequested() { + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + // These are not pushed. + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + + mSelinuxAutidLogsCollector.mStopRequested.set(true); + boolean done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG); + assertThat(done).isFalse(); + verify( + () -> + FrameworkStatsLog.write( + anyInt(), + anyBoolean(), + any(), + anyString(), + any(), + anyString(), + any(), + anyString(), + any(), + anyBoolean()), + never()); + + mSelinuxAutidLogsCollector.mStopRequested.set(false); + done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG); + assertThat(done).isTrue(); + verify( + () -> + FrameworkStatsLog.write( + anyInt(), + anyBoolean(), + any(), + anyString(), + any(), + anyString(), + any(), + anyString(), + any(), + anyBoolean()), + times(5)); + } + + @Test + public void testAuditLogs_resumeJobDoesNotExceedLimit() { + writeTestLog("denied", "perm", "sdk_sandbox_audit", "ttype", "tclass"); + mSelinuxAutidLogsCollector.mStopRequested.set(true); + + boolean done = mSelinuxAutidLogsCollector.collect(ANSWER_TAG); + + assertThat(done).isFalse(); + verify( + () -> + FrameworkStatsLog.write( + anyInt(), + anyBoolean(), + any(), + anyString(), + any(), + anyString(), + any(), + anyString(), + any(), + anyBoolean()), + never()); + } + + private static void writeTestLog( + String granted, String permissions, String sType, String tType, String tClass) { + EventLog.writeEvent( + ANSWER_TAG, + String.format( + "avc: %s { %s } scontext=u:r:%s:s0 tcontext=u:object_r:%s:s0 tclass=%s", + granted, permissions, sType, tType, tClass)); + } + + private static void writeTestLog( + String granted, + String permissions, + String sType, + String tType, + String tClass, + String path) { + EventLog.writeEvent( + ANSWER_TAG, + String.format( + "avc: %s { %s } path=\"%s\" scontext=u:r:%s:s0 tcontext=u:object_r:%s:s0" + + " tclass=%s", + granted, permissions, path, sType, tType, tClass)); + } + + private static void writeTestLog( + String granted, + String permissions, + String sType, + int[] sCategories, + String tType, + int[] tCategories, + String tClass) { + EventLog.writeEvent( + ANSWER_TAG, + String.format( + "avc: %s { %s } scontext=u:r:%s:s0%s tcontext=u:object_r:%s:s0%s tclass=%s", + granted, + permissions, + sType, + toCategoriesString(sCategories), + tType, + toCategoriesString(tCategories), + tClass)); + } + + private static void writeTestLog( + String granted, + String permissions, + String sType, + int[] sCategories, + String tType, + int[] tCategories, + String tClass, + String path) { + EventLog.writeEvent( + ANSWER_TAG, + String.format( + "avc: %s { %s } path=\"%s\" scontext=u:r:%s:s0%s" + + " tcontext=u:object_r:%s:s0%s tclass=%s", + granted, + permissions, + path, + sType, + toCategoriesString(sCategories), + tType, + toCategoriesString(tCategories), + tClass)); + } + + private static void writeTestLog( + String granted, + String permissions, + String sType, + String tType, + String tClass, + boolean permissive) { + EventLog.writeEvent( + ANSWER_TAG, + String.format( + "avc: %s { %s } scontext=u:r:%s:s0 tcontext=u:object_r:%s:s0 tclass=%s" + + " permissive=%s", + granted, permissions, sType, tType, tClass, permissive ? "1" : "0")); + } + + private static String toCategoriesString(int[] categories) { + return (categories == null || categories.length == 0) + ? "" + : ":c" + + Arrays.stream(categories) + .mapToObj(String::valueOf) + .collect(Collectors.joining(",c")); + } + + private static final class MockClock extends Clock { + + public long currentTimeMillis = 0; + + @Override + public long currentTimeMillis() { + return currentTimeMillis; + } + } +} 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 be68e9c3c01f..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", 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/companion/virtual/InputControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java index ccbbaa52ac21..5943832586b3 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/InputControllerTest.java @@ -33,19 +33,21 @@ import android.os.Binder; import android.os.Handler; import android.os.IBinder; import android.platform.test.annotations.Presubmit; +import android.platform.test.flag.junit.SetFlagsRule; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; -import android.view.Display; import android.view.DisplayInfo; import android.view.WindowManager; import androidx.test.InstrumentationRegistry; +import com.android.input.flags.Flags; import com.android.server.LocalServices; import com.android.server.input.InputManagerInternal; import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -58,6 +60,9 @@ public class InputControllerTest { private static final String LANGUAGE_TAG = "en-US"; private static final String LAYOUT_TYPE = "qwerty"; + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + @Mock private InputManagerInternal mInputManagerInternalMock; @Mock @@ -72,11 +77,12 @@ public class InputControllerTest { @Before public void setUp() throws Exception { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_POINTER_CHOREOGRAPHER); + MockitoAnnotations.initMocks(this); mInputManagerMockHelper = new InputManagerMockHelper( TestableLooper.get(this), mNativeWrapperMock, mIInputManagerMock); - doReturn(true).when(mInputManagerInternalMock).setVirtualMousePointerDisplayId(anyInt()); LocalServices.removeServiceForTest(InputManagerInternal.class); LocalServices.addService(InputManagerInternal.class, mInputManagerInternalMock); @@ -129,11 +135,7 @@ public class InputControllerTest { mInputController.createMouse("name", /*vendorId= */ 1, /*productId= */ 1, deviceToken, /* displayId= */ 1); verify(mNativeWrapperMock).openUinputMouse(eq("name"), eq(1), eq(1), anyString()); - verify(mInputManagerInternalMock).setVirtualMousePointerDisplayId(eq(1)); - doReturn(1).when(mInputManagerInternalMock).getVirtualMousePointerDisplayId(); mInputController.unregisterInputDevice(deviceToken); - verify(mInputManagerInternalMock).setVirtualMousePointerDisplayId( - eq(Display.INVALID_DISPLAY)); } @Test @@ -143,14 +145,11 @@ public class InputControllerTest { mInputController.createMouse("mouse1", /*vendorId= */ 1, /*productId= */ 1, deviceToken, /* displayId= */ 1); verify(mNativeWrapperMock).openUinputMouse(eq("mouse1"), eq(1), eq(1), anyString()); - verify(mInputManagerInternalMock).setVirtualMousePointerDisplayId(eq(1)); final IBinder deviceToken2 = new Binder(); mInputController.createMouse("mouse2", /*vendorId= */ 1, /*productId= */ 1, deviceToken2, /* displayId= */ 2); verify(mNativeWrapperMock).openUinputMouse(eq("mouse2"), eq(1), eq(1), anyString()); - verify(mInputManagerInternalMock).setVirtualMousePointerDisplayId(eq(2)); mInputController.unregisterInputDevice(deviceToken); - verify(mInputManagerInternalMock).setVirtualMousePointerDisplayId(eq(1)); } @Test diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java index 9ff29d208dc0..5442af875e86 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java @@ -339,8 +339,8 @@ public class VirtualDeviceManagerServiceTest { LocalServices.addService(DisplayManagerInternal.class, mDisplayManagerInternalMock); mSetFlagsRule.initAllFlagsToReleaseConfigDefault(); + mSetFlagsRule.enableFlags(com.android.input.flags.Flags.FLAG_ENABLE_POINTER_CHOREOGRAPHER); - doReturn(true).when(mInputManagerInternalMock).setVirtualMousePointerDisplayId(anyInt()); doNothing().when(mInputManagerInternalMock) .setMousePointerAccelerationEnabled(anyBoolean(), anyInt()); doNothing().when(mInputManagerInternalMock).setPointerIconVisible(anyBoolean(), anyInt()); @@ -1333,7 +1333,6 @@ public class VirtualDeviceManagerServiceTest { mInputController.addDeviceForTesting(BINDER, fd, InputController.InputDeviceDescriptor.TYPE_MOUSE, DISPLAY_ID_1, PHYS, DEVICE_NAME_1, INPUT_DEVICE_ID); - doReturn(DISPLAY_ID_1).when(mInputManagerInternalMock).getVirtualMousePointerDisplayId(); assertThat(mDeviceImpl.sendButtonEvent(BINDER, new VirtualMouseButtonEvent.Builder() .setButtonCode(buttonCode) @@ -1363,7 +1362,6 @@ public class VirtualDeviceManagerServiceTest { mInputController.addDeviceForTesting(BINDER, fd, InputController.InputDeviceDescriptor.TYPE_MOUSE, DISPLAY_ID_1, PHYS, DEVICE_NAME_1, INPUT_DEVICE_ID); - doReturn(DISPLAY_ID_1).when(mInputManagerInternalMock).getVirtualMousePointerDisplayId(); assertThat(mDeviceImpl.sendRelativeEvent(BINDER, new VirtualMouseRelativeEvent.Builder() .setRelativeX(x) @@ -1394,7 +1392,6 @@ public class VirtualDeviceManagerServiceTest { mInputController.addDeviceForTesting(BINDER, fd, InputController.InputDeviceDescriptor.TYPE_MOUSE, DISPLAY_ID_1, PHYS, DEVICE_NAME_1, INPUT_DEVICE_ID); - doReturn(DISPLAY_ID_1).when(mInputManagerInternalMock).getVirtualMousePointerDisplayId(); assertThat(mDeviceImpl.sendScrollEvent(BINDER, new VirtualMouseScrollEvent.Builder() .setXAxisMovement(x) diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraControllerTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraControllerTest.java index 3e4f1df0e1d4..81981e6b16ca 100644 --- a/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraControllerTest.java +++ b/services/tests/servicestests/src/com/android/server/companion/virtual/camera/VirtualCameraControllerTest.java @@ -183,9 +183,8 @@ public class VirtualCameraControllerTest { private VirtualCameraConfig createVirtualCameraConfig( int width, int height, int format, int maximumFramesPerSecond, String name, int sensorOrientation, int lensFacing) { - return new VirtualCameraConfig.Builder() + return new VirtualCameraConfig.Builder(name) .addStreamConfig(width, height, format, maximumFramesPerSecond) - .setName(name) .setVirtualCameraCallback(mCallbackHandler, mVirtualCameraCallbackMock) .setSensorOrientation(sensorOrientation) .setLensFacing(lensFacing) 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/os/BugreportManagerServiceImplTest.java b/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java index dc1d2c5e54b6..1c6d36b0a0d2 100644 --- a/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java +++ b/services/tests/servicestests/src/com/android/server/os/BugreportManagerServiceImplTest.java @@ -17,16 +17,19 @@ package com.android.server.os; import android.app.admin.flags.Flags; -import static android.app.admin.flags.Flags.onboardingBugreportV2Enabled; import static com.android.compatibility.common.util.SystemUtil.runWithShellPermissionIdentity; import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; import android.app.role.RoleManager; import android.content.Context; +import android.content.pm.PackageManager; import android.os.Binder; import android.os.BugreportManager.BugreportCallback; import android.os.IBinder; @@ -48,6 +51,8 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; import java.io.FileDescriptor; import java.util.concurrent.CompletableFuture; @@ -66,6 +71,9 @@ public class BugreportManagerServiceImplTest { private BugreportManagerServiceImpl mService; private BugreportManagerServiceImpl.BugreportFileManager mBugreportFileManager; + @Mock + private PackageManager mPackageManager; + private int mCallingUid = 1234; private String mCallingPackage = "test.package"; private AtomicFile mMappingFile; @@ -74,7 +82,8 @@ public class BugreportManagerServiceImplTest { private String mBugreportFile2 = "bugreport-file2.zip"; @Before - public void setUp() { + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); mContext = InstrumentationRegistry.getInstrumentation().getContext(); mMappingFile = new AtomicFile(mContext.getFilesDir(), "bugreport-mapping.xml"); ArraySet<String> mAllowlistedPackages = new ArraySet<>(); @@ -83,6 +92,7 @@ public class BugreportManagerServiceImplTest { new BugreportManagerServiceImpl.Injector(mContext, mAllowlistedPackages, mMappingFile)); mBugreportFileManager = new BugreportManagerServiceImpl.BugreportFileManager(mMappingFile); + when(mPackageManager.getPackageUidAsUser(anyString(), anyInt())).thenReturn(mCallingUid); } @After @@ -115,12 +125,13 @@ public class BugreportManagerServiceImplTest { assertThrows(IllegalArgumentException.class, () -> mBugreportFileManager.ensureCallerPreviouslyGeneratedFile( - mContext, callingInfo, Process.myUserHandle().getIdentifier(), - "unknown-file.zip", /* forceUpdateMapping= */ true)); + mContext, mPackageManager, callingInfo, + Process.myUserHandle().getIdentifier(), "unknown-file.zip", + /* forceUpdateMapping= */ true)); // No exception should be thrown. mBugreportFileManager.ensureCallerPreviouslyGeneratedFile( - mContext, callingInfo, mContext.getUserId(), mBugreportFile, + mContext, mPackageManager, callingInfo, mContext.getUserId(), mBugreportFile, /* forceUpdateMapping= */ true); } @@ -132,7 +143,7 @@ public class BugreportManagerServiceImplTest { callingInfo, mBugreportFile, /* keepOnRetrieval= */ true); mBugreportFileManager.ensureCallerPreviouslyGeneratedFile( - mContext, callingInfo, mContext.getUserId(), mBugreportFile, + mContext, mPackageManager, callingInfo, mContext.getUserId(), mBugreportFile, /* forceUpdateMapping= */ true); assertThat(mBugreportFileManager.mBugreportFilesToPersist).containsExactly(mBugreportFile); @@ -148,10 +159,10 @@ public class BugreportManagerServiceImplTest { // No exception should be thrown. mBugreportFileManager.ensureCallerPreviouslyGeneratedFile( - mContext, callingInfo, mContext.getUserId(), mBugreportFile, + mContext, mPackageManager, callingInfo, mContext.getUserId(), mBugreportFile, /* forceUpdateMapping= */ true); mBugreportFileManager.ensureCallerPreviouslyGeneratedFile( - mContext, callingInfo, mContext.getUserId(), mBugreportFile2, + mContext, mPackageManager, callingInfo, mContext.getUserId(), mBugreportFile2, /* forceUpdateMapping= */ true); } @@ -160,8 +171,9 @@ public class BugreportManagerServiceImplTest { Pair<Integer, String> callingInfo = new Pair<>(mCallingUid, mCallingPackage); assertThrows(IllegalArgumentException.class, () -> mBugreportFileManager.ensureCallerPreviouslyGeneratedFile( - mContext, callingInfo, Process.myUserHandle().getIdentifier(), - "test-file.zip", /* forceUpdateMapping= */ true)); + mContext, mPackageManager, callingInfo, + Process.myUserHandle().getIdentifier(), "test-file.zip", + /* forceUpdateMapping= */ true)); } @Test diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java index a743fff5d2ea..06be456be0db 100644 --- a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java @@ -19,6 +19,7 @@ package com.android.server.pm; import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; +import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.junit.Assume.assumeTrue; import static org.testng.Assert.assertEquals; @@ -33,6 +34,7 @@ import android.content.pm.UserProperties; import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.os.Bundle; +import android.os.PersistableBundle; import android.os.UserHandle; import android.os.UserManager; import android.platform.test.annotations.Postsubmit; @@ -1632,6 +1634,106 @@ public final class UserManagerTest { assertThat(mainUserCount).isEqualTo(1); } + @Test + public void testAddUserAccountData_validStringValuesAreSaved_validBundleIsSaved() { + assumeManagedUsersSupported(); + + String userName = "User"; + String accountName = "accountName"; + String accountType = "accountType"; + String arrayKey = "StringArrayKey"; + String stringKey = "StringKey"; + String intKey = "IntKey"; + String nestedBundleKey = "PersistableBundleKey"; + String value1 = "Value 1"; + String value2 = "Value 2"; + String value3 = "Value 3"; + + UserInfo userInfo = mUserManager.createUser(userName, + UserManager.USER_TYPE_FULL_SECONDARY, 0); + + PersistableBundle accountOptions = new PersistableBundle(); + String[] stringArray = {value1, value2}; + accountOptions.putInt(intKey, 1234); + PersistableBundle nested = new PersistableBundle(); + nested.putString(stringKey, value3); + accountOptions.putPersistableBundle(nestedBundleKey, nested); + accountOptions.putStringArray(arrayKey, stringArray); + + mUserManager.clearSeedAccountData(); + mUserManager.setSeedAccountData(mContext.getUserId(), accountName, + accountType, accountOptions); + + //assert userName accountName and accountType were saved correctly + assertTrue(mUserManager.getUserInfo(userInfo.id).name.equals(userName)); + assertTrue(mUserManager.getSeedAccountName().equals(accountName)); + assertTrue(mUserManager.getSeedAccountType().equals(accountType)); + + //assert bundle with correct values was added + assertThat(mUserManager.getSeedAccountOptions().containsKey(arrayKey)).isTrue(); + assertThat(mUserManager.getSeedAccountOptions().getPersistableBundle(nestedBundleKey) + .getString(stringKey)).isEqualTo(value3); + assertThat(mUserManager.getSeedAccountOptions().getStringArray(arrayKey)[0]) + .isEqualTo(value1); + + mUserManager.removeUser(userInfo.id); + } + + @Test + public void testAddUserAccountData_invalidStringValuesAreTruncated_invalidBundleIsDropped() { + assumeManagedUsersSupported(); + + String tooLongString = generateLongString(); + String userName = "User " + tooLongString; + String accountType = "Account Type " + tooLongString; + String accountName = "accountName " + tooLongString; + String arrayKey = "StringArrayKey"; + String stringKey = "StringKey"; + String intKey = "IntKey"; + String nestedBundleKey = "PersistableBundleKey"; + String value1 = "Value 1"; + String value2 = "Value 2"; + + UserInfo userInfo = mUserManager.createUser(userName, + UserManager.USER_TYPE_FULL_SECONDARY, 0); + + PersistableBundle accountOptions = new PersistableBundle(); + String[] stringArray = {value1, value2}; + accountOptions.putInt(intKey, 1234); + PersistableBundle nested = new PersistableBundle(); + nested.putString(stringKey, tooLongString); + accountOptions.putPersistableBundle(nestedBundleKey, nested); + accountOptions.putStringArray(arrayKey, stringArray); + mUserManager.clearSeedAccountData(); + mUserManager.setSeedAccountData(mContext.getUserId(), accountName, + accountType, accountOptions); + + //assert userName was truncated + assertTrue(mUserManager.getUserInfo(userInfo.id).name.length() + == UserManager.MAX_USER_NAME_LENGTH); + + //assert accountName and accountType got truncated + assertTrue(mUserManager.getSeedAccountName().length() + == UserManager.MAX_ACCOUNT_STRING_LENGTH); + assertTrue(mUserManager.getSeedAccountType().length() + == UserManager.MAX_ACCOUNT_STRING_LENGTH); + + //assert bundle with invalid values was dropped + assertThat(mUserManager.getSeedAccountOptions() == null).isTrue(); + + mUserManager.removeUser(userInfo.id); + } + + private String generateLongString() { + String partialString = "Test Name Test Name Test Name Test Name Test Name Test Name Test " + + "Name Test Name Test Name Test Name "; //String of length 100 + StringBuilder resultString = new StringBuilder(); + for (int i = 0; i < 600; i++) { + resultString.append(partialString); + } + return resultString.toString(); + } + private boolean isPackageInstalledForUser(String packageName, int userId) { try { return mPackageManager.getPackageInfoAsUser(packageName, 0, userId) != null; 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/ZenAdaptersTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java index 0e20daf2c0f1..99d5a6d9118a 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenAdaptersTest.java @@ -143,12 +143,13 @@ public class ZenAdaptersTest extends UiServiceTestCase { Policy.policyState(false, true), 0); ZenPolicy zenPolicy = notificationPolicyToZenPolicy(policy); - assertThat(zenPolicy.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_ALLOW); + assertThat(zenPolicy.getPriorityChannelsAllowed()).isEqualTo(ZenPolicy.STATE_ALLOW); Policy notAllowed = new Policy(0, 0, 0, 0, Policy.policyState(false, false), 0); ZenPolicy zenPolicyNotAllowed = notificationPolicyToZenPolicy(notAllowed); - assertThat(zenPolicyNotAllowed.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_DISALLOW); + assertThat(zenPolicyNotAllowed.getPriorityChannelsAllowed()).isEqualTo( + ZenPolicy.STATE_DISALLOW); } @Test @@ -158,11 +159,12 @@ public class ZenAdaptersTest extends UiServiceTestCase { Policy.policyState(false, true), 0); ZenPolicy zenPolicy = notificationPolicyToZenPolicy(policy); - assertThat(zenPolicy.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_UNSET); + assertThat(zenPolicy.getPriorityChannelsAllowed()).isEqualTo(ZenPolicy.STATE_UNSET); Policy notAllowed = new Policy(0, 0, 0, 0, Policy.policyState(false, false), 0); ZenPolicy zenPolicyNotAllowed = notificationPolicyToZenPolicy(notAllowed); - assertThat(zenPolicyNotAllowed.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_UNSET); + assertThat(zenPolicyNotAllowed.getPriorityChannelsAllowed()).isEqualTo( + ZenPolicy.STATE_UNSET); } } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java index e523e79f6370..539bb37419f1 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java @@ -284,7 +284,7 @@ public class ZenModeConfigTest extends UiServiceTestCase { actual.getPriorityConversationSenders()); assertEquals(expected.getPriorityCallSenders(), actual.getPriorityCallSenders()); assertEquals(expected.getPriorityMessageSenders(), actual.getPriorityMessageSenders()); - assertEquals(expected.getPriorityChannels(), actual.getPriorityChannels()); + assertEquals(expected.getPriorityChannelsAllowed(), actual.getPriorityChannelsAllowed()); } @Test @@ -716,7 +716,7 @@ public class ZenModeConfigTest extends UiServiceTestCase { assertEquals(policy.getPriorityCategorySystem(), fromXml.getPriorityCategorySystem()); assertEquals(policy.getPriorityCategoryReminders(), fromXml.getPriorityCategoryReminders()); assertEquals(policy.getPriorityCategoryEvents(), fromXml.getPriorityCategoryEvents()); - assertEquals(policy.getPriorityChannels(), fromXml.getPriorityChannels()); + assertEquals(policy.getPriorityChannelsAllowed(), fromXml.getPriorityChannelsAllowed()); assertEquals(policy.getVisualEffectFullScreenIntent(), fromXml.getVisualEffectFullScreenIntent()); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java index 25ad7dbac30c..227265aa0a5b 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java @@ -4075,7 +4075,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { // UPDATE_ORIGIN_USER should change the bitmask and change the values. assertThat(rule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_PRIORITY); - assertThat(rule.getZenPolicy().getPriorityChannels()).isEqualTo(ZenPolicy.STATE_DISALLOW); + assertThat(rule.getZenPolicy().getPriorityChannelsAllowed()).isEqualTo( + ZenPolicy.STATE_DISALLOW); assertThat(rule.getDeviceEffects().shouldDisplayGrayscale()).isTrue(); @@ -4339,7 +4340,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { // New ZenPolicy differs from the default config assertThat(rule.getZenPolicy()).isNotNull(); - assertThat(rule.getZenPolicy().getPriorityChannels()).isEqualTo(ZenPolicy.STATE_DISALLOW); + assertThat(rule.getZenPolicy().getPriorityChannelsAllowed()).isEqualTo( + ZenPolicy.STATE_DISALLOW); ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); assertThat(storedRule.canBeUpdatedByApp()).isFalse(); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java index 57e11328d5e1..3a88294a0fa1 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenPolicyTest.java @@ -218,7 +218,7 @@ public class ZenPolicyTest extends UiServiceTestCase { // unset applied, channels setting keeps its state channelsPriority.apply(unset); - assertThat(channelsPriority.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_ALLOW); + assertThat(channelsPriority.getPriorityChannelsAllowed()).isEqualTo(ZenPolicy.STATE_ALLOW); } @Test @@ -234,7 +234,7 @@ public class ZenPolicyTest extends UiServiceTestCase { // priority channels (less strict state) cannot override a setting that sets it to none none.apply(priority); - assertThat(none.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_DISALLOW); + assertThat(none.getPriorityChannelsAllowed()).isEqualTo(ZenPolicy.STATE_DISALLOW); } @Test @@ -250,7 +250,7 @@ public class ZenPolicyTest extends UiServiceTestCase { // applying a policy with channelType=none overrides priority setting priority.apply(none); - assertThat(priority.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_DISALLOW); + assertThat(priority.getPriorityChannelsAllowed()).isEqualTo(ZenPolicy.STATE_DISALLOW); } @Test @@ -265,7 +265,7 @@ public class ZenPolicyTest extends UiServiceTestCase { // applying a policy with a set channel type actually goes through unset.apply(priority); - assertThat(unset.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_ALLOW); + assertThat(unset.getPriorityChannelsAllowed()).isEqualTo(ZenPolicy.STATE_ALLOW); } @Test @@ -379,7 +379,7 @@ public class ZenPolicyTest extends UiServiceTestCase { ZenPolicy.Builder builder = new ZenPolicy.Builder(); ZenPolicy policy = builder.build(); - assertThat(policy.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_UNSET); + assertThat(policy.getPriorityChannelsAllowed()).isEqualTo(ZenPolicy.STATE_UNSET); } @Test @@ -696,7 +696,7 @@ public class ZenPolicyTest extends UiServiceTestCase { builder.allowPriorityChannels(true); ZenPolicy policy = builder.build(); - assertThat(policy.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_UNSET); + assertThat(policy.getPriorityChannelsAllowed()).isEqualTo(ZenPolicy.STATE_UNSET); assertThat(policy.toString().contains("allowChannels")).isFalse(); } @@ -708,12 +708,12 @@ public class ZenPolicyTest extends UiServiceTestCase { ZenPolicy.Builder builder = new ZenPolicy.Builder(); builder.allowPriorityChannels(true); ZenPolicy policy = builder.build(); - assertThat(policy.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_ALLOW); + assertThat(policy.getPriorityChannelsAllowed()).isEqualTo(ZenPolicy.STATE_ALLOW); // disallow priority channels builder.allowPriorityChannels(false); policy = builder.build(); - assertThat(policy.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_DISALLOW); + assertThat(policy.getPriorityChannelsAllowed()).isEqualTo(ZenPolicy.STATE_DISALLOW); } @Test @@ -734,7 +734,7 @@ public class ZenPolicyTest extends UiServiceTestCase { assertThat(newPolicy.getVisualEffectBadge()).isEqualTo(ZenPolicy.STATE_DISALLOW); assertThat(newPolicy.getVisualEffectPeek()).isEqualTo(ZenPolicy.STATE_UNSET); - assertThat(newPolicy.getPriorityChannels()).isEqualTo(ZenPolicy.STATE_ALLOW); + assertThat(newPolicy.getPriorityChannelsAllowed()).isEqualTo(ZenPolicy.STATE_ALLOW); } @Test diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java index 7c2f7eedff9d..c8abd8d7297e 100644 --- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java +++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java @@ -166,6 +166,7 @@ class TestPhoneWindowManager { @Mock private PhoneWindowManager.ButtonOverridePermissionChecker mButtonOverridePermissionChecker; + @Mock private WindowWakeUpPolicy mWindowWakeUpPolicy; @Mock private IBinder mInputToken; @Mock private IBinder mImeTargetWindowToken; @@ -230,6 +231,10 @@ class TestPhoneWindowManager { TalkbackShortcutController getTalkbackShortcutController() { return new TestTalkbackShortcutController(mContext); } + + WindowWakeUpPolicy getWindowWakeUpPolicy() { + return mWindowWakeUpPolicy; + } } TestPhoneWindowManager(Context context, boolean supportSettingsUpdate) { @@ -620,7 +625,8 @@ class TestPhoneWindowManager { void assertPowerWakeUp() { mTestLooper.dispatchAll(); - verify(mPowerManager).wakeUp(anyLong(), anyInt(), anyString()); + verify(mWindowWakeUpPolicy) + .wakeUpFromKey(anyLong(), eq(KeyEvent.KEYCODE_POWER), anyBoolean()); } void assertNoPowerSleep() { 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 2a89b02482b3..31d6fa3e91f8 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityRecordTests.java @@ -3722,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/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java index 752dc5e8e7f6..0c1a9c3ab5b9 100644 --- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java @@ -886,8 +886,6 @@ public class SizeCompatTests extends WindowTestsBase { spyOn(mActivity.mLetterboxUiController); doReturn(true).when(mActivity.mLetterboxUiController) - .isSurfaceReadyToShow(any()); - doReturn(true).when(mActivity.mLetterboxUiController) .isSurfaceVisible(any()); assertTrue(mActivity.mLetterboxUiController.shouldShowLetterboxUi( diff --git a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java index 51df1d4cb14d..7d8eb90ea79c 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/TransitionTests.java @@ -1602,6 +1602,33 @@ public class TransitionTests extends WindowTestsBase { } @Test + public void testTransientWithParallelLaunch() { + final Task recentTask = mDisplayContent.getDefaultTaskDisplayArea().getRootHomeTask(); + final ActivityRecord recent = new ActivityBuilder(mAtm).setTask(recentTask) + .setVisible(false).build(); + final ActivityRecord app = new ActivityBuilder(mAtm).setCreateTask(true).build(); + final Task appTask = app.getTask(); + registerTestTransitionPlayer(); + final TransitionController controller = mRootWindowContainer.mTransitionController; + final Transition transition = createTestTransition(TRANSIT_OPEN, controller); + transition.mParallelCollectType = Transition.PARALLEL_TYPE_RECENTS; + controller.moveToCollecting(transition); + transition.collect(recentTask); + transition.collect(appTask); + transition.setTransientLaunch(recent, appTask); + recentTask.moveToFront("move-recent-to-front"); + transition.setAllReady(); + transition.start(); + // Assume that the app starts another activity in its task. + final Transition newTransition = controller.createAndStartCollecting(TRANSIT_OPEN); + + assertEquals(newTransition, controller.getCollectingTransition()); + assertTrue(controller.mWaitingTransitions.contains(transition)); + assertTrue(controller.isTransientHide(appTask)); + assertTrue(controller.isTransientVisible(appTask)); + } + + @Test public void testNotReadyPushPop() { final TransitionController controller = new TestTransitionController(mAtm); controller.setSyncEngine(mWm.mSyncEngine); diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java index fe9d83776ad9..06afa381c9c0 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java @@ -1068,7 +1068,7 @@ public class WindowManagerServiceTests extends WindowTestsBase { invocationOnMock.callRealMethod(); return null; }).when(surface).lockCanvas(any()); - mWm.mAccessibilityController.drawMagnifiedRegionBorderIfNeeded(displayId, mTransaction); + mWm.mAccessibilityController.drawMagnifiedRegionBorderIfNeeded(displayId); waitUntilHandlersIdle(); try { verify(surface).lockCanvas(any()); @@ -1076,14 +1076,9 @@ public class WindowManagerServiceTests extends WindowTestsBase { clearInvocations(surface); // Invalidate and redraw. mWm.mAccessibilityController.onDisplaySizeChanged(mDisplayContent); - mWm.mAccessibilityController.drawMagnifiedRegionBorderIfNeeded(displayId, mTransaction); + mWm.mAccessibilityController.drawMagnifiedRegionBorderIfNeeded(displayId); // Turn off magnification to release surface. mWm.mAccessibilityController.setMagnificationCallbacks(displayId, null); - if (!com.android.window.flags.Flags.drawMagnifierBorderOutsideWmlock()) { - verify(surface).release(); - assertTrue(lockCanvasInWmLock[0]); - return; - } waitUntilHandlersIdle(); // lockCanvas must not be called after releasing. verify(surface, never()).lockCanvas(any()); 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/android/telecom/TelecomManager.java b/telecomm/java/android/telecom/TelecomManager.java index e81f48280e46..2c6e1e48b57f 100644 --- a/telecomm/java/android/telecom/TelecomManager.java +++ b/telecomm/java/android/telecom/TelecomManager.java @@ -1360,7 +1360,7 @@ public class TelecomManager { /** * Returns a list of {@link PhoneAccountHandle}s which can be used to make and receive phone * calls. The returned list includes those accounts which have been explicitly enabled by - * the user or other users visible to the user. + * the user or enabled by other users but visible to the user. * * @see #EXTRA_PHONE_ACCOUNT_HANDLE * @return A list of {@code PhoneAccountHandle} objects. @@ -1486,7 +1486,7 @@ public class TelecomManager { return service.getCallCapablePhoneAccounts(includeDisabledAccounts, mContext.getOpPackageName(), mContext.getAttributionTag(), true).getList(); } catch (RemoteException e) { - throw e.rethrowAsRuntimeException(); + throw e.rethrowFromSystemServer(); } } 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/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java index cbd552454642..c1ceaef64d5d 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -25,6 +25,7 @@ import static com.android.internal.util.Preconditions.checkNotNull; import android.Manifest; import android.annotation.BytesLong; import android.annotation.CallbackExecutor; +import android.annotation.CurrentTimeMillisLong; import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.LongDef; @@ -18714,51 +18715,93 @@ public class TelephonyManager { * call diagnostic data * @hide */ - public static class EmergencyCallDiagnosticParams { + @SystemApi + @FlaggedApi(com.android.server.telecom.flags.Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES) + public static final class EmergencyCallDiagnosticParams { + public static final class Builder { + private boolean mCollectTelecomDumpSys; + private boolean mCollectTelephonyDumpsys; + + // If this is set to a value other than -1L, then the logcat collection is enabled. + // Logcat lines with this time or greater are collected how much is collected is + // dependent on internal implementation. Time represented as milliseconds since boot. + private long mLogcatStartTimeMillis = sUnsetLogcatStartTime; + + /** + * Allows enabling of telecom dumpsys collection. + * @param collectTelecomDumpsys Determines whether telecom dumpsys should be collected. + * @return Builder instance corresponding to the configured call diagnostic params. + */ + public @NonNull Builder setTelecomDumpSysCollectionEnabled( + boolean collectTelecomDumpsys) { + mCollectTelecomDumpSys = collectTelecomDumpsys; + return this; + } + + /** + * Allows enabling of telephony dumpsys collection. + * @param collectTelephonyDumpsys Determines if telephony dumpsys should be collected. + * @return Builder instance corresponding to the configured call diagnostic params. + */ + public @NonNull Builder setTelephonyDumpSysCollectionEnabled( + boolean collectTelephonyDumpsys) { + mCollectTelephonyDumpsys = collectTelephonyDumpsys; + return this; + } + + /** + * Allows enabling of logcat (system,radio) collection. + * @param startTimeMillis Enables logcat collection as of the indicated timestamp. + * @return Builder instance corresponding to the configured call diagnostic params. + */ + public @NonNull Builder setLogcatCollectionStartTimeMillis( + @CurrentTimeMillisLong long startTimeMillis) { + mLogcatStartTimeMillis = startTimeMillis; + return this; + } - private boolean mCollectTelecomDumpSys; - private boolean mCollectTelephonyDumpsys; - private boolean mCollectLogcat; + /** + * Build the EmergencyCallDiagnosticParams from the provided Builder config. + * @return {@link EmergencyCallDiagnosticParams} instance from provided builder. + */ + public @NonNull EmergencyCallDiagnosticParams build() { + return new EmergencyCallDiagnosticParams(mCollectTelecomDumpSys, + mCollectTelephonyDumpsys, mLogcatStartTimeMillis); + } + } - //logcat lines with this time or greater are collected - //how much is collected is dependent on internal implementation. - //Time represented as milliseconds since January 1, 1970 UTC + private boolean mCollectTelecomDumpSys; + private boolean mCollectTelephonyDumpsys; + private boolean mCollectLogcat; private long mLogcatStartTimeMillis; + private static long sUnsetLogcatStartTime = -1L; - public boolean isTelecomDumpSysCollectionEnabled() { - return mCollectTelecomDumpSys; + private EmergencyCallDiagnosticParams(boolean collectTelecomDumpSys, + boolean collectTelephonyDumpsys, long logcatStartTimeMillis) { + mCollectTelecomDumpSys = collectTelecomDumpSys; + mCollectTelephonyDumpsys = collectTelephonyDumpsys; + mLogcatStartTimeMillis = logcatStartTimeMillis; + mCollectLogcat = logcatStartTimeMillis != sUnsetLogcatStartTime; } - public void setTelecomDumpSysCollection(boolean collectTelecomDumpSys) { - mCollectTelecomDumpSys = collectTelecomDumpSys; + public boolean isTelecomDumpSysCollectionEnabled() { + return mCollectTelecomDumpSys; } public boolean isTelephonyDumpSysCollectionEnabled() { return mCollectTelephonyDumpsys; } - public void setTelephonyDumpSysCollection(boolean collectTelephonyDumpsys) { - mCollectTelephonyDumpsys = collectTelephonyDumpsys; - } - public boolean isLogcatCollectionEnabled() { return mCollectLogcat; } - public long getLogcatStartTime() + public long getLogcatCollectionStartTimeMillis() { return mLogcatStartTimeMillis; } - public void setLogcatCollection(boolean collectLogcat, long startTimeMillis) { - mCollectLogcat = collectLogcat; - if(mCollectLogcat) - { - mLogcatStartTimeMillis = startTimeMillis; - } - } - @Override public String toString() { return "EmergencyCallDiagnosticParams{" + @@ -18774,11 +18817,12 @@ public class TelephonyManager { * Request telephony to persist state for debugging emergency call failures. * * @param dropboxTag Tag to use when persisting data to dropbox service. - * - * @see params Parameters controlling what is collected + * @param params Parameters controlling what is collected. * * @hide */ + @SystemApi + @FlaggedApi(com.android.server.telecom.flags.Flags.FLAG_TELECOM_RESOLVE_HIDDEN_DEPENDENCIES) @RequiresPermission(android.Manifest.permission.DUMP) public void persistEmergencyCallDiagnosticData(@NonNull String dropboxTag, @NonNull EmergencyCallDiagnosticParams params) { @@ -18791,7 +18835,7 @@ public class TelephonyManager { if (telephony != null) { telephony.persistEmergencyCallDiagnosticData(dropboxTag, params.isLogcatCollectionEnabled(), - params.getLogcatStartTime(), + params.getLogcatCollectionStartTimeMillis(), params.isTelecomDumpSysCollectionEnabled(), params.isTelephonyDumpSysCollectionEnabled()); } 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/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt index 256a4696b763..566e51a9062a 100644 --- a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt +++ b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt @@ -24,6 +24,7 @@ import android.hardware.input.InputManager import android.hardware.input.InputManagerGlobal import android.os.test.TestLooper import android.platform.test.annotations.Presubmit +import android.platform.test.flag.junit.SetFlagsRule import android.provider.Settings import android.test.mock.MockContentResolver import android.view.Display @@ -72,6 +73,9 @@ class InputManagerServiceTests { @get:Rule val fakeSettingsProviderRule = FakeSettingsProvider.rule()!! + @get:Rule + val setFlagsRule = SetFlagsRule() + @Mock private lateinit var native: NativeInputManagerService @@ -170,6 +174,8 @@ class InputManagerServiceTests { @Test fun testSetVirtualMousePointerDisplayId() { + setFlagsRule.disableFlags(com.android.input.flags.Flags.FLAG_ENABLE_POINTER_CHOREOGRAPHER) + // Set the virtual mouse pointer displayId, and ensure that the calling thread is blocked // until the native callback happens. var countDownLatch = CountDownLatch(1) @@ -221,6 +227,8 @@ class InputManagerServiceTests { @Test fun testSetVirtualMousePointerDisplayId_unsuccessfulUpdate() { + setFlagsRule.disableFlags(com.android.input.flags.Flags.FLAG_ENABLE_POINTER_CHOREOGRAPHER) + // Set the virtual mouse pointer displayId, and ensure that the calling thread is blocked // until the native callback happens. val countDownLatch = CountDownLatch(1) @@ -246,6 +254,8 @@ class InputManagerServiceTests { @Test fun testSetVirtualMousePointerDisplayId_competingRequests() { + setFlagsRule.disableFlags(com.android.input.flags.Flags.FLAG_ENABLE_POINTER_CHOREOGRAPHER) + val firstRequestSyncLatch = CountDownLatch(1) doAnswer { firstRequestSyncLatch.countDown() @@ -289,6 +299,8 @@ class InputManagerServiceTests { @Test fun onDisplayRemoved_resetAllAdditionalInputProperties() { + setFlagsRule.disableFlags(com.android.input.flags.Flags.FLAG_ENABLE_POINTER_CHOREOGRAPHER) + setVirtualMousePointerDisplayIdAndVerify(10) localService.setPointerIconVisible(false, 10) @@ -313,6 +325,8 @@ class InputManagerServiceTests { @Test fun updateAdditionalInputPropertiesForOverrideDisplay() { + setFlagsRule.disableFlags(com.android.input.flags.Flags.FLAG_ENABLE_POINTER_CHOREOGRAPHER) + setVirtualMousePointerDisplayIdAndVerify(10) localService.setPointerIconVisible(false, 10) @@ -341,6 +355,8 @@ class InputManagerServiceTests { @Test fun setAdditionalInputPropertiesBeforeOverride() { + setFlagsRule.disableFlags(com.android.input.flags.Flags.FLAG_ENABLE_POINTER_CHOREOGRAPHER) + localService.setPointerIconVisible(false, 10) localService.setMousePointerAccelerationEnabled(false, 10) 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\"") } |