diff options
344 files changed, 8708 insertions, 2786 deletions
diff --git a/Android.bp b/Android.bp index 54cb2684068d..49a6a2b3ec65 100644 --- a/Android.bp +++ b/Android.bp @@ -103,10 +103,10 @@ filegroup { ":android.hardware.gnss-V2-java-source", ":android.hardware.graphics.common-V3-java-source", ":android.hardware.keymaster-V4-java-source", - ":android.hardware.radio-V3-java-source", - ":android.hardware.radio.data-V3-java-source", - ":android.hardware.radio.network-V3-java-source", - ":android.hardware.radio.voice-V3-java-source", + ":android.hardware.radio-V4-java-source", + ":android.hardware.radio.data-V4-java-source", + ":android.hardware.radio.network-V4-java-source", + ":android.hardware.radio.voice-V4-java-source", ":android.hardware.security.secureclock-V1-java-source", ":android.hardware.thermal-V3-java-source", ":android.hardware.tv.tuner-V3-java-source", @@ -232,13 +232,13 @@ java_library { "android.hardware.gnss-V2.1-java", "android.hardware.health-V1.0-java-constants", "android.hardware.radio-V1.6-java", - "android.hardware.radio.data-V3-java", - "android.hardware.radio.ims-V2-java", - "android.hardware.radio.messaging-V3-java", - "android.hardware.radio.modem-V3-java", - "android.hardware.radio.network-V3-java", - "android.hardware.radio.sim-V3-java", - "android.hardware.radio.voice-V3-java", + "android.hardware.radio.data-V4-java", + "android.hardware.radio.ims-V3-java", + "android.hardware.radio.messaging-V4-java", + "android.hardware.radio.modem-V4-java", + "android.hardware.radio.network-V4-java", + "android.hardware.radio.sim-V4-java", + "android.hardware.radio.voice-V4-java", "android.hardware.thermal-V1.0-java-constants", "android.hardware.thermal-V1.0-java", "android.hardware.thermal-V1.1-java", diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java index 8bd3ef4f4d1a..637c726a9bd1 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java @@ -36,10 +36,14 @@ import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.AlarmManager; import android.app.UidObserver; +import android.app.compat.CompatChanges; import android.app.job.JobInfo; import android.app.usage.UsageEvents; import android.app.usage.UsageStatsManagerInternal; import android.app.usage.UsageStatsManagerInternal.UsageEventListener; +import android.compat.annotation.ChangeId; +import android.compat.annotation.Disabled; +import android.compat.annotation.Overridable; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; @@ -132,6 +136,27 @@ public final class QuotaController extends StateController { return (int) (val ^ (val >>> 32)); } + /** + * When enabled this change id overrides the default quota policy enforcement to the jobs + * running in the foreground process state. + */ + // TODO: b/379681266 - Might need some refactoring for a better app-compat strategy. + @VisibleForTesting + @ChangeId + @Disabled // Disabled by default + @Overridable // The change can be overridden in user build + static final long OVERRIDE_QUOTA_ENFORCEMENT_TO_FGS_JOBS = 341201311L; + + /** + * When enabled this change id overrides the default quota policy enforcement policy + * the jobs started when app was in the TOP state. + */ + @VisibleForTesting + @ChangeId + @Disabled // Disabled by default + @Overridable // The change can be overridden in user build. + static final long OVERRIDE_QUOTA_ENFORCEMENT_TO_TOP_STARTED_JOBS = 374323858L; + @VisibleForTesting static class ExecutionStats { /** @@ -622,7 +647,9 @@ public final class QuotaController extends StateController { } final int uid = jobStatus.getSourceUid(); - if (!Flags.enforceQuotaPolicyToTopStartedJobs() && mTopAppCache.get(uid)) { + if ((!Flags.enforceQuotaPolicyToTopStartedJobs() + || CompatChanges.isChangeEnabled(OVERRIDE_QUOTA_ENFORCEMENT_TO_TOP_STARTED_JOBS, + uid)) && mTopAppCache.get(uid)) { if (DEBUG) { Slog.d(TAG, jobStatus.toShortString() + " is top started job"); } @@ -659,7 +686,9 @@ public final class QuotaController extends StateController { timer.stopTrackingJob(jobStatus); } } - if (!Flags.enforceQuotaPolicyToTopStartedJobs()) { + if (!Flags.enforceQuotaPolicyToTopStartedJobs() + || CompatChanges.isChangeEnabled(OVERRIDE_QUOTA_ENFORCEMENT_TO_TOP_STARTED_JOBS, + jobStatus.getSourceUid())) { mTopStartedJobs.remove(jobStatus); } } @@ -772,7 +801,13 @@ public final class QuotaController extends StateController { /** @return true if the job was started while the app was in the TOP state. */ private boolean isTopStartedJobLocked(@NonNull final JobStatus jobStatus) { - return !Flags.enforceQuotaPolicyToTopStartedJobs() && mTopStartedJobs.contains(jobStatus); + if (!Flags.enforceQuotaPolicyToTopStartedJobs() + || CompatChanges.isChangeEnabled(OVERRIDE_QUOTA_ENFORCEMENT_TO_TOP_STARTED_JOBS, + jobStatus.getSourceUid())) { + return mTopStartedJobs.contains(jobStatus); + } + + return false; } /** Returns the maximum amount of time this job could run for. */ @@ -2634,9 +2669,13 @@ public final class QuotaController extends StateController { } @VisibleForTesting - int getProcessStateQuotaFreeThreshold() { - return Flags.enforceQuotaPolicyToFgsJobs() ? ActivityManager.PROCESS_STATE_BOUND_TOP : - ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE; + int getProcessStateQuotaFreeThreshold(int uid) { + if (Flags.enforceQuotaPolicyToFgsJobs() + && !CompatChanges.isChangeEnabled(OVERRIDE_QUOTA_ENFORCEMENT_TO_FGS_JOBS, uid)) { + return ActivityManager.PROCESS_STATE_BOUND_TOP; + } + + return ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE; } private class QcHandler extends Handler { @@ -2776,7 +2815,7 @@ public final class QuotaController extends StateController { isQuotaFree = true; } else { final boolean reprocess; - if (procState <= getProcessStateQuotaFreeThreshold()) { + if (procState <= getProcessStateQuotaFreeThreshold(uid)) { reprocess = !mForegroundUids.get(uid); mForegroundUids.put(uid, true); isQuotaFree = true; diff --git a/core/api/current.txt b/core/api/current.txt index 81c85a9dc8c0..76f3e5a3cd39 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -3830,7 +3830,7 @@ package android.accounts { method @RequiresPermission(value="android.permission.AUTHENTICATE_ACCOUNTS", apis="..22") public boolean notifyAccountAuthenticated(android.accounts.Account); method @RequiresPermission(value="android.permission.AUTHENTICATE_ACCOUNTS", apis="..22") public String peekAuthToken(android.accounts.Account, String); method @Deprecated @RequiresPermission(value="android.permission.MANAGE_ACCOUNTS", apis="..22") public android.accounts.AccountManagerFuture<java.lang.Boolean> removeAccount(android.accounts.Account, android.accounts.AccountManagerCallback<java.lang.Boolean>, android.os.Handler); - method @RequiresPermission(value="android.permission.MANAGE_ACCOUNTS", apis="..22") public android.accounts.AccountManagerFuture<android.os.Bundle> removeAccount(android.accounts.Account, android.app.Activity, android.accounts.AccountManagerCallback<android.os.Bundle>, android.os.Handler); + method @FlaggedApi("android.app.admin.flags.split_create_managed_profile_enabled") @RequiresPermission(value="android.permission.REMOVE_ACCOUNTS", conditional=true) public android.accounts.AccountManagerFuture<android.os.Bundle> removeAccount(android.accounts.Account, android.app.Activity, android.accounts.AccountManagerCallback<android.os.Bundle>, android.os.Handler); method @RequiresPermission(value="android.permission.AUTHENTICATE_ACCOUNTS", apis="..22") public boolean removeAccountExplicitly(android.accounts.Account); method public void removeOnAccountsUpdatedListener(android.accounts.OnAccountsUpdateListener); method @RequiresPermission(value="android.permission.AUTHENTICATE_ACCOUNTS", apis="..22") public android.accounts.AccountManagerFuture<android.accounts.Account> renameAccount(android.accounts.Account, @Size(min=1) String, android.accounts.AccountManagerCallback<android.accounts.Account>, android.os.Handler); @@ -4614,6 +4614,7 @@ package android.app { method public void reportFullyDrawn(); method public android.view.DragAndDropPermissions requestDragAndDropPermissions(android.view.DragEvent); method public void requestFullscreenMode(int, @Nullable android.os.OutcomeReceiver<java.lang.Void,java.lang.Throwable>); + method @FlaggedApi("com.android.window.flags.enable_desktop_windowing_app_to_web_education") public final void requestOpenInBrowserEducation(); method public final void requestPermissions(@NonNull String[], int); method @FlaggedApi("android.permission.flags.device_aware_permission_apis_enabled") public final void requestPermissions(@NonNull String[], int, int); method public final void requestShowKeyboardShortcuts(); @@ -4639,7 +4640,6 @@ package android.app { method public void setInheritShowWhenLocked(boolean); method public void setIntent(android.content.Intent); method @FlaggedApi("android.security.content_uri_permission_apis") public void setIntent(@Nullable android.content.Intent, @Nullable android.app.ComponentCaller); - method @FlaggedApi("com.android.window.flags.enable_desktop_windowing_app_to_web_education") public final void setLimitSystemEducationDialogs(boolean); method public void setLocusContext(@Nullable android.content.LocusId, @Nullable android.os.Bundle); method public final void setMediaController(android.media.session.MediaController); method public void setPictureInPictureParams(@NonNull android.app.PictureInPictureParams); @@ -13399,6 +13399,7 @@ package android.content.pm { field public static final String FEATURE_BACKUP = "android.software.backup"; field public static final String FEATURE_BLUETOOTH = "android.hardware.bluetooth"; field public static final String FEATURE_BLUETOOTH_LE = "android.hardware.bluetooth_le"; + field @FlaggedApi("com.android.ranging.flags.ranging_cs_enabled") public static final String FEATURE_BLUETOOTH_LE_CHANNEL_SOUNDING = "android.hardware.bluetooth_le.channel_sounding"; field public static final String FEATURE_CAMERA = "android.hardware.camera"; field public static final String FEATURE_CAMERA_ANY = "android.hardware.camera.any"; field public static final String FEATURE_CAMERA_AR = "android.hardware.camera.ar"; @@ -13808,7 +13809,7 @@ package android.content.pm { field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_CAMERA}, anyOf={android.Manifest.permission.CAMERA}, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_CAMERA = 64; // 0x40 field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE}, anyOf={android.Manifest.permission.BLUETOOTH_ADVERTISE, android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_SCAN, android.Manifest.permission.CHANGE_NETWORK_STATE, android.Manifest.permission.CHANGE_WIFI_STATE, android.Manifest.permission.CHANGE_WIFI_MULTICAST_STATE, android.Manifest.permission.NFC, android.Manifest.permission.TRANSMIT_IR, android.Manifest.permission.UWB_RANGING, android.Manifest.permission.RANGING}, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE = 16; // 0x10 field @RequiresPermission(value=android.Manifest.permission.FOREGROUND_SERVICE_DATA_SYNC, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_DATA_SYNC = 1; // 0x1 - field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_HEALTH}, anyOf={android.Manifest.permission.ACTIVITY_RECOGNITION, android.Manifest.permission.BODY_SENSORS, android.Manifest.permission.HIGH_SAMPLING_RATE_SENSORS}) public static final int FOREGROUND_SERVICE_TYPE_HEALTH = 256; // 0x100 + field @FlaggedApi("android.permission.flags.replace_body_sensor_permission_enabled") @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_HEALTH}, anyOf={android.Manifest.permission.ACTIVITY_RECOGNITION, android.Manifest.permission.HIGH_SAMPLING_RATE_SENSORS, android.health.connect.HealthPermissions.READ_HEART_RATE, android.health.connect.HealthPermissions.READ_SKIN_TEMPERATURE, android.health.connect.HealthPermissions.READ_OXYGEN_SATURATION}) public static final int FOREGROUND_SERVICE_TYPE_HEALTH = 256; // 0x100 field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_LOCATION}, anyOf={android.Manifest.permission.ACCESS_COARSE_LOCATION, android.Manifest.permission.ACCESS_FINE_LOCATION}, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_LOCATION = 8; // 0x8 field public static final int FOREGROUND_SERVICE_TYPE_MANIFEST = -1; // 0xffffffff field @RequiresPermission(value=android.Manifest.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK = 2; // 0x2 @@ -21213,6 +21214,7 @@ package android.inputmethodservice { method public void setExtractView(android.view.View); method public void setExtractViewShown(boolean); method public void setInputView(android.view.View); + method @FlaggedApi("android.view.inputmethod.adaptive_handwriting_bounds") public final void setStylusHandwritingRegion(@NonNull android.graphics.Region); method public final void setStylusHandwritingSessionTimeout(@NonNull java.time.Duration); method public final boolean shouldOfferSwitchingToNextInputMethod(); method public void showStatusIcon(@DrawableRes int); @@ -23189,7 +23191,6 @@ package android.media { method public boolean isVendor(); field @FlaggedApi("android.media.codec.in_process_sw_audio_codec") public static final int SECURITY_MODEL_MEMORY_SAFE = 1; // 0x1 field @FlaggedApi("android.media.codec.in_process_sw_audio_codec") public static final int SECURITY_MODEL_SANDBOXED = 0; // 0x0 - field @FlaggedApi("android.media.codec.in_process_sw_audio_codec") public static final int SECURITY_MODEL_TRUSTED_CONTENT_ONLY = 2; // 0x2 } public static final class MediaCodecInfo.AudioCapabilities { @@ -24080,7 +24081,6 @@ package android.media { field public static final int COLOR_TRANSFER_ST2084 = 6; // 0x6 field @FlaggedApi("android.media.codec.in_process_sw_audio_codec") public static final int FLAG_SECURITY_MODEL_MEMORY_SAFE = 2; // 0x2 field @FlaggedApi("android.media.codec.in_process_sw_audio_codec") public static final int FLAG_SECURITY_MODEL_SANDBOXED = 1; // 0x1 - field @FlaggedApi("android.media.codec.in_process_sw_audio_codec") public static final int FLAG_SECURITY_MODEL_TRUSTED_CONTENT_ONLY = 4; // 0x4 field public static final String KEY_AAC_DRC_ALBUM_MODE = "aac-drc-album-mode"; field public static final String KEY_AAC_DRC_ATTENUATION_FACTOR = "aac-drc-cut-level"; field public static final String KEY_AAC_DRC_BOOST_FACTOR = "aac-drc-boost-level"; @@ -33809,7 +33809,7 @@ package android.os { } public interface IBinder { - method @FlaggedApi("android.os.binder_frozen_state_change_callback") public default void addFrozenStateChangeCallback(@NonNull android.os.IBinder.FrozenStateChangeCallback) throws android.os.RemoteException; + method @FlaggedApi("android.os.binder_frozen_state_change_callback") public default void addFrozenStateChangeCallback(@NonNull java.util.concurrent.Executor, @NonNull android.os.IBinder.FrozenStateChangeCallback) throws android.os.RemoteException; method public void dump(@NonNull java.io.FileDescriptor, @Nullable String[]) throws android.os.RemoteException; method public void dumpAsync(@NonNull java.io.FileDescriptor, @Nullable String[]) throws android.os.RemoteException; method @Nullable public String getInterfaceDescriptor() throws android.os.RemoteException; @@ -34440,6 +34440,7 @@ package android.os { method public void finishBroadcast(); method public Object getBroadcastCookie(int); method public E getBroadcastItem(int); + method @FlaggedApi("android.os.binder_frozen_state_change_callback") @Nullable public java.util.concurrent.Executor getExecutor(); method @FlaggedApi("android.os.binder_frozen_state_change_callback") public int getFrozenCalleePolicy(); method @FlaggedApi("android.os.binder_frozen_state_change_callback") public int getMaxQueueSize(); method public Object getRegisteredCallbackCookie(int); @@ -34460,6 +34461,7 @@ package android.os { @FlaggedApi("android.os.binder_frozen_state_change_callback") public static final class RemoteCallbackList.Builder<E extends android.os.IInterface> { ctor public RemoteCallbackList.Builder(int); method @NonNull public android.os.RemoteCallbackList<E> build(); + method @NonNull public android.os.RemoteCallbackList.Builder setExecutor(@NonNull java.util.concurrent.Executor); method @NonNull public android.os.RemoteCallbackList.Builder setInterfaceDiedCallback(@NonNull android.os.RemoteCallbackList.Builder.InterfaceDiedCallback<E>); method @NonNull public android.os.RemoteCallbackList.Builder setMaxQueueSize(int); } @@ -41179,6 +41181,18 @@ package android.service.carrier { method @Deprecated public void onSendTextSms(@NonNull String, int, @NonNull String, @NonNull android.service.carrier.CarrierMessagingService.ResultCallback<android.service.carrier.CarrierMessagingService.SendSmsResult>); method public void onSendTextSms(@NonNull String, int, @NonNull String, int, @NonNull android.service.carrier.CarrierMessagingService.ResultCallback<android.service.carrier.CarrierMessagingService.SendSmsResult>); field public static final int DOWNLOAD_STATUS_ERROR = 2; // 0x2 + field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int DOWNLOAD_STATUS_MMS_ERROR_CONFIGURATION_ERROR = 606; // 0x25e + field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int DOWNLOAD_STATUS_MMS_ERROR_DATA_DISABLED = 610; // 0x262 + field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int DOWNLOAD_STATUS_MMS_ERROR_HTTP_FAILURE = 603; // 0x25b + field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int DOWNLOAD_STATUS_MMS_ERROR_INACTIVE_SUBSCRIPTION = 609; // 0x261 + field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int DOWNLOAD_STATUS_MMS_ERROR_INVALID_APN = 601; // 0x259 + field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int DOWNLOAD_STATUS_MMS_ERROR_INVALID_SUBSCRIPTION_ID = 608; // 0x260 + field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int DOWNLOAD_STATUS_MMS_ERROR_IO_ERROR = 604; // 0x25c + field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int DOWNLOAD_STATUS_MMS_ERROR_MMS_DISABLED_BY_CARRIER = 611; // 0x263 + field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int DOWNLOAD_STATUS_MMS_ERROR_NO_DATA_NETWORK = 607; // 0x25f + field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int DOWNLOAD_STATUS_MMS_ERROR_RETRY = 605; // 0x25d + field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int DOWNLOAD_STATUS_MMS_ERROR_UNABLE_CONNECT_MMS = 602; // 0x25a + field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int DOWNLOAD_STATUS_MMS_ERROR_UNSPECIFIED = 600; // 0x258 field public static final int DOWNLOAD_STATUS_OK = 0; // 0x0 field public static final int DOWNLOAD_STATUS_RETRY_ON_CARRIER_NETWORK = 1; // 0x1 field public static final int RECEIVE_OPTIONS_DEFAULT = 0; // 0x0 @@ -41186,7 +41200,38 @@ package android.service.carrier { field public static final int RECEIVE_OPTIONS_SKIP_NOTIFY_WHEN_CREDENTIAL_PROTECTED_STORAGE_UNAVAILABLE = 2; // 0x2 field public static final int SEND_FLAG_REQUEST_DELIVERY_STATUS = 1; // 0x1 field public static final int SEND_STATUS_ERROR = 2; // 0x2 + field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_MMS_ERROR_CONFIGURATION_ERROR = 406; // 0x196 + field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_MMS_ERROR_DATA_DISABLED = 410; // 0x19a + field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_MMS_ERROR_HTTP_FAILURE = 403; // 0x193 + field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_MMS_ERROR_INACTIVE_SUBSCRIPTION = 409; // 0x199 + field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_MMS_ERROR_INVALID_APN = 401; // 0x191 + field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_MMS_ERROR_INVALID_SUBSCRIPTION_ID = 408; // 0x198 + field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_MMS_ERROR_IO_ERROR = 404; // 0x194 + field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_MMS_ERROR_MMS_DISABLED_BY_CARRIER = 411; // 0x19b + field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_MMS_ERROR_NO_DATA_NETWORK = 407; // 0x197 + field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_MMS_ERROR_RETRY = 405; // 0x195 + field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_MMS_ERROR_UNABLE_CONNECT_MMS = 402; // 0x192 + field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_MMS_ERROR_UNSPECIFIED = 400; // 0x190 field public static final int SEND_STATUS_OK = 0; // 0x0 + field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_RESULT_CANCELLED = 215; // 0xd7 + field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_RESULT_ENCODING_ERROR = 212; // 0xd4 + field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_RESULT_ERROR_FDN_CHECK_FAILURE = 204; // 0xcc + field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_RESULT_ERROR_GENERIC_FAILURE = 200; // 0xc8 + field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_RESULT_ERROR_LIMIT_EXCEEDED = 203; // 0xcb + field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_RESULT_ERROR_NO_SERVICE = 202; // 0xca + field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_RESULT_ERROR_NULL_PDU = 201; // 0xc9 + field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_RESULT_ERROR_SHORT_CODE_NEVER_ALLOWED = 206; // 0xce + field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_RESULT_ERROR_SHORT_CODE_NOT_ALLOWED = 205; // 0xcd + field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_RESULT_INVALID_ARGUMENTS = 208; // 0xd0 + field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_RESULT_INVALID_SMSC_ADDRESS = 213; // 0xd5 + field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_RESULT_INVALID_SMS_FORMAT = 210; // 0xd2 + field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_RESULT_INVALID_STATE = 209; // 0xd1 + field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_RESULT_NETWORK_ERROR = 211; // 0xd3 + field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_RESULT_NETWORK_REJECT = 207; // 0xcf + field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_RESULT_OPERATION_NOT_ALLOWED = 214; // 0xd6 + field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_RESULT_REQUEST_NOT_SUPPORTED = 216; // 0xd8 + field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_RESULT_SMS_BLOCKED_DURING_EMERGENCY = 217; // 0xd9 + field @FlaggedApi("com.android.internal.telephony.flags.temporary_failures_in_carrier_messaging_service") public static final int SEND_STATUS_RESULT_SMS_SEND_RETRY_FAILED = 218; // 0xda field public static final int SEND_STATUS_RETRY_ON_CARRIER_NETWORK = 1; // 0x1 field public static final String SERVICE_INTERFACE = "android.service.carrier.CarrierMessagingService"; } diff --git a/core/api/system-current.txt b/core/api/system-current.txt index bc0037d9318f..a171f3dc4c17 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -103,6 +103,7 @@ package android { field public static final String BRIGHTNESS_SLIDER_USAGE = "android.permission.BRIGHTNESS_SLIDER_USAGE"; field public static final String BROADCAST_CLOSE_SYSTEM_DIALOGS = "android.permission.BROADCAST_CLOSE_SYSTEM_DIALOGS"; field @Deprecated public static final String BROADCAST_NETWORK_PRIVILEGED = "android.permission.BROADCAST_NETWORK_PRIVILEGED"; + field @FlaggedApi("android.media.audio.concurrent_audio_record_bypass_permission") public static final String BYPASS_CONCURRENT_RECORD_AUDIO_RESTRICTION = "android.permission.BYPASS_CONCURRENT_RECORD_AUDIO_RESTRICTION"; field public static final String BYPASS_ROLE_QUALIFICATION = "android.permission.BYPASS_ROLE_QUALIFICATION"; field public static final String CALL_AUDIO_INTERCEPTION = "android.permission.CALL_AUDIO_INTERCEPTION"; field public static final String CAMERA_DISABLE_TRANSMIT_LED = "android.permission.CAMERA_DISABLE_TRANSMIT_LED"; @@ -134,6 +135,7 @@ package android { field public static final String CONTROL_KEYGUARD_SECURE_NOTIFICATIONS = "android.permission.CONTROL_KEYGUARD_SECURE_NOTIFICATIONS"; field public static final String CONTROL_OEM_PAID_NETWORK_PREFERENCE = "android.permission.CONTROL_OEM_PAID_NETWORK_PREFERENCE"; field public static final String CONTROL_VPN = "android.permission.CONTROL_VPN"; + field @FlaggedApi("android.app.admin.flags.split_create_managed_profile_enabled") public static final String COPY_ACCOUNTS = "android.permission.COPY_ACCOUNTS"; field public static final String CREATE_USERS = "android.permission.CREATE_USERS"; field public static final String CREATE_VIRTUAL_DEVICE = "android.permission.CREATE_VIRTUAL_DEVICE"; field public static final String CRYPT_KEEPER = "android.permission.CRYPT_KEEPER"; @@ -229,6 +231,7 @@ package android { field public static final String MANAGE_ROTATION_RESOLVER = "android.permission.MANAGE_ROTATION_RESOLVER"; field public static final String MANAGE_SAFETY_CENTER = "android.permission.MANAGE_SAFETY_CENTER"; field public static final String MANAGE_SEARCH_UI = "android.permission.MANAGE_SEARCH_UI"; + field @FlaggedApi("android.security.secure_lockdown") public static final String MANAGE_SECURE_LOCK_DEVICE = "android.permission.MANAGE_SECURE_LOCK_DEVICE"; field public static final String MANAGE_SENSOR_PRIVACY = "android.permission.MANAGE_SENSOR_PRIVACY"; field public static final String MANAGE_SMARTSPACE = "android.permission.MANAGE_SMARTSPACE"; field public static final String MANAGE_SOUND_TRIGGER = "android.permission.MANAGE_SOUND_TRIGGER"; @@ -345,6 +348,7 @@ package android { field public static final String REGISTER_SIM_SUBSCRIPTION = "android.permission.REGISTER_SIM_SUBSCRIPTION"; field public static final String REGISTER_STATS_PULL_ATOM = "android.permission.REGISTER_STATS_PULL_ATOM"; field public static final String REMOTE_DISPLAY_PROVIDER = "android.permission.REMOTE_DISPLAY_PROVIDER"; + field @FlaggedApi("android.app.admin.flags.split_create_managed_profile_enabled") public static final String REMOVE_ACCOUNTS = "android.permission.REMOVE_ACCOUNTS"; field public static final String REMOVE_DRM_CERTIFICATES = "android.permission.REMOVE_DRM_CERTIFICATES"; field public static final String REMOVE_TASKS = "android.permission.REMOVE_TASKS"; field public static final String RENOUNCE_PERMISSIONS = "android.permission.RENOUNCE_PERMISSIONS"; @@ -570,6 +574,7 @@ package android.accessibilityservice { package android.accounts { public class AccountManager { + method @FlaggedApi("android.app.admin.flags.split_create_managed_profile_enabled") @NonNull @RequiresPermission(anyOf={android.Manifest.permission.COPY_ACCOUNTS, android.Manifest.permission.INTERACT_ACROSS_USERS_FULL}) public android.accounts.AccountManagerFuture<java.lang.Boolean> copyAccountToUser(@NonNull android.accounts.Account, @NonNull android.os.UserHandle, @NonNull android.os.UserHandle, @Nullable android.accounts.AccountManagerCallback<java.lang.Boolean>, @Nullable android.os.Handler); method @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL) public android.accounts.AccountManagerFuture<android.os.Bundle> finishSessionAsUser(android.os.Bundle, android.app.Activity, android.os.UserHandle, android.accounts.AccountManagerCallback<android.os.Bundle>, android.os.Handler); } @@ -3869,6 +3874,7 @@ package android.content { field public static final String APP_INTEGRITY_SERVICE = "app_integrity"; field public static final String APP_PREDICTION_SERVICE = "app_prediction"; field public static final String AUDIO_DEVICE_VOLUME_SERVICE = "audio_device_volume"; + field @FlaggedApi("android.security.secure_lockdown") public static final String AUTHENTICATION_POLICY_SERVICE = "authentication_policy"; field public static final String BACKUP_SERVICE = "backup"; field public static final String BATTERY_STATS_SERVICE = "batterystats"; field public static final int BIND_ALLOW_BACKGROUND_ACTIVITY_STARTS = 1048576; // 0x100000 @@ -9893,7 +9899,7 @@ package android.media.tv.tuner.frontend { method public int getSignalStrength(); method public int getSnr(); method public int getSpectralInversion(); - method @FlaggedApi("android.media.tv.flags.tuner_w_apis") @NonNull public android.media.tv.tuner.frontend.StandardExt getStandardExt(); + method @FlaggedApi("android.media.tv.flags.tuner_w_apis") @NonNull public android.media.tv.tuner.frontend.StandardExtension getStandardExtension(); method @NonNull public int[] getStreamIds(); method public int getSymbolRate(); method @IntRange(from=0, to=65535) public int getSystemId(); @@ -9948,7 +9954,7 @@ package android.media.tv.tuner.frontend { field public static final int FRONTEND_STATUS_TYPE_SIGNAL_STRENGTH = 6; // 0x6 field public static final int FRONTEND_STATUS_TYPE_SNR = 1; // 0x1 field public static final int FRONTEND_STATUS_TYPE_SPECTRAL = 10; // 0xa - field @FlaggedApi("android.media.tv.flags.tuner_w_apis") public static final int FRONTEND_STATUS_TYPE_STANDARD_EXT = 47; // 0x2f + field @FlaggedApi("android.media.tv.flags.tuner_w_apis") public static final int FRONTEND_STATUS_TYPE_STANDARD_EXTENSION = 47; // 0x2f field public static final int FRONTEND_STATUS_TYPE_STREAM_IDS = 39; // 0x27 field public static final int FRONTEND_STATUS_TYPE_SYMBOL_RATE = 7; // 0x7 field public static final int FRONTEND_STATUS_TYPE_T2_SYSTEM_ID = 29; // 0x1d @@ -10240,9 +10246,9 @@ package android.media.tv.tuner.frontend { method public default void onUnlocked(); } - @FlaggedApi("android.media.tv.flags.tuner_w_apis") public final class StandardExt { - method public int getDvbsStandardExt(); - method public int getDvbtStandardExt(); + @FlaggedApi("android.media.tv.flags.tuner_w_apis") public final class StandardExtension { + method public int getDvbsStandardExtension(); + method public int getDvbtStandardExtension(); } } @@ -12858,6 +12864,36 @@ package android.security.advancedprotection { } +package android.security.authenticationpolicy { + + @FlaggedApi("android.security.secure_lockdown") public final class AuthenticationPolicyManager { + method @FlaggedApi("android.security.secure_lockdown") @RequiresPermission(android.Manifest.permission.MANAGE_SECURE_LOCK_DEVICE) public int disableSecureLockDevice(@NonNull android.security.authenticationpolicy.DisableSecureLockDeviceParams); + method @FlaggedApi("android.security.secure_lockdown") @RequiresPermission(android.Manifest.permission.MANAGE_SECURE_LOCK_DEVICE) public int enableSecureLockDevice(@NonNull android.security.authenticationpolicy.EnableSecureLockDeviceParams); + field @FlaggedApi("android.security.secure_lockdown") public static final int ERROR_ALREADY_ENABLED = 6; // 0x6 + field @FlaggedApi("android.security.secure_lockdown") public static final int ERROR_INSUFFICIENT_BIOMETRICS = 5; // 0x5 + field @FlaggedApi("android.security.secure_lockdown") public static final int ERROR_INVALID_PARAMS = 3; // 0x3 + field @FlaggedApi("android.security.secure_lockdown") public static final int ERROR_NO_BIOMETRICS_ENROLLED = 4; // 0x4 + field @FlaggedApi("android.security.secure_lockdown") public static final int ERROR_UNKNOWN = 0; // 0x0 + field @FlaggedApi("android.security.secure_lockdown") public static final int ERROR_UNSUPPORTED = 2; // 0x2 + field @FlaggedApi("android.security.secure_lockdown") public static final int SUCCESS = 1; // 0x1 + } + + @FlaggedApi("android.security.secure_lockdown") public final class DisableSecureLockDeviceParams implements android.os.Parcelable { + ctor public DisableSecureLockDeviceParams(@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.security.authenticationpolicy.DisableSecureLockDeviceParams> CREATOR; + } + + @FlaggedApi("android.security.secure_lockdown") public final class EnableSecureLockDeviceParams implements android.os.Parcelable { + ctor public EnableSecureLockDeviceParams(@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.security.authenticationpolicy.EnableSecureLockDeviceParams> CREATOR; + } + +} + package android.security.forensic { @FlaggedApi("android.security.afl_api") public class ForensicManager { diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 44bcc2a737b9..ad1d937a3d10 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -1805,6 +1805,7 @@ package android.hardware.input { method @RequiresPermission(android.Manifest.permission.REMAP_MODIFIER_KEYS) public void remapModifierKey(int, int); method @FlaggedApi("com.android.input.flags.device_associations") @RequiresPermission("android.permission.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY") public void removeUniqueIdAssociationByDescriptor(@NonNull String); method @RequiresPermission("android.permission.ASSOCIATE_INPUT_DEVICE_TO_DISPLAY") public void removeUniqueIdAssociationByPort(@NonNull String); + method public void resetLockedModifierState(); field public static final long BLOCK_UNTRUSTED_TOUCHES = 158002302L; // 0x96aec7eL } diff --git a/core/java/android/accounts/AccountManager.java b/core/java/android/accounts/AccountManager.java index 87acbbf65b2f..72450999993d 100644 --- a/core/java/android/accounts/AccountManager.java +++ b/core/java/android/accounts/AccountManager.java @@ -16,9 +16,13 @@ package android.accounts; +import static android.Manifest.permission.COPY_ACCOUNTS; +import static android.Manifest.permission.REMOVE_ACCOUNTS; import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL; +import static android.app.admin.flags.Flags.FLAG_SPLIT_CREATE_MANAGED_PROFILE_ENABLED; import android.annotation.BroadcastBehavior; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -26,6 +30,7 @@ import android.annotation.RequiresPermission; import android.annotation.SdkConstant; import android.annotation.SdkConstant.SdkConstantType; import android.annotation.Size; +import android.annotation.SuppressLint; import android.annotation.SystemApi; import android.annotation.SystemService; import android.annotation.UserHandleAware; @@ -1312,7 +1317,8 @@ public class AccountManager { * {@link AccountManagerFuture} must not be used on the main thread. * * <p>This method requires the caller to have a signature match with the - * authenticator that manages the specified account. + * authenticator that manages the specified account, be a profile owner or have the + * {@link android.Manifest.permission#REMOVE_ACCOUNTS} permission. * * <p><b>NOTE:</b> If targeting your app to work on API level 22 and before, * MANAGE_ACCOUNTS permission is needed for those platforms. See docs for @@ -1344,6 +1350,8 @@ public class AccountManager { * </ul> */ @UserHandleAware + @RequiresPermission(value = REMOVE_ACCOUNTS, conditional = true) + @FlaggedApi(FLAG_SPLIT_CREATE_MANAGED_PROFILE_ENABLED) public AccountManagerFuture<Bundle> removeAccount(final Account account, final Activity activity, AccountManagerCallback<Bundle> callback, Handler handler) { return removeAccountAsUser(account, activity, callback, handler, mContext.getUser()); @@ -2019,9 +2027,15 @@ public class AccountManager { * succeeded. * @hide */ + @SuppressLint("SamShouldBeLast") + @NonNull + @SystemApi + @RequiresPermission(anyOf = {COPY_ACCOUNTS, INTERACT_ACROSS_USERS_FULL}) + @FlaggedApi(FLAG_SPLIT_CREATE_MANAGED_PROFILE_ENABLED) public AccountManagerFuture<Boolean> copyAccountToUser( - final Account account, final UserHandle fromUser, final UserHandle toUser, - AccountManagerCallback<Boolean> callback, Handler handler) { + @NonNull final Account account, @NonNull final UserHandle fromUser, + @NonNull final UserHandle toUser, @Nullable AccountManagerCallback<Boolean> callback, + @Nullable Handler handler) { if (account == null) throw new IllegalArgumentException("account is null"); if (toUser == null || fromUser == null) { throw new IllegalArgumentException("fromUser and toUser cannot be null"); diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index 419eb7dac5f0..38aea64386a0 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -1270,27 +1270,22 @@ public class Activity extends ContextThemeWrapper } /** - * To make users aware of system features such as the app header menu and its various - * functionalities, educational dialogs are shown to demonstrate how to find and utilize these - * features. Using this method, an activity can specify if it wants these educational dialogs to - * be shown. When set to {@code true}, these dialogs are not completely blocked; however, the - * system will be notified that they should not be shown unless necessary. If this API is not - * called, the system's educational dialogs are not limited by default. - * - * <p>This method can be utilized when activities have states where showing an - * educational dialog would be disruptive to the user. For example, if a game application is - * expecting prompt user input, this method can be used to limit educational dialogs such as the - * dialogs that showcase the app header's features which, in this instance, would disrupt the - * user's experience if shown.</p> - * - * <p>Note that educational dialogs may be shown soon after this activity is launched, so - * this method must be called early if the intent is to limit the dialogs from the start.</p> + * Requests to show the “Open in browser” education. “Open in browser” is a feature + * within the app header that allows users to switch from an app to the web. The feature + * is made available when an application is opened by a user clicking a link or when a + * link is provided by an application. Links can be provided by utilizing + * {@link AssistContent#EXTRA_AUTHENTICATING_USER_WEB_URI} or + * {@link AssistContent#setWebUri}. + * + * <p>This method should be utilized when an activity wants to nudge the user to switch + * to the web application in cases where the web may provide the user with a better + * experience. Note that this method does not guarantee that the education will be shown.</p> */ @FlaggedApi(com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB_EDUCATION) - public final void setLimitSystemEducationDialogs(boolean limitSystemEducationDialogs) { + public final void requestOpenInBrowserEducation() { try { ActivityTaskManager - .getService().setLimitSystemEducationDialogs(mToken, limitSystemEducationDialogs); + .getService().requestOpenInBrowserEducation(mToken); } catch (RemoteException e) { // Empty } diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index ab75069cc5d8..33ba05865042 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -2679,62 +2679,6 @@ public class ActivityManager { */ public static class RecentTaskInfo extends TaskInfo implements Parcelable { /** - * @hide - */ - public static class PersistedTaskSnapshotData { - /** - * The bounds of the task when the last snapshot was taken, may be null if the task is - * not yet attached to the hierarchy. - * @see {@link android.window.TaskSnapshot#mTaskSize}. - * @hide - */ - public @Nullable Point taskSize; - - /** - * The content insets of the task when the task snapshot was taken. - * @see {@link android.window.TaskSnapshot#mContentInsets}. - * @hide - */ - public @Nullable Rect contentInsets; - - /** - * The size of the last snapshot taken, may be null if there is no associated snapshot. - * @see {@link android.window.TaskSnapshot#mSnapshot}. - * @hide - */ - public @Nullable Point bufferSize; - - /** - * Sets the data from the other data. - * @hide - */ - public void set(PersistedTaskSnapshotData other) { - taskSize = other.taskSize; - contentInsets = other.contentInsets; - bufferSize = other.bufferSize; - } - - /** - * Sets the data from the provided {@param snapshot}. - * @hide - */ - public void set(TaskSnapshot snapshot) { - if (snapshot == null) { - taskSize = null; - contentInsets = null; - bufferSize = null; - return; - } - final HardwareBuffer buffer = snapshot.getHardwareBuffer(); - taskSize = new Point(snapshot.getTaskSize()); - contentInsets = new Rect(snapshot.getContentInsets()); - bufferSize = buffer != null - ? new Point(buffer.getWidth(), buffer.getHeight()) - : null; - } - } - - /** * If this task is currently running, this is the identifier for it. * If it is not running, this will be -1. * @@ -2770,24 +2714,6 @@ public class ActivityManager { @Deprecated public int affiliatedTaskId; - /** - * Information of organized child tasks. - * - * @deprecated No longer used - * @hide - */ - @Deprecated - public ArrayList<RecentTaskInfo> childrenTaskInfos = new ArrayList<>(); - - /** - * Information about the last snapshot taken for this task. - * - * @deprecated No longer used - * @hide - */ - @Deprecated - public PersistedTaskSnapshotData lastSnapshotData = new PersistedTaskSnapshotData(); - public RecentTaskInfo() { } @@ -2803,10 +2729,6 @@ public class ActivityManager { public void readFromParcel(Parcel source) { id = source.readInt(); persistentId = source.readInt(); - childrenTaskInfos = source.readArrayList(RecentTaskInfo.class.getClassLoader(), android.app.ActivityManager.RecentTaskInfo.class); - lastSnapshotData.taskSize = source.readTypedObject(Point.CREATOR); - lastSnapshotData.contentInsets = source.readTypedObject(Rect.CREATOR); - lastSnapshotData.bufferSize = source.readTypedObject(Point.CREATOR); super.readTaskFromParcel(source); } @@ -2814,10 +2736,6 @@ public class ActivityManager { public void writeToParcel(Parcel dest, int flags) { dest.writeInt(id); dest.writeInt(persistentId); - dest.writeList(childrenTaskInfos); - dest.writeTypedObject(lastSnapshotData.taskSize, flags); - dest.writeTypedObject(lastSnapshotData.contentInsets, flags); - dest.writeTypedObject(lastSnapshotData.bufferSize, flags); super.writeTaskToParcel(dest, flags); } @@ -2884,11 +2802,6 @@ public class ActivityManager { pw.println(" }"); } pw.print(" "); - pw.print(" lastSnapshotData {"); - pw.print(" taskSize=" + lastSnapshotData.taskSize); - pw.print(" contentInsets=" + lastSnapshotData.contentInsets); - pw.print(" bufferSize=" + lastSnapshotData.bufferSize); - pw.println(" }"); } } diff --git a/core/java/android/app/ForegroundServiceTypePolicy.java b/core/java/android/app/ForegroundServiceTypePolicy.java index 16444dc5adde..6efc4ef55180 100644 --- a/core/java/android/app/ForegroundServiceTypePolicy.java +++ b/core/java/android/app/ForegroundServiceTypePolicy.java @@ -62,6 +62,7 @@ import android.content.pm.ServiceInfo.ForegroundServiceType; import android.hardware.usb.UsbAccessory; import android.hardware.usb.UsbDevice; import android.hardware.usb.UsbManager; +import android.health.connect.HealthPermissions; import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; @@ -484,21 +485,35 @@ public abstract class ForegroundServiceTypePolicy { */ public static final @NonNull ForegroundServiceTypePolicyInfo FGS_TYPE_POLICY_HEALTH = new ForegroundServiceTypePolicyInfo( - FOREGROUND_SERVICE_TYPE_HEALTH, - ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID, - ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID, - new ForegroundServiceTypePermissions(new ForegroundServiceTypePermission[] { - new RegularPermission(Manifest.permission.FOREGROUND_SERVICE_HEALTH) - }, true), - new ForegroundServiceTypePermissions(new ForegroundServiceTypePermission[] { - new RegularPermission(Manifest.permission.ACTIVITY_RECOGNITION), - new RegularPermission(Manifest.permission.BODY_SENSORS), - new RegularPermission(Manifest.permission.HIGH_SAMPLING_RATE_SENSORS), - }, false), - FGS_TYPE_PERM_ENFORCEMENT_FLAG_HEALTH /* permissionEnforcementFlag */, - true /* permissionEnforcementFlagDefaultValue */, - false /* foregroundOnlyPermission */ - ); + FOREGROUND_SERVICE_TYPE_HEALTH, + ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID, + ForegroundServiceTypePolicyInfo.INVALID_CHANGE_ID, + new ForegroundServiceTypePermissions( + new ForegroundServiceTypePermission[] { + new RegularPermission(Manifest.permission.FOREGROUND_SERVICE_HEALTH) + }, + true), + new ForegroundServiceTypePermissions(getAllowedHealthPermissions(), false), + FGS_TYPE_PERM_ENFORCEMENT_FLAG_HEALTH /* permissionEnforcementFlag */, + true /* permissionEnforcementFlagDefaultValue */, + false /* foregroundOnlyPermission */); + + /** Returns the permissions needed for the policy of the health foreground service type. */ + private static ForegroundServiceTypePermission[] getAllowedHealthPermissions() { + final ArrayList<ForegroundServiceTypePermission> permissions = new ArrayList<>(); + permissions.add(new RegularPermission(Manifest.permission.ACTIVITY_RECOGNITION)); + permissions.add(new RegularPermission(Manifest.permission.HIGH_SAMPLING_RATE_SENSORS)); + + if (android.permission.flags.Flags.replaceBodySensorPermissionEnabled()) { + permissions.add(new RegularPermission(HealthPermissions.READ_HEART_RATE)); + permissions.add(new RegularPermission(HealthPermissions.READ_SKIN_TEMPERATURE)); + permissions.add(new RegularPermission(HealthPermissions.READ_OXYGEN_SATURATION)); + } else { + permissions.add(new RegularPermission(Manifest.permission.BODY_SENSORS)); + } + + return permissions.toArray(new ForegroundServiceTypePermission[permissions.size()]); + } /** * The policy for the {@link ServiceInfo#FOREGROUND_SERVICE_TYPE_REMOTE_MESSAGING}. diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl index a8412fa66609..0668958b2d5c 100644 --- a/core/java/android/app/IActivityManager.aidl +++ b/core/java/android/app/IActivityManager.aidl @@ -864,7 +864,8 @@ interface IActivityManager { /** * Suppress or reenable the rate limit on foreground service notification deferral. - * This is for use within CTS and is protected by android.permission.WRITE_DEVICE_CONFIG. + * This is for use within CTS and is protected by android.permission.WRITE_DEVICE_CONFIG + * and WRITE_ALLOWLISTED_DEVICE_CONFIG. * * @param enable false to suppress rate-limit policy; true to reenable it. */ diff --git a/core/java/android/app/IActivityTaskManager.aidl b/core/java/android/app/IActivityTaskManager.aidl index ec7b72ec677e..c6f62a21641d 100644 --- a/core/java/android/app/IActivityTaskManager.aidl +++ b/core/java/android/app/IActivityTaskManager.aidl @@ -242,8 +242,8 @@ interface IActivityTaskManager { boolean supportsLocalVoiceInteraction(); - // Sets whether system educational dialogs should be limited - void setLimitSystemEducationDialogs(IBinder appToken, boolean limitSystemEducationDialogs); + // Requests the "Open in browser" education to be shown + void requestOpenInBrowserEducation(IBinder appToken); // Get device configuration ConfigurationInfo getDeviceConfigurationInfo(); diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 8545bd5ade73..b78f11148178 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -814,8 +814,8 @@ public class Notification implements Parcelable if (Flags.notificationsRedesignTemplates()) { return switch (layoutId) { case R.layout.notification_2025_template_collapsed_base, + R.layout.notification_2025_template_heads_up_base, R.layout.notification_2025_template_header, - R.layout.notification_template_material_heads_up_base, R.layout.notification_template_material_big_base, R.layout.notification_template_material_big_picture, R.layout.notification_template_material_big_text, @@ -3257,7 +3257,7 @@ public class Notification implements Parcelable */ @FlaggedApi(Flags.FLAG_UI_RICH_ONGOING) public boolean hasPromotableCharacteristics() { - return isColorized() + return isColorizedRequested() && hasTitle() && !containsCustomViews() && hasPromotableStyle(); @@ -4083,6 +4083,12 @@ public class Notification implements Parcelable flags &= ~FLAG_LIFETIME_EXTENDED_BY_DIRECT_REPLY; } } + if (Flags.apiRichOngoing()) { + if ((flags & FLAG_PROMOTED_ONGOING) != 0) { + flagStrings.add("PROMOTED_ONGOING"); + flags &= ~FLAG_PROMOTED_ONGOING; + } + } if (android.service.notification.Flags.notificationSilentFlag()) { if ((flags & FLAG_SILENT) != 0) { @@ -7517,7 +7523,11 @@ public class Notification implements Parcelable } private int getHeadsUpBaseLayoutResource() { - return R.layout.notification_template_material_heads_up_base; + if (Flags.notificationsRedesignTemplates()) { + return R.layout.notification_2025_template_heads_up_base; + } else { + return R.layout.notification_template_material_heads_up_base; + } } private int getCompactHeadsUpBaseLayoutResource() { @@ -7788,8 +7798,16 @@ public class Notification implements Parcelable * @hide */ public boolean isColorized() { - return extras.getBoolean(EXTRA_COLORIZED) - && (hasColorizedPermission() || isFgsOrUij()); + return isColorizedRequested() + && (hasColorizedPermission() || isFgsOrUij() || isPromotedOngoing()); + } + + /** + * @return true if this notification has requested to be colorized, regardless of whether it + * meets the requirements to be displayed that way. + */ + private boolean isColorizedRequested() { + return extras.getBoolean(EXTRA_COLORIZED); } /** @@ -7803,6 +7821,19 @@ public class Notification implements Parcelable } /** + * Returns whether this notification is a promoted ongoing notification. + * + * This requires the Notification.FLAG_PROMOTED_ONGOING flag to be set + * (which may be true once the api_rich_ongoing feature flag is enabled), + * and requires that the ui_rich_ongoing feature flag is enabled. + * + * @hide + */ + public boolean isPromotedOngoing() { + return Flags.uiRichOngoing() && (flags & Notification.FLAG_PROMOTED_ONGOING) != 0; + } + + /** * @return true if this is a media style notification with a media session * * @hide @@ -11559,11 +11590,9 @@ public class Notification implements Parcelable contentView.setBundle(R.id.progress, "setProgressModel", model.toBundle()); - if (mTrackerIcon != null) { - contentView.setIcon(R.id.progress, - "setProgressTrackerIcon", - mTrackerIcon); - } + contentView.setIcon(R.id.progress, + "setProgressTrackerIcon", + mTrackerIcon); return contentView; } @@ -11669,8 +11698,10 @@ public class Notification implements Parcelable return points; } - @NonNull - private NotificationProgressModel createProgressModel(int defaultProgressColor, + /** + * @hide + */ + public @NonNull NotificationProgressModel createProgressModel(int defaultProgressColor, int backgroundColor) { final NotificationProgressModel model; if (mIndeterminate) { diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java index c49b02210dd4..c310f95814dc 100644 --- a/core/java/android/app/NotificationManager.java +++ b/core/java/android/app/NotificationManager.java @@ -16,6 +16,8 @@ package android.app; +import static android.Manifest.permission.POST_NOTIFICATIONS; +import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.service.notification.Flags.notificationClassification; import android.annotation.CallbackExecutor; @@ -1597,11 +1599,15 @@ public class NotificationManager { * Returns whether notifications from the calling package are enabled. */ public boolean areNotificationsEnabled() { - INotificationManager service = getService(); - try { - return service.areNotificationsEnabled(mContext.getPackageName()); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); + if (Flags.nmBinderPerfPermissionCheck()) { + return mContext.checkSelfPermission(POST_NOTIFICATIONS) == PERMISSION_GRANTED; + } else { + INotificationManager service = getService(); + try { + return service.areNotificationsEnabled(mContext.getPackageName()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } } } diff --git a/core/java/android/app/ResourcesManager.java b/core/java/android/app/ResourcesManager.java index 087e246e8841..599a46b131d5 100644 --- a/core/java/android/app/ResourcesManager.java +++ b/core/java/android/app/ResourcesManager.java @@ -174,54 +174,22 @@ public class ResourcesManager { } /** - * Apply the registered library paths to the passed AssetManager. If may create a new - * AssetManager if any changes are needed and it isn't allowed to reuse the old one. - * - * @return new AssetManager and the hash code for the current version of the registered paths + * Apply the registered library paths to the passed impl object + * @return the hash code for the current version of the registered paths */ - public @NonNull Pair<AssetManager, Integer> updateResourceImplAssetsWithRegisteredLibs( - @NonNull AssetManager assets, boolean reuseAssets) { + public int updateResourceImplWithRegisteredLibs(@NonNull ResourcesImpl impl) { if (!Flags.registerResourcePaths()) { - return new Pair<>(assets, 0); + return 0; } - final int size; - final PathCollector collector; - - synchronized (mLock) { - size = mSharedLibAssetsMap.size(); - if (assets == AssetManager.getSystem()) { - return new Pair<>(assets, size); - } - collector = new PathCollector(resourcesKeyFromAssets(assets)); - for (int i = 0; i < size; i++) { - final var libraryKey = mSharedLibAssetsMap.valueAt(i).getResourcesKey(); - collector.appendKey(libraryKey); - } - } - if (collector.isSameAsOriginal()) { - return new Pair<>(assets, size); - } - if (reuseAssets) { - assets.addPresetApkKeys(extractApkKeys(collector.collectedKey())); - return new Pair<>(assets, size); - } - final var newAssetsBuilder = new AssetManager.Builder(); - for (final var asset : assets.getApkAssets()) { - if (!asset.isForLoader()) { - newAssetsBuilder.addApkAssets(asset); - } + final var collector = new PathCollector(null); + final int size = mSharedLibAssetsMap.size(); + for (int i = 0; i < size; i++) { + final var libraryKey = mSharedLibAssetsMap.valueAt(i).getResourcesKey(); + collector.appendKey(libraryKey); } - for (final var key : extractApkKeys(collector.collectedKey())) { - try { - final var asset = loadApkAssets(key); - newAssetsBuilder.addApkAssets(asset); - } catch (IOException e) { - Log.e(TAG, "Couldn't load assets for key " + key, e); - } - } - assets.getLoaders().forEach(newAssetsBuilder::addLoader); - return new Pair<>(newAssetsBuilder.build(), size); + impl.getAssets().addPresetApkKeys(extractApkKeys(collector.collectedKey())); + return size; } public static class ApkKey { @@ -656,23 +624,6 @@ public class ResourcesManager { return apkKeys; } - private ResourcesKey resourcesKeyFromAssets(@NonNull AssetManager assets) { - final var libs = new ArrayList<String>(); - final var overlays = new ArrayList<String>(); - for (final ApkAssets asset : assets.getApkAssets()) { - if (asset.isSystem() || asset.isForLoader()) { - continue; - } - if (asset.isOverlay()) { - overlays.add(asset.getAssetPath()); - } else if (asset.isSharedLib()) { - libs.add(asset.getAssetPath()); - } - } - return new ResourcesKey(null, null, overlays.toArray(new String[0]), - libs.toArray(new String[0]), 0, null, null); - } - /** * Creates an AssetManager from the paths within the ResourcesKey. * @@ -801,7 +752,7 @@ public class ResourcesManager { final Configuration config = generateConfig(key); final DisplayMetrics displayMetrics = getDisplayMetrics(generateDisplayId(key), daj); - final ResourcesImpl impl = new ResourcesImpl(assets, displayMetrics, config, daj, true); + final ResourcesImpl impl = new ResourcesImpl(assets, displayMetrics, config, daj); if (DEBUG) { Slog.d(TAG, "- creating impl=" + impl + " with key: " + key); @@ -1881,32 +1832,31 @@ public class ResourcesManager { for (int i = 0; i < resourcesCount; i++) { final WeakReference<Resources> ref = mAllResourceReferences.get(i); final Resources r = ref != null ? ref.get() : null; - if (r == null) { - continue; - } - final ResourcesKey key = updatedResourceKeys.get(r.getImpl()); - if (key != null) { - final ResourcesImpl impl = findOrCreateResourcesImplForKeyLocked(key); - if (impl == null) { - throw new Resources.NotFoundException("failed to redirect ResourcesImpl"); - } - r.setImpl(impl); - } else { - // ResourcesKey is null which means the ResourcesImpl could belong to a - // Resources created by application through Resources constructor and was not - // managed by ResourcesManager, so the ResourcesImpl needs to be recreated to - // have shared library asset paths appended if there are any. - final ResourcesImpl oldImpl = r.getImpl(); - if (oldImpl != null) { - final AssetManager oldAssets = oldImpl.getAssets(); - // ResourcesImpl constructor will help to append shared library asset paths. - if (oldAssets != AssetManager.getSystem()) { - if (oldAssets.isUpToDate()) { - final ResourcesImpl newImpl = new ResourcesImpl(oldImpl); + if (r != null) { + final ResourcesKey key = updatedResourceKeys.get(r.getImpl()); + if (key != null) { + final ResourcesImpl impl = findOrCreateResourcesImplForKeyLocked(key); + if (impl == null) { + throw new Resources.NotFoundException("failed to redirect ResourcesImpl"); + } + r.setImpl(impl); + } else { + // ResourcesKey is null which means the ResourcesImpl could belong to a + // Resources created by application through Resources constructor and was not + // managed by ResourcesManager, so the ResourcesImpl needs to be recreated to + // have shared library asset paths appended if there are any. + if (r.getImpl() != null) { + final ResourcesImpl oldImpl = r.getImpl(); + final AssetManager oldAssets = oldImpl.getAssets(); + // ResourcesImpl constructor will help to append shared library asset paths. + if (oldAssets != AssetManager.getSystem() && oldAssets.isUpToDate()) { + final ResourcesImpl newImpl = new ResourcesImpl(oldAssets, + oldImpl.getMetrics(), oldImpl.getConfiguration(), + oldImpl.getDisplayAdjustments()); r.setImpl(newImpl); } else { - Slog.w(TAG, "Skip appending shared library asset paths for " - + "the Resources as its assets are not up to date."); + Slog.w(TAG, "Skip appending shared library asset paths for the " + + "Resource as its assets are not up to date."); } } } diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index 53a7dad76788..6a23349bf8aa 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -241,6 +241,8 @@ import android.security.advancedprotection.AdvancedProtectionManager; import android.security.advancedprotection.IAdvancedProtectionService; import android.security.attestationverification.AttestationVerificationManager; import android.security.attestationverification.IAttestationVerificationManagerService; +import android.security.authenticationpolicy.AuthenticationPolicyManager; +import android.security.authenticationpolicy.IAuthenticationPolicyService; import android.security.forensic.ForensicManager; import android.security.forensic.IForensicService; import android.security.keystore.KeyStoreManager; @@ -1025,6 +1027,25 @@ public final class SystemServiceRegistry { } }); + registerService(Context.AUTHENTICATION_POLICY_SERVICE, + AuthenticationPolicyManager.class, + new CachedServiceFetcher<AuthenticationPolicyManager>() { + @Override + public AuthenticationPolicyManager createService(ContextImpl ctx) + throws ServiceNotFoundException { + if (!android.security.Flags.secureLockdown()) { + throw new ServiceNotFoundException( + Context.AUTHENTICATION_POLICY_SERVICE); + } + + final IBinder binder = ServiceManager.getServiceOrThrow( + Context.AUTHENTICATION_POLICY_SERVICE); + final IAuthenticationPolicyService service = + IAuthenticationPolicyService.Stub.asInterface(binder); + return new AuthenticationPolicyManager(ctx.getOuterContext(), service); + } + }); + registerService(Context.TV_INTERACTIVE_APP_SERVICE, TvInteractiveAppManager.class, new CachedServiceFetcher<TvInteractiveAppManager>() { @Override diff --git a/core/java/android/app/TaskInfo.java b/core/java/android/app/TaskInfo.java index aac963ade726..01cc9d82d56d 100644 --- a/core/java/android/app/TaskInfo.java +++ b/core/java/android/app/TaskInfo.java @@ -340,10 +340,10 @@ public class TaskInfo { public int requestedVisibleTypes; /** - * Whether the top activity has requested to limit educational dialogs shown by the system. + * The timestamp of the top activity's last request to show the "Open in Browser" education. * @hide */ - public boolean isTopActivityLimitSystemEducationDialogs; + public long topActivityRequestOpenInBrowserEducationTimestamp; /** * Encapsulate specific App Compat information. @@ -493,8 +493,8 @@ public class TaskInfo { && Objects.equals(capturedLink, that.capturedLink) && capturedLinkTimestamp == that.capturedLinkTimestamp && requestedVisibleTypes == that.requestedVisibleTypes - && isTopActivityLimitSystemEducationDialogs - == that.isTopActivityLimitSystemEducationDialogs + && topActivityRequestOpenInBrowserEducationTimestamp + == that.topActivityRequestOpenInBrowserEducationTimestamp && appCompatTaskInfo.equalsForTaskOrganizer(that.appCompatTaskInfo) && Objects.equals(topActivityMainWindowFrame, that.topActivityMainWindowFrame); } @@ -571,7 +571,7 @@ public class TaskInfo { capturedLink = source.readTypedObject(Uri.CREATOR); capturedLinkTimestamp = source.readLong(); requestedVisibleTypes = source.readInt(); - isTopActivityLimitSystemEducationDialogs = source.readBoolean(); + topActivityRequestOpenInBrowserEducationTimestamp = source.readLong(); appCompatTaskInfo = source.readTypedObject(AppCompatTaskInfo.CREATOR); topActivityMainWindowFrame = source.readTypedObject(Rect.CREATOR); } @@ -627,7 +627,7 @@ public class TaskInfo { dest.writeTypedObject(capturedLink, flags); dest.writeLong(capturedLinkTimestamp); dest.writeInt(requestedVisibleTypes); - dest.writeBoolean(isTopActivityLimitSystemEducationDialogs); + dest.writeLong(topActivityRequestOpenInBrowserEducationTimestamp); dest.writeTypedObject(appCompatTaskInfo, flags); dest.writeTypedObject(topActivityMainWindowFrame, flags); } @@ -672,8 +672,8 @@ public class TaskInfo { + " capturedLink=" + capturedLink + " capturedLinkTimestamp=" + capturedLinkTimestamp + " requestedVisibleTypes=" + requestedVisibleTypes - + " isTopActivityLimitSystemEducationDialogs=" - + isTopActivityLimitSystemEducationDialogs + + " topActivityRequestOpenInBrowserEducationTimestamp=" + + topActivityRequestOpenInBrowserEducationTimestamp + " appCompatTaskInfo=" + appCompatTaskInfo + " topActivityMainWindowFrame=" + topActivityMainWindowFrame + "}"; diff --git a/core/java/android/app/jank/FrameOverrunHistogram.java b/core/java/android/app/jank/FrameOverrunHistogram.java index e28ac126a90a..3ad6531a46bf 100644 --- a/core/java/android/app/jank/FrameOverrunHistogram.java +++ b/core/java/android/app/jank/FrameOverrunHistogram.java @@ -39,7 +39,7 @@ public class FrameOverrunHistogram { * Create a new instance of FrameOverrunHistogram. */ public FrameOverrunHistogram() { - mBucketCounts = new int[sBucketEndpoints.length - 1]; + mBucketCounts = new int[sBucketEndpoints.length]; } /** diff --git a/core/java/android/app/jank/JankDataProcessor.java b/core/java/android/app/jank/JankDataProcessor.java index 7525d0402ee4..7ceaeb3fb070 100644 --- a/core/java/android/app/jank/JankDataProcessor.java +++ b/core/java/android/app/jank/JankDataProcessor.java @@ -59,7 +59,6 @@ public class JankDataProcessor { * @param appUid the uid of the app. */ public void processJankData(List<JankData> jankData, String activityName, int appUid) { - mCurrentBatchCount++; // add all the previous and active states to the pending states list. mStateTracker.retrieveAllStates(mPendingStates); @@ -79,9 +78,8 @@ public class JankDataProcessor { } } // At this point we have attributed all frames to a state. - if (mCurrentBatchCount >= LOG_BATCH_FREQUENCY) { - logMetricCounts(); - } + incrementBatchCountAndMaybeLogStats(); + // return the StatData object back to the pool to be reused. jankDataProcessingComplete(); } @@ -91,7 +89,73 @@ public class JankDataProcessor { * stats */ public void mergeJankStats(AppJankStats jankStats, String activityName) { - // TODO b/377572463 Add Merging Logic + // Each state has a key which is a combination of widget category, widget id and widget + // state, this key is also used to identify pending stats, a pending stat is essentially a + // state with frames associated with it. + String stateKey = mStateTracker.getStateKey(jankStats.getWidgetCategory(), + jankStats.getWidgetId(), jankStats.getWidgetState()); + + if (mPendingJankStats.containsKey(stateKey)) { + mergeExistingStat(stateKey, jankStats); + } else { + mergeNewStat(stateKey, activityName, jankStats); + } + + incrementBatchCountAndMaybeLogStats(); + } + + private void mergeExistingStat(String stateKey, AppJankStats jankStat) { + PendingJankStat pendingStat = mPendingJankStats.get(stateKey); + + pendingStat.mJankyFrames += jankStat.getJankyFrameCount(); + pendingStat.mTotalFrames += jankStat.getTotalFrameCount(); + + mergeOverrunHistograms(pendingStat.mFrameOverrunBuckets, + jankStat.getFrameOverrunHistogram().getBucketCounters()); + } + + private void mergeNewStat(String stateKey, String activityName, AppJankStats jankStats) { + // Check if we have space for a new stat + if (mPendingJankStats.size() > MAX_IN_MEMORY_STATS) { + return; + } + + PendingJankStat pendingStat = mPendingJankStatsPool.acquire(); + if (pendingStat == null) { + pendingStat = new PendingJankStat(); + + } + pendingStat.clearStats(); + + pendingStat.mActivityName = activityName; + pendingStat.mUid = jankStats.getUid(); + pendingStat.mWidgetId = jankStats.getWidgetId(); + pendingStat.mWidgetCategory = jankStats.getWidgetCategory(); + pendingStat.mWidgetState = jankStats.getWidgetState(); + pendingStat.mTotalFrames = jankStats.getTotalFrameCount(); + pendingStat.mJankyFrames = jankStats.getJankyFrameCount(); + + mergeOverrunHistograms(pendingStat.mFrameOverrunBuckets, + jankStats.getFrameOverrunHistogram().getBucketCounters()); + + mPendingJankStats.put(stateKey, pendingStat); + } + + private void mergeOverrunHistograms(int[] mergeTarget, int[] mergeSource) { + // The length of each histogram should be identical, if they are not then its possible the + // buckets are not in sync, these records should not be recorded. + if (mergeTarget.length != mergeSource.length) return; + + for (int i = 0; i < mergeTarget.length; i++) { + mergeTarget[i] += mergeSource[i]; + } + } + + private void incrementBatchCountAndMaybeLogStats() { + mCurrentBatchCount++; + if (mCurrentBatchCount >= LOG_BATCH_FREQUENCY) { + logMetricCounts(); + } } /** diff --git a/core/java/android/app/jank/JankTracker.java b/core/java/android/app/jank/JankTracker.java index 202281f98c97..469521668d25 100644 --- a/core/java/android/app/jank/JankTracker.java +++ b/core/java/android/app/jank/JankTracker.java @@ -89,7 +89,12 @@ public class JankTracker { * stats */ public void mergeAppJankStats(AppJankStats appJankStats) { - mJankDataProcessor.mergeJankStats(appJankStats, mActivityName); + getHandler().post(new Runnable() { + @Override + public void run() { + mJankDataProcessor.mergeJankStats(appJankStats, mActivityName); + } + }); } public void setActivityName(@NonNull String activityName) { diff --git a/core/java/android/app/jank/StateTracker.java b/core/java/android/app/jank/StateTracker.java index c86d5a5cff20..21bb5e8280ee 100644 --- a/core/java/android/app/jank/StateTracker.java +++ b/core/java/android/app/jank/StateTracker.java @@ -180,7 +180,11 @@ public class StateTracker { } } - private String getStateKey(String widgetCategory, String widgetId, String widgetState) { + /** + * Returns a concatenated string of the inputs. This key can be used to retrieve both pending + * stats and the state that was used to create the pending stat. + */ + public String getStateKey(String widgetCategory, String widgetId, String widgetState) { return widgetCategory + widgetId + widgetState; } diff --git a/core/java/android/app/notification.aconfig b/core/java/android/app/notification.aconfig index 2e3d5e15e037..8b6840c1b552 100644 --- a/core/java/android/app/notification.aconfig +++ b/core/java/android/app/notification.aconfig @@ -277,6 +277,13 @@ flag { } flag { + name: "nm_binder_perf_permission_check" + namespace: "systemui" + description: "Use PermissionManager for areNotificationsEnabled() instead of NMS" + bug: "362981561" +} + +flag { name: "no_sbnholder" namespace: "systemui" description: "removes sbnholder from NLS" diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 6086f2455a31..19cd2e69cfdf 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -18,6 +18,7 @@ package android.content; import static android.app.appfunctions.flags.Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER; import static android.content.flags.Flags.FLAG_ENABLE_BIND_PACKAGE_ISOLATED_PROCESS; +import static android.security.Flags.FLAG_SECURE_LOCKDOWN; import android.annotation.AttrRes; import android.annotation.CallbackExecutor; @@ -4256,6 +4257,7 @@ public abstract class Context { FINGERPRINT_SERVICE, //@hide: FACE_SERVICE, BIOMETRIC_SERVICE, + AUTHENTICATION_POLICY_SERVICE, MEDIA_ROUTER_SERVICE, TELEPHONY_SERVICE, TELEPHONY_SUBSCRIPTION_SERVICE, @@ -4437,6 +4439,9 @@ public abstract class Context { * web domain approval state. * <dt> {@link #DISPLAY_HASH_SERVICE} ("display_hash") * <dd> A {@link android.view.displayhash.DisplayHashManager} for management of display hashes. + * <dt> {@link #AUTHENTICATION_POLICY_SERVICE} ("authentication_policy") + * <dd> A {@link android.security.authenticationpolicy.AuthenticationPolicyManager} + * for managing authentication related policies on the device. * </dl> * * <p>Note: System services obtained via this API may be closely associated with @@ -4521,8 +4526,9 @@ public abstract class Context { * @see android.content.pm.verify.domain.DomainVerificationManager * @see #DISPLAY_HASH_SERVICE * @see android.view.displayhash.DisplayHashManager + * @see #AUTHENTICATION_POLICY_SERVICE + * @see android.security.authenticationpolicy.AuthenticationPolicyManager */ - // TODO(b/347269120): Re-add @Nullable public abstract Object getSystemService(@ServiceName @NonNull String name); /** @@ -4543,7 +4549,8 @@ public abstract class Context { * {@link android.os.BatteryManager}, {@link android.app.job.JobScheduler}, * {@link android.app.usage.NetworkStatsManager}, * {@link android.content.pm.verify.domain.DomainVerificationManager}, - * {@link android.view.displayhash.DisplayHashManager}. + * {@link android.view.displayhash.DisplayHashManager} + * {@link android.security.authenticationpolicy.AuthenticationPolicyManager}. * </p> * * <p> @@ -4568,7 +4575,6 @@ public abstract class Context { */ @SuppressWarnings("unchecked") @RavenwoodKeep - // TODO(b/347269120): Re-add @Nullable public final <T> T getSystemService(@NonNull Class<T> serviceClass) { // Because subclasses may override getSystemService(String) we cannot // perform a lookup by class alone. We must first map the class to its @@ -5183,6 +5189,18 @@ public abstract class Context { public static final String AUTH_SERVICE = "auth"; /** + * Use with {@link #getSystemService(String)} to retrieve an {@link + * android.security.authenticationpolicy.AuthenticationPolicyManager}. + * @see #getSystemService + * @see android.security.authenticationpolicy.AuthenticationPolicyManager + * + * @hide + */ + @SystemApi + @FlaggedApi(FLAG_SECURE_LOCKDOWN) + public static final String AUTHENTICATION_POLICY_SERVICE = "authentication_policy"; + + /** * Use with {@link #getSystemService(String)} to retrieve a * {@link android.hardware.fingerprint.FingerprintManager} for handling management * of fingerprints. diff --git a/core/java/android/content/ContextWrapper.java b/core/java/android/content/ContextWrapper.java index 23d17cb5ce50..413eb9886392 100644 --- a/core/java/android/content/ContextWrapper.java +++ b/core/java/android/content/ContextWrapper.java @@ -960,7 +960,6 @@ public class ContextWrapper extends Context { } @Override - // TODO(b/347269120): Re-add @Nullable public Object getSystemService(String name) { return mBase.getSystemService(name); } diff --git a/core/java/android/content/pm/ActivityInfo.java b/core/java/android/content/pm/ActivityInfo.java index ce52825ddb73..b10f5e4fe66c 100644 --- a/core/java/android/content/pm/ActivityInfo.java +++ b/core/java/android/content/pm/ActivityInfo.java @@ -1320,23 +1320,23 @@ public class ActivityInfo extends ComponentInfo implements Parcelable { 264301586L; // buganizer id /** - * Excludes the packages the override is applied to from the camera compatibility treatment - * in free-form windowing mode for fixed-orientation apps. + * Includes the packages the override is applied to in the camera compatibility treatment in + * free-form windowing mode for fixed-orientation apps. * * <p>In free-form windowing mode, the compatibility treatment emulates running on a portrait * device by letterboxing the app window and changing the camera characteristics to what apps * commonly expect in a portrait device: 90 and 270 degree sensor rotation for back and front * cameras, respectively, and setting display rotation to 0. * - * <p>Use this flag to disable the compatibility treatment for apps that do not respond well to - * the treatment. + * <p>Use this flag to enable the compatibility treatment for apps in which camera doesn't work + * well in freeform windowing. * * @hide */ @ChangeId @Overridable @Disabled - public static final long OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT = + public static final long OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT = 314961188L; /** diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index 37295ac94823..5b305b466dd6 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -3200,6 +3200,16 @@ public abstract class PackageManager { /** * Feature for {@link #getSystemAvailableFeatures} and + * {@link #hasSystemFeature}: The device is capable of ranging with + * other devices using channel sounding via Bluetooth Low Energy radio. + */ + @FlaggedApi(com.android.ranging.flags.Flags.FLAG_RANGING_CS_ENABLED) + @SdkConstant(SdkConstantType.FEATURE) + public static final String FEATURE_BLUETOOTH_LE_CHANNEL_SOUNDING = + "android.hardware.bluetooth_le.channel_sounding"; + + /** + * Feature for {@link #getSystemAvailableFeatures} and * {@link #hasSystemFeature}: The device has a camera facing away * from the screen. */ diff --git a/core/java/android/content/pm/ServiceInfo.java b/core/java/android/content/pm/ServiceInfo.java index 4285b0a2b91a..8243d88e6260 100644 --- a/core/java/android/content/pm/ServiceInfo.java +++ b/core/java/android/content/pm/ServiceInfo.java @@ -20,6 +20,7 @@ import android.Manifest; import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.RequiresPermission; +import android.health.connect.HealthPermissions; import android.os.Parcel; import android.os.Parcelable; import android.util.Printer; @@ -361,8 +362,10 @@ public class ServiceInfo extends ComponentInfo * {@link android.Manifest.permission#FOREGROUND_SERVICE_HEALTH} and one of the following * permissions: * {@link android.Manifest.permission#ACTIVITY_RECOGNITION}, - * {@link android.Manifest.permission#BODY_SENSORS}, * {@link android.Manifest.permission#HIGH_SAMPLING_RATE_SENSORS}. + * {@link android.health.connect.HealthPermissions#READ_HEART_RATE}, + * {@link android.health.connect.HealthPermissions#READ_SKIN_TEMPERATURE}, + * {@link android.health.connect.HealthPermissions#READ_OXYGEN_SATURATION}, */ @RequiresPermission( allOf = { @@ -370,10 +373,13 @@ public class ServiceInfo extends ComponentInfo }, anyOf = { Manifest.permission.ACTIVITY_RECOGNITION, - Manifest.permission.BODY_SENSORS, Manifest.permission.HIGH_SAMPLING_RATE_SENSORS, + HealthPermissions.READ_HEART_RATE, + HealthPermissions.READ_SKIN_TEMPERATURE, + HealthPermissions.READ_OXYGEN_SATURATION, } ) + @FlaggedApi(android.permission.flags.Flags.FLAG_REPLACE_BODY_SENSOR_PERMISSION_ENABLED) public static final int FOREGROUND_SERVICE_TYPE_HEALTH = 1 << 8; /** diff --git a/core/java/android/content/res/ApkAssets.java b/core/java/android/content/res/ApkAssets.java index 908999b64961..68b5d782bfbf 100644 --- a/core/java/android/content/res/ApkAssets.java +++ b/core/java/android/content/res/ApkAssets.java @@ -124,13 +124,11 @@ public final class ApkAssets { @Nullable @GuardedBy("this") - private StringBlock mStringBlock; // null or closed if mNativePtr = 0. + private final StringBlock mStringBlock; // null or closed if mNativePtr = 0. @PropertyFlags private final int mFlags; - private final boolean mIsOverlay; - @Nullable private final AssetsProvider mAssets; @@ -304,43 +302,40 @@ public final class ApkAssets { private ApkAssets(@FormatType int format, @NonNull String path, @PropertyFlags int flags, @Nullable AssetsProvider assets) throws IOException { - this(format, flags, assets); Objects.requireNonNull(path, "path"); + mFlags = flags; mNativePtr = nativeLoad(format, path, flags, assets); mStringBlock = new StringBlock(nativeGetStringBlock(mNativePtr), true /*useSparse*/); + mAssets = assets; } private ApkAssets(@FormatType int format, @NonNull FileDescriptor fd, @NonNull String friendlyName, @PropertyFlags int flags, @Nullable AssetsProvider assets) throws IOException { - this(format, flags, assets); Objects.requireNonNull(fd, "fd"); Objects.requireNonNull(friendlyName, "friendlyName"); + mFlags = flags; mNativePtr = nativeLoadFd(format, fd, friendlyName, flags, assets); mStringBlock = new StringBlock(nativeGetStringBlock(mNativePtr), true /*useSparse*/); + mAssets = assets; } private ApkAssets(@FormatType int format, @NonNull FileDescriptor fd, @NonNull String friendlyName, long offset, long length, @PropertyFlags int flags, @Nullable AssetsProvider assets) throws IOException { - this(format, flags, assets); Objects.requireNonNull(fd, "fd"); Objects.requireNonNull(friendlyName, "friendlyName"); + mFlags = flags; mNativePtr = nativeLoadFdOffsets(format, fd, friendlyName, offset, length, flags, assets); mStringBlock = new StringBlock(nativeGetStringBlock(mNativePtr), true /*useSparse*/); + mAssets = assets; } private ApkAssets(@PropertyFlags int flags, @Nullable AssetsProvider assets) { - this(FORMAT_APK, flags, assets); + mFlags = flags; mNativePtr = nativeLoadEmpty(flags, assets); mStringBlock = null; - } - - private ApkAssets(@FormatType int format, @PropertyFlags int flags, - @Nullable AssetsProvider assets) { - mFlags = flags; mAssets = assets; - mIsOverlay = format == FORMAT_IDMAP; } @UnsupportedAppUsage @@ -430,18 +425,6 @@ public final class ApkAssets { } } - public boolean isSystem() { - return (mFlags & PROPERTY_SYSTEM) != 0; - } - - public boolean isSharedLib() { - return (mFlags & PROPERTY_DYNAMIC) != 0; - } - - public boolean isOverlay() { - return mIsOverlay; - } - @Override public String toString() { return "ApkAssets{path=" + getDebugName() + "}"; diff --git a/core/java/android/content/res/ResourcesImpl.java b/core/java/android/content/res/ResourcesImpl.java index bcaceb24d767..e6b93427f413 100644 --- a/core/java/android/content/res/ResourcesImpl.java +++ b/core/java/android/content/res/ResourcesImpl.java @@ -203,25 +203,9 @@ public class ResourcesImpl { @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) public ResourcesImpl(@NonNull AssetManager assets, @Nullable DisplayMetrics metrics, @Nullable Configuration config, @NonNull DisplayAdjustments displayAdjustments) { - // Don't reuse assets by default as we have no control over whether they're already - // inside some other ResourcesImpl. - this(assets, metrics, config, displayAdjustments, false); - } - - public ResourcesImpl(@NonNull ResourcesImpl orig) { - // We know for sure that the other assets are in use, so can't reuse the object here. - this(orig.getAssets(), orig.getMetrics(), orig.getConfiguration(), - orig.getDisplayAdjustments(), false); - } - - public ResourcesImpl(@NonNull AssetManager assets, @Nullable DisplayMetrics metrics, - @Nullable Configuration config, @NonNull DisplayAdjustments displayAdjustments, - boolean reuseAssets) { - final var assetsAndHash = - ResourcesManager.getInstance().updateResourceImplAssetsWithRegisteredLibs(assets, - reuseAssets); - mAssets = assetsAndHash.first; - mAppliedSharedLibsHash = assetsAndHash.second; + mAssets = assets; + mAppliedSharedLibsHash = + ResourcesManager.getInstance().updateResourceImplWithRegisteredLibs(this); mMetrics.setToDefaults(); mDisplayAdjustments = displayAdjustments; mConfiguration.setToDefaults(); diff --git a/core/java/android/content/res/flags.aconfig b/core/java/android/content/res/flags.aconfig index f23c193e2da0..6fc7d90a8237 100644 --- a/core/java/android/content/res/flags.aconfig +++ b/core/java/android/content/res/flags.aconfig @@ -105,3 +105,12 @@ flag { # This flag is used to control aapt2 behavior. is_fixed_read_only: true } + +flag { + name: "resources_minor_version_support" + is_exported: true + namespace: "resource_manager" + description: "Feature flag for supporting minor version in Resources" + bug: "373535266" + is_fixed_read_only: true +} diff --git a/core/java/android/hardware/contexthub/HubEndpointInfo.java b/core/java/android/hardware/contexthub/HubEndpointInfo.java index b1d55239ac43..5265d5663459 100644 --- a/core/java/android/hardware/contexthub/HubEndpointInfo.java +++ b/core/java/android/hardware/contexthub/HubEndpointInfo.java @@ -156,7 +156,7 @@ public final class HubEndpointInfo implements Parcelable { mRequiredPermissions = Arrays.asList(endpointInfo.requiredPermissions); mHubServiceInfos = new ArrayList<>(endpointInfo.services.length); for (int i = 0; i < endpointInfo.services.length; i++) { - mHubServiceInfos.set(i, new HubServiceInfo(endpointInfo.services[i])); + mHubServiceInfos.add(new HubServiceInfo(endpointInfo.services[i])); } } diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl index 3284761eb273..ed510e467f82 100644 --- a/core/java/android/hardware/input/IInputManager.aidl +++ b/core/java/android/hardware/input/IInputManager.aidl @@ -281,4 +281,6 @@ interface IInputManager { AidlInputGestureData[] getCustomInputGestures(int userId, int tag); AidlInputGestureData[] getAppLaunchBookmarks(); + + void resetLockedModifierState(); } diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java index f8241925dff0..10224c1be788 100644 --- a/core/java/android/hardware/input/InputManager.java +++ b/core/java/android/hardware/input/InputManager.java @@ -260,7 +260,7 @@ public final class InputManager { } /** - * Custom input gesture error: Input gesture already exists + * Custom input gesture result success * * @hide */ @@ -1590,6 +1590,21 @@ public final class InputManager { } /** + * Resets locked modifier state (i.e.. Caps Lock, Num Lock, Scroll Lock state) + * + * @hide + */ + @TestApi + @SuppressLint("UnflaggedApi") // @TestApi without associated feature. + public void resetLockedModifierState() { + try { + mIm.resetLockedModifierState(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * A callback used to be notified about battery state changes for an input device. The * {@link #onBatteryStateChanged(int, long, BatteryState)} method will be called once after the * listener is successfully registered to provide the initial battery state of the device. diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java index dadb5c386b76..977c5bd927cf 100644 --- a/core/java/android/inputmethodservice/InputMethodService.java +++ b/core/java/android/inputmethodservice/InputMethodService.java @@ -407,6 +407,12 @@ public class InputMethodService extends AbstractInputMethodService { private boolean mUsingCtrlShiftShortcut = false; /** + * Last handwriting bounds used for stylus handwriting + * {@link #setStylusHandwritingRegion(Region)}. + */ + private Region mLastHandwritingRegion; + + /** * Returns whether {@link InputMethodService} is responsible for rendering the back button and * the IME switcher button or not when the gestural navigation is enabled. * @@ -1532,6 +1538,7 @@ public class InputMethodService extends AbstractInputMethodService { return; } editorInfo.makeCompatible(getApplicationInfo().targetSdkVersion); + mLastHandwritingRegion = null; getInputMethodInternal().restartInput(new RemoteInputConnection(ric, sessionId), editorInfo); } @@ -2840,6 +2847,7 @@ public class InputMethodService extends AbstractInputMethodService { mHandler.removeCallbacks(mFinishHwRunnable); } mFinishHwRunnable = null; + mLastHandwritingRegion = null; final int requestId = mHandwritingRequestId.getAsInt(); mHandwritingRequestId = OptionalInt.empty(); @@ -3166,6 +3174,40 @@ public class InputMethodService extends AbstractInputMethodService { registerDefaultOnBackInvokedCallback(); } + /** + * Sets a new stylus handwriting region as user continues to write on an editor on screen. + * Stylus strokes that are started within the {@code touchableRegion} are treated as + * continuation of handwriting and all the events outside are passed-through to the IME target + * app, causing stylus handwriting to finish {@link #finishStylusHandwriting()}. + * By default, {@link WindowManager#getMaximumWindowMetrics()} is handwritable and + * {@code touchableRegion} resets after each handwriting session. + * <p> + * For example, the IME can use this API to dynamically expand the stylus handwriting region on + * every stylus stroke as user continues to write on an editor. The region should grow around + * the last stroke so that a UI element below the IME window is still interactable when it is + * spaced sufficiently away (~2 character dimensions) from last stroke. + * </p> + * <p> + * Note: Setting handwriting touchable region is supported on IMEs that support stylus + * handwriting {@link InputMethodInfo#supportsStylusHandwriting()}. + * </p> + * + * @param handwritingRegion new stylus handwritable {@link Region} that can accept stylus touch. + */ + @FlaggedApi(Flags.FLAG_ADAPTIVE_HANDWRITING_BOUNDS) + public final void setStylusHandwritingRegion(@NonNull Region handwritingRegion) { + if (handwritingRegion.equals(mLastHandwritingRegion)) { + Log.v(TAG, "Failed to set setStylusHandwritingRegion():" + + " same region set twice."); + return; + } + + if (DEBUG) { + Log.d(TAG, "Setting new handwriting region for stylus handwriting " + + handwritingRegion + " from last " + mLastHandwritingRegion); + } + mLastHandwritingRegion = handwritingRegion; + } /** * Registers an {@link OnBackInvokedCallback} to handle back invocation when ahead-of-time diff --git a/core/java/android/os/BinderProxy.java b/core/java/android/os/BinderProxy.java index 3b5a99ed089a..01222cdd38b3 100644 --- a/core/java/android/os/BinderProxy.java +++ b/core/java/android/os/BinderProxy.java @@ -36,6 +36,7 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; @@ -651,28 +652,39 @@ public final class BinderProxy implements IBinder { private native boolean unlinkToDeathNative(DeathRecipient recipient, int flags); /** - * This list is to hold strong reference to the frozen state callbacks. The callbacks are only - * weakly referenced by JNI so the strong references here are needed to keep the callbacks - * around until the proxy is GC'ed. + * This map is to hold strong reference to the frozen state callbacks. + * + * The callbacks are only weakly referenced by JNI so the strong references here are needed to + * keep the callbacks around until the proxy is GC'ed. + * + * The key is the original callback passed into {@link #addFrozenStateChangeCallback}. The value + * is the wrapped callback created in {@link #addFrozenStateChangeCallback} to dispatch the + * calls on the desired executor. */ - private List<FrozenStateChangeCallback> mFrozenStateChangeCallbacks = - Collections.synchronizedList(new ArrayList<>()); + private Map<FrozenStateChangeCallback, FrozenStateChangeCallback> mFrozenStateChangeCallbacks = + Collections.synchronizedMap(new HashMap<>()); /** * See {@link IBinder#addFrozenStateChangeCallback(FrozenStateChangeCallback)} */ - public void addFrozenStateChangeCallback(FrozenStateChangeCallback callback) + public void addFrozenStateChangeCallback(Executor executor, FrozenStateChangeCallback callback) throws RemoteException { - addFrozenStateChangeCallbackNative(callback); - mFrozenStateChangeCallbacks.add(callback); + FrozenStateChangeCallback wrappedCallback = (who, state) -> + executor.execute(() -> callback.onFrozenStateChanged(who, state)); + addFrozenStateChangeCallbackNative(wrappedCallback); + mFrozenStateChangeCallbacks.put(callback, wrappedCallback); } /** * See {@link IBinder#removeFrozenStateChangeCallback} */ - public boolean removeFrozenStateChangeCallback(FrozenStateChangeCallback callback) { - mFrozenStateChangeCallbacks.remove(callback); - return removeFrozenStateChangeCallbackNative(callback); + public boolean removeFrozenStateChangeCallback(FrozenStateChangeCallback callback) + throws IllegalArgumentException { + FrozenStateChangeCallback wrappedCallback = mFrozenStateChangeCallbacks.remove(callback); + if (wrappedCallback == null) { + throw new IllegalArgumentException("callback not found"); + } + return removeFrozenStateChangeCallbackNative(wrappedCallback); } private native void addFrozenStateChangeCallbackNative(FrozenStateChangeCallback callback) diff --git a/core/java/android/os/CombinedMessageQueue/MessageQueue.java b/core/java/android/os/CombinedMessageQueue/MessageQueue.java index 036ccd84a600..4c9f08d80d4b 100644 --- a/core/java/android/os/CombinedMessageQueue/MessageQueue.java +++ b/core/java/android/os/CombinedMessageQueue/MessageQueue.java @@ -123,11 +123,6 @@ public final class MessageQueue { // We can lift this restriction in the future after we've made it possible for test authors // to test Looper and MessageQueue without resorting to reflection. - // Holdback study. - if (mUseConcurrent && Flags.messageQueueForceLegacy()) { - mUseConcurrent = false; - } - mQuitAllowed = quitAllowed; mPtr = nativeInit(); } diff --git a/core/java/android/os/IBinder.java b/core/java/android/os/IBinder.java index a997f4c86704..8cfd32449537 100644 --- a/core/java/android/os/IBinder.java +++ b/core/java/android/os/IBinder.java @@ -16,6 +16,7 @@ package android.os; +import android.annotation.CallbackExecutor; import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; @@ -25,6 +26,7 @@ import android.compat.annotation.UnsupportedAppUsage; import java.io.FileDescriptor; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.concurrent.Executor; /** * Base interface for a remotable object, the core part of a lightweight @@ -397,12 +399,31 @@ public interface IBinder { @interface State { } + /** + * Represents the frozen state of the remote process. + * + * While in this state, the remote process won't be able to receive and handle a + * transaction. Therefore, any asynchronous transactions will be buffered and delivered when + * the process is unfrozen, and any synchronous transactions will result in an error. + * + * Buffered transactions may be stale by the time that the process is unfrozen and handles + * them. To avoid overwhelming the remote process with stale events or overflowing their + * buffers, it's best to avoid sending binder transactions to a frozen process. + */ int STATE_FROZEN = 0; + + /** + * Represents the unfrozen state of the remote process. + * + * In this state, the process hosting the object can execute and is not restricted + * by the freezer from using the CPU or responding to binder transactions. + */ int STATE_UNFROZEN = 1; /** * Interface for receiving a callback when the process hosting an IBinder * has changed its frozen state. + * * @param who The IBinder whose hosting process has changed state. * @param state The latest state. */ @@ -427,16 +448,32 @@ public interface IBinder { * <p>You will only receive state change notifications for remote binders, as local binders by * definition can't be frozen without you being frozen too.</p> * + * @param executor The executor on which to run the callback. + * @param callback The callback used to deliver state change notifications. + * * <p>@throws {@link UnsupportedOperationException} if the kernel binder driver does not support * this feature. */ @FlaggedApi(Flags.FLAG_BINDER_FROZEN_STATE_CHANGE_CALLBACK) - default void addFrozenStateChangeCallback(@NonNull FrozenStateChangeCallback callback) + default void addFrozenStateChangeCallback( + @NonNull @CallbackExecutor Executor executor, + @NonNull FrozenStateChangeCallback callback) throws RemoteException { throw new UnsupportedOperationException(); } /** + * Same as {@link #addFrozenStateChangeCallback(Executor, FrozenStateChangeCallback)} except + * that callbacks are invoked on a binder thread. + * + * @hide + */ + default void addFrozenStateChangeCallback(@NonNull FrozenStateChangeCallback callback) + throws RemoteException { + addFrozenStateChangeCallback(Runnable::run, callback); + } + + /** * Unregister a {@link FrozenStateChangeCallback}. The callback will no longer be invoked when * the hosting process changes its frozen state. */ diff --git a/core/java/android/os/PerformanceHintManager.java b/core/java/android/os/PerformanceHintManager.java index 224b10d0eaca..2b0042d653b1 100644 --- a/core/java/android/os/PerformanceHintManager.java +++ b/core/java/android/os/PerformanceHintManager.java @@ -22,6 +22,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemService; import android.annotation.TestApi; +import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; import com.android.internal.util.Preconditions; @@ -106,7 +107,9 @@ public final class PerformanceHintManager { * All timings should be in {@link SystemClock#uptimeNanos()}. */ public static class Session implements Closeable { - private long mNativeSessionPtr; + /** @hide */ + @UnsupportedAppUsage + public long mNativeSessionPtr; /** @hide */ public Session(long nativeSessionPtr) { diff --git a/core/java/android/os/RemoteCallbackList.java b/core/java/android/os/RemoteCallbackList.java index 91c482faf7d7..d5630fd46eb4 100644 --- a/core/java/android/os/RemoteCallbackList.java +++ b/core/java/android/os/RemoteCallbackList.java @@ -16,6 +16,7 @@ package android.os; +import android.annotation.CallbackExecutor; import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; @@ -29,6 +30,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.Executor; import java.util.function.BiConsumer; import java.util.function.Consumer; @@ -134,6 +136,7 @@ public class RemoteCallbackList<E extends IInterface> { private final @FrozenCalleePolicy int mFrozenCalleePolicy; private final int mMaxQueueSize; + private final Executor mExecutor; private final class Interface implements IBinder.DeathRecipient, IBinder.FrozenStateChangeCallback { @@ -197,7 +200,7 @@ public class RemoteCallbackList<E extends IInterface> { void maybeSubscribeToFrozenCallback() throws RemoteException { if (mFrozenCalleePolicy != FROZEN_CALLEE_POLICY_UNSET) { try { - mBinder.addFrozenStateChangeCallback(this); + mBinder.addFrozenStateChangeCallback(mExecutor, this); } catch (UnsupportedOperationException e) { // The kernel does not support frozen notifications. In this case we want to // silently fall back to FROZEN_CALLEE_POLICY_UNSET. This is done by simply @@ -237,6 +240,7 @@ public class RemoteCallbackList<E extends IInterface> { private @FrozenCalleePolicy int mFrozenCalleePolicy; private int mMaxQueueSize = DEFAULT_MAX_QUEUE_SIZE; private InterfaceDiedCallback mInterfaceDiedCallback; + private Executor mExecutor; /** * Creates a Builder for {@link RemoteCallbackList}. @@ -285,6 +289,18 @@ public class RemoteCallbackList<E extends IInterface> { } /** + * Sets the executor to be used when invoking callbacks asynchronously. + * + * This is only used when callbacks need to be invoked asynchronously, e.g. when the process + * hosting a callback becomes unfrozen. Callbacks that can be invoked immediately run on the + * same thread that calls {@link #broadcast} synchronously. + */ + public @NonNull Builder setExecutor(@NonNull @CallbackExecutor Executor executor) { + mExecutor = executor; + return this; + } + + /** * For notifying when the process hosting a callback interface has died. * * @param <E> The remote callback interface type. @@ -308,15 +324,21 @@ public class RemoteCallbackList<E extends IInterface> { * @return The built {@link RemoteCallbackList} object. */ public @NonNull RemoteCallbackList<E> build() { + Executor executor = mExecutor; + if (executor == null && mFrozenCalleePolicy != FROZEN_CALLEE_POLICY_UNSET) { + // TODO Throw an exception here once the existing API caller is updated to provide + // an executor. + executor = new HandlerExecutor(Handler.getMain()); + } if (mInterfaceDiedCallback != null) { - return new RemoteCallbackList<E>(mFrozenCalleePolicy, mMaxQueueSize) { + return new RemoteCallbackList<E>(mFrozenCalleePolicy, mMaxQueueSize, executor) { @Override public void onCallbackDied(E deadInterface, Object cookie) { mInterfaceDiedCallback.onInterfaceDied(this, deadInterface, cookie); } }; } - return new RemoteCallbackList<E>(mFrozenCalleePolicy, mMaxQueueSize); + return new RemoteCallbackList<E>(mFrozenCalleePolicy, mMaxQueueSize, executor); } } @@ -341,13 +363,23 @@ public class RemoteCallbackList<E extends IInterface> { } /** + * Returns the executor used when invoking callbacks asynchronously. + * + * @return The executor. + */ + @FlaggedApi(Flags.FLAG_BINDER_FROZEN_STATE_CHANGE_CALLBACK) + public @Nullable Executor getExecutor() { + return mExecutor; + } + + /** * Creates a RemoteCallbackList with {@link #FROZEN_CALLEE_POLICY_UNSET}. This is equivalent to * <pre> * new RemoteCallbackList.Build(RemoteCallbackList.FROZEN_CALLEE_POLICY_UNSET).build() * </pre> */ public RemoteCallbackList() { - this(FROZEN_CALLEE_POLICY_UNSET, DEFAULT_MAX_QUEUE_SIZE); + this(FROZEN_CALLEE_POLICY_UNSET, DEFAULT_MAX_QUEUE_SIZE, null); } /** @@ -362,10 +394,14 @@ public class RemoteCallbackList<E extends IInterface> { * recipient's process is frozen. Once the limit is reached, the oldest callbacks would be * dropped to keep the size under limit. Ignored except for * {@link #FROZEN_CALLEE_POLICY_ENQUEUE_ALL}. + * + * @param executor The executor used when invoking callbacks asynchronously. */ - private RemoteCallbackList(@FrozenCalleePolicy int frozenCalleePolicy, int maxQueueSize) { + private RemoteCallbackList(@FrozenCalleePolicy int frozenCalleePolicy, int maxQueueSize, + @CallbackExecutor Executor executor) { mFrozenCalleePolicy = frozenCalleePolicy; mMaxQueueSize = maxQueueSize; + mExecutor = executor; } /** diff --git a/core/java/android/os/flags.aconfig b/core/java/android/os/flags.aconfig index 9b1bf057b815..2ef8764a5221 100644 --- a/core/java/android/os/flags.aconfig +++ b/core/java/android/os/flags.aconfig @@ -4,15 +4,6 @@ container: "system" # keep-sorted start block=yes newline_separated=yes flag { - # Holdback study for concurrent MessageQueue. - # Do not promote beyond trunkfood. - namespace: "system_performance" - name: "message_queue_force_legacy" - description: "Whether to holdback concurrent MessageQueue (force legacy)." - bug: "336880969" -} - -flag { name: "adpf_gpu_report_actual_work_duration" is_exported: true namespace: "game" diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig index b5a822b715d5..1093503a45a5 100644 --- a/core/java/android/permission/flags.aconfig +++ b/core/java/android/permission/flags.aconfig @@ -418,3 +418,12 @@ flag { description: "Add new checkOp APIs that accept attributionTag" bug: "240617242" } + +flag { + name: "device_policy_management_role_split_create_managed_profile_enabled" + is_fixed_read_only: true + is_exported: true + namespace: "enterprise" + description: "Gives the device policy management role the ability to create a managed profile using new APIs" + bug: "375382324" +} diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index a35c9c1cd4ec..19b0c6fcdd77 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -20454,6 +20454,29 @@ public final class Settings { * @hide */ public static final String AUTO_BEDTIME_MODE = "auto_bedtime_mode"; + + /** + * Indicates that all elements of the system status tray on wear should be rendered + * by default wear system. + * + * @hide + */ + public static final int STATUS_TRAY_CONFIGURATION_DEFAULT = 0; + + /** + * Indicates that all elements of the system status tray on wear should be hidden. + * + * @hide + */ + public static final int STATUS_TRAY_CONFIGURATION_SYSTEM_HIDDEN = 1; + + /** + * Configuration of system status tray in wear. + * + * @hide + */ + public static final String WEAR_SYSTEM_STATUS_TRAY_CONFIGURATION = + "wear_system_status_tray_configuration"; } } diff --git a/core/java/android/security/authenticationpolicy/AuthenticationPolicyManager.java b/core/java/android/security/authenticationpolicy/AuthenticationPolicyManager.java new file mode 100644 index 000000000000..75abd5fa4bb0 --- /dev/null +++ b/core/java/android/security/authenticationpolicy/AuthenticationPolicyManager.java @@ -0,0 +1,237 @@ +/* + * 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.security.authenticationpolicy; + +import static android.Manifest.permission.MANAGE_SECURE_LOCK_DEVICE; +import static android.security.Flags.FLAG_SECURE_LOCKDOWN; + +import android.annotation.FlaggedApi; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.RequiresPermission; +import android.annotation.SystemApi; +import android.annotation.SystemService; +import android.content.Context; +import android.os.RemoteException; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * AuthenticationPolicyManager is a centralized interface for managing authentication related + * policies on the device. This includes device locking capabilities to protect users in "at risk" + * environments. + * + * AuthenticationPolicyManager is designed to protect Android users by integrating with apps and + * key system components, such as the lock screen. It is not related to enterprise control surfaces + * and does not offer additional administrative controls. + * + * <p> + * To use this class, call {@link #enableSecureLockDevice} to enable secure lock on the device. + * This will require the caller to have the + * {@link android.Manifest.permission#MANAGE_SECURE_LOCK_DEVICE} permission. + * + * <p> + * To disable secure lock on the device, call {@link #disableSecureLockDevice}. This will require + * the caller to have the {@link android.Manifest.permission#MANAGE_SECURE_LOCK_DEVICE} permission. + * + * @hide + */ +@SystemApi +@FlaggedApi(FLAG_SECURE_LOCKDOWN) +@SystemService(Context.AUTHENTICATION_POLICY_SERVICE) +public final class AuthenticationPolicyManager { + private static final String TAG = "AuthenticationPolicyManager"; + + @NonNull private final IAuthenticationPolicyService mAuthenticationPolicyService; + @NonNull private final Context mContext; + + /** + * Error result code for {@link #enableSecureLockDevice} and {@link + * #disableSecureLockDevice}. + * + * Secure lock device request status unknown. + * + * @hide + */ + @SystemApi + @FlaggedApi(FLAG_SECURE_LOCKDOWN) + public static final int ERROR_UNKNOWN = 0; + + /** + * Success result code for {@link #enableSecureLockDevice} and {@link #disableSecureLockDevice}. + * + * Secure lock device request successful. + * + * @hide + */ + @SystemApi + @FlaggedApi(FLAG_SECURE_LOCKDOWN) + public static final int SUCCESS = 1; + + /** + * Error result code for {@link #enableSecureLockDevice} and {@link #disableSecureLockDevice}. + * + * Secure lock device is unsupported. + * + * @hide + */ + @SystemApi + @FlaggedApi(FLAG_SECURE_LOCKDOWN) + public static final int ERROR_UNSUPPORTED = 2; + + + /** + * Error result code for {@link #enableSecureLockDevice} and {@link #disableSecureLockDevice}. + * + * Invalid secure lock device request params provided. + * + * @hide + */ + @SystemApi + @FlaggedApi(FLAG_SECURE_LOCKDOWN) + public static final int ERROR_INVALID_PARAMS = 3; + + + /** + * Error result code for {@link #enableSecureLockDevice} and {@link #disableSecureLockDevice}. + * + * Secure lock device is unavailable because there are no biometrics enrolled on the device. + * + * @hide + */ + @SystemApi + @FlaggedApi(FLAG_SECURE_LOCKDOWN) + public static final int ERROR_NO_BIOMETRICS_ENROLLED = 4; + + /** + * Error result code for {@link #enableSecureLockDevice} and {@link #disableSecureLockDevice}. + * + * Secure lock device is unavailable because the device has no biometric hardware or the + * biometric sensors do not meet + * {@link android.hardware.biometrics.BiometricManager.Authenticators#BIOMETRIC_STRONG} + * + * @hide + */ + @SystemApi + @FlaggedApi(FLAG_SECURE_LOCKDOWN) + public static final int ERROR_INSUFFICIENT_BIOMETRICS = 5; + + /** + * Error result code for {@link #enableSecureLockDevice}. + * + * Secure lock is already enabled. + * + * @hide + */ + @SystemApi + @FlaggedApi(FLAG_SECURE_LOCKDOWN) + public static final int ERROR_ALREADY_ENABLED = 6; + + /** + * Communicates the current status of a request to enable secure lock on the device. + * + * @hide + */ + @IntDef(prefix = {"ENABLE_SECURE_LOCK_DEVICE_STATUS_"}, value = { + ERROR_UNKNOWN, + SUCCESS, + ERROR_UNSUPPORTED, + ERROR_INVALID_PARAMS, + ERROR_NO_BIOMETRICS_ENROLLED, + ERROR_INSUFFICIENT_BIOMETRICS, + ERROR_ALREADY_ENABLED + }) + @Retention(RetentionPolicy.SOURCE) + public @interface EnableSecureLockDeviceRequestStatus {} + + /** + * Communicates the current status of a request to disable secure lock on the device. + * + * @hide + */ + @IntDef(prefix = {"DISABLE_SECURE_LOCK_DEVICE_STATUS_"}, value = { + ERROR_UNKNOWN, + SUCCESS, + ERROR_UNSUPPORTED, + ERROR_INVALID_PARAMS, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface DisableSecureLockDeviceRequestStatus {} + + /** @hide */ + public AuthenticationPolicyManager(@NonNull Context context, + @NonNull IAuthenticationPolicyService authenticationPolicyService) { + mContext = context; + mAuthenticationPolicyService = authenticationPolicyService; + } + + /** + * Called by a privileged component to remotely enable secure lock on the device. + * + * Secure lock is an enhanced security state that restricts access to sensitive data (app + * notifications, widgets, quick settings, assistant, etc) and requires multi-factor + * authentication for device entry, such as + * {@link android.hardware.biometrics.BiometricManager.Authenticators#DEVICE_CREDENTIAL} and + * {@link android.hardware.biometrics.BiometricManager.Authenticators#BIOMETRIC_STRONG}. + * + * If secure lock is already enabled when this method is called, it will return + * {@link ERROR_ALREADY_ENABLED}. + * + * @param params EnableSecureLockDeviceParams for caller to supply params related to the secure + * lock device request + * @return @EnableSecureLockDeviceRequestStatus int indicating the result of the secure lock + * device request + * + * @hide + */ + @EnableSecureLockDeviceRequestStatus + @RequiresPermission(MANAGE_SECURE_LOCK_DEVICE) + @SystemApi + @FlaggedApi(FLAG_SECURE_LOCKDOWN) + public int enableSecureLockDevice(@NonNull EnableSecureLockDeviceParams params) { + try { + return mAuthenticationPolicyService.enableSecureLockDevice(params); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Called by a privileged component to disable secure lock on the device. + * + * If secure lock is already disabled when this method is called, it will return + * {@link SUCCESS}. + * + * @param params @DisableSecureLockDeviceParams for caller to supply params related to the + * secure lock device request + * @return @DisableSecureLockDeviceRequestStatus int indicating the result of the secure lock + * device request + * + * @hide + */ + @DisableSecureLockDeviceRequestStatus + @RequiresPermission(MANAGE_SECURE_LOCK_DEVICE) + @SystemApi + @FlaggedApi(FLAG_SECURE_LOCKDOWN) + public int disableSecureLockDevice(@NonNull DisableSecureLockDeviceParams params) { + try { + return mAuthenticationPolicyService.disableSecureLockDevice(params); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } +} diff --git a/core/java/android/security/authenticationpolicy/DisableSecureLockDeviceParams.aidl b/core/java/android/security/authenticationpolicy/DisableSecureLockDeviceParams.aidl new file mode 100644 index 000000000000..81f7726a500c --- /dev/null +++ b/core/java/android/security/authenticationpolicy/DisableSecureLockDeviceParams.aidl @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.security.authenticationpolicy; + +/** + * @hide + */ +parcelable DisableSecureLockDeviceParams;
\ No newline at end of file diff --git a/core/java/android/security/authenticationpolicy/DisableSecureLockDeviceParams.java b/core/java/android/security/authenticationpolicy/DisableSecureLockDeviceParams.java new file mode 100644 index 000000000000..64a3f0f60f96 --- /dev/null +++ b/core/java/android/security/authenticationpolicy/DisableSecureLockDeviceParams.java @@ -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 android.security.authenticationpolicy; + +import static android.security.Flags.FLAG_SECURE_LOCKDOWN; + +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Objects; + +/** + * Parameters related to a request to disable secure lock on the device. + * + * @hide + */ +@SystemApi +@FlaggedApi(FLAG_SECURE_LOCKDOWN) +public final class DisableSecureLockDeviceParams implements Parcelable { + + /** + * Client message associated with the request to disable secure lock on the device. This message + * will be shown on the device when secure lock mode is disabled. + */ + private final @NonNull String mMessage; + + /** + * Creates DisableSecureLockDeviceParams with the given params. + * + * @param message Allows clients to pass in a message with information about the request to + * disable secure lock on the device. This message will be shown to the user when + * secure lock mode is disabled. If an empty string is provided, it will default + * to a system-defined string (e.g. "Secure lock mode has been disabled.") + */ + public DisableSecureLockDeviceParams(@NonNull String message) { + mMessage = message; + } + + private DisableSecureLockDeviceParams(@NonNull Parcel in) { + mMessage = Objects.requireNonNull(in.readString8()); + } + + public static final @NonNull Creator<DisableSecureLockDeviceParams> CREATOR = + new Creator<DisableSecureLockDeviceParams>() { + @Override + public DisableSecureLockDeviceParams createFromParcel(Parcel in) { + return new DisableSecureLockDeviceParams(in); + } + + @Override + public DisableSecureLockDeviceParams[] newArray(int size) { + return new DisableSecureLockDeviceParams[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeString8(mMessage); + } +} diff --git a/core/java/android/security/authenticationpolicy/EnableSecureLockDeviceParams.aidl b/core/java/android/security/authenticationpolicy/EnableSecureLockDeviceParams.aidl new file mode 100644 index 000000000000..9e496f82ec69 --- /dev/null +++ b/core/java/android/security/authenticationpolicy/EnableSecureLockDeviceParams.aidl @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.security.authenticationpolicy; + +/** + * @hide + */ +parcelable EnableSecureLockDeviceParams;
\ No newline at end of file diff --git a/core/java/android/security/authenticationpolicy/EnableSecureLockDeviceParams.java b/core/java/android/security/authenticationpolicy/EnableSecureLockDeviceParams.java new file mode 100644 index 000000000000..1d727727ce37 --- /dev/null +++ b/core/java/android/security/authenticationpolicy/EnableSecureLockDeviceParams.java @@ -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 android.security.authenticationpolicy; + +import static android.security.Flags.FLAG_SECURE_LOCKDOWN; + +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.Objects; + + +/** + * Parameters related to a request to enable secure lock on the device. + * + * @hide + */ +@SystemApi +@FlaggedApi(FLAG_SECURE_LOCKDOWN) +public final class EnableSecureLockDeviceParams implements Parcelable { + + /** + * Client message associated with the request to enable secure lock on the device. This message + * will be shown on the device when secure lock mode is enabled. + */ + private final @NonNull String mMessage; + + /** + * Creates EnableSecureLockDeviceParams with the given params. + * + * @param message Allows clients to pass in a message with information about the request to + * enable secure lock on the device. This message will be shown to the user when + * secure lock mode is enabled. If an empty string is provided, it will default + * to a system-defined string (e.g. "Device is securely locked remotely.") + */ + public EnableSecureLockDeviceParams(@NonNull String message) { + mMessage = message; + } + + private EnableSecureLockDeviceParams(@NonNull Parcel in) { + mMessage = Objects.requireNonNull(in.readString8()); + } + + public static final @NonNull Creator<EnableSecureLockDeviceParams> CREATOR = + new Creator<EnableSecureLockDeviceParams>() { + @Override + public EnableSecureLockDeviceParams createFromParcel(Parcel in) { + return new EnableSecureLockDeviceParams(in); + } + + @Override + public EnableSecureLockDeviceParams[] newArray(int size) { + return new EnableSecureLockDeviceParams[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeString8(mMessage); + } +} diff --git a/core/java/android/security/authenticationpolicy/IAuthenticationPolicyService.aidl b/core/java/android/security/authenticationpolicy/IAuthenticationPolicyService.aidl new file mode 100644 index 000000000000..5ad4534c257a --- /dev/null +++ b/core/java/android/security/authenticationpolicy/IAuthenticationPolicyService.aidl @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.security.authenticationpolicy; + +import android.security.authenticationpolicy.EnableSecureLockDeviceParams; +import android.security.authenticationpolicy.DisableSecureLockDeviceParams; + +/** + * Communication channel from AuthenticationPolicyManager to AuthenticationPolicyService. + * @hide + */ +interface IAuthenticationPolicyService { + @EnforcePermission("MANAGE_SECURE_LOCK_DEVICE") + int enableSecureLockDevice(in EnableSecureLockDeviceParams params); + + @EnforcePermission("MANAGE_SECURE_LOCK_DEVICE") + int disableSecureLockDevice(in DisableSecureLockDeviceParams params); +}
\ No newline at end of file diff --git a/core/java/android/security/authenticationpolicy/OWNERS b/core/java/android/security/authenticationpolicy/OWNERS new file mode 100644 index 000000000000..4310d1a3a9db --- /dev/null +++ b/core/java/android/security/authenticationpolicy/OWNERS @@ -0,0 +1 @@ +include /services/core/java/com/android/server/security/authenticationpolicy/OWNERS
\ No newline at end of file diff --git a/core/java/android/service/carrier/CarrierMessagingService.java b/core/java/android/service/carrier/CarrierMessagingService.java index 61213e6293ba..a825a7e110f5 100644 --- a/core/java/android/service/carrier/CarrierMessagingService.java +++ b/core/java/android/service/carrier/CarrierMessagingService.java @@ -16,6 +16,7 @@ package android.service.carrier; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -26,6 +27,8 @@ import android.net.Uri; import android.os.IBinder; import android.os.RemoteException; +import com.android.internal.telephony.flags.Flags; + import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.List; @@ -97,16 +100,317 @@ public abstract class CarrierMessagingService extends Service { public static final int SEND_STATUS_RETRY_ON_CARRIER_NETWORK = 1; /** - * SMS/MMS sending failed. We should not retry via the carrier network. + * SMS/MMS sending failed due to an unspecified issue. Sending will not be retried via the + * carrier network. + * + * <p>Maps to SmsManager.RESULT_RIL_GENERIC_FAILURE for SMS and SmsManager.MMS_ERROR_UNSPECIFIED + * for MMS. */ public static final int SEND_STATUS_ERROR = 2; + /** + * More precise error reasons for outbound SMS send requests. These will not be retried on the + * carrier network. + * + * <p>Each code maps directly to an SmsManager code (e.g. SEND_STATS_RESULT_ERROR_NULL_PDU maps + * to SmsManager.RESULT_ERROR_NULL_PDU). + */ + + /** + * Generic failure cause. + * + * @see android.telephony.SmsManager.RESULT_ERROR_GENERIC_FAILURE + */ + @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE) + public static final int SEND_STATUS_RESULT_ERROR_GENERIC_FAILURE = 200; + + /** + * Failed because no pdu provided. + * + * @see android.telephony.SmsManager.RESULT_ERROR_NULL_PDU + */ + @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE) + public static final int SEND_STATUS_RESULT_ERROR_NULL_PDU = 201; + + /** + * Failed because service is currently unavailable. + * + * @see android.telephony.SmsManager.RESULT_ERROR_NO_SERVICE + */ + @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE) + public static final int SEND_STATUS_RESULT_ERROR_NO_SERVICE = 202; + + /** + * Failed because we reached the sending queue limit. + * + * @see android.telephony.SmsManager.RESULT_ERROR_LIMIT_EXCEEDED + */ + @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE) + public static final int SEND_STATUS_RESULT_ERROR_LIMIT_EXCEEDED = 203; + + /** + * Failed because FDN is enabled. + * + * @see android.telephony.SmsManager.RESULT_ERROR_FDN_CHECK_FAILURE + */ + @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE) + public static final int SEND_STATUS_RESULT_ERROR_FDN_CHECK_FAILURE = 204; + + /** + * Failed because user denied the sending of this short code. + * + * @see android.telephony.SmsManager.RESULT_ERROR_SHORT_CODE_NOT_ALLOWED + */ + @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE) + public static final int SEND_STATUS_RESULT_ERROR_SHORT_CODE_NOT_ALLOWED = 205; + + /** + * Failed because the user has denied this app ever send premium short codes. + * + * @see android.telephony.SmsManager.RESULT_ERROR_SHORT_CODE_NEVER_ALLOWED + */ + @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE) + public static final int SEND_STATUS_RESULT_ERROR_SHORT_CODE_NEVER_ALLOWED = 206; + + /** + * Failed because of network rejection. + * + * @see android.telephony.SmsManager.RESULT_NETWORK_REJECT + */ + @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE) + public static final int SEND_STATUS_RESULT_NETWORK_REJECT = 207; + + /** + * Failed because of invalid arguments. + * + * @see android.telephony.SmsManager.RESULT_INVALID_ARGUMENTS + */ + @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE) + public static final int SEND_STATUS_RESULT_INVALID_ARGUMENTS = 208; + + /** + * Failed because of an invalid state. + * + * @see android.telephony.SmsManager.RESULT_INVALID_STATE + */ + @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE) + public static final int SEND_STATUS_RESULT_INVALID_STATE = 209; + + /** + * Failed because the sms format is not valid. + * + * @see android.telephony.SmsManager.RESULT_INVALID_SMS_FORMAT + */ + @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE) + public static final int SEND_STATUS_RESULT_INVALID_SMS_FORMAT = 210; + + /** + * Failed because of a network error. + * + * @see android.telephony.SmsManager.RESULT_NETWORK_ERROR + */ + @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE) + public static final int SEND_STATUS_RESULT_NETWORK_ERROR = 211; + + /** + * Failed because of an encoding error. + * + * @see android.telephony.SmsManager.RESULT_ENCODING_ERROR + */ + @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE) + public static final int SEND_STATUS_RESULT_ENCODING_ERROR = 212; + + /** + * Failed because of an invalid smsc address + * + * @see android.telephony.SmsManager.RESULT_INVALID_SMSC_ADDRESS + */ + @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE) + public static final int SEND_STATUS_RESULT_INVALID_SMSC_ADDRESS = 213; + + /** + * Failed because the operation is not allowed. + * + * @see android.telephony.SmsManager.RESULT_OPERATION_NOT_ALLOWED + */ + @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE) + public static final int SEND_STATUS_RESULT_OPERATION_NOT_ALLOWED = 214; + + /** + * Failed because the operation was cancelled. + * + * @see android.telephony.SmsManager.RESULT_CANCELLED + */ + @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE) + public static final int SEND_STATUS_RESULT_CANCELLED = 215; + + /** + * Failed because the request is not supported. + * + * @see android.telephony.SmsManager.RESULT_REQUEST_NOT_SUPPORTED + */ + @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE) + public static final int SEND_STATUS_RESULT_REQUEST_NOT_SUPPORTED = 216; + + /** + * Failed sending during an emergency call. + * + * @see android.telephony.SmsManager.RESULT_SMS_BLOCKED_DURING_EMERGENCY + */ + @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE) + public static final int SEND_STATUS_RESULT_SMS_BLOCKED_DURING_EMERGENCY = 217; + + /** + * Failed to send an sms retry. + * + * @see android.telephony.SmsManager.RESULT_SMS_SEND_RETRY_FAILED + */ + @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE) + public static final int SEND_STATUS_RESULT_SMS_SEND_RETRY_FAILED = 218; + + /** + * More precise error reasons for outbound MMS send requests. These will not be retried on the + * carrier network. + * + * <p>Each code maps directly to an SmsManager code (e.g. + * SEND_STATUS_MMS_ERROR_UNABLE_CONNECT_MMS maps to SmsManager.MMS_ERROR_UNABLE_CONNECT_MMS). + */ + + /** + * Unspecific MMS error occurred during send. + * + * @see android.telephony.SmsManager.MMS_ERROR_UNSPECIFIED + */ + @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE) + public static final int SEND_STATUS_MMS_ERROR_UNSPECIFIED = 400; + + /** + * ApnException occurred during MMS network setup. + * + * @see android.telephony.SmsManager.MMS_ERROR_INVALID_APN + */ + @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE) + public static final int SEND_STATUS_MMS_ERROR_INVALID_APN = 401; + + /** + * An error occurred during the MMS connection setup. + * + * @see android.telephony.SmsManager.MMS_ERROR_UNABLE_CONNECT_MMS + */ + @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE) + public static final int SEND_STATUS_MMS_ERROR_UNABLE_CONNECT_MMS = 402; + + /** + * An error occurred during the HTTP client setup. + * + * @see android.telephony.SmsManager.MMS_ERROR_HTTP_FAILURE + */ + @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE) + public static final int SEND_STATUS_MMS_ERROR_HTTP_FAILURE = 403; + + /** + * An I/O error occurred reading the PDU. + * + * @see android.telephony.SmsManager.MMS_ERROR_IO_ERROR + */ + @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE) + public static final int SEND_STATUS_MMS_ERROR_IO_ERROR = 404; + + /** + * An error occurred while retrying sending the MMS. + * + * @see android.telephony.SmsManager.MMS_ERROR_RETRY + */ + @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE) + public static final int SEND_STATUS_MMS_ERROR_RETRY = 405; + + /** + * The carrier-dependent configuration values could not be loaded. + * + * @see android.telephony.SmsManager.MMS_ERROR_CONFIGURATION_ERROR + */ + @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE) + public static final int SEND_STATUS_MMS_ERROR_CONFIGURATION_ERROR = 406; + + /** + * There is neither Wi-Fi nor mobile data network. + * + * @see android.telephony.SmsManager.MMS_ERROR_NO_DATA_NETWORK + */ + @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE) + public static final int SEND_STATUS_MMS_ERROR_NO_DATA_NETWORK = 407; + + /** + * The subscription id for the send is invalid. + * + * @see android.telephony.SmsManager.MMS_ERROR_INVALID_SUBSCRIPTION_ID + */ + @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE) + public static final int SEND_STATUS_MMS_ERROR_INVALID_SUBSCRIPTION_ID = 408; + + /** + * The subscription id for the send is inactive. + * + * @see android.telephony.SmsManager.MMS_ERROR_INACTIVE_SUBSCRIPTION + */ + @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE) + public static final int SEND_STATUS_MMS_ERROR_INACTIVE_SUBSCRIPTION = 409; + + /** + * Data is disabled for the MMS APN. + * + * @see android.telephony.SmsManager.MMS_ERROR_DATA_DISABLED + */ + @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE) + public static final int SEND_STATUS_MMS_ERROR_DATA_DISABLED = 410; + + /** + * MMS is disabled by a carrier. + * + * @see android.telephony.SmsManager.MMS_ERROR_MMS_DISABLED_BY_CARRIER + */ + @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE) + public static final int SEND_STATUS_MMS_ERROR_MMS_DISABLED_BY_CARRIER = 411; + /** @hide */ - @IntDef(prefix = { "SEND_STATUS_" }, value = { - SEND_STATUS_OK, - SEND_STATUS_RETRY_ON_CARRIER_NETWORK, - SEND_STATUS_ERROR - }) + @IntDef( + prefix = {"SEND_STATUS_"}, + value = { + SEND_STATUS_OK, + SEND_STATUS_RETRY_ON_CARRIER_NETWORK, + SEND_STATUS_ERROR, + SEND_STATUS_RESULT_ERROR_GENERIC_FAILURE, + SEND_STATUS_RESULT_ERROR_NULL_PDU, + SEND_STATUS_RESULT_ERROR_NO_SERVICE, + SEND_STATUS_RESULT_ERROR_LIMIT_EXCEEDED, + SEND_STATUS_RESULT_ERROR_FDN_CHECK_FAILURE, + SEND_STATUS_RESULT_ERROR_SHORT_CODE_NOT_ALLOWED, + SEND_STATUS_RESULT_ERROR_SHORT_CODE_NEVER_ALLOWED, + SEND_STATUS_RESULT_NETWORK_REJECT, + SEND_STATUS_RESULT_INVALID_ARGUMENTS, + SEND_STATUS_RESULT_INVALID_STATE, + SEND_STATUS_RESULT_INVALID_SMS_FORMAT, + SEND_STATUS_RESULT_NETWORK_ERROR, + SEND_STATUS_RESULT_ENCODING_ERROR, + SEND_STATUS_RESULT_INVALID_SMSC_ADDRESS, + SEND_STATUS_RESULT_OPERATION_NOT_ALLOWED, + SEND_STATUS_RESULT_CANCELLED, + SEND_STATUS_RESULT_REQUEST_NOT_SUPPORTED, + SEND_STATUS_RESULT_SMS_BLOCKED_DURING_EMERGENCY, + SEND_STATUS_RESULT_SMS_SEND_RETRY_FAILED, + SEND_STATUS_MMS_ERROR_UNSPECIFIED, + SEND_STATUS_MMS_ERROR_INVALID_APN, + SEND_STATUS_MMS_ERROR_UNABLE_CONNECT_MMS, + SEND_STATUS_MMS_ERROR_HTTP_FAILURE, + SEND_STATUS_MMS_ERROR_IO_ERROR, + SEND_STATUS_MMS_ERROR_RETRY, + SEND_STATUS_MMS_ERROR_CONFIGURATION_ERROR, + SEND_STATUS_MMS_ERROR_NO_DATA_NETWORK, + SEND_STATUS_MMS_ERROR_INVALID_SUBSCRIPTION_ID, + SEND_STATUS_MMS_ERROR_INACTIVE_SUBSCRIPTION, + SEND_STATUS_MMS_ERROR_DATA_DISABLED, + SEND_STATUS_MMS_ERROR_MMS_DISABLED_BY_CARRIER + }) @Retention(RetentionPolicy.SOURCE) public @interface SendResult {} @@ -121,16 +425,138 @@ public abstract class CarrierMessagingService extends Service { public static final int DOWNLOAD_STATUS_RETRY_ON_CARRIER_NETWORK = 1; /** - * MMS downloading failed. We should not retry via the carrier network. + * MMS downloading failed due to an unspecified issue. Downloading will not be retried via the + * carrier network. + * + * <p>Maps to SmsManager.MMR_ERROR_UNSPECIFIED. */ public static final int DOWNLOAD_STATUS_ERROR = 2; + /** + * More precise error reasons for inbound MMS download requests. These will not be retried on + * the carrier network. + * + * <p>Each code maps directly to an SmsManager code (e.g. + * DOWNLOAD_STATUS_MMS_ERROR_UNABLE_CONNECT_MMS maps to + * SmsManager.MMS_ERROR_UNABLE_CONNECT_MMS). + */ + + /** + * Unspecific MMS error occurred during download. + * + * @see android.telephony.SmsManager.MMS_ERROR_UNSPECIFIED + */ + @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE) + public static final int DOWNLOAD_STATUS_MMS_ERROR_UNSPECIFIED = 600; + + /** + * ApnException occurred during MMS network setup. + * + * @see android.telephony.SmsManager.MMS_ERROR_INVALID_APN + */ + @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE) + public static final int DOWNLOAD_STATUS_MMS_ERROR_INVALID_APN = 601; + + /** + * An error occurred during the MMS connection setup. + * + * @see android.telephony.SmsManager.MMS_ERROR_UNABLE_CONNECT_MMS + */ + @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE) + public static final int DOWNLOAD_STATUS_MMS_ERROR_UNABLE_CONNECT_MMS = 602; + + /** + * An error occurred during the HTTP client setup. + * + * @see android.telephony.SmsManager.MMS_ERROR_HTTP_FAILURE + */ + @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE) + public static final int DOWNLOAD_STATUS_MMS_ERROR_HTTP_FAILURE = 603; + + /** + * An I/O error occurred reading the PDU. + * + * @see android.telephony.SmsManager.MMS_ERROR_IO_ERROR + */ + @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE) + public static final int DOWNLOAD_STATUS_MMS_ERROR_IO_ERROR = 604; + + /** + * An error occurred while retrying downloading the MMS. + * + * @see android.telephony.SmsManager.MMS_ERROR_RETRY + */ + @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE) + public static final int DOWNLOAD_STATUS_MMS_ERROR_RETRY = 605; + + /** + * The carrier-dependent configuration values could not be loaded. + * + * @see android.telephony.SmsManager.MMS_ERROR_CONFIGURATION_ERROR + */ + @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE) + public static final int DOWNLOAD_STATUS_MMS_ERROR_CONFIGURATION_ERROR = 606; + + /** + * There is neither Wi-Fi nor mobile data network. + * + * @see android.telephony.SmsManager.MMS_ERROR_NO_DATA_NETWORK + */ + @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE) + public static final int DOWNLOAD_STATUS_MMS_ERROR_NO_DATA_NETWORK = 607; + + /** + * The subscription id for the download is invalid. + * + * @see android.telephony.SmsManager.MMS_ERROR_INVALID_SUBSCRIPTION_ID + */ + @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE) + public static final int DOWNLOAD_STATUS_MMS_ERROR_INVALID_SUBSCRIPTION_ID = 608; + + /** + * The subscription id for the download is inactive. + * + * @see android.telephony.SmsManager.MMS_ERROR_INACTIVE_SUBSCRIPTION + */ + @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE) + public static final int DOWNLOAD_STATUS_MMS_ERROR_INACTIVE_SUBSCRIPTION = 609; + + /** + * Data is disabled for the MMS APN. + * + * @see android.telephony.SmsManager.MMS_ERROR_DATA_DISABLED + */ + @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE) + public static final int DOWNLOAD_STATUS_MMS_ERROR_DATA_DISABLED = 610; + + /** + * MMS is disabled by a carrier. + * + * @see android.telephony.SmsManager.MMS_ERROR_MMS_DISABLED_BY_CARRIER + */ + @FlaggedApi(Flags.FLAG_TEMPORARY_FAILURES_IN_CARRIER_MESSAGING_SERVICE) + public static final int DOWNLOAD_STATUS_MMS_ERROR_MMS_DISABLED_BY_CARRIER = 611; + /** @hide */ - @IntDef(prefix = { "DOWNLOAD_STATUS_" }, value = { - DOWNLOAD_STATUS_OK, - DOWNLOAD_STATUS_RETRY_ON_CARRIER_NETWORK, - DOWNLOAD_STATUS_ERROR - }) + @IntDef( + prefix = {"DOWNLOAD_STATUS_"}, + value = { + DOWNLOAD_STATUS_OK, + DOWNLOAD_STATUS_RETRY_ON_CARRIER_NETWORK, + DOWNLOAD_STATUS_ERROR, + DOWNLOAD_STATUS_MMS_ERROR_UNSPECIFIED, + DOWNLOAD_STATUS_MMS_ERROR_INVALID_APN, + DOWNLOAD_STATUS_MMS_ERROR_UNABLE_CONNECT_MMS, + DOWNLOAD_STATUS_MMS_ERROR_HTTP_FAILURE, + DOWNLOAD_STATUS_MMS_ERROR_IO_ERROR, + DOWNLOAD_STATUS_MMS_ERROR_RETRY, + DOWNLOAD_STATUS_MMS_ERROR_CONFIGURATION_ERROR, + DOWNLOAD_STATUS_MMS_ERROR_NO_DATA_NETWORK, + DOWNLOAD_STATUS_MMS_ERROR_INVALID_SUBSCRIPTION_ID, + DOWNLOAD_STATUS_MMS_ERROR_INACTIVE_SUBSCRIPTION, + DOWNLOAD_STATUS_MMS_ERROR_DATA_DISABLED, + DOWNLOAD_STATUS_MMS_ERROR_MMS_DISABLED_BY_CARRIER + }) @Retention(RetentionPolicy.SOURCE) public @interface DownloadResult {} diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java index 24328eb1e825..13887781f1ec 100644 --- a/core/java/android/service/notification/ZenModeConfig.java +++ b/core/java/android/service/notification/ZenModeConfig.java @@ -2532,7 +2532,7 @@ public class ZenModeConfig implements Parcelable { /** Returns whether the rule id corresponds to an implicit rule. */ public static boolean isImplicitRuleId(@NonNull String ruleId) { - return ruleId.startsWith(IMPLICIT_RULE_ID_PREFIX); + return ruleId != null && ruleId.startsWith(IMPLICIT_RULE_ID_PREFIX); } private static int[] tryParseHourAndMinute(String value) { diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig index eebdeadcdeb2..c7d5a9fe6041 100644 --- a/core/java/android/window/flags/lse_desktop_experience.aconfig +++ b/core/java/android/window/flags/lse_desktop_experience.aconfig @@ -253,6 +253,9 @@ flag { namespace: "lse_desktop_experience" description: "Enables enter desktop windowing transition & motion polish changes" bug: "369763947" + metadata { + purpose: PURPOSE_BUGFIX + } } flag { @@ -260,6 +263,9 @@ flag { namespace: "lse_desktop_experience" description: "Enables exit desktop windowing transition & motion polish changes" bug: "353650462" + metadata { + purpose: PURPOSE_BUGFIX + } } flag { @@ -343,6 +349,9 @@ flag { namespace: "lse_desktop_experience" description: "Enables custom transitions for alt-tab app launches in Desktop Mode." bug: "370735595" + metadata { + purpose: PURPOSE_BUGFIX + } } flag { @@ -350,6 +359,9 @@ flag { namespace: "lse_desktop_experience" description: "Enables custom transitions for app launches in Desktop Mode." bug: "375992828" + metadata { + purpose: PURPOSE_BUGFIX + } } flag { diff --git a/core/java/android/window/flags/window_surfaces.aconfig b/core/java/android/window/flags/window_surfaces.aconfig index 96b9dc7cab0e..bb4770768cb1 100644 --- a/core/java/android/window/flags/window_surfaces.aconfig +++ b/core/java/android/window/flags/window_surfaces.aconfig @@ -29,14 +29,6 @@ flag { flag { namespace: "window_surfaces" - name: "secure_window_state" - description: "Move SC secure flag to WindowState level" - is_fixed_read_only: true - bug: "308662081" -} - -flag { - namespace: "window_surfaces" name: "trusted_presentation_listener_for_window" is_exported: true description: "Enable trustedPresentationListener on windows public API" diff --git a/core/jni/android_os_PerformanceHintManager.cpp b/core/jni/android_os_PerformanceHintManager.cpp index aebe7ea7ee61..0f78c9e93a00 100644 --- a/core/jni/android_os_PerformanceHintManager.cpp +++ b/core/jni/android_os_PerformanceHintManager.cpp @@ -88,9 +88,10 @@ void ensureAPerformanceHintBindingInitialized() { "Failed to find required symbol " "APerformanceHint_getPreferredUpdateRateNanos!"); - gAPH_createSessionFn = (APH_createSession)dlsym(handle_, "APerformanceHint_createSession"); + gAPH_createSessionFn = + (APH_createSession)dlsym(handle_, "APerformanceHint_createSessionFromJava"); LOG_ALWAYS_FATAL_IF(gAPH_createSessionFn == nullptr, - "Failed to find required symbol APerformanceHint_createSession!"); + "Failed to find required symbol APerformanceHint_createSessionFromJava!"); gAPH_updateTargetWorkDurationFn = (APH_updateTargetWorkDuration)dlsym(handle_, @@ -106,9 +107,9 @@ void ensureAPerformanceHintBindingInitialized() { "Failed to find required symbol " "APerformanceHint_reportActualWorkDuration!"); - gAPH_closeSessionFn = (APH_closeSession)dlsym(handle_, "APerformanceHint_closeSession"); + gAPH_closeSessionFn = (APH_closeSession)dlsym(handle_, "APerformanceHint_closeSessionFromJava"); LOG_ALWAYS_FATAL_IF(gAPH_closeSessionFn == nullptr, - "Failed to find required symbol APerformanceHint_closeSession!"); + "Failed to find required symbol APerformanceHint_closeSessionFromJava!"); gAPH_sendHintFn = (APH_sendHint)dlsym(handle_, "APerformanceHint_sendHint"); LOG_ALWAYS_FATAL_IF(gAPH_sendHintFn == nullptr, diff --git a/core/res/Android.bp b/core/res/Android.bp index 0e4e22b09e24..8042b30df4dc 100644 --- a/core/res/Android.bp +++ b/core/res/Android.bp @@ -161,6 +161,7 @@ android_app { "android.app.flags-aconfig", "android.appwidget.flags-aconfig", "android.content.pm.flags-aconfig", + "android.media.audio-aconfig", "android.provider.flags-aconfig", "camera_platform_flags", "android.net.platform.flags-aconfig", @@ -170,6 +171,7 @@ android_app { "android.os.vibrator.flags-aconfig", "android.media.tv.flags-aconfig", "android.security.flags-aconfig", + "device_policy_aconfig_flags", "com.android.hardware.input.input-aconfig", "aconfig_trade_in_mode_flags", "art-aconfig-flags", diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 7fcbf19d137f..9b2d3384d99c 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -2649,6 +2649,22 @@ android:label="@string/permlab_getAccounts" /> <uses-permission android:name="android.permission.GET_ACCOUNTS"/> + <!-- @SystemApi Allows access to remove an account. + @FlaggedApi(android.app.admin.flags.Flags.FLAG_SPLIT_CREATE_MANAGED_PROFILE_ENABLED) + <p>Not for use by third-party applications. + @hide --> + <permission android:name="android.permission.REMOVE_ACCOUNTS" + android:protectionLevel="signature|role" + android:featureFlag="android.app.admin.flags.split_create_managed_profile_enabled" /> + + <!-- @SystemApi Allows access to copy an account to another user. + @FlaggedApi(android.app.admin.flags.Flags.FLAG_SPLIT_CREATE_MANAGED_PROFILE_ENABLED) + <p>Not for use by third-party applications. + @hide --> + <permission android:name="android.permission.COPY_ACCOUNTS" + android:protectionLevel="signature|role" + android:featureFlag="android.app.admin.flags.split_create_managed_profile_enabled" /> + <!-- Allows applications to call into AccountAuthenticators. <p>Not for use by third-party applications. --> <permission android:name="android.permission.ACCOUNT_MANAGER" @@ -6492,6 +6508,15 @@ <permission android:name="android.permission.CAPTURE_VOICE_COMMUNICATION_OUTPUT" android:protectionLevel="signature|privileged|role" /> + <!-- @SystemApi Allows an application to bypass concurrency restrictions while + recording audio. For example, apps with this permission can continue to record + while a voice call is active.</p> + @FlaggedApi(android.media.audio.Flags.FLAG_CONCURRENT_AUDIO_RECORD_BYPASS_PERMISSION) + @hide --> + <permission android:name="android.permission.BYPASS_CONCURRENT_RECORD_AUDIO_RESTRICTION" + android:featureFlag="android.media.audio.concurrent_audio_record_bypass_permission" + android:protectionLevel="signature|privileged|role" /> + <!-- @SystemApi Allows an application to capture audio for hotword detection. <p>Not for use by third-party applications.</p> @hide --> @@ -8651,6 +8676,17 @@ <permission android:name="android.permission.SETUP_FSVERITY" android:protectionLevel="signature|privileged"/> + <!-- @SystemApi + @FlaggedApi(android.security.Flags.FLAG_SECURE_LOCKDOWN) + Allows an application to lock down the device into an enhanced security state. + <p>Not for use by third-party applications. + <p>Protection level: signature|privileged + @hide + --> + <permission android:name="android.permission.MANAGE_SECURE_LOCK_DEVICE" + android:protectionLevel="signature|privileged" + android:featureFlag="android.security.secure_lockdown" /> + <!-- Allows app to enter trade-in-mode. <p>Protection level: signature|privileged @hide diff --git a/core/res/res/layout/notification_2025_template_heads_up_base.xml b/core/res/res/layout/notification_2025_template_heads_up_base.xml new file mode 100644 index 000000000000..e4ff835a3524 --- /dev/null +++ b/core/res/res/layout/notification_2025_template_heads_up_base.xml @@ -0,0 +1,66 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2014 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License + --> + +<FrameLayout + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/status_bar_latest_event_content" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:clipChildren="false" + android:tag="headsUp" + > + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:clipChildren="false" + android:orientation="vertical" + > + + <include + layout="@layout/notification_2025_template_collapsed_base" + android:id="@null" + /> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="-20dp" + android:clipChildren="false" + android:orientation="vertical" + > + + <ViewStub + android:layout="@layout/notification_material_reply_text" + android:id="@+id/notification_material_reply_container" + android:layout_width="match_parent" + android:layout_height="wrap_content" + /> + + <include + layout="@layout/notification_template_smart_reply_container" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/notification_2025_content_margin_start" + android:layout_marginEnd="@dimen/notification_content_margin_end" + android:layout_marginTop="@dimen/notification_content_margin" + /> + + <include layout="@layout/notification_material_action_list" /> + </LinearLayout> + </LinearLayout> +</FrameLayout> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index a2a19a23d431..5a6b66cc1504 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -2390,6 +2390,7 @@ <java-symbol type="layout" name="notification_material_action_list" /> <java-symbol type="layout" name="notification_material_action_tombstone" /> <java-symbol type="layout" name="notification_2025_template_collapsed_base" /> + <java-symbol type="layout" name="notification_2025_template_heads_up_base" /> <java-symbol type="layout" name="notification_2025_template_header" /> <java-symbol type="layout" name="notification_template_material_base" /> <java-symbol type="layout" name="notification_template_material_heads_up_base" /> diff --git a/core/tests/coretests/BinderFrozenStateChangeCallbackTestApp/src/com/android/frameworks/coretests/bfscctestapp/BfsccTestAppCmdService.java b/core/tests/coretests/BinderFrozenStateChangeCallbackTestApp/src/com/android/frameworks/coretests/bfscctestapp/BfsccTestAppCmdService.java index fe54aa8d87f0..945147db1ef5 100644 --- a/core/tests/coretests/BinderFrozenStateChangeCallbackTestApp/src/com/android/frameworks/coretests/bfscctestapp/BfsccTestAppCmdService.java +++ b/core/tests/coretests/BinderFrozenStateChangeCallbackTestApp/src/com/android/frameworks/coretests/bfscctestapp/BfsccTestAppCmdService.java @@ -18,6 +18,8 @@ package com.android.frameworks.coretests.bfscctestapp; import android.app.Service; import android.content.Intent; +import android.os.Handler; +import android.os.HandlerExecutor; import android.os.IBinder; import android.os.RemoteException; @@ -36,6 +38,7 @@ public class BfsccTestAppCmdService extends Service { @Override public void listenTo(IBinder binder) throws RemoteException { binder.addFrozenStateChangeCallback( + new HandlerExecutor(Handler.getMain()), (IBinder who, int state) -> mNotifications.offer(state)); } diff --git a/core/tests/coretests/src/android/os/BinderFrozenStateChangeNotificationTest.java b/core/tests/coretests/src/android/os/BinderFrozenStateChangeNotificationTest.java index 195a18a5f521..523fe1a8aa5d 100644 --- a/core/tests/coretests/src/android/os/BinderFrozenStateChangeNotificationTest.java +++ b/core/tests/coretests/src/android/os/BinderFrozenStateChangeNotificationTest.java @@ -200,7 +200,7 @@ public class BinderFrozenStateChangeNotificationTest { IBinder.FrozenStateChangeCallback callback = (IBinder who, int state) -> results.offer(who); try { - binder.addFrozenStateChangeCallback(callback); + binder.addFrozenStateChangeCallback(new HandlerExecutor(Handler.getMain()), callback); } catch (UnsupportedOperationException e) { return; } @@ -227,7 +227,7 @@ public class BinderFrozenStateChangeNotificationTest { final IBinder.FrozenStateChangeCallback callback = (IBinder who, int state) -> queue.offer(state == IBinder.FrozenStateChangeCallback.STATE_FROZEN); - binder.addFrozenStateChangeCallback(callback); + binder.addFrozenStateChangeCallback(new HandlerExecutor(Handler.getMain()), callback); return callback; } catch (UnsupportedOperationException e) { return null; diff --git a/core/tests/coretests/src/com/android/internal/os/BinderDeathDispatcherTest.java b/core/tests/coretests/src/com/android/internal/os/BinderDeathDispatcherTest.java index 67de25eede42..75118873dd75 100644 --- a/core/tests/coretests/src/com/android/internal/os/BinderDeathDispatcherTest.java +++ b/core/tests/coretests/src/com/android/internal/os/BinderDeathDispatcherTest.java @@ -42,6 +42,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import java.io.FileDescriptor; +import java.util.concurrent.Executor; @SmallTest @RunWith(AndroidJUnit4.class) @@ -125,7 +126,7 @@ public class BinderDeathDispatcherTest { } @Override - public void addFrozenStateChangeCallback(FrozenStateChangeCallback callback) + public void addFrozenStateChangeCallback(Executor e, FrozenStateChangeCallback callback) throws RemoteException { } diff --git a/core/tests/systemproperties/src/android/os/SystemPropertiesTest.java b/core/tests/systemproperties/src/android/os/SystemPropertiesTest.java index 75aca1b8820c..7ce2ed823540 100644 --- a/core/tests/systemproperties/src/android/os/SystemPropertiesTest.java +++ b/core/tests/systemproperties/src/android/os/SystemPropertiesTest.java @@ -23,10 +23,11 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; -import android.platform.test.ravenwood.RavenwoodConfig; +import android.platform.test.ravenwood.RavenwoodRule; import androidx.test.filters.SmallTest; +import org.junit.Rule; import org.junit.Test; import java.util.Objects; @@ -39,8 +40,8 @@ public class SystemPropertiesTest { private static final String PERSIST_KEY = "persist.sys.testkey"; private static final String NONEXIST_KEY = "doesnotexist_2341431"; - @RavenwoodConfig.Config - public static final RavenwoodConfig mRavenwood = new RavenwoodConfig.Builder() + @Rule + public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder() .setSystemPropertyMutable(KEY, null) .setSystemPropertyMutable(UNSET_KEY, null) .setSystemPropertyMutable(PERSIST_KEY, null) diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index fea7cb4b422c..329e5de15f5e 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -533,6 +533,8 @@ applications that come with the platform <permission name="com.android.cellbroadcastservice.FULL_ACCESS_CELL_BROADCAST_HISTORY" /> <!-- Permission required for ATS test - CarDevicePolicyManagerTest --> <permission name="android.permission.LOCK_DEVICE" /> + <!-- Permission required for AuthenticationPolicyManagerTest --> + <permission name="android.permission.MANAGE_SECURE_LOCK_DEVICE" /> <!-- Permissions required for CTS test - CtsSafetyCenterTestCases --> <permission name="android.permission.SEND_SAFETY_CENTER_UPDATE" /> <permission name="android.permission.READ_SAFETY_CENTER_STATUS" /> diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml index 5e41865cd31e..375968ab0ad2 100644 --- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml @@ -113,7 +113,7 @@ style="?android:attr/buttonBarButtonStyle" android:layout_width="41dp" android:layout_height="@dimen/desktop_mode_maximize_menu_button_height" - android:layout_marginRight="4dp" + android:layout_marginEnd="4dp" android:background="@drawable/desktop_mode_maximize_menu_button_background" android:importantForAccessibility="yes" android:contentDescription="@string/desktop_mode_maximize_menu_snap_left_button_text" diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index 7078d66d265c..21ec84d9bc0a 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -504,17 +504,15 @@ <dimen name="desktop_mode_maximize_menu_buttons_fill_radius">4dp</dimen> <!-- The padding between the outline and fill of the maximize menu snap and maximize buttons. --> <dimen name="desktop_mode_maximize_menu_snap_and_maximize_buttons_fill_padding">4dp</dimen> - <!-- The padding between the outline and fill of the maximize menu snap and maximize buttons. --> - <dimen name="desktop_mode_maximize_menu_snap_and_maximize_buttons_fill_padding_bottom">8dp</dimen> <!-- The vertical padding between the outline and fill of the maximize menu restore button. --> <dimen name="desktop_mode_maximize_menu_restore_button_fill_vertical_padding">13dp</dimen> <!-- The horizontal padding between the outline and fill of the maximize menu restore button. --> - <dimen name="desktop_mode_maximize_menu_restore_button_fill_horizontal_padding">21dp</dimen> + <dimen name="desktop_mode_maximize_menu_restore_button_fill_horizontal_padding">15dp</dimen> <!-- The padding between the outline and fill of the maximize menu immersive button. --> - <dimen name="desktop_mode_maximize_menu_immersive_button_fill_padding">4dp</dimen> + <dimen name="desktop_mode_maximize_menu_immersive_button_fill_padding">0dp</dimen> <!-- The corner radius of the maximize menu. --> - <dimen name="desktop_mode_maximize_menu_corner_radius">8dp</dimen> + <dimen name="desktop_mode_maximize_menu_corner_radius">16dp</dimen> <!-- The radius of the Maximize menu shadow. --> <dimen name="desktop_mode_maximize_menu_shadow_radius">8dp</dimen> diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml index 012579a6d40c..468c345259d0 100644 --- a/libs/WindowManager/Shell/res/values/strings.xml +++ b/libs/WindowManager/Shell/res/values/strings.xml @@ -318,7 +318,7 @@ <!-- Maximize menu maximize button string. --> <string name="desktop_mode_maximize_menu_maximize_text">Maximize Screen</string> <!-- Maximize menu snap buttons string. --> - <string name="desktop_mode_maximize_menu_snap_text">Snap Screen</string> + <string name="desktop_mode_maximize_menu_snap_text">Resize</string> <!-- Snap resizing non-resizable string. --> <string name="desktop_mode_non_resizable_snap_text">App can\'t be moved here</string> <!-- Accessibility text for the Maximize Menu's immersive button [CHAR LIMIT=NONE] --> diff --git a/libs/WindowManager/Shell/res/values/styles.xml b/libs/WindowManager/Shell/res/values/styles.xml index 55cda783005f..597a921302b7 100644 --- a/libs/WindowManager/Shell/res/values/styles.xml +++ b/libs/WindowManager/Shell/res/values/styles.xml @@ -29,6 +29,7 @@ <item name="android:navigationBarColor">@android:color/transparent</item> <item name="android:windowDrawsSystemBarBackgrounds">true</item> <item name="android:windowAnimationStyle">@null</item> + <item name="android:windowIsTranslucent">true</item> </style> <style name="Animation.ForcedResizable" parent="@android:style/Animation"> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index 603a9ec6bedb..b82496e45415 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -1327,7 +1327,11 @@ public class BubbleController implements ConfigurationChangeListener, /** Promote the provided bubble from the overflow view. */ public void promoteBubbleFromOverflow(Bubble bubble) { - mLogger.log(bubble, BubbleLogger.Event.BUBBLE_OVERFLOW_REMOVE_BACK_TO_STACK); + if (isShowingAsBubbleBar()) { + mLogger.log(bubble, BubbleLogger.Event.BUBBLE_BAR_OVERFLOW_REMOVE_BACK_TO_BAR); + } else { + mLogger.log(bubble, BubbleLogger.Event.BUBBLE_OVERFLOW_REMOVE_BACK_TO_STACK); + } ProtoLog.d(WM_SHELL_BUBBLES, "promoteBubbleFromOverflow=%s", bubble.getKey()); bubble.setInflateSynchronously(mInflateSynchronously); bubble.setShouldAutoExpand(true); @@ -1350,11 +1354,7 @@ public class BubbleController implements ConfigurationChangeListener, if (BubbleOverflow.KEY.equals(key)) { mBubbleData.setSelectedBubbleFromLauncher(mBubbleData.getOverflow()); mLayerView.showExpandedView(mBubbleData.getOverflow()); - if (wasExpanded) { - mLogger.log(BubbleLogger.Event.BUBBLE_BAR_BUBBLE_SWITCHED); - } else { - mLogger.log(BubbleLogger.Event.BUBBLE_BAR_EXPANDED); - } + mLogger.log(BubbleLogger.Event.BUBBLE_BAR_OVERFLOW_SELECTED); return; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java index 4de9dfa54c5d..294569190f68 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java @@ -759,7 +759,9 @@ public class BubbleData { if (b != null) { b.stopInflation(); } - mLogger.logOverflowRemove(b, reason); + if (!mPositioner.isShowingInBubbleBar()) { + mLogger.logStackOverflowRemove(b, reason); + } mOverflowBubbles.remove(b); mStateChange.bubbleRemoved(b, reason); mStateChange.removedOverflowBubble = b; @@ -802,6 +804,27 @@ public class BubbleData { setNewSelectedIndex(indexToRemove); } maybeSendDeleteIntent(reason, bubbleToRemove); + + if (mPositioner.isShowingInBubbleBar()) { + logBubbleBarBubbleRemoved(bubbleToRemove, reason); + } + } + + private void logBubbleBarBubbleRemoved(Bubble bubble, @DismissReason int reason) { + switch (reason) { + case Bubbles.DISMISS_NOTIF_CANCEL: + mLogger.log(bubble, BubbleLogger.Event.BUBBLE_BAR_BUBBLE_REMOVED_CANCELED); + break; + case Bubbles.DISMISS_TASK_FINISHED: + mLogger.log(bubble, BubbleLogger.Event.BUBBLE_BAR_BUBBLE_ACTIVITY_FINISH); + break; + case Bubbles.DISMISS_BLOCKED: + case Bubbles.DISMISS_NO_LONGER_BUBBLE: + mLogger.log(bubble, BubbleLogger.Event.BUBBLE_BAR_BUBBLE_REMOVED_BLOCKED); + break; + default: + // skip logging other events + } } private void setNewSelectedIndex(int indexOfSelected) { @@ -862,7 +885,7 @@ public class BubbleData { return; } ProtoLog.d(WM_SHELL_BUBBLES, "overflowBubble=%s", bubble.getKey()); - mLogger.logOverflowAdd(bubble, reason); + mLogger.logOverflowAdd(bubble, mPositioner.isShowingInBubbleBar(), reason); if (mOverflowBubbles.isEmpty()) { mStateChange.showOverflowChanged = true; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleLogger.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleLogger.java index 36630733e1da..347df330c4b3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleLogger.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleLogger.java @@ -182,10 +182,12 @@ public class BubbleLogger { } /** + * Log when a bubble is removed from overflow in stack view + * * @param b Bubble removed from overflow * @param r Reason that bubble was removed */ - public void logOverflowRemove(Bubble b, @Bubbles.DismissReason int r) { + public void logStackOverflowRemove(Bubble b, @Bubbles.DismissReason int r) { if (r == Bubbles.DISMISS_NOTIF_CANCEL) { log(b, BubbleLogger.Event.BUBBLE_OVERFLOW_REMOVE_CANCEL); } else if (r == Bubbles.DISMISS_GROUP_CANCELLED) { @@ -201,13 +203,19 @@ public class BubbleLogger { * @param b Bubble added to overflow * @param r Reason that bubble was added to overflow */ - public void logOverflowAdd(Bubble b, @Bubbles.DismissReason int r) { - if (r == Bubbles.DISMISS_AGED) { - log(b, Event.BUBBLE_OVERFLOW_ADD_AGED); - } else if (r == Bubbles.DISMISS_USER_GESTURE) { - log(b, Event.BUBBLE_OVERFLOW_ADD_USER_GESTURE); - } else if (r == Bubbles.DISMISS_RELOAD_FROM_DISK) { - log(b, Event.BUBBLE_OVERFLOW_RECOVER); + public void logOverflowAdd(Bubble b, boolean bubbleBar, @Bubbles.DismissReason int r) { + if (bubbleBar) { + if (r == Bubbles.DISMISS_AGED) { + log(b, Event.BUBBLE_BAR_OVERFLOW_ADD_AGED); + } + } else { + if (r == Bubbles.DISMISS_AGED) { + log(b, Event.BUBBLE_OVERFLOW_ADD_AGED); + } else if (r == Bubbles.DISMISS_USER_GESTURE) { + log(b, Event.BUBBLE_OVERFLOW_ADD_USER_GESTURE); + } else if (r == Bubbles.DISMISS_RELOAD_FROM_DISK) { + log(b, Event.BUBBLE_OVERFLOW_RECOVER); + } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java index c386c9398624..068b2d246500 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java @@ -830,6 +830,13 @@ public class BubblePositioner { mShowingInBubbleBar = showingInBubbleBar; } + /** + * Whether bubbles ar showing in the bubble bar from launcher. + */ + boolean isShowingInBubbleBar() { + return mShowingInBubbleBar; + } + public void setBubbleBarLocation(BubbleBarLocation location) { mBubbleBarLocation = location; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java index dab30b0f0b96..0f21756bb863 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java @@ -88,6 +88,7 @@ import com.android.wm.shell.shared.split.SplitScreenConstants.SplitPosition; import com.android.wm.shell.splitscreen.StageTaskListener; import java.io.PrintWriter; +import java.util.List; import java.util.function.Consumer; /** @@ -139,15 +140,21 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange private final Rect mTempRect = new Rect(); private final Rect mRootBounds = new Rect(); private final Rect mDividerBounds = new Rect(); - // Bounds1 final position should be always at top or left - private final Rect mBounds1 = new Rect(); - // Bounds2 final position should be always at bottom or right - private final Rect mBounds2 = new Rect(); + /** + * A list of stage bounds, kept in order from top/left to bottom/right. These are the sizes of + * the app surfaces, not necessarily the same as the size of the rendered content. + * See {@link #mContentBounds}. + */ + private final List<Rect> mStageBounds = List.of(new Rect(), new Rect()); + /** + * A list of app content bounds, kept in order from top/left to bottom/right. These are the + * sizes of the rendered app contents, not necessarily the same as the size of the drawn app + * surfaces. See {@link #mStageBounds}. + */ + private final List<Rect> mContentBounds = List.of(new Rect(), new Rect()); // The temp bounds outside of display bounds for side stage when split screen inactive to avoid // flicker next time active split screen. private final Rect mInvisibleBounds = new Rect(); - private final Rect mWinBounds1 = new Rect(); - private final Rect mWinBounds2 = new Rect(); private final SplitLayoutHandler mSplitLayoutHandler; private final SplitWindowManager mSplitWindowManager; private final DisplayController mDisplayController; @@ -233,26 +240,26 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange mDividerWindowWidth = mDividerSize + 2 * mDividerInsets; } - /** Gets bounds of the primary split with screen based coordinate. */ - public Rect getBounds1() { - return new Rect(mBounds1); + /** Gets the bounds of the top/left app in screen-based coordinates. */ + public Rect getTopLeftBounds() { + return mStageBounds.getFirst(); } - /** Gets bounds of the primary split with parent based coordinate. */ - public Rect getRefBounds1() { - Rect outBounds = getBounds1(); - outBounds.offset(-mRootBounds.left, -mRootBounds.top); - return outBounds; + /** Gets the bounds of the bottom/right app in screen-based coordinates. */ + public Rect getBottomRightBounds() { + return mStageBounds.getLast(); } - /** Gets bounds of the secondary split with screen based coordinate. */ - public Rect getBounds2() { - return new Rect(mBounds2); + /** Gets the bounds of the top/left app in parent-based coordinates. */ + public Rect getTopLeftRefBounds() { + Rect outBounds = getTopLeftBounds(); + outBounds.offset(-mRootBounds.left, -mRootBounds.top); + return outBounds; } - /** Gets bounds of the secondary split with parent based coordinate. */ - public Rect getRefBounds2() { - final Rect outBounds = getBounds2(); + /** Gets the bounds of the bottom/right app in parent-based coordinates. */ + public Rect getBottomRightRefBounds() { + Rect outBounds = getBottomRightBounds(); outBounds.offset(-mRootBounds.left, -mRootBounds.top); return outBounds; } @@ -274,31 +281,42 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange return outBounds; } - /** Gets bounds of the primary split with screen based coordinate on the param Rect. */ - public void getBounds1(Rect rect) { - rect.set(mBounds1); + /** Copies the top/left bounds to the provided Rect (screen-based coordinates). */ + public void copyTopLeftBounds(Rect rect) { + rect.set(getTopLeftBounds()); } - /** Gets bounds of the primary split with parent based coordinate on the param Rect. */ - public void getRefBounds1(Rect rect) { - getBounds1(rect); + /** Copies the top/left bounds to the provided Rect (parent-based coordinates). */ + public void copyTopLeftRefBounds(Rect rect) { + copyTopLeftBounds(rect); rect.offset(-mRootBounds.left, -mRootBounds.top); } - /** Gets bounds of the secondary split with screen based coordinate on the param Rect. */ - public void getBounds2(Rect rect) { - rect.set(mBounds2); + /** Copies the bottom/right bounds to the provided Rect (screen-based coordinates). */ + public void copyBottomRightBounds(Rect rect) { + rect.set(getBottomRightBounds()); } - /** Gets bounds of the secondary split with parent based coordinate on the param Rect. */ - public void getRefBounds2(Rect rect) { - getBounds2(rect); + /** Copies the bottom/right bounds to the provided Rect (parent-based coordinates). */ + public void copyBottomRightRefBounds(Rect rect) { + copyBottomRightBounds(rect); rect.offset(-mRootBounds.left, -mRootBounds.top); } - /** Gets root bounds of the whole split layout on the param Rect. */ - public void getRootBounds(Rect rect) { - rect.set(mRootBounds); + /** + * Gets the content bounds of the top/left app (the bounds of where the app contents would be + * drawn). Might be larger than the available surface space. + */ + public Rect getTopLeftContentBounds() { + return mContentBounds.getFirst(); + } + + /** + * Gets the content bounds of the bottom/right app (the bounds of where the app contents would + * be drawn). Might be larger than the available surface space. + */ + public Rect getBottomRightContentBounds() { + return mContentBounds.getLast(); } /** Gets bounds of divider window with screen based coordinate on the param Rect. */ @@ -340,8 +358,10 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange */ public float getDividerPositionAsFraction() { return Math.min(1f, Math.max(0f, mIsLeftRightSplit - ? (float) ((mBounds1.right + mBounds2.left) / 2f) / mBounds2.right - : (float) ((mBounds1.bottom + mBounds2.top) / 2f) / mBounds2.bottom)); + ? (float) ((getTopLeftBounds().right + getBottomRightBounds().left) / 2f) + / getBottomRightBounds().right + : (float) ((getTopLeftBounds().bottom + getBottomRightBounds().top) / 2f) + / getBottomRightBounds().bottom)); } private void updateInvisibleRect() { @@ -435,7 +455,8 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange } private void updateBounds(int position) { - updateBounds(position, mBounds1, mBounds2, mDividerBounds, true /* setEffectBounds */); + updateBounds(position, getTopLeftBounds(), getBottomRightBounds(), mDividerBounds, + true /* setEffectBounds */); } /** Updates recording bounds of divider window and both of the splits. */ @@ -638,8 +659,8 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange updateBounds(mDividerPosition); mWinToken1 = null; mWinToken2 = null; - mWinBounds1.setEmpty(); - mWinBounds2.setEmpty(); + getTopLeftContentBounds().setEmpty(); + getBottomRightContentBounds().setEmpty(); } /** @@ -835,7 +856,8 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange insets.left != 0 || insets.top != 0 || insets.right != 0 || insets.bottom != 0; final int dividerPos = mDividerSnapAlgorithm.calculateNonDismissingSnapTarget( - mIsLeftRightSplit ? mBounds2.width() : mBounds2.height()).position; + mIsLeftRightSplit ? getBottomRightBounds().width() : getBottomRightBounds().height() + ).position; final Rect endBounds1 = new Rect(); final Rect endBounds2 = new Rect(); final Rect endDividerBounds = new Rect(); @@ -847,12 +869,12 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange endBounds2.offset(-mRootBounds.left, -mRootBounds.top); endDividerBounds.offset(-mRootBounds.left, -mRootBounds.top); - ValueAnimator animator1 = moveSurface(t, topLeftStage, getRefBounds1(), endBounds1, + ValueAnimator animator1 = moveSurface(t, topLeftStage, getTopLeftRefBounds(), endBounds1, -insets.left, -insets.top, true /* roundCorners */, true /* isGoingBehind */, shouldVeil); - ValueAnimator animator2 = moveSurface(t, bottomRightStage, getRefBounds2(), endBounds2, - insets.left, insets.top, true /* roundCorners */, false /* isGoingBehind */, - shouldVeil); + ValueAnimator animator2 = moveSurface(t, bottomRightStage, getBottomRightRefBounds(), + endBounds2, insets.left, insets.top, true /* roundCorners */, + false /* isGoingBehind */, shouldVeil); ValueAnimator animator3 = moveSurface(t, null /* stage */, getRefDividerBounds(), endDividerBounds, 0 /* offsetX */, 0 /* offsetY */, false /* roundCorners */, false /* isGoingBehind */, false /* addVeil */); @@ -1059,10 +1081,10 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange // Resets layer of divider bar to make sure it is always on top. t.setLayer(dividerLeash, Integer.MAX_VALUE); } - getRefBounds1(mTempRect); + copyTopLeftRefBounds(mTempRect); t.setPosition(leash1, mTempRect.left, mTempRect.top) .setWindowCrop(leash1, mTempRect.width(), mTempRect.height()); - getRefBounds2(mTempRect); + copyBottomRightRefBounds(mTempRect); t.setPosition(leash2, mTempRect.left, mTempRect.top) .setWindowCrop(leash2, mTempRect.width(), mTempRect.height()); @@ -1084,15 +1106,17 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange public boolean applyTaskChanges(WindowContainerTransaction wct, ActivityManager.RunningTaskInfo task1, ActivityManager.RunningTaskInfo task2) { boolean boundsChanged = false; - if (!mBounds1.equals(mWinBounds1) || !task1.token.equals(mWinToken1)) { - setTaskBounds(wct, task1, mBounds1); - mWinBounds1.set(mBounds1); + if (!getTopLeftBounds().equals(getTopLeftContentBounds()) + || !task1.token.equals(mWinToken1)) { + setTaskBounds(wct, task1, getTopLeftBounds()); + getTopLeftContentBounds().set(getTopLeftBounds()); mWinToken1 = task1.token; boundsChanged = true; } - if (!mBounds2.equals(mWinBounds2) || !task2.token.equals(mWinToken2)) { - setTaskBounds(wct, task2, mBounds2); - mWinBounds2.set(mBounds2); + if (!getBottomRightBounds().equals(getBottomRightContentBounds()) + || !task2.token.equals(mWinToken2)) { + setTaskBounds(wct, task2, getBottomRightBounds()); + getBottomRightContentBounds().set(getBottomRightBounds()); mWinToken2 = task2.token; boundsChanged = true; } @@ -1129,22 +1153,22 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange public void applyLayoutOffsetTarget(WindowContainerTransaction wct, int offsetX, int offsetY, ActivityManager.RunningTaskInfo taskInfo1, ActivityManager.RunningTaskInfo taskInfo2) { if (offsetX == 0 && offsetY == 0) { - wct.setBounds(taskInfo1.token, mBounds1); + wct.setBounds(taskInfo1.token, getTopLeftBounds()); wct.setScreenSizeDp(taskInfo1.token, SCREEN_WIDTH_DP_UNDEFINED, SCREEN_HEIGHT_DP_UNDEFINED); - wct.setBounds(taskInfo2.token, mBounds2); + wct.setBounds(taskInfo2.token, getBottomRightBounds()); wct.setScreenSizeDp(taskInfo2.token, SCREEN_WIDTH_DP_UNDEFINED, SCREEN_HEIGHT_DP_UNDEFINED); } else { - getBounds1(mTempRect); + copyTopLeftBounds(mTempRect); mTempRect.offset(offsetX, offsetY); wct.setBounds(taskInfo1.token, mTempRect); wct.setScreenSizeDp(taskInfo1.token, taskInfo1.configuration.screenWidthDp, taskInfo1.configuration.screenHeightDp); - getBounds2(mTempRect); + copyBottomRightBounds(mTempRect); mTempRect.offset(offsetX, offsetY); wct.setBounds(taskInfo2.token, mTempRect); wct.setScreenSizeDp(taskInfo2.token, @@ -1162,9 +1186,9 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange pw.println(innerPrefix + "mFreezeDividerWindow=" + mFreezeDividerWindow); pw.println(innerPrefix + "mDimNonImeSide=" + mDimNonImeSide); pw.println(innerPrefix + "mDividerPosition=" + mDividerPosition); - pw.println(innerPrefix + "bounds1=" + mBounds1.toShortString()); + pw.println(innerPrefix + "bounds1=" + getTopLeftBounds().toShortString()); pw.println(innerPrefix + "dividerBounds=" + mDividerBounds.toShortString()); - pw.println(innerPrefix + "bounds2=" + mBounds2.toShortString()); + pw.println(innerPrefix + "bounds2=" + getBottomRightBounds().toShortString()); } /** Handles layout change event. */ @@ -1274,15 +1298,16 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange } final boolean topLeftShrink = isLeftRightSplit - ? position < mWinBounds1.right : position < mWinBounds1.bottom; + ? position < getTopLeftContentBounds().right + : position < getTopLeftContentBounds().bottom; if (topLeftShrink) { mShrinkSide = isLeftRightSplit ? DOCKED_LEFT : DOCKED_TOP; - mContentBounds.set(mWinBounds1); - mSurfaceBounds.set(mBounds1); + mContentBounds.set(getTopLeftContentBounds()); + mSurfaceBounds.set(getTopLeftBounds()); } else { mShrinkSide = isLeftRightSplit ? DOCKED_RIGHT : DOCKED_BOTTOM; - mContentBounds.set(mWinBounds2); - mSurfaceBounds.set(mBounds2); + mContentBounds.set(getBottomRightContentBounds()); + mSurfaceBounds.set(getBottomRightBounds()); } if (mDismissingSide != DOCKED_INVALID) { @@ -1334,12 +1359,12 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange case DOCKED_TOP: case DOCKED_LEFT: targetLeash = leash1; - mTempRect.set(mBounds1); + mTempRect.set(getTopLeftBounds()); break; case DOCKED_BOTTOM: case DOCKED_RIGHT: targetLeash = leash2; - mTempRect.set(mBounds2); + mTempRect.set(getBottomRightBounds()); break; } } else if (mParallaxType == PARALLAX_ALIGN_CENTER) { @@ -1347,12 +1372,12 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange case DOCKED_TOP: case DOCKED_LEFT: targetLeash = leash1; - mTempRect.set(mBounds1); + mTempRect.set(getTopLeftBounds()); break; case DOCKED_BOTTOM: case DOCKED_RIGHT: targetLeash = leash2; - mTempRect.set(mBounds2); + mTempRect.set(getBottomRightBounds()); break; } } @@ -1530,7 +1555,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange private int getTargetYOffset() { final int desireOffset = Math.abs(mEndImeTop - mStartImeTop); // Make sure to keep at least 30% visible for the top split. - final int maxOffset = (int) (mBounds1.height() * ADJUSTED_SPLIT_FRACTION_MAX); + final int maxOffset = (int) (getTopLeftBounds().height() * ADJUSTED_SPLIT_FRACTION_MAX); return -Math.min(desireOffset, maxOffset); } @@ -1580,11 +1605,11 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange t.setPosition(dividerLeash, mTempRect.left, mTempRect.top); } - getRefBounds1(mTempRect); + copyTopLeftRefBounds(mTempRect); mTempRect.offset(0, mYOffsetForIme); t.setPosition(leash1, mTempRect.left, mTempRect.top); - getRefBounds2(mTempRect); + copyBottomRightRefBounds(mTempRect); mTempRect.offset(0, mYOffsetForIme); t.setPosition(leash2, mTempRect.left, mTempRect.top); adjusted = true; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index 0a39076a8768..a0bdd9fad510 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -70,6 +70,8 @@ import com.android.internal.jank.InteractionJankMonitor import com.android.internal.policy.ScreenDecorationsUtils import com.android.internal.protolog.ProtoLog import com.android.window.flags.Flags +import com.android.window.flags.Flags.enableMoveToNextDisplayShortcut +import com.android.wm.shell.Flags.enableFlexibleSplit import com.android.wm.shell.R import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTaskOrganizer @@ -102,6 +104,7 @@ import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.shared.desktopmode.DesktopModeStatus.DESKTOP_DENSITY_OVERRIDE import com.android.wm.shell.shared.desktopmode.DesktopModeStatus.useDesktopOverrideDensity import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource +import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_INDEX_UNDEFINED import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT import com.android.wm.shell.splitscreen.SplitScreenController @@ -1512,11 +1515,15 @@ class DesktopTasksController( WINDOWING_MODE_MULTI_WINDOW -> { val splitPosition = splitScreenController .determineNewInstancePosition(callingTaskInfo) + // TODO(b/349828130) currently pass in index_undefined until we can revisit these + // specific cases in the future. + val splitIndex = if (enableFlexibleSplit()) + splitScreenController.determineNewInstanceIndex(callingTaskInfo) else + SPLIT_INDEX_UNDEFINED splitScreenController.startIntent( launchIntent, context.userId, fillIn, splitPosition, options.toBundle(), null /* hideTaskToken */, - true /* forceLaunchNewTask */ - ) + true /* forceLaunchNewTask */, splitIndex) } WINDOWING_MODE_FREEFORM -> { val wct = WindowContainerTransaction() @@ -1661,6 +1668,13 @@ class DesktopTasksController( transition, wct, task.displayId ) } + } else if (taskRepository.isActiveTask(task.taskId)) { + // If a freeform task receives a request for a fullscreen launch, apply the same + // changes we do for similar transitions. The task not having WINDOWING_MODE_UNDEFINED + // set when needed can interfere with future split / multi-instance transitions. + return WindowContainerTransaction().also { wct -> + addMoveToFullscreenChanges(wct, task) + } } return null } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/SplitDragPolicy.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/SplitDragPolicy.java index 5d22c1edf8fe..ae9d21f621de 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/SplitDragPolicy.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/SplitDragPolicy.java @@ -41,6 +41,9 @@ import static com.android.wm.shell.draganddrop.SplitDragPolicy.Target.TYPE_SPLIT import static com.android.wm.shell.draganddrop.SplitDragPolicy.Target.TYPE_SPLIT_TOP; import static com.android.wm.shell.shared.draganddrop.DragAndDropConstants.EXTRA_DISALLOW_HIT_REGION; import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_INDEX_0; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_INDEX_1; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_INDEX_UNDEFINED; import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; @@ -81,6 +84,7 @@ import com.android.wm.shell.draganddrop.anim.HoverAnimProps; import com.android.wm.shell.draganddrop.anim.TwoFiftyFiftyTargetAnimator; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.shared.split.SplitScreenConstants; +import com.android.wm.shell.shared.split.SplitScreenConstants.SplitIndex; import com.android.wm.shell.shared.split.SplitScreenConstants.SplitPosition; import com.android.wm.shell.splitscreen.SplitScreenController; @@ -219,8 +223,10 @@ public class SplitDragPolicy implements DropTarget { displayRegion.splitHorizontally(startHitRegion, endHitRegion); } - mTargets.add(new Target(TYPE_SPLIT_LEFT, startHitRegion, startBounds, -1)); - mTargets.add(new Target(TYPE_SPLIT_RIGHT, endHitRegion, endBounds, -1)); + mTargets.add(new Target(TYPE_SPLIT_LEFT, startHitRegion, startBounds, + SPLIT_INDEX_0)); + mTargets.add(new Target(TYPE_SPLIT_RIGHT, endHitRegion, endBounds, + SPLIT_INDEX_1)); } else { // TODO(b/349828130), move this into init function and/or the insets updating // callback @@ -287,9 +293,10 @@ public class SplitDragPolicy implements DropTarget { displayRegion.splitVertically(leftHitRegion, rightHitRegion); } - mTargets.add(new Target(TYPE_SPLIT_LEFT, leftHitRegion, topOrLeftBounds, -1)); + mTargets.add(new Target(TYPE_SPLIT_LEFT, leftHitRegion, topOrLeftBounds, + SPLIT_INDEX_UNDEFINED)); mTargets.add(new Target(TYPE_SPLIT_RIGHT, rightHitRegion, bottomOrRightBounds, - -1)); + SPLIT_INDEX_UNDEFINED)); } else { final Rect topHitRegion = new Rect(); final Rect bottomHitRegion = new Rect(); @@ -308,9 +315,10 @@ public class SplitDragPolicy implements DropTarget { displayRegion.splitHorizontally(topHitRegion, bottomHitRegion); } - mTargets.add(new Target(TYPE_SPLIT_TOP, topHitRegion, topOrLeftBounds, -1)); + mTargets.add(new Target(TYPE_SPLIT_TOP, topHitRegion, topOrLeftBounds, + SPLIT_INDEX_UNDEFINED)); mTargets.add(new Target(TYPE_SPLIT_BOTTOM, bottomHitRegion, bottomOrRightBounds, - -1)); + SPLIT_INDEX_UNDEFINED)); } } } else { @@ -378,9 +386,9 @@ public class SplitDragPolicy implements DropTarget { ? mFullscreenStarter : mSplitscreenStarter; if (mSession.appData != null) { - launchApp(mSession, starter, position, hideTaskToken); + launchApp(mSession, starter, position, hideTaskToken, target.index); } else { - launchIntent(mSession, starter, position, hideTaskToken); + launchIntent(mSession, starter, position, hideTaskToken, target.index); } if (enableFlexibleSplit()) { @@ -392,9 +400,10 @@ public class SplitDragPolicy implements DropTarget { * Launches an app provided by SysUI. */ private void launchApp(DragSession session, Starter starter, @SplitPosition int position, - @Nullable WindowContainerToken hideTaskToken) { - ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Launching app data at position=%d", - position); + @Nullable WindowContainerToken hideTaskToken, @SplitIndex int splitIndex) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, + "Launching app data at position=%d index=%d", + position, splitIndex); final ClipDescription description = session.getClipDescription(); final boolean isTask = description.hasMimeType(MIMETYPE_APPLICATION_TASK); final boolean isShortcut = description.hasMimeType(MIMETYPE_APPLICATION_SHORTCUT); @@ -429,7 +438,7 @@ public class SplitDragPolicy implements DropTarget { } } starter.startIntent(launchIntent, user.getIdentifier(), null /* fillIntent */, - position, opts, hideTaskToken); + position, opts, hideTaskToken, splitIndex); } } @@ -437,7 +446,7 @@ public class SplitDragPolicy implements DropTarget { * Launches an intent sender provided by an application. */ private void launchIntent(DragSession session, Starter starter, @SplitPosition int position, - @Nullable WindowContainerToken hideTaskToken) { + @Nullable WindowContainerToken hideTaskToken, @SplitIndex int index) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Launching intent at position=%d", position); final ActivityOptions baseActivityOpts = ActivityOptions.makeBasic(); @@ -452,7 +461,7 @@ public class SplitDragPolicy implements DropTarget { final Bundle opts = baseActivityOpts.toBundle(); starter.startIntent(session.launchableIntent, session.launchableIntent.getCreatorUserHandle().getIdentifier(), - null /* fillIntent */, position, opts, hideTaskToken); + null /* fillIntent */, position, opts, hideTaskToken, index); } @Override @@ -541,7 +550,7 @@ public class SplitDragPolicy implements DropTarget { @Nullable Bundle options, UserHandle user); void startIntent(PendingIntent intent, int userId, Intent fillInIntent, @SplitPosition int position, @Nullable Bundle options, - @Nullable WindowContainerToken hideTaskToken); + @Nullable WindowContainerToken hideTaskToken, @SplitIndex int index); void enterSplitScreen(int taskId, boolean leftOrTop); /** @@ -592,7 +601,7 @@ public class SplitDragPolicy implements DropTarget { @Override public void startIntent(PendingIntent intent, int userId, @Nullable Intent fillInIntent, int position, @Nullable Bundle options, - @Nullable WindowContainerToken hideTaskToken) { + @Nullable WindowContainerToken hideTaskToken, @SplitIndex int index) { if (hideTaskToken != null) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Default starter does not support hide task token"); @@ -641,13 +650,13 @@ public class SplitDragPolicy implements DropTarget { final Rect hitRegion; // The approximate visual region for where the task will start final Rect drawRegion; - int index; + @SplitIndex int index; /** * @param index 0-indexed, represents which position of drop target this object represents, * 0 to N for left to right, top to bottom */ - public Target(@Type int t, Rect hit, Rect draw, int index) { + public Target(@Type int t, Rect hit, Rect draw, @SplitIndex int index) { type = t; hitRegion = hit; drawRegion = draw; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/anim/TwoFiftyFiftyTargetAnimator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/anim/TwoFiftyFiftyTargetAnimator.kt index 9f532f57961d..54619528f2bd 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/anim/TwoFiftyFiftyTargetAnimator.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/anim/TwoFiftyFiftyTargetAnimator.kt @@ -22,6 +22,10 @@ import android.graphics.Rect import com.android.wm.shell.R import com.android.wm.shell.common.DisplayLayout import com.android.wm.shell.draganddrop.SplitDragPolicy.Target +import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_INDEX_0 +import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_INDEX_1 +import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_INDEX_2 +import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_INDEX_3 /** * Represents Drop Zone targets and animations for when the system is currently in a 2 app 50/50 @@ -98,7 +102,7 @@ class TwoFiftyFiftyTargetAnimator : DropTargetAnimSupplier { farStartBounds.right + halfDividerWidth, farStartBounds.bottom ), - farStartBounds, 0 + farStartBounds, SPLIT_INDEX_0 ) ) targets.add( @@ -110,7 +114,7 @@ class TwoFiftyFiftyTargetAnimator : DropTargetAnimSupplier { startBounds.right + halfDividerWidth, startBounds.bottom ), - startBounds, 1 + startBounds, SPLIT_INDEX_1 ) ) targets.add( @@ -120,7 +124,7 @@ class TwoFiftyFiftyTargetAnimator : DropTargetAnimSupplier { endBounds.left - halfDividerWidth, endBounds.top, endBounds.right, endBounds.bottom ), - endBounds, 2 + endBounds, SPLIT_INDEX_2 ) ) targets.add( @@ -130,7 +134,7 @@ class TwoFiftyFiftyTargetAnimator : DropTargetAnimSupplier { farEndBounds.left - halfDividerWidth, farEndBounds.top, farEndBounds.right, farEndBounds.bottom ), - farEndBounds, 3 + farEndBounds, SPLIT_INDEX_3 ) ) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java index 319bfac734ba..e4f83333edbf 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java @@ -173,6 +173,10 @@ public class KeyguardTransitionHandler return mKeyguardShowing; } + public boolean isKeyguardAnimating() { + return !mStartedTransitions.isEmpty(); + } + @Override public void onTaskMovedToFront(ActivityManager.RunningTaskInfo taskInfo) { mDreamToken = taskInfo.getActivityType() == ACTIVITY_TYPE_DREAM ? taskInfo.token : null; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java index 3e6d36ce0ca3..39ed9abcfd26 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreen.java @@ -54,10 +54,27 @@ public interface SplitScreen { */ int STAGE_TYPE_SIDE = 1; + /** + * Position independent stage identifier for a given Stage + */ + int STAGE_TYPE_A = 2; + /** + * Position independent stage identifier for a given Stage + */ + int STAGE_TYPE_B = 3; + /** + * Position independent stage identifier for a given Stage + */ + int STAGE_TYPE_C = 4; + @IntDef(prefix = { "STAGE_TYPE_" }, value = { STAGE_TYPE_UNDEFINED, STAGE_TYPE_MAIN, - STAGE_TYPE_SIDE + STAGE_TYPE_SIDE, + // Used for flexible split + STAGE_TYPE_A, + STAGE_TYPE_B, + STAGE_TYPE_C }) @interface StageType {} @@ -128,6 +145,9 @@ public interface SplitScreen { case STAGE_TYPE_UNDEFINED: return "UNDEFINED"; case STAGE_TYPE_MAIN: return "MAIN"; case STAGE_TYPE_SIDE: return "SIDE"; + case STAGE_TYPE_A: return "STAGE_A"; + case STAGE_TYPE_B: return "STAGE_B"; + case STAGE_TYPE_C: return "STAGE_C"; default: return "UNKNOWN(" + stage + ")"; } } 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 6398d31b4f82..4f0f6760a951 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 @@ -24,6 +24,7 @@ import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; import static android.content.Intent.FLAG_ACTIVITY_NO_USER_ACTION; import static android.view.Display.DEFAULT_DISPLAY; +import static com.android.wm.shell.Flags.enableFlexibleSplit; import static com.android.wm.shell.common.MultiInstanceHelper.getComponent; import static com.android.wm.shell.common.MultiInstanceHelper.getShortcutComponent; import static com.android.wm.shell.common.MultiInstanceHelper.samePackage; @@ -33,6 +34,9 @@ import static com.android.wm.shell.common.split.SplitScreenUtils.splitFailureMes import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN; import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_SPLIT_SCREEN; import static com.android.wm.shell.shared.split.SplitScreenConstants.KEY_EXTRA_WIDGET_INTENT; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_INDEX_0; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_INDEX_1; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_INDEX_UNDEFINED; import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; @@ -98,6 +102,7 @@ import com.android.wm.shell.recents.RecentTasksController; import com.android.wm.shell.shared.TransactionPool; import com.android.wm.shell.shared.annotations.ExternalThread; import com.android.wm.shell.shared.split.SplitScreenConstants.PersistentSnapPosition; +import com.android.wm.shell.shared.split.SplitScreenConstants.SplitIndex; import com.android.wm.shell.shared.split.SplitScreenConstants.SplitPosition; import com.android.wm.shell.splitscreen.SplitScreen.StageType; import com.android.wm.shell.sysui.KeyguardChangeListener; @@ -325,7 +330,6 @@ public class SplitScreenController implements SplitDragPolicy.Starter, /** * @return an Array of RunningTaskInfo's ordered by leftToRight or topTopBottom */ - @Nullable public ActivityManager.RunningTaskInfo[] getAllTaskInfos() { // TODO(b/349828130) Add the third stage task info and not rely on positions ActivityManager.RunningTaskInfo topLeftTask = getTaskInfo(SPLIT_POSITION_TOP_OR_LEFT); @@ -335,7 +339,7 @@ public class SplitScreenController implements SplitDragPolicy.Starter, return new ActivityManager.RunningTaskInfo[]{topLeftTask, bottomRightTask}; } - return null; + return new ActivityManager.RunningTaskInfo[0]; } /** Check task is under split or not by taskId. */ @@ -405,7 +409,7 @@ public class SplitScreenController implements SplitDragPolicy.Starter, public void prepareEnterSplitScreen(WindowContainerTransaction wct, ActivityManager.RunningTaskInfo taskInfo, int startPosition) { mStageCoordinator.prepareEnterSplitScreen(wct, taskInfo, startPosition, - false /* resizeAnim */); + false /* resizeAnim */, SPLIT_INDEX_UNDEFINED); } /** @@ -451,6 +455,24 @@ public class SplitScreenController implements SplitDragPolicy.Starter, } } + /** + * Determines which split index a new instance of a task should take. + * @param callingTask The task requesting a new instance. + * @return the split index of the new instance + */ + @SplitIndex + public int determineNewInstanceIndex(@NonNull ActivityManager.RunningTaskInfo callingTask) { + if (!enableFlexibleSplit()) { + throw new IllegalStateException("Use determineNewInstancePosition"); + } + if (callingTask.getWindowingMode() == WINDOWING_MODE_FULLSCREEN + || getSplitPosition(callingTask.taskId) == SPLIT_POSITION_TOP_OR_LEFT) { + return SPLIT_INDEX_1; + } else { + return SPLIT_INDEX_0; + } + } + public void enterSplitScreen(int taskId, boolean leftOrTop) { enterSplitScreen(taskId, leftOrTop, new WindowContainerTransaction()); } @@ -685,7 +707,10 @@ public class SplitScreenController implements SplitDragPolicy.Starter, ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "startIntentWithInstanceId: reason=%d", ENTER_REASON_LAUNCHER); mStageCoordinator.getLogger().enterRequested(instanceId, ENTER_REASON_LAUNCHER); - startIntent(intent, userId, fillInIntent, position, options, null /* hideTaskToken */); + // TODO(b/349828130) currently pass in index_undefined until we can revisit these + // specific cases in the future. Only focusing on parity with starting intent/task + startIntent(intent, userId, fillInIntent, position, options, null /* hideTaskToken */, + SPLIT_INDEX_UNDEFINED); } private void startIntentAndTask(PendingIntent pendingIntent, int userId1, @@ -775,9 +800,9 @@ public class SplitScreenController implements SplitDragPolicy.Starter, @Override public void startIntent(PendingIntent intent, int userId1, @Nullable Intent fillInIntent, @SplitPosition int position, @Nullable Bundle options, - @Nullable WindowContainerToken hideTaskToken) { + @Nullable WindowContainerToken hideTaskToken, @SplitIndex int index) { startIntent(intent, userId1, fillInIntent, position, options, hideTaskToken, - false /* forceLaunchNewTask */); + false /* forceLaunchNewTask */, index); } /** @@ -790,7 +815,8 @@ public class SplitScreenController implements SplitDragPolicy.Starter, */ public void startIntent(PendingIntent intent, int userId1, @Nullable Intent fillInIntent, @SplitPosition int position, @Nullable Bundle options, - @Nullable WindowContainerToken hideTaskToken, boolean forceLaunchNewTask) { + @Nullable WindowContainerToken hideTaskToken, boolean forceLaunchNewTask, + @SplitIndex int index) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "startIntent(): intent=%s user=%d fillInIntent=%s position=%d", intent, userId1, fillInIntent, position); @@ -816,7 +842,7 @@ public class SplitScreenController implements SplitDragPolicy.Starter, if (taskInfo != null) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Found suitable background task=%s", taskInfo); - mStageCoordinator.startTask(taskInfo.taskId, position, options, hideTaskToken); + mStageCoordinator.startTask(taskInfo.taskId, position, options, hideTaskToken, index); ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "Start task in background"); return; @@ -841,7 +867,8 @@ public class SplitScreenController implements SplitDragPolicy.Starter, } } - mStageCoordinator.startIntent(intent, fillInIntent, position, options, hideTaskToken); + mStageCoordinator.startIntent(intent, fillInIntent, position, options, hideTaskToken, + index); } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java index 840049412db4..3091be574a53 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java @@ -21,6 +21,7 @@ import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_BACK; +import static com.android.wm.shell.Flags.enableFlexibleSplit; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_TRANSITIONS; import static com.android.wm.shell.shared.animation.Interpolators.ALPHA_IN; @@ -55,6 +56,9 @@ import com.android.wm.shell.transition.OneShotRemoteHandler; import com.android.wm.shell.transition.Transitions; import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.concurrent.Executor; /** Manages transition animations for split-screen. */ @@ -268,22 +272,21 @@ class SplitScreenTransitions { @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback, - @NonNull WindowContainerToken mainRoot, @NonNull WindowContainerToken sideRoot, - @NonNull SplitDecorManager mainDecor, @NonNull SplitDecorManager sideDecor) { + @NonNull Map<WindowContainerToken, SplitDecorManager> rootDecorMap) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "playResizeAnimation: transition=%d", info.getDebugId()); initTransition(transition, finishTransaction, finishCallback); + Set<WindowContainerToken> rootDecorKeys = rootDecorMap.keySet(); for (int i = info.getChanges().size() - 1; i >= 0; --i) { final TransitionInfo.Change change = info.getChanges().get(i); - if (mainRoot.equals(change.getContainer()) || sideRoot.equals(change.getContainer())) { + if (rootDecorKeys.contains(change.getContainer())) { final SurfaceControl leash = change.getLeash(); startTransaction.setPosition(leash, change.getEndAbsBounds().left, change.getEndAbsBounds().top); startTransaction.setWindowCrop(leash, change.getEndAbsBounds().width(), change.getEndAbsBounds().height()); - SplitDecorManager decor = mainRoot.equals(change.getContainer()) - ? mainDecor : sideDecor; + SplitDecorManager decor = rootDecorMap.get(change.getContainer()); // This is to ensure onFinished be called after all animations ended. ValueAnimator va = new ValueAnimator(); @@ -433,15 +436,22 @@ class SplitScreenTransitions { Transitions.TransitionHandler handler, @Nullable TransitionConsumedCallback consumedCallback, @Nullable TransitionFinishedCallback finishCallback, - @NonNull SplitDecorManager mainDecor, @NonNull SplitDecorManager sideDecor) { + @Nullable SplitDecorManager mainDecor, @Nullable SplitDecorManager sideDecor, + @Nullable List<SplitDecorManager> decorManagers) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " splitTransition deduced Resize split screen."); ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "setResizeTransition: hasPendingResize=%b", mPendingResize != null); if (mPendingResize != null) { mPendingResize.cancel(null); - mainDecor.cancelRunningAnimations(); - sideDecor.cancelRunningAnimations(); + if (enableFlexibleSplit()) { + for (SplitDecorManager stage : decorManagers) { + stage.cancelRunningAnimations(); + } + } else { + mainDecor.cancelRunningAnimations(); + sideDecor.cancelRunningAnimations(); + } mAnimations.clear(); onFinish(null /* wct */); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index e692c61cd493..45ecfa95b494 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -33,6 +33,7 @@ import static android.view.WindowManager.TRANSIT_TO_FRONT; import static android.window.TransitionInfo.FLAG_IS_DISPLAY; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER; +import static com.android.wm.shell.Flags.enableFlexibleSplit; import static com.android.wm.shell.common.split.SplitLayout.PARALLAX_ALIGN_CENTER; import static com.android.wm.shell.common.split.SplitScreenUtils.isPartiallyOffscreen; import static com.android.wm.shell.common.split.SplitScreenUtils.reverseSplitPosition; @@ -43,6 +44,7 @@ import static com.android.wm.shell.shared.TransitionUtil.isOpeningType; import static com.android.wm.shell.shared.TransitionUtil.isOrderOnly; import static com.android.wm.shell.shared.split.SplitScreenConstants.FLAG_IS_DIVIDER_BAR; import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_10_90; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50; import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_90_10; import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_INDEX_0; import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_INDEX_1; @@ -51,9 +53,12 @@ import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSIT import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; import static com.android.wm.shell.shared.split.SplitScreenConstants.splitPositionToString; +import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_A; +import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_B; import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_MAIN; import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_SIDE; import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED; +import static com.android.wm.shell.splitscreen.SplitScreen.stageTypeToString; import static com.android.wm.shell.splitscreen.SplitScreenController.ENTER_REASON_LAUNCHER; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_FINISHED; @@ -142,6 +147,7 @@ import com.android.wm.shell.shared.TransactionPool; import com.android.wm.shell.shared.TransitionUtil; import com.android.wm.shell.shared.split.SplitBounds; import com.android.wm.shell.shared.split.SplitScreenConstants.PersistentSnapPosition; +import com.android.wm.shell.shared.split.SplitScreenConstants.SplitIndex; import com.android.wm.shell.shared.split.SplitScreenConstants.SplitPosition; import com.android.wm.shell.splitscreen.SplitScreen.StageType; import com.android.wm.shell.splitscreen.SplitScreenController.ExitReason; @@ -153,11 +159,15 @@ import dalvik.annotation.optimization.NeverCompile; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.Executor; +import java.util.function.Consumer; +import java.util.function.Predicate; /** * Coordinates the staging (visibility, sizing, ...) of the split-screen stages. @@ -178,10 +188,11 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // entered private static final int DISABLE_LAUNCH_ADJACENT_AFTER_ENTER_TIMEOUT_MS = 1000; - private final StageTaskListener mMainStage; - private final StageTaskListener mSideStage; + private StageTaskListener mMainStage; + private StageTaskListener mSideStage; @SplitPosition private int mSideStagePosition = SPLIT_POSITION_BOTTOM_OR_RIGHT; + private StageOrderOperator mStageOrderOperator; private final int mDisplayId; private SplitLayout mSplitLayout; @@ -335,22 +346,32 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, taskOrganizer.createRootTask(displayId, WINDOWING_MODE_FULLSCREEN, this /* listener */); ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "Creating main/side root task"); - mMainStage = new StageTaskListener( - mContext, - mTaskOrganizer, - mDisplayId, - this /*stageListenerCallbacks*/, - mSyncQueue, - iconProvider, - mWindowDecorViewModel); - mSideStage = new StageTaskListener( - mContext, - mTaskOrganizer, - mDisplayId, - this /*stageListenerCallbacks*/, - mSyncQueue, - iconProvider, - mWindowDecorViewModel); + if (enableFlexibleSplit()) { + mStageOrderOperator = new StageOrderOperator(mContext, + mTaskOrganizer, + mDisplayId, + this /*stageListenerCallbacks*/, + mSyncQueue, + iconProvider, + mWindowDecorViewModel); + } else { + mMainStage = new StageTaskListener( + mContext, + mTaskOrganizer, + mDisplayId, + this /*stageListenerCallbacks*/, + mSyncQueue, + iconProvider, + mWindowDecorViewModel, STAGE_TYPE_MAIN); + mSideStage = new StageTaskListener( + mContext, + mTaskOrganizer, + mDisplayId, + this /*stageListenerCallbacks*/, + mSyncQueue, + iconProvider, + mWindowDecorViewModel, STAGE_TYPE_SIDE); + } mDisplayController = displayController; mDisplayImeController = displayImeController; mDisplayInsetsController = displayInsetsController; @@ -422,24 +443,63 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } public boolean isSplitScreenVisible() { - return mSideStage.mVisible && mMainStage.mVisible; + if (enableFlexibleSplit()) { + return runForActiveStagesAllMatch((stage) -> stage.mVisible); + } else { + return mSideStage.mVisible && mMainStage.mVisible; + } } - private void activateSplit(WindowContainerTransaction wct, boolean includingTopTask) { - mMainStage.activate(wct, includingTopTask); + /** + * @param includingTopTask reparents the current top task into the stage defined by index + * (or mainStage in legacy split) + * @param index the index to move the current visible task into, if undefined will arbitrarily + * choose a stage to launch into + */ + private void activateSplit(WindowContainerTransaction wct, boolean includingTopTask, + int index) { + if (enableFlexibleSplit()) { + mStageOrderOperator.onEnteringSplit(SNAP_TO_2_50_50); + if (index == SPLIT_INDEX_UNDEFINED || !includingTopTask) { + // If we aren't includingTopTask, then the call to activate on the stage is + // effectively a no-op. Previously the stage kept track of the "isActive" state, + // but now that gets set in the "onEnteringSplit" call above. + // + // index == UNDEFINED case might change, but as of now no use case where we activate + // without an index specified. + return; + } + @SplitIndex int oppositeIndex = index == SPLIT_INDEX_0 ? SPLIT_INDEX_1 : SPLIT_INDEX_0; + StageTaskListener activatingStage = mStageOrderOperator.getStageForIndex(oppositeIndex); + activatingStage.activate(wct, includingTopTask); + } else { + mMainStage.activate(wct, includingTopTask); + } } public boolean isSplitActive() { - return mMainStage.isActive(); + if (enableFlexibleSplit()) { + return mStageOrderOperator.isActive(); + } else { + return mMainStage.isActive(); + } } /** * Deactivates main stage by removing the stage from the top level split root (usually when a * task underneath gets removed from the stage root). - * @param reparentToTop whether we want to put the stage root back on top + * @param stageToTop stage which we want to put on top */ - private void deactivateSplit(WindowContainerTransaction wct, boolean reparentToTop) { - mMainStage.deactivate(wct, reparentToTop); + private void deactivateSplit(WindowContainerTransaction wct, @StageType int stageToTop) { + if (enableFlexibleSplit()) { + StageTaskListener stageToDeactivate = mStageOrderOperator.getAllStages().stream() + .filter(stage -> stage.getId() == stageToTop) + .findFirst().orElseThrow(); + stageToDeactivate.deactivate(wct, true /*toTop*/); + mStageOrderOperator.onExitingSplit(); + } else { + mMainStage.deactivate(wct, stageToTop == STAGE_TYPE_MAIN); + } } /** @return whether this transition-request has the launch-adjacent flag. */ @@ -463,11 +523,12 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // If one of the splitting tasks support auto-pip, wm-core might reparent the task to TDA // and file a TRANSIT_PIP transition when finishing transitions. // @see com.android.server.wm.RootWindowContainer#moveActivityToPinnedRootTask - if (mMainStage.getChildCount() == 0 || mSideStage.getChildCount() == 0) { - return true; + if (enableFlexibleSplit()) { + return mStageOrderOperator.getActiveStages().stream() + .anyMatch(stage -> stage.getChildCount() == 0); + } else { + return mMainStage.getChildCount() == 0 || mSideStage.getChildCount() == 0; } - - return false; } /** Checks if `transition` is a pending enter-split transition. */ @@ -477,10 +538,19 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, @StageType int getStageOfTask(int taskId) { - if (mMainStage.containsTask(taskId)) { - return STAGE_TYPE_MAIN; - } else if (mSideStage.containsTask(taskId)) { - return STAGE_TYPE_SIDE; + if (enableFlexibleSplit()) { + StageTaskListener stageTaskListener = mStageOrderOperator.getActiveStages().stream() + .filter(stage -> stage.containsTask(taskId)) + .findFirst().orElse(null); + if (stageTaskListener != null) { + return stageTaskListener.getId(); + } + } else { + if (mMainStage.containsTask(taskId)) { + return STAGE_TYPE_MAIN; + } else if (mSideStage.containsTask(taskId)) { + return STAGE_TYPE_SIDE; + } } return STAGE_TYPE_UNDEFINED; @@ -490,14 +560,22 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, if (mRootTaskInfo != null && mRootTaskInfo.taskId == taskId) { return true; } - return mMainStage.isRootTaskId(taskId) || mSideStage.isRootTaskId(taskId); + if (enableFlexibleSplit()) { + return mStageOrderOperator.getActiveStages().stream() + .anyMatch((stage) -> stage.isRootTaskId(taskId)); + } else { + return mMainStage.isRootTaskId(taskId) || mSideStage.isRootTaskId(taskId); + } } boolean moveToStage(ActivityManager.RunningTaskInfo task, @SplitPosition int stagePosition, WindowContainerTransaction wct) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "moveToStage: task=%d position=%d", task.taskId, stagePosition); - prepareEnterSplitScreen(wct, task, stagePosition, false /* resizeAnim */); + // TODO(b/349828130) currently pass in index_undefined until we can revisit these + // specific cases in the future. Only focusing on parity with starting intent/task + prepareEnterSplitScreen(wct, task, stagePosition, false /* resizeAnim */, + SPLIT_INDEX_UNDEFINED); mSplitTransitions.startEnterTransition(TRANSIT_TO_FRONT, wct, null, this, isSplitScreenVisible() @@ -595,11 +673,14 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, * same window container transaction as the starting of the intent. */ void startTask(int taskId, @SplitPosition int position, @Nullable Bundle options, - @Nullable WindowContainerToken hideTaskToken) { - ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "startTask: task=%d position=%d", taskId, position); + @Nullable WindowContainerToken hideTaskToken, @SplitIndex int index) { + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "startTask: task=%d position=%d index=%d", + taskId, position, index); mSplitRequest = new SplitRequest(taskId, position); final WindowContainerTransaction wct = new WindowContainerTransaction(); - options = resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, null /* wct */); + options = enableFlexibleSplit() + ? resolveStartStageForIndex(options, null /*wct*/, index) + : resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, null /* wct */); if (hideTaskToken != null) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "Reordering hide-task to bottom"); wct.reorder(hideTaskToken, false /* onTop */); @@ -623,7 +704,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // If split screen is not activated, we're expecting to open a pair of apps to split. final int extraTransitType = isSplitActive() ? TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE : TRANSIT_SPLIT_SCREEN_PAIR_OPEN; - prepareEnterSplitScreen(wct, null /* taskInfo */, position, !mIsDropEntering); + prepareEnterSplitScreen(wct, null /* taskInfo */, position, !mIsDropEntering, index); mSplitTransitions.startEnterTransition(TRANSIT_TO_FRONT, wct, null, this, extraTransitType, !mIsDropEntering); @@ -635,13 +716,16 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, * same window container transaction as the starting of the intent. */ void startIntent(PendingIntent intent, Intent fillInIntent, @SplitPosition int position, - @Nullable Bundle options, @Nullable WindowContainerToken hideTaskToken) { + @Nullable Bundle options, @Nullable WindowContainerToken hideTaskToken, + @SplitIndex int index) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "startIntent: intent=%s position=%d", intent.getIntent(), position); mSplitRequest = new SplitRequest(intent.getIntent(), position); final WindowContainerTransaction wct = new WindowContainerTransaction(); - options = resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, null /* wct */); + options = enableFlexibleSplit() + ? resolveStartStageForIndex(options, null /*wct*/, index) + : resolveStartStage(STAGE_TYPE_UNDEFINED, position, options, null /* wct */); if (hideTaskToken != null) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "Reordering hide-task to bottom"); wct.reorder(hideTaskToken, false /* onTop */); @@ -666,13 +750,17 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // If split screen is not activated, we're expecting to open a pair of apps to split. final int extraTransitType = isSplitActive() ? TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE : TRANSIT_SPLIT_SCREEN_PAIR_OPEN; - prepareEnterSplitScreen(wct, null /* taskInfo */, position, !mIsDropEntering); + prepareEnterSplitScreen(wct, null /* taskInfo */, position, !mIsDropEntering, index); mSplitTransitions.startEnterTransition(TRANSIT_TO_FRONT, wct, null, this, extraTransitType, !mIsDropEntering); } - /** Starts 2 tasks in one transition. */ + /** + * Starts 2 tasks in one transition. + * @param taskId1 starts in the mSideStage + * @param taskId2 starts in the mainStage #startWithTask() + */ void startTasks(int taskId1, @Nullable Bundle options1, int taskId2, @Nullable Bundle options2, @SplitPosition int splitPosition, @PersistentSnapPosition int snapPosition, @Nullable RemoteTransition remoteTransition, InstanceId instanceId) { @@ -687,11 +775,19 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, setSideStagePosition(splitPosition, wct); options1 = options1 != null ? options1 : new Bundle(); - addActivityOptions(options1, mSideStage); + StageTaskListener stageForTask1; + if (enableFlexibleSplit()) { + stageForTask1 = mStageOrderOperator.getStageForLegacyPosition(splitPosition, + true /*checkAllStagesIfNotActive*/); + } else { + stageForTask1 = mSideStage; + } + addActivityOptions(options1, stageForTask1); prepareTasksForSplitScreen(new int[] {taskId1, taskId2}, wct); wct.startTask(taskId1, options1); - startWithTask(wct, taskId2, options2, snapPosition, remoteTransition, instanceId); + startWithTask(wct, taskId2, options2, snapPosition, remoteTransition, instanceId, + splitPosition); } /** Start an intent and a task to a split pair in one transition. */ @@ -721,7 +817,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, wct.sendPendingIntent(pendingIntent, fillInIntent, options1); prepareTasksForSplitScreen(new int[] {taskId}, wct); - startWithTask(wct, taskId, options2, snapPosition, remoteTransition, instanceId); + startWithTask(wct, taskId, options2, snapPosition, remoteTransition, instanceId, + splitPosition); } /** @@ -765,7 +862,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, wct.startShortcut(mContext.getPackageName(), shortcutInfo, options1); prepareTasksForSplitScreen(new int[] {taskId}, wct); - startWithTask(wct, taskId, options2, snapPosition, remoteTransition, instanceId); + startWithTask(wct, taskId, options2, snapPosition, remoteTransition, instanceId, + splitPosition); } /** @@ -795,11 +893,14 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, */ private void startWithTask(WindowContainerTransaction wct, int mainTaskId, @Nullable Bundle mainOptions, @PersistentSnapPosition int snapPosition, - @Nullable RemoteTransition remoteTransition, InstanceId instanceId) { + @Nullable RemoteTransition remoteTransition, InstanceId instanceId, + @SplitPosition int splitPosition) { if (!isSplitActive()) { // Build a request WCT that will launch both apps such that task 0 is on the main stage // while task 1 is on the side stage. - activateSplit(wct, false /* reparentToTop */); + // TODO(b/349828130) currently pass in index_undefined until we can revisit these + // specific cases in the future. Only focusing on parity with starting intent/task + activateSplit(wct, false /* reparentToTop */, SPLIT_INDEX_UNDEFINED); } mSplitLayout.setDivideRatio(snapPosition); updateWindowBounds(mSplitLayout, wct); @@ -807,10 +908,19 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, wct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token, false /* reparentLeafTaskIfRelaunch */); setRootForceTranslucent(false, wct); - + // All callers of this method set the correct activity options on mSideStage, + // so we choose the opposite stage for this method + StageTaskListener stage; + if (enableFlexibleSplit()) { + stage = mStageOrderOperator + .getStageForLegacyPosition(reverseSplitPosition(splitPosition), + false /*checkAllStagesIfNotActive*/); + } else { + stage = mMainStage; + } // Make sure the launch options will put tasks in the corresponding split roots mainOptions = mainOptions != null ? mainOptions : new Bundle(); - addActivityOptions(mainOptions, mMainStage); + addActivityOptions(mainOptions, stage); // Add task launch requests wct.startTask(mainTaskId, mainOptions); @@ -866,7 +976,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, if (!isSplitActive()) { // Build a request WCT that will launch both apps such that task 0 is on the main stage // while task 1 is on the side stage. - activateSplit(wct, false /* reparentToTop */); + // TODO(b/349828130) currently pass in index_undefined until we can revisit these + // specific cases in the future. Only focusing on parity with starting intent/task + activateSplit(wct, false /* reparentToTop */, SPLIT_INDEX_UNDEFINED); } setSideStagePosition(splitPosition, wct); @@ -974,6 +1086,31 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSideStage.evictInvisibleChildren(wct); } + /** + * @param index for the new stage that will be opening. Ex. if app is dragged to + * index=1, then this will tell the stage at index=1 to launch the task + * in the wct in that stage. This doesn't verify that the non-specified + * indices' stages have their tasks correctly set/re-parented. + */ + Bundle resolveStartStageForIndex(@Nullable Bundle options, + @Nullable WindowContainerTransaction wct, + @SplitIndex int index) { + StageTaskListener oppositeStage; + if (index == SPLIT_INDEX_UNDEFINED) { + // Arbitrarily choose a stage + oppositeStage = mStageOrderOperator.getStageForIndex(SPLIT_INDEX_1); + } else { + oppositeStage = mStageOrderOperator.getStageForIndex(index); + } + if (options == null) { + options = new Bundle(); + } + updateStageWindowBoundsForIndex(wct, index); + addActivityOptions(options, oppositeStage); + + return options; + } + Bundle resolveStartStage(@StageType int stage, @SplitPosition int position, @Nullable Bundle options, @Nullable WindowContainerTransaction wct) { switch (stage) { @@ -1041,20 +1178,35 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, return INVALID_TASK_ID; } - return mSideStagePosition == splitPosition - ? mSideStage.getTopVisibleChildTaskId() - : mMainStage.getTopVisibleChildTaskId(); + if (enableFlexibleSplit()) { + StageTaskListener stage = mStageOrderOperator.getStageForLegacyPosition(splitPosition, + true /*checkAllStagesIfNotActive*/); + return stage != null ? stage.getTopVisibleChildTaskId() : INVALID_TASK_ID; + } else { + return mSideStagePosition == splitPosition + ? mSideStage.getTopVisibleChildTaskId() + : mMainStage.getTopVisibleChildTaskId(); + } } void switchSplitPosition(String reason) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "switchSplitPosition"); final SurfaceControl.Transaction t = mTransactionPool.acquire(); mTempRect1.setEmpty(); - final StageTaskListener topLeftStage = - mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage; - final StageTaskListener bottomRightStage = - mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mMainStage : mSideStage; - + final StageTaskListener topLeftStage; + final StageTaskListener bottomRightStage; + if (enableFlexibleSplit()) { + topLeftStage = mStageOrderOperator.getStageForLegacyPosition(SPLIT_POSITION_TOP_OR_LEFT, + false /*checkAllStagesIfNotActive*/); + bottomRightStage = mStageOrderOperator + .getStageForLegacyPosition(SPLIT_POSITION_BOTTOM_OR_RIGHT, + false /*checkAllStagesIfNotActive*/); + } else { + topLeftStage = + mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage; + bottomRightStage = + mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mMainStage : mSideStage; + } // Don't allow windows or divider to be focused during animation (mRootTaskInfo is the // parent of all 3 leaves). We don't want the user to be able to tap and focus a window // while it is moving across the screen, because granting focus also recalculates the @@ -1091,9 +1243,13 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, }); ProtoLog.v(WM_SHELL_SPLIT_SCREEN, "Switch split position: %s", reason); - mLogger.logSwap(getMainStagePosition(), mMainStage.getTopChildTaskUid(), - getSideStagePosition(), mSideStage.getTopChildTaskUid(), - mSplitLayout.isLeftRightSplit()); + if (enableFlexibleSplit()) { + // TODO(b/374825718) update logging for 2+ apps + } else { + mLogger.logSwap(getMainStagePosition(), mMainStage.getTopChildTaskUid(), + getSideStagePosition(), mSideStage.getTopChildTaskUid(), + mSplitLayout.isLeftRightSplit()); + } } void setSideStagePosition(@SplitPosition int sideStagePosition, @@ -1101,8 +1257,26 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, if (mSideStagePosition == sideStagePosition) return; mSideStagePosition = sideStagePosition; sendOnStagePositionChanged(); + StageTaskListener stage = enableFlexibleSplit() + ? mStageOrderOperator.getStageForLegacyPosition(mSideStagePosition, + true /*checkAllStagesIfNotActive*/) + : mSideStage; - if (mSideStage.mVisible) { + if (stage.mVisible) { + if (wct == null) { + // onLayoutChanged builds/applies a wct with the contents of updateWindowBounds. + onLayoutSizeChanged(mSplitLayout); + } else { + updateWindowBounds(mSplitLayout, wct); + sendOnBoundsChanged(); + } + } + } + + private void updateStageWindowBoundsForIndex(@Nullable WindowContainerTransaction wct, + @SplitIndex int index) { + StageTaskListener stage = mStageOrderOperator.getStageForIndex(index); + if (stage.mVisible) { if (wct == null) { // onLayoutChanged builds/applies a wct with the contents of updateWindowBounds. onLayoutSizeChanged(mSplitLayout); @@ -1152,10 +1326,17 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, void recordLastActiveStage() { if (!isSplitActive() || !isSplitScreenVisible()) { mLastActiveStage = STAGE_TYPE_UNDEFINED; - } else if (mMainStage.isFocused()) { - mLastActiveStage = STAGE_TYPE_MAIN; - } else if (mSideStage.isFocused()) { - mLastActiveStage = STAGE_TYPE_SIDE; + } else if (enableFlexibleSplit()) { + mStageOrderOperator.getActiveStages().stream() + .filter(StageTaskListener::isFocused) + .findFirst() + .ifPresent(stage -> mLastActiveStage = stage.getId()); + } else { + if (mMainStage.isFocused()) { + mLastActiveStage = STAGE_TYPE_MAIN; + } else if (mSideStage.isFocused()) { + mLastActiveStage = STAGE_TYPE_SIDE; + } } } @@ -1212,7 +1393,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSplitLayout.getInvisibleBounds(mTempRect1); if (childrenToTop == null || childrenToTop.getTopVisibleChildTaskId() == INVALID_TASK_ID) { mSideStage.removeAllTasks(wct, false /* toTop */); - deactivateSplit(wct, false /* reparentToTop */); + deactivateSplit(wct, STAGE_TYPE_UNDEFINED); wct.reorder(mRootTaskInfo.token, false /* onTop */); setRootForceTranslucent(true, wct); wct.setBounds(mSideStage.mRootTaskInfo.token, mTempRect1); @@ -1241,7 +1422,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, childrenToTop.fadeOutDecor(() -> { WindowContainerTransaction finishedWCT = new WindowContainerTransaction(); mIsExiting = false; - deactivateSplit(finishedWCT, childrenToTop == mMainStage /* reparentToTop */); + deactivateSplit(finishedWCT, childrenToTop.getId()); mSideStage.removeAllTasks(finishedWCT, childrenToTop == mSideStage /* toTop */); finishedWCT.reorder(mRootTaskInfo.token, false /* toTop */); setRootForceTranslucent(true, finishedWCT); @@ -1369,8 +1550,13 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mRecentTasks.ifPresent(recentTasks -> { // Notify recents if we are exiting in a way that breaks the pair, and disable further // updates to splits in the recents until we enter split again - mMainStage.doForAllChildTasks(taskId -> recentTasks.removeSplitPair(taskId)); - mSideStage.doForAllChildTasks(taskId -> recentTasks.removeSplitPair(taskId)); + if (enableFlexibleSplit()) { + runForActiveStages((stage) -> + stage.doForAllChildTasks(taskId -> recentTasks.removeSplitPair(taskId))); + } else { + mMainStage.doForAllChildTasks(taskId -> recentTasks.removeSplitPair(taskId)); + mSideStage.doForAllChildTasks(taskId -> recentTasks.removeSplitPair(taskId)); + } }); logExit(exitReason); } @@ -1383,15 +1569,22 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, void prepareExitSplitScreen(@StageType int stageToTop, @NonNull WindowContainerTransaction wct) { if (!isSplitActive()) return; - ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "prepareExitSplitScreen: stageToTop=%d", stageToTop); - mSideStage.removeAllTasks(wct, stageToTop == STAGE_TYPE_SIDE); - deactivateSplit(wct, stageToTop == STAGE_TYPE_MAIN); + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "prepareExitSplitScreen: stageToTop=%s", + stageTypeToString(stageToTop)); + if (enableFlexibleSplit()) { + mStageOrderOperator.getActiveStages().stream() + .filter(stage -> stage.getId() != stageToTop) + .forEach(stage -> stage.removeAllTasks(wct, false /*toTop*/)); + } else { + mSideStage.removeAllTasks(wct, stageToTop == STAGE_TYPE_SIDE); + } + deactivateSplit(wct, stageToTop); } private void prepareEnterSplitScreen(WindowContainerTransaction wct) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "prepareEnterSplitScreen"); prepareEnterSplitScreen(wct, null /* taskInfo */, SPLIT_POSITION_UNDEFINED, - !mIsDropEntering); + !mIsDropEntering, SPLIT_INDEX_UNDEFINED); } /** @@ -1400,7 +1593,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, */ void prepareEnterSplitScreen(WindowContainerTransaction wct, @Nullable ActivityManager.RunningTaskInfo taskInfo, @SplitPosition int startPosition, - boolean resizeAnim) { + boolean resizeAnim, @SplitIndex int index) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "prepareEnterSplitScreen: position=%d resize=%b", startPosition, resizeAnim); onSplitScreenEnter(); @@ -1412,7 +1605,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, if (isSplitActive()) { prepareBringSplit(wct, taskInfo, startPosition, resizeAnim); } else { - prepareActiveSplit(wct, taskInfo, startPosition, resizeAnim); + prepareActiveSplit(wct, taskInfo, startPosition, resizeAnim, index); } } @@ -1433,14 +1626,22 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, if (!mSkipEvictingMainStageChildren) { mMainStage.evictAllChildren(wct); } - mMainStage.reparentTopTask(wct); + // TODO(b/349828130) revisit bring split from BG to FG scenarios + if (enableFlexibleSplit()) { + runForActiveStages(stage -> stage.reparentTopTask(wct)); + } else { + mMainStage.reparentTopTask(wct); + } prepareSplitLayout(wct, resizeAnim); } } + /** + * @param index The index that has already been assigned a stage + */ private void prepareActiveSplit(WindowContainerTransaction wct, @Nullable ActivityManager.RunningTaskInfo taskInfo, @SplitPosition int startPosition, - boolean resizeAnim) { + boolean resizeAnim, @SplitIndex int index) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "prepareActiveSplit: task=%d isSplitVisible=%b", taskInfo != null ? taskInfo.taskId : -1, isSplitScreenVisible()); // We handle split visibility itself on shell transition, but sometimes we didn't @@ -1450,7 +1651,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, setSideStagePosition(startPosition, wct); mSideStage.addTask(taskInfo, wct); } - activateSplit(wct, true /* reparentToTop */); + activateSplit(wct, true /* reparentToTop */, index); prepareSplitLayout(wct, resizeAnim); } @@ -1477,8 +1678,13 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, void finishEnterSplitScreen(SurfaceControl.Transaction finishT) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "finishEnterSplitScreen"); mSplitLayout.update(null, true /* resetImePosition */); - mMainStage.getSplitDecorManager().inflate(mContext, mMainStage.mRootLeash); - mSideStage.getSplitDecorManager().inflate(mContext, mSideStage.mRootLeash); + if (enableFlexibleSplit()) { + runForActiveStages((stage) -> + stage.getSplitDecorManager().inflate(mContext, stage.mRootLeash)); + } else { + mMainStage.getSplitDecorManager().inflate(mContext, mMainStage.mRootLeash); + mSideStage.getSplitDecorManager().inflate(mContext, mSideStage.mRootLeash); + } setDividerVisibility(true, finishT); // Ensure divider surface are re-parented back into the hierarchy at the end of the // transition. See Transition#buildFinishTransaction for more detail. @@ -1492,6 +1698,10 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSplitRequest = null; updateRecentTasksSplitPair(); + if (enableFlexibleSplit()) { + // TODO(b/374825718) log 2+ apps + return; + } mLogger.logEnter(mSplitLayout.getDividerPositionAsFraction(), getMainStagePosition(), mMainStage.getTopChildTaskUid(), getSideStagePosition(), mSideStage.getTopChildTaskUid(), @@ -1499,8 +1709,17 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } void getStageBounds(Rect outTopOrLeftBounds, Rect outBottomOrRightBounds) { - outTopOrLeftBounds.set(mSplitLayout.getBounds1()); - outBottomOrRightBounds.set(mSplitLayout.getBounds2()); + outTopOrLeftBounds.set(mSplitLayout.getTopLeftBounds()); + outBottomOrRightBounds.set(mSplitLayout.getBottomRightBounds()); + } + + private void runForActiveStages(Consumer<StageTaskListener> consumer) { + mStageOrderOperator.getActiveStages().forEach(consumer); + } + + private boolean runForActiveStagesAllMatch(Predicate<StageTaskListener> predicate) { + List<StageTaskListener> activeStages = mStageOrderOperator.getActiveStages(); + return !activeStages.isEmpty() && activeStages.stream().allMatch(predicate); } @SplitPosition @@ -1516,6 +1735,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, private void addActivityOptions(Bundle opts, @Nullable StageTaskListener launchTarget) { ActivityOptions options = ActivityOptions.fromBundle(opts); if (launchTarget != null) { + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, + "addActivityOptions setting launch root for stage=%s", + stageTypeToString(launchTarget.getId())); options.setLaunchRootTask(launchTarget.mRootTaskInfo.token); } // Put BAL flags to avoid activity start aborted. Otherwise, flows like shortcut to split @@ -1559,8 +1781,15 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, listener.onSplitBoundsChanged(mSplitLayout.getRootBounds(), getMainStageBounds(), getSideStageBounds()); } - mSideStage.onSplitScreenListenerRegistered(listener, STAGE_TYPE_SIDE); - mMainStage.onSplitScreenListenerRegistered(listener, STAGE_TYPE_MAIN); + if (enableFlexibleSplit()) { + // TODO(b/349828130) replace w/ stageID + mStageOrderOperator.getAllStages().forEach( + stage -> stage.onSplitScreenListenerRegistered(listener, STAGE_TYPE_UNDEFINED) + ); + } else { + mSideStage.onSplitScreenListenerRegistered(listener, STAGE_TYPE_SIDE); + mMainStage.onSplitScreenListenerRegistered(listener, STAGE_TYPE_MAIN); + } } private void sendOnStagePositionChanged() { @@ -1584,17 +1813,25 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, boolean present, boolean visible) { int stage; if (present) { - stage = stageListener == mSideStage ? STAGE_TYPE_SIDE : STAGE_TYPE_MAIN; + if (enableFlexibleSplit()) { + stage = stageListener.getId(); + } else { + stage = stageListener == mSideStage ? STAGE_TYPE_SIDE : STAGE_TYPE_MAIN; + } } else { // No longer on any stage stage = STAGE_TYPE_UNDEFINED; } - if (stage == STAGE_TYPE_MAIN) { - mLogger.logMainStageAppChange(getMainStagePosition(), mMainStage.getTopChildTaskUid(), - mSplitLayout.isLeftRightSplit()); - } else if (stage == STAGE_TYPE_SIDE) { - mLogger.logSideStageAppChange(getSideStagePosition(), mSideStage.getTopChildTaskUid(), - mSplitLayout.isLeftRightSplit()); + if (!enableFlexibleSplit()) { + if (stage == STAGE_TYPE_MAIN) { + mLogger.logMainStageAppChange(getMainStagePosition(), + mMainStage.getTopChildTaskUid(), + mSplitLayout.isLeftRightSplit()); + } else if (stage == STAGE_TYPE_SIDE) { + mLogger.logSideStageAppChange(getSideStagePosition(), + mSideStage.getTopChildTaskUid(), + mSplitLayout.isLeftRightSplit()); + } } if (present) { updateRecentTasksSplitPair(); @@ -1620,19 +1857,40 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, return; } mRecentTasks.ifPresent(recentTasks -> { - Rect topLeftBounds = mSplitLayout.getBounds1(); - Rect bottomRightBounds = mSplitLayout.getBounds2(); - int mainStageTopTaskId = mMainStage.getTopVisibleChildTaskId(); - int sideStageTopTaskId = mSideStage.getTopVisibleChildTaskId(); + Rect topLeftBounds = new Rect(); + mSplitLayout.copyTopLeftBounds(topLeftBounds); + Rect bottomRightBounds = new Rect(); + mSplitLayout.copyBottomRightBounds(bottomRightBounds); + + int sideStageTopTaskId; + int mainStageTopTaskId; + if (enableFlexibleSplit()) { + List<StageTaskListener> activeStages = mStageOrderOperator.getActiveStages(); + if (activeStages.size() != 2) { + sideStageTopTaskId = mainStageTopTaskId = INVALID_TASK_ID; + } else { + // doesn't matter which one we assign to? What matters is the order of 0 and 1? + mainStageTopTaskId = activeStages.get(0).getTopVisibleChildTaskId(); + sideStageTopTaskId = activeStages.get(1).getTopVisibleChildTaskId(); + } + } else { + mainStageTopTaskId = mMainStage.getTopVisibleChildTaskId(); + sideStageTopTaskId= mSideStage.getTopVisibleChildTaskId(); + } boolean sideStageTopLeft = mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT; int leftTopTaskId; int rightBottomTaskId; - if (sideStageTopLeft) { - leftTopTaskId = sideStageTopTaskId; - rightBottomTaskId = mainStageTopTaskId; - } else { + if (enableFlexibleSplit()) { leftTopTaskId = mainStageTopTaskId; rightBottomTaskId = sideStageTopTaskId; + } else { + if (sideStageTopLeft) { + leftTopTaskId = sideStageTopTaskId; + rightBottomTaskId = mainStageTopTaskId; + } else { + leftTopTaskId = mainStageTopTaskId; + rightBottomTaskId = sideStageTopTaskId; + } } if (Flags.enableFlexibleTwoAppSplit()) { @@ -1741,29 +1999,59 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, @VisibleForTesting @Override public void onRootTaskAppeared() { - ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onRootTaskAppeared: rootTask=%s mainRoot=%b sideRoot=%b", - mRootTaskInfo, mMainStage.mHasRootTask, mSideStage.mHasRootTask); + if (enableFlexibleSplit()) { + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onRootTaskAppeared: rootTask=%s", + mRootTaskInfo); + mStageOrderOperator.getAllStages().forEach(stage -> { + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, + " onRootStageAppeared stageId=%s hasRoot=%b", + stageTypeToString(stage.getId()), stage.mHasRootTask); + }); + } else { + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, + "onRootTaskAppeared: rootTask=%s mainRoot=%b sideRoot=%b", + mRootTaskInfo, mMainStage.mHasRootTask, mSideStage.mHasRootTask); + } + boolean notAllStagesHaveRootTask; + if (enableFlexibleSplit()) { + notAllStagesHaveRootTask = mStageOrderOperator.getAllStages().stream() + .anyMatch((stage) -> !stage.mHasRootTask); + } else { + notAllStagesHaveRootTask = !mMainStage.mHasRootTask + || !mSideStage.mHasRootTask; + } // Wait unit all root tasks appeared. - if (mRootTaskInfo == null - || !mMainStage.mHasRootTask - || !mSideStage.mHasRootTask) { + if (mRootTaskInfo == null || notAllStagesHaveRootTask) { return; } final WindowContainerTransaction wct = new WindowContainerTransaction(); - wct.reparent(mMainStage.mRootTaskInfo.token, mRootTaskInfo.token, true); - wct.reparent(mSideStage.mRootTaskInfo.token, mRootTaskInfo.token, true); + if (enableFlexibleSplit()) { + mStageOrderOperator.getAllStages().forEach(stage -> + wct.reparent(stage.mRootTaskInfo.token, mRootTaskInfo.token, true)); + } else { + wct.reparent(mMainStage.mRootTaskInfo.token, mRootTaskInfo.token, true); + wct.reparent(mSideStage.mRootTaskInfo.token, mRootTaskInfo.token, true); + } - // Make the stages adjacent to each other so they occlude what's behind them. - wct.setAdjacentRoots(mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token); setRootForceTranslucent(true, wct); - mSplitLayout.getInvisibleBounds(mTempRect1); - wct.setBounds(mSideStage.mRootTaskInfo.token, mTempRect1); + if (!enableFlexibleSplit()) { + //TODO(b/373709676) Need to figure out how adjacentRoots work for flex split + + // Make the stages adjacent to each other so they occlude what's behind them. + wct.setAdjacentRoots(mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token); + mSplitLayout.getInvisibleBounds(mTempRect1); + wct.setBounds(mSideStage.mRootTaskInfo.token, mTempRect1); + } mSyncQueue.queue(wct); - mSyncQueue.runInSync(t -> { - t.setPosition(mSideStage.mRootLeash, mTempRect1.left, mTempRect1.top); - }); - mLaunchAdjacentController.setLaunchAdjacentRoot(mSideStage.mRootTaskInfo.token); + if (!enableFlexibleSplit()) { + mSyncQueue.runInSync(t -> { + t.setPosition(mSideStage.mRootLeash, mTempRect1.left, mTempRect1.top); + }); + mLaunchAdjacentController.setLaunchAdjacentRoot(mSideStage.mRootTaskInfo.token); + } else { + // TODO(b/373709676) Need to figure out how adjacentRoots work for flex split + } } @Override @@ -1958,15 +2246,21 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } @Override - public void onSnappedToDismiss(boolean bottomOrRight, @ExitReason int exitReason) { + public void onSnappedToDismiss(boolean closedBottomRightStage, @ExitReason int exitReason) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onSnappedToDismiss: bottomOrRight=%b reason=%s", - bottomOrRight, exitReasonToString(exitReason)); - final boolean mainStageToTop = - bottomOrRight ? mSideStagePosition == SPLIT_POSITION_BOTTOM_OR_RIGHT + closedBottomRightStage, exitReasonToString(exitReason)); + boolean mainStageToTop = + closedBottomRightStage ? mSideStagePosition == SPLIT_POSITION_BOTTOM_OR_RIGHT : mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT; - final StageTaskListener toTopStage = mainStageToTop ? mMainStage : mSideStage; - - final int dismissTop = mainStageToTop ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE; + StageTaskListener toTopStage = mainStageToTop ? mMainStage : mSideStage; + int dismissTop = mainStageToTop ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE; + if (enableFlexibleSplit()) { + toTopStage = mStageOrderOperator.getStageForLegacyPosition(closedBottomRightStage + ? SPLIT_POSITION_TOP_OR_LEFT + : SPLIT_POSITION_BOTTOM_OR_RIGHT, + false /*checkAllStagesIfNotActive*/); + dismissTop = toTopStage.getId(); + } final WindowContainerTransaction wct = new WindowContainerTransaction(); toTopStage.resetBounds(wct); prepareExitSplitScreen(dismissTop, wct); @@ -1998,8 +2292,21 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, updateSurfaceBounds(layout, t, shouldUseParallaxEffect); getMainStageBounds(mTempRect1); getSideStageBounds(mTempRect2); - mMainStage.onResizing(mTempRect1, mTempRect2, t, offsetX, offsetY, mShowDecorImmediately); - mSideStage.onResizing(mTempRect2, mTempRect1, t, offsetX, offsetY, mShowDecorImmediately); + if (enableFlexibleSplit()) { + StageTaskListener ltStage = + mStageOrderOperator.getStageForLegacyPosition(SPLIT_POSITION_TOP_OR_LEFT, + false /*checkAllStagesIfNotActive*/); + StageTaskListener brStage = + mStageOrderOperator.getStageForLegacyPosition(SPLIT_POSITION_BOTTOM_OR_RIGHT, + false /*checkAllStagesIfNotActive*/); + ltStage.onResizing(mTempRect1, mTempRect2, t, offsetX, offsetY, mShowDecorImmediately); + brStage.onResizing(mTempRect2, mTempRect1, t, offsetX, offsetY, mShowDecorImmediately); + } else { + mMainStage.onResizing(mTempRect1, mTempRect2, t, offsetX, offsetY, + mShowDecorImmediately); + mSideStage.onResizing(mTempRect2, mTempRect1, t, offsetX, offsetY, + mShowDecorImmediately); + } t.apply(); mTransactionPool.release(t); } @@ -2015,19 +2322,33 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, if (!sizeChanged) { // We still need to resize on decor for ensure all current status clear. final SurfaceControl.Transaction t = mTransactionPool.acquire(); - mMainStage.onResized(t); - mSideStage.onResized(t); + if (enableFlexibleSplit()) { + runForActiveStages(stage -> stage.onResized(t)); + } else { + mMainStage.onResized(t); + mSideStage.onResized(t); + } mTransactionPool.release(t); return; } - + List<SplitDecorManager> decorManagers = new ArrayList<>(); + SplitDecorManager mainDecor = null; + SplitDecorManager sideDecor = null; + if (enableFlexibleSplit()) { + decorManagers = mStageOrderOperator.getActiveStages().stream() + .map(StageTaskListener::getSplitDecorManager) + .toList(); + } else { + mainDecor = mMainStage.getSplitDecorManager(); + sideDecor = mSideStage.getSplitDecorManager(); + } sendOnBoundsChanged(); mSplitLayout.setDividerInteractive(false, false, "onSplitResizeStart"); mSplitTransitions.startResizeTransition(wct, this, (aborted) -> { mSplitLayout.setDividerInteractive(true, false, "onSplitResizeConsumed"); }, (finishWct, t) -> { mSplitLayout.setDividerInteractive(true, false, "onSplitResizeFinish"); - }, mMainStage.getSplitDecorManager(), mSideStage.getSplitDecorManager()); + }, mainDecor, sideDecor, decorManagers); if (Flags.enableFlexibleTwoAppSplit()) { switch (layout.calculateCurrentSnapPosition()) { @@ -2054,29 +2375,55 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, * @return true if stage bounds actually . */ private boolean updateWindowBounds(SplitLayout layout, WindowContainerTransaction wct) { - final StageTaskListener topLeftStage = - mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage; - final StageTaskListener bottomRightStage = - mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mMainStage : mSideStage; + final StageTaskListener topLeftStage; + final StageTaskListener bottomRightStage; + if (enableFlexibleSplit()) { + topLeftStage = mStageOrderOperator + .getStageForLegacyPosition(SPLIT_POSITION_TOP_OR_LEFT, + true /*checkAllStagesIfNotActive*/); + bottomRightStage = mStageOrderOperator + .getStageForLegacyPosition(SPLIT_POSITION_BOTTOM_OR_RIGHT, + true /*checkAllStagesIfNotActive*/); + } else { + topLeftStage = mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT + ? mSideStage + : mMainStage; + bottomRightStage = mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT + ? mMainStage + : mSideStage; + } boolean updated = layout.applyTaskChanges(wct, topLeftStage.mRootTaskInfo, bottomRightStage.mRootTaskInfo); ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "updateWindowBounds: topLeftStage=%s bottomRightStage=%s", - layout.getBounds1(), layout.getBounds2()); + layout.getTopLeftBounds(), layout.getBottomRightBounds()); return updated; } void updateSurfaceBounds(@Nullable SplitLayout layout, @NonNull SurfaceControl.Transaction t, boolean applyResizingOffset) { - final StageTaskListener topLeftStage = - mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mSideStage : mMainStage; - final StageTaskListener bottomRightStage = - mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT ? mMainStage : mSideStage; + final StageTaskListener topLeftStage; + final StageTaskListener bottomRightStage; + if (enableFlexibleSplit()) { + topLeftStage = mStageOrderOperator + .getStageForLegacyPosition(SPLIT_POSITION_TOP_OR_LEFT, + true /*checkAllStagesIfNotActive*/); + bottomRightStage = mStageOrderOperator + .getStageForLegacyPosition(SPLIT_POSITION_BOTTOM_OR_RIGHT, + true /*checkAllStagesIfNotActive*/); + } else { + topLeftStage = mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT + ? mSideStage + : mMainStage; + bottomRightStage = mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT + ? mMainStage + : mSideStage; + } (layout != null ? layout : mSplitLayout).applySurfaceChanges(t, topLeftStage.mRootLeash, bottomRightStage.mRootLeash, topLeftStage.mDimLayer, bottomRightStage.mDimLayer, applyResizingOffset); ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "updateSurfaceBounds: topLeftStage=%s bottomRightStage=%s", - layout.getBounds1(), layout.getBounds2()); + layout.getTopLeftBounds(), layout.getBottomRightBounds()); } @Override @@ -2085,10 +2432,22 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, return SPLIT_POSITION_UNDEFINED; } - if (mMainStage.containsToken(token)) { - return getMainStagePosition(); - } else if (mSideStage.containsToken(token)) { - return getSideStagePosition(); + if (enableFlexibleSplit()) { + // We could migrate to/return the new INDEX enums here since most callers just care that + // this value isn't SPLIT_POSITION_UNDEFINED, but + // ImePositionProcessor#getImeTargetPosition actually uses the leftTop/bottomRight value + StageTaskListener stageForToken = mStageOrderOperator.getAllStages().stream() + .filter(stage -> stage.containsToken(token)) + .findFirst().orElse(null); + return stageForToken == null + ? SPLIT_POSITION_UNDEFINED + : mStageOrderOperator.getLegacyPositionForStage(stageForToken); + } else { + if (mMainStage.containsToken(token)) { + return getMainStagePosition(); + } else if (mSideStage.containsToken(token)) { + return getSideStagePosition(); + } } return SPLIT_POSITION_UNDEFINED; @@ -2185,27 +2544,49 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, private Rect getSideStageBounds() { return mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT - ? mSplitLayout.getBounds1() : mSplitLayout.getBounds2(); + ? mSplitLayout.getTopLeftBounds() : mSplitLayout.getBottomRightBounds(); } private Rect getMainStageBounds() { return mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT - ? mSplitLayout.getBounds2() : mSplitLayout.getBounds1(); + ? mSplitLayout.getBottomRightBounds() : mSplitLayout.getTopLeftBounds(); } + /** + * TODO(b/349828130) Currently the way this is being used is only to to get the bottomRight + * stage. Eventually we'll need to rename and for now we'll repurpose the method to return + * the bottomRight bounds under the flex split flag + */ private void getSideStageBounds(Rect rect) { - if (mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT) { - mSplitLayout.getBounds1(rect); + if (enableFlexibleSplit()) { + // Split Layout doesn't actually keep track of the bounds based on the stage, + // it only knows that bounds1 is leftTop position and bounds2 is bottomRight position + // We'll then assume this method is to get bounds of bottomRight stage + mSplitLayout.copyBottomRightBounds(rect); + } else if (mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT) { + mSplitLayout.copyTopLeftBounds(rect); } else { - mSplitLayout.getBounds2(rect); + mSplitLayout.copyBottomRightBounds(rect); } } + /** + * TODO(b/349828130) Currently the way this is being used is only to to get the leftTop + * stage. Eventually we'll need to rename and for now we'll repurpose the method to return + * the leftTop bounds under the flex split flag + */ private void getMainStageBounds(Rect rect) { - if (mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT) { - mSplitLayout.getBounds2(rect); + if (enableFlexibleSplit()) { + // Split Layout doesn't actually keep track of the bounds based on the stage, + // it only knows that bounds1 is leftTop position and bounds2 is bottomRight position + // We'll then assume this method is to get bounds of topLeft stage + mSplitLayout.copyTopLeftBounds(rect); } else { - mSplitLayout.getBounds1(rect); + if (mSideStagePosition == SPLIT_POSITION_TOP_OR_LEFT) { + mSplitLayout.copyBottomRightBounds(rect); + } else { + mSplitLayout.copyTopLeftBounds(rect); + } } } @@ -2214,14 +2595,23 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, * this task (yet) so this can also be used to identify which stage to put a task into. */ private StageTaskListener getStageOfTask(ActivityManager.RunningTaskInfo taskInfo) { - // TODO(b/184679596): Find a way to either include task-org information in the transition, - // or synchronize task-org callbacks so we can use stage.containsTask - if (mMainStage.mRootTaskInfo != null - && taskInfo.parentTaskId == mMainStage.mRootTaskInfo.taskId) { - return mMainStage; - } else if (mSideStage.mRootTaskInfo != null - && taskInfo.parentTaskId == mSideStage.mRootTaskInfo.taskId) { - return mSideStage; + if (enableFlexibleSplit()) { + return mStageOrderOperator.getActiveStages().stream() + .filter((stage) -> stage.mRootTaskInfo != null && + taskInfo.parentTaskId == stage.mRootTaskInfo.taskId + ) + .findFirst() + .orElse(null); + } else { + // TODO(b/184679596): Find a way to either include task-org information in the + // transition, or synchronize task-org callbacks so we can use stage.containsTask + if (mMainStage.mRootTaskInfo != null + && taskInfo.parentTaskId == mMainStage.mRootTaskInfo.taskId) { + return mMainStage; + } else if (mSideStage.mRootTaskInfo != null + && taskInfo.parentTaskId == mSideStage.mRootTaskInfo.taskId) { + return mSideStage; + } } return null; } @@ -2229,7 +2619,11 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, @StageType private int getStageType(StageTaskListener stage) { if (stage == null) return STAGE_TYPE_UNDEFINED; - return stage == mMainStage ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE; + if (enableFlexibleSplit()) { + return stage.getId(); + } else { + return stage == mMainStage ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE; + } } @Override @@ -2276,11 +2670,17 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, if (isSplitActive()) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "handleRequest: transition=%d split active", request.getDebugId()); + StageTaskListener primaryStage = enableFlexibleSplit() + ? mStageOrderOperator.getActiveStages().get(0) + : mMainStage; + StageTaskListener secondaryStage = enableFlexibleSplit() + ? mStageOrderOperator.getActiveStages().get(1) + : mSideStage; // Try to handle everything while in split-screen, so return a WCT even if it's empty. ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " split is active so using split" + "Transition to handle request. triggerTask=%d type=%s mainChildren=%d" + " sideChildren=%d", triggerTask.taskId, transitTypeToString(type), - mMainStage.getChildCount(), mSideStage.getChildCount()); + primaryStage.getChildCount(), secondaryStage.getChildCount()); out = new WindowContainerTransaction(); if (stage != null) { if (isClosingType(type) && stage.getChildCount() == 1) { @@ -2320,11 +2720,21 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // the remote handler. return null; } - - if ((mMainStage.containsTask(triggerTask.taskId) - && mMainStage.getChildCount() == 1) - || (mSideStage.containsTask(triggerTask.taskId) - && mSideStage.getChildCount() == 1)) { + boolean anyStageContainsSingleFullscreenTask; + if (enableFlexibleSplit()) { + anyStageContainsSingleFullscreenTask = + mStageOrderOperator.getActiveStages().stream() + .anyMatch(stageListener -> + stageListener.containsTask(triggerTask.taskId) + && stageListener.getChildCount() == 1); + } else { + anyStageContainsSingleFullscreenTask = + (mMainStage.containsTask(triggerTask.taskId) + && mMainStage.getChildCount() == 1) + || (mSideStage.containsTask(triggerTask.taskId) + && mSideStage.getChildCount() == 1); + } + if (anyStageContainsSingleFullscreenTask) { // A splitting task is opening to fullscreen causes one side of the split empty, // so appends operations to exit split. prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, out); @@ -2344,11 +2754,19 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // One of the cases above handled it return out; } else if (isSplitScreenVisible()) { + boolean allStagesHaveChildren; + if (enableFlexibleSplit()) { + allStagesHaveChildren = runForActiveStagesAllMatch(stageTaskListener -> + stageTaskListener.getChildCount() != 0); + } else { + allStagesHaveChildren = mMainStage.getChildCount() != 0 + && mSideStage.getChildCount() != 0; + } // If split is visible, only defer handling this transition if it's launching // adjacent while there is already a split pair -- this may trigger PIP and // that should be handled by the mixed handler. final boolean deferTransition = requestHasLaunchAdjacentFlag(request) - && mMainStage.getChildCount() != 0 && mSideStage.getChildCount() != 0; + && allStagesHaveChildren; return !deferTransition ? out : null; } // Don't intercept the transition if we are not handling it as a part of one of the @@ -2588,8 +3006,15 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } final ArraySet<StageTaskListener> dismissStages = record.getShouldDismissedStage(); - if (mMainStage.getChildCount() == 0 || mSideStage.getChildCount() == 0 - || dismissStages.size() == 1) { + boolean anyStageHasNoChildren; + if (enableFlexibleSplit()) { + anyStageHasNoChildren = mStageOrderOperator.getActiveStages().stream() + .anyMatch(stage -> stage.getChildCount() == 0); + } else { + anyStageHasNoChildren = mMainStage.getChildCount() == 0 + || mSideStage.getChildCount() == 0; + } + if (anyStageHasNoChildren || dismissStages.size() == 1) { // If the size of dismissStages == 1, one of the task is closed without prepare // pending transition, which could happen if all activities were finished after // finish top activity in a task, so the trigger task is null when handleRequest. @@ -2735,24 +3160,48 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, shouldAnimate = startPendingDismissAnimation( dismiss, info, startTransaction, finishTransaction); if (shouldAnimate && dismiss.mReason == EXIT_REASON_DRAG_DIVIDER) { - final StageTaskListener toTopStage = - dismiss.mDismissTop == STAGE_TYPE_MAIN ? mMainStage : mSideStage; + StageTaskListener toTopStage; + if (enableFlexibleSplit()) { + toTopStage = mStageOrderOperator.getAllStages().stream() + .filter(stage -> stage.getId() == dismiss.mDismissTop) + .findFirst().orElseThrow(); + } else { + toTopStage = dismiss.mDismissTop == STAGE_TYPE_MAIN ? mMainStage : mSideStage; + } mSplitTransitions.playDragDismissAnimation(transition, info, startTransaction, finishTransaction, finishCallback, toTopStage.mRootTaskInfo.token, toTopStage.getSplitDecorManager(), mRootTaskInfo.token); return true; } } else if (mSplitTransitions.isPendingResize(transition)) { + Map<WindowContainerToken, SplitDecorManager> tokenDecorMap = new HashMap<>(); + if (enableFlexibleSplit()) { + runForActiveStages(stageTaskListener -> + tokenDecorMap.put(stageTaskListener.mRootTaskInfo.getToken(), + stageTaskListener.getSplitDecorManager())); + } else { + tokenDecorMap.put(mMainStage.mRootTaskInfo.getToken(), + mMainStage.getSplitDecorManager()); + tokenDecorMap.put(mSideStage.mRootTaskInfo.getToken(), + mSideStage.getSplitDecorManager()); + } mSplitTransitions.playResizeAnimation(transition, info, startTransaction, - finishTransaction, finishCallback, mMainStage.mRootTaskInfo.token, - mSideStage.mRootTaskInfo.token, mMainStage.getSplitDecorManager(), - mSideStage.getSplitDecorManager()); + finishTransaction, finishCallback, tokenDecorMap); return true; } if (!shouldAnimate) return false; + WindowContainerToken mainToken; + WindowContainerToken sideToken; + if (enableFlexibleSplit()) { + mainToken = mStageOrderOperator.getActiveStages().get(0).mRootTaskInfo.token; + sideToken = mStageOrderOperator.getActiveStages().get(1).mRootTaskInfo.token; + } else { + mainToken = mMainStage.mRootTaskInfo.token; + sideToken = mSideStage.mRootTaskInfo.token; + } mSplitTransitions.playAnimation(transition, info, startTransaction, finishTransaction, - finishCallback, mMainStage.mRootTaskInfo.token, mSideStage.mRootTaskInfo.token, + finishCallback, mainToken, sideToken, mRootTaskInfo.token); return true; } @@ -2777,6 +3226,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // First, verify that we actually have opened apps in both splits. TransitionInfo.Change mainChild = null; TransitionInfo.Change sideChild = null; + StageTaskListener firstAppStage = null; + StageTaskListener secondAppStage = null; final WindowContainerTransaction evictWct = new WindowContainerTransaction(); for (int iC = 0; iC < info.getChanges().size(); ++iC) { final TransitionInfo.Change change = info.getChanges().get(iC); @@ -2785,14 +3236,19 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, if (mPausingTasks.contains(taskInfo.taskId)) { continue; } - final @StageType int stageType = getStageType(getStageOfTask(taskInfo)); - if (mainChild == null && stageType == STAGE_TYPE_MAIN + StageTaskListener stage = getStageOfTask(taskInfo); + final @StageType int stageType = getStageType(stage); + if (mainChild == null + && stageType == (enableFlexibleSplit() ? STAGE_TYPE_A : STAGE_TYPE_MAIN) && (isOpeningType(change.getMode()) || change.getMode() == TRANSIT_CHANGE)) { // Includes TRANSIT_CHANGE to cover reparenting top-most task to split. mainChild = change; - } else if (sideChild == null && stageType == STAGE_TYPE_SIDE + firstAppStage = getStageOfTask(taskInfo); + } else if (sideChild == null + && stageType == (enableFlexibleSplit() ? STAGE_TYPE_B : STAGE_TYPE_SIDE) && (isOpeningType(change.getMode()) || change.getMode() == TRANSIT_CHANGE)) { sideChild = change; + secondAppStage = stage; } else if (stageType != STAGE_TYPE_UNDEFINED && change.getMode() == TRANSIT_TO_BACK) { // Collect all to back task's and evict them when transition finished. evictWct.reparent(taskInfo.token, null /* parent */, false /* onTop */); @@ -2848,9 +3304,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // TODO(b/184679596): Find a way to either include task-org information in // the transition, or synchronize task-org callbacks. final boolean mainNotContainOpenTask = - mainChild != null && !mMainStage.containsTask(mainChild.getTaskInfo().taskId); + mainChild != null && !firstAppStage.containsTask(mainChild.getTaskInfo().taskId); final boolean sideNotContainOpenTask = - sideChild != null && !mSideStage.containsTask(sideChild.getTaskInfo().taskId); + sideChild != null && !secondAppStage.containsTask(sideChild.getTaskInfo().taskId); if (mainNotContainOpenTask) { Log.w(TAG, "Expected onTaskAppeared on " + mMainStage + " to have been called with " + mainChild.getTaskInfo().taskId @@ -2863,6 +3319,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } final TransitionInfo.Change finalMainChild = mainChild; final TransitionInfo.Change finalSideChild = sideChild; + final StageTaskListener finalFirstAppStage = firstAppStage; + final StageTaskListener finalSecondAppStage = secondAppStage; enterTransition.setFinishedCallback((callbackWct, callbackT) -> { if (!enterTransition.mResizeAnim) { // If resizing, we'll call notify at the end of the resizing animation (below) @@ -2870,16 +3328,18 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } if (finalMainChild != null) { if (!mainNotContainOpenTask) { - mMainStage.evictOtherChildren(callbackWct, finalMainChild.getTaskInfo().taskId); + finalFirstAppStage.evictOtherChildren(callbackWct, + finalMainChild.getTaskInfo().taskId); } else { - mMainStage.evictInvisibleChildren(callbackWct); + finalFirstAppStage.evictInvisibleChildren(callbackWct); } } if (finalSideChild != null) { if (!sideNotContainOpenTask) { - mSideStage.evictOtherChildren(callbackWct, finalSideChild.getTaskInfo().taskId); + finalSecondAppStage.evictOtherChildren(callbackWct, + finalSideChild.getTaskInfo().taskId); } else { - mSideStage.evictInvisibleChildren(callbackWct); + finalSecondAppStage.evictInvisibleChildren(callbackWct); } } if (!evictWct.isEmpty()) { @@ -2961,8 +3421,10 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, public void onPipExpandToSplit(WindowContainerTransaction wct, ActivityManager.RunningTaskInfo taskInfo) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onPipExpandToSplit: task=%s", taskInfo); + // TODO(b/349828130) currently pass in index_undefined until we can revisit these + // flex split + pip interactions in the future prepareEnterSplitScreen(wct, taskInfo, getActivateSplitPosition(taskInfo), - false /*resizeAnim*/); + false /*resizeAnim*/, SPLIT_INDEX_UNDEFINED); if (!isSplitScreenVisible() || mSplitRequest == null) { return; @@ -3067,13 +3529,28 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // Wait until after animation to update divider // Reset crops so they don't interfere with subsequent launches - t.setCrop(mMainStage.mRootLeash, null); - t.setCrop(mSideStage.mRootLeash, null); + if (enableFlexibleSplit()) { + runForActiveStages(stage -> t.setCrop(stage.mRootLeash, null /*crop*/)); + } else { + t.setCrop(mMainStage.mRootLeash, null); + t.setCrop(mSideStage.mRootLeash, null); + } // Hide the non-top stage and set the top one to the fullscreen position. if (toStage != STAGE_TYPE_UNDEFINED) { - t.hide(toStage == STAGE_TYPE_MAIN ? mSideStage.mRootLeash : mMainStage.mRootLeash); - t.setPosition(toStage == STAGE_TYPE_MAIN - ? mMainStage.mRootLeash : mSideStage.mRootLeash, 0, 0); + if (enableFlexibleSplit()) { + StageTaskListener stageToKeep = mStageOrderOperator.getAllStages().stream() + .filter(stage -> stage.getId() == toStage) + .findFirst().orElseThrow(); + List<StageTaskListener> stagesToHide = mStageOrderOperator.getAllStages().stream() + .filter(stage -> stage.getId() != toStage) + .toList(); + stagesToHide.forEach(stage -> t.hide(stage.mRootLeash)); + t.setPosition(stageToKeep.mRootLeash, 0, 0); + } else { + t.hide(toStage == STAGE_TYPE_MAIN ? mSideStage.mRootLeash : mMainStage.mRootLeash); + t.setPosition(toStage == STAGE_TYPE_MAIN + ? mMainStage.mRootLeash : mSideStage.mRootLeash, 0, 0); + } } else { for (int i = dismissingTasks.keySet().size() - 1; i >= 0; --i) { finishT.hide(dismissingTasks.valueAt(i)); @@ -3088,8 +3565,12 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // Hide divider and dim layer on transition finished. setDividerVisibility(false, t); - finishT.hide(mMainStage.mDimLayer); - finishT.hide(mSideStage.mDimLayer); + if (enableFlexibleSplit()) { + runForActiveStages(stage -> finishT.hide(stage.mRootLeash)); + } else { + finishT.hide(mMainStage.mDimLayer); + finishT.hide(mSideStage.mDimLayer); + } } private boolean startPendingDismissAnimation( @@ -3110,8 +3591,12 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, return false; } dismissTransition.setFinishedCallback((callbackWct, callbackT) -> { - mMainStage.getSplitDecorManager().release(callbackT); - mSideStage.getSplitDecorManager().release(callbackT); + if (enableFlexibleSplit()) { + runForActiveStages(stage -> stage.getSplitDecorManager().release(callbackT)); + } else { + mMainStage.getSplitDecorManager().release(callbackT); + mSideStage.getSplitDecorManager().release(callbackT); + } callbackWct.setReparentLeafTaskIfRelaunch(mRootTaskInfo.token, false); }); return true; @@ -3128,8 +3613,15 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, if (TransitionUtil.isClosingType(change.getMode()) && change.getTaskInfo() != null) { final int taskId = change.getTaskInfo().taskId; - if (mMainStage.getTopVisibleChildTaskId() == taskId - || mSideStage.getTopVisibleChildTaskId() == taskId) { + boolean anyStagesHaveTask; + if (enableFlexibleSplit()) { + anyStagesHaveTask = mStageOrderOperator.getActiveStages().stream() + .anyMatch(stage -> stage.getTopVisibleChildTaskId() == taskId); + } else { + anyStagesHaveTask = mMainStage.getTopVisibleChildTaskId() == taskId + || mSideStage.getTopVisibleChildTaskId() == taskId; + } + if (anyStagesHaveTask) { mPausingTasks.add(taskId); } } @@ -3161,9 +3653,16 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, final WindowContainerTransaction.HierarchyOp op = finishWct.getHierarchyOps().get(i); final IBinder container = op.getContainer(); + boolean anyStageContainsContainer; + if (enableFlexibleSplit()) { + anyStageContainsContainer = mStageOrderOperator.getActiveStages().stream() + .anyMatch(stage -> stage.containsContainer(container)); + } else { + anyStageContainsContainer = mMainStage.containsContainer(container) + || mSideStage.containsContainer(container); + } if (op.getType() == HIERARCHY_OP_TYPE_REORDER && op.getToTop() - && (mMainStage.containsContainer(container) - || mSideStage.containsContainer(container))) { + && anyStageContainsContainer) { updateSurfaceBounds(mSplitLayout, finishT, false /* applyResizingOffset */); finishT.reparent(mSplitLayout.getDividerLeash(), mRootTaskLeash); @@ -3190,10 +3689,18 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // user entering recents. for (int i = mPausingTasks.size() - 1; i >= 0; --i) { final int taskId = mPausingTasks.get(i); - if (mMainStage.containsTask(taskId)) { - mMainStage.evictChild(finishWct, taskId, "recentsPairToPair"); - } else if (mSideStage.containsTask(taskId)) { - mSideStage.evictChild(finishWct, taskId, "recentsPairToPair"); + if (enableFlexibleSplit()) { + mStageOrderOperator.getActiveStages().stream() + .filter(stage -> stage.containsTask(taskId)) + .findFirst() + .ifPresent(stageToEvict -> + stageToEvict.evictChild(finishWct, taskId, "recentsPairToPair")); + } else { + if (mMainStage.containsTask(taskId)) { + mMainStage.evictChild(finishWct, taskId, "recentsPairToPair"); + } else if (mSideStage.containsTask(taskId)) { + mSideStage.evictChild(finishWct, taskId, "recentsPairToPair"); + } } } // If pending enter hasn't consumed, the mix handler will invoke start pending @@ -3256,8 +3763,15 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, */ private void setSplitsVisible(boolean visible) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "setSplitsVisible: visible=%b", visible); - mMainStage.mVisible = mSideStage.mVisible = visible; - mMainStage.mHasChildren = mSideStage.mHasChildren = visible; + if (enableFlexibleSplit()) { + runForActiveStages(stage -> { + stage.mVisible = visible; + stage.mHasChildren = visible; + }); + } else { + mMainStage.mVisible = mSideStage.mVisible = visible; + mMainStage.mHasChildren = mSideStage.mHasChildren = visible; + } } /** @@ -3300,6 +3814,10 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, * executed. */ private void logExitToStage(@ExitReason int exitReason, boolean toMainStage) { + if (enableFlexibleSplit()) { + // TODO(b/374825718) update logging for 2+ apps + return; + } mLogger.logExit(exitReason, toMainStage ? getMainStagePosition() : SPLIT_POSITION_UNDEFINED, toMainStage ? mMainStage.getTopChildTaskUid() : 0 /* mainStageUid */, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageOrderOperator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageOrderOperator.kt new file mode 100644 index 000000000000..b7b3c9b62a58 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageOrderOperator.kt @@ -0,0 +1,185 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.splitscreen + +import android.content.Context +import com.android.internal.protolog.ProtoLog +import com.android.launcher3.icons.IconProvider +import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.common.SyncTransactionQueue +import com.android.wm.shell.protolog.ShellProtoLogGroup +import com.android.wm.shell.shared.split.SplitScreenConstants +import com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_NONE +import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_INDEX_0 +import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_INDEX_1 +import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_INDEX_2 +import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT +import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT +import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED +import com.android.wm.shell.shared.split.SplitScreenConstants.SnapPosition +import com.android.wm.shell.shared.split.SplitScreenConstants.SplitIndex +import com.android.wm.shell.shared.split.SplitScreenConstants.SplitPosition +import com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_A +import com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_B +import com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_C +import com.android.wm.shell.splitscreen.SplitScreen.stageTypeToString +import com.android.wm.shell.windowdecor.WindowDecorViewModel +import java.util.Optional + +/** + * Responsible for creating [StageTaskListener]s and maintaining their ordering on screen. + * Must be notified whenever stages positions change via swapping or starting/ending tasks + */ +class StageOrderOperator ( + context: Context, + taskOrganizer: ShellTaskOrganizer, + displayId: Int, + stageCallbacks: StageTaskListener.StageListenerCallbacks, + syncQueue: SyncTransactionQueue, + iconProvider: IconProvider, + windowDecorViewModel: Optional<WindowDecorViewModel> + ) { + + private val MAX_STAGES = 3 + /** + * This somewhat acts as a replacement to stageTypes in the intermediary, so we want to start + * it after the @StageType constant values just to be safe and avoid potentially subtle bugs. + */ + private var stageIds = listOf(STAGE_TYPE_A, STAGE_TYPE_B, STAGE_TYPE_C) + + /** + * Active Stages, this list represent the current, ordered list of stages that are + * currently visible to the user. This map should be empty if the user is currently + * not in split screen. Note that this is different than if split screen is visible, which + * is determined by [StageListenerImpl.mVisible]. + * Split stages can be active and in the background + */ + val activeStages = mutableListOf<StageTaskListener>() + val allStages = mutableListOf<StageTaskListener>() + var isActive: Boolean = false + var isVisible: Boolean = false + @SnapPosition private var currentLayout: Int = SNAP_TO_NONE + + init { + for(i in 0 until MAX_STAGES) { + allStages.add(StageTaskListener(context, + taskOrganizer, + displayId, + stageCallbacks, + syncQueue, + iconProvider, + windowDecorViewModel, + stageIds[i]) + ) + } + } + + /** + * Updates internal state to keep record of "active" stages. Note that this does NOT call + * [StageTaskListener.activate] on the stages. + */ + fun onEnteringSplit(@SnapPosition goingToLayout: Int) { + if (goingToLayout == currentLayout) { + // Add protolog here. Return for now, but maybe we want to handle swap case, TBD + return + } + val freeStages: List<StageTaskListener> = + allStages.filterNot { activeStages.contains(it) } + when(goingToLayout) { + SplitScreenConstants.SNAP_TO_2_50_50 -> { + if (activeStages.size < 2) { + // take from allStages and add into activeStages + for (i in 0 until (2 - activeStages.size)) { + val stage = freeStages[i] + activeStages.add(stage) + } + } + } + } + ProtoLog.d( + ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, + "Activated stages: %d ids=%s", + activeStages.size, + activeStages.joinToString(",") { stageTypeToString(it.id) } + ) + isActive = true + } + + fun onExitingSplit() { + activeStages.clear() + isActive = false + } + + /** + * Given a legacy [SplitPosition] returns one of the stages from the actives stages. + * If there are no active stages and [checkAllStagesIfNotActive] is not true, then will return + * null + */ + fun getStageForLegacyPosition(@SplitPosition position: Int, + checkAllStagesIfNotActive : Boolean = false) : + StageTaskListener? { + if (activeStages.size != 2 && !checkAllStagesIfNotActive) { + return null + } + val listToCheck = if (activeStages.isEmpty() and checkAllStagesIfNotActive) + allStages else + activeStages + if (position == SPLIT_POSITION_TOP_OR_LEFT) { + return listToCheck[0] + } else if (position == SPLIT_POSITION_BOTTOM_OR_RIGHT) { + return listToCheck[1] + } else { + throw IllegalArgumentException("No stage for invalid position") + } + } + + /** + * Returns a legacy split position for the given stage. If no stages are active then this will + * return [SPLIT_POSITION_UNDEFINED] + */ + @SplitPosition + fun getLegacyPositionForStage(stage: StageTaskListener) : Int { + if (allStages[0] == stage) { + return SPLIT_POSITION_TOP_OR_LEFT + } else if (allStages[1] == stage) { + return SPLIT_POSITION_BOTTOM_OR_RIGHT + } else { + return SPLIT_POSITION_UNDEFINED + } + } + + /** + * Returns the stageId from a given splitIndex. This will default to checking from all stages if + * [isActive] is false, otherwise will only check active stages. + */ + fun getStageForIndex(@SplitIndex splitIndex: Int) : StageTaskListener { + // Probably should do a check for index to be w/in the bounds of the current split layout + // that we're currently in + val listToCheck = if (isActive) activeStages else allStages + if (splitIndex == SPLIT_INDEX_0) { + return listToCheck[0] + } else if (splitIndex == SPLIT_INDEX_1) { + return listToCheck[1] + } else if (splitIndex == SPLIT_INDEX_2) { + return listToCheck[2] + } else { + // Though I guess what if we're adding to the end? Maybe that indexing needs to be + // resolved elsewhere + throw IllegalStateException("No stage for the given splitIndex") + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java index 08cdfdb3b5a9..4a37169add36 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java @@ -22,10 +22,12 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.content.res.Configuration.SMALLEST_SCREEN_WIDTH_DP_UNDEFINED; import static android.view.RemoteAnimationTarget.MODE_OPENING; +import static com.android.wm.shell.Flags.enableFlexibleSplit; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN; import static com.android.wm.shell.shared.split.SplitScreenConstants.CONTROLLED_ACTIVITY_TYPES; import static com.android.wm.shell.shared.split.SplitScreenConstants.CONTROLLED_WINDOWING_MODES; import static com.android.wm.shell.shared.split.SplitScreenConstants.CONTROLLED_WINDOWING_MODES_WHEN_ACTIVE; +import static com.android.wm.shell.splitscreen.SplitScreen.stageTypeToString; import android.annotation.CallSuper; import android.annotation.Nullable; @@ -72,6 +74,8 @@ public class StageTaskListener implements ShellTaskOrganizer.TaskListener { // No current way to enforce this but if enableFlexibleSplit() is enabled, then only 1 of the // stages should have this be set/being used private boolean mIsActive; + /** Unique identifier for this state, > 0 */ + @StageType private final int mId; /** Callback interface for listening to changes in a split-screen stage. */ public interface StageListenerCallbacks { void onRootTaskAppeared(); @@ -110,13 +114,14 @@ public class StageTaskListener implements ShellTaskOrganizer.TaskListener { StageTaskListener(Context context, ShellTaskOrganizer taskOrganizer, int displayId, StageListenerCallbacks callbacks, SyncTransactionQueue syncQueue, IconProvider iconProvider, - Optional<WindowDecorViewModel> windowDecorViewModel) { + Optional<WindowDecorViewModel> windowDecorViewModel, int id) { mContext = context; mCallbacks = callbacks; mSyncQueue = syncQueue; mIconProvider = iconProvider; mWindowDecorViewModel = windowDecorViewModel; taskOrganizer.createRootTask(displayId, WINDOWING_MODE_MULTI_WINDOW, this); + mId = id; } int getChildCount() { @@ -161,6 +166,11 @@ public class StageTaskListener implements ShellTaskOrganizer.TaskListener { return contains(t -> t.isFocused); } + @StageType + int getId() { + return mId; + } + private boolean contains(Predicate<ActivityManager.RunningTaskInfo> predicate) { if (mRootTaskInfo != null && predicate.test(mRootTaskInfo)) { return true; @@ -197,10 +207,10 @@ public class StageTaskListener implements ShellTaskOrganizer.TaskListener { @CallSuper public void onTaskAppeared(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onTaskAppeared: taskId=%d taskParent=%d rootTask=%d " - + "taskActivity=%s", + + "stageId=%s taskActivity=%s", taskInfo.taskId, taskInfo.parentTaskId, mRootTaskInfo != null ? mRootTaskInfo.taskId : -1, - taskInfo.baseActivity); + stageTypeToString(mId), taskInfo.baseActivity); if (mRootTaskInfo == null) { mRootLeash = leash; mRootTaskInfo = taskInfo; @@ -230,8 +240,9 @@ public class StageTaskListener implements ShellTaskOrganizer.TaskListener { @Override @CallSuper public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { - ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onTaskInfoChanged: taskId=%d taskAct=%s", - taskInfo.taskId, taskInfo.baseActivity); + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onTaskInfoChanged: taskId=%d taskAct=%s " + + "stageId=%s", + taskInfo.taskId, taskInfo.baseActivity, stageTypeToString(mId)); mWindowDecorViewModel.ifPresent(viewModel -> viewModel.onTaskInfoChanged(taskInfo)); if (mRootTaskInfo.taskId == taskInfo.taskId) { mRootTaskInfo = taskInfo; @@ -261,7 +272,8 @@ public class StageTaskListener implements ShellTaskOrganizer.TaskListener { @Override @CallSuper public void onTaskVanished(ActivityManager.RunningTaskInfo taskInfo) { - ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onTaskVanished: task=%d", taskInfo.taskId); + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onTaskVanished: task=%d stageId=%s", + taskInfo.taskId, stageTypeToString(mId)); final int taskId = taskInfo.taskId; mWindowDecorViewModel.ifPresent(vm -> vm.onTaskVanished(taskInfo)); if (mRootTaskInfo.taskId == taskId) { @@ -466,14 +478,17 @@ public class StageTaskListener implements ShellTaskOrganizer.TaskListener { } void activate(WindowContainerTransaction wct, boolean includingTopTask) { - if (mIsActive) return; - ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "activate: includingTopTask=%b", - includingTopTask); + if (mIsActive && !enableFlexibleSplit()) return; + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "activate: includingTopTask=%b stage=%s", + includingTopTask, stageTypeToString(mId)); if (includingTopTask) { reparentTopTask(wct); } + if (enableFlexibleSplit()) { + return; + } mIsActive = true; } @@ -481,11 +496,14 @@ public class StageTaskListener implements ShellTaskOrganizer.TaskListener { deactivate(wct, false /* toTop */); } - void deactivate(WindowContainerTransaction wct, boolean toTop) { - if (!mIsActive) return; - ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "deactivate: toTop=%b rootTaskInfo=%s", - toTop, mRootTaskInfo); - mIsActive = false; + void deactivate(WindowContainerTransaction wct, boolean reparentTasksToTop) { + if (!mIsActive && !enableFlexibleSplit()) return; + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "deactivate: reparentTasksToTop=%b " + + "rootTaskInfo=%s stage=%s", + reparentTasksToTop, mRootTaskInfo, stageTypeToString(mId)); + if (!enableFlexibleSplit()) { + mIsActive = false; + } if (mRootTaskInfo == null) return; final WindowContainerToken rootToken = mRootTaskInfo.token; @@ -494,14 +512,15 @@ public class StageTaskListener implements ShellTaskOrganizer.TaskListener { null /* newParent */, null /* windowingModes */, null /* activityTypes */, - toTop); + reparentTasksToTop); } // -------- - // Previously only used in SideStage + // Previously only used in SideStage. With flexible split this is called for all stages boolean removeAllTasks(WindowContainerTransaction wct, boolean toTop) { - ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "remove all side stage tasks: childCount=%d toTop=%b", - mChildrenTaskInfo.size(), toTop); + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "remove all side stage tasks: childCount=%d toTop=%b " + + " stageI=%s", + mChildrenTaskInfo.size(), toTop, stageTypeToString(mId)); if (mChildrenTaskInfo.size() == 0) return false; wct.reparentTasks( mRootTaskInfo.token, @@ -522,6 +541,15 @@ public class StageTaskListener implements ShellTaskOrganizer.TaskListener { } @Override + public String toString() { + return "mId: " + stageTypeToString(mId) + + " mVisible: " + mVisible + + " mActive: " + mIsActive + + " mHasRootTask: " + mHasRootTask + + " childSize: " + mChildrenTaskInfo.size(); + } + + @Override @CallSuper public void dump(@NonNull PrintWriter pw, String prefix) { final String innerPrefix = prefix + " "; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java index 17483dd68632..92d1f9c26bbc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java @@ -370,7 +370,8 @@ public class DefaultMixedHandler implements MixedTransitionHandler, if (mRecentsHandler != null) { if (mSplitHandler.isSplitScreenVisible()) { return this::setRecentsTransitionDuringSplit; - } else if (mKeyguardHandler.isKeyguardShowing()) { + } else if (mKeyguardHandler.isKeyguardShowing() + && !mKeyguardHandler.isKeyguardAnimating()) { return this::setRecentsTransitionDuringKeyguard; } else if (mDesktopTasksController != null // Check on the default display. Recents/gesture nav is only available there diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java index fd4d568326d0..8cdbe26a2c76 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java @@ -117,6 +117,11 @@ class RecentsMixedTransition extends DefaultMixedHandler.MixedTransition { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Mixed transition for Recents during" + " Keyguard #%d", info.getDebugId()); + if (!mKeyguardHandler.isKeyguardShowing() || mKeyguardHandler.isKeyguardAnimating()) { + ProtoLog.w(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Cancel mixed transition because " + + "keyguard state was changed #%d", info.getDebugId()); + return false; + } if (mInfo == null) { mInfo = info; mFinishT = finishTransaction; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/SplitTaskUnfoldAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/SplitTaskUnfoldAnimator.java index d28287da83b6..32f3cd820421 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/SplitTaskUnfoldAnimator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/animation/SplitTaskUnfoldAnimator.java @@ -334,8 +334,8 @@ public class SplitTaskUnfoldAnimator implements UnfoldTaskAnimator, // Sides adjacent to split bar or task bar are not be animated. Insets margins; - final boolean isLandscape = mRootStageBounds.width() > mRootStageBounds.height(); - if (isLandscape) { // Left and right splits. + final boolean isLeftRightSplit = mSplitScreenController.get().get().isLeftRightSplit(); + if (isLeftRightSplit) { margins = getLandscapeMargins(margin, taskbarExpanded); } else { // Top and bottom splits. margins = getPortraitMargins(margin, taskbarExpanded); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt index 4bb1e7b6cc05..11a7cf8da8d8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt @@ -70,6 +70,7 @@ import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewHo import com.android.wm.shell.windowdecor.common.DecorThemeUtil import com.android.wm.shell.windowdecor.common.OPACITY_12 import com.android.wm.shell.windowdecor.common.OPACITY_40 +import com.android.wm.shell.windowdecor.common.OPACITY_60 import com.android.wm.shell.windowdecor.common.withAlpha import java.util.function.Supplier @@ -310,8 +311,6 @@ class MaximizeMenu( .desktop_mode_maximize_menu_immersive_button_fill_padding) private val maximizeFillPaddingDefault = context.resources.getDimensionPixelSize(R.dimen .desktop_mode_maximize_menu_snap_and_maximize_buttons_fill_padding) - private val maximizeFillPaddingBottom = context.resources.getDimensionPixelSize(R.dimen - .desktop_mode_maximize_menu_snap_and_maximize_buttons_fill_padding_bottom) private val maximizeRestoreFillPaddingVertical = context.resources.getDimensionPixelSize( R.dimen.desktop_mode_maximize_menu_restore_button_fill_vertical_padding) private val maximizeRestoreFillPaddingHorizontal = context.resources.getDimensionPixelSize( @@ -320,7 +319,7 @@ class MaximizeMenu( maximizeFillPaddingDefault, maximizeFillPaddingDefault, maximizeFillPaddingDefault, - maximizeFillPaddingBottom + maximizeFillPaddingDefault ) private val maximizeRestoreFillPaddingRect = Rect( maximizeRestoreFillPaddingHorizontal, @@ -684,7 +683,7 @@ class MaximizeMenu( inactiveSnapSideColor = colorScheme.outlineVariant.toArgb(), semiActiveSnapSideColor = colorScheme.primary.toArgb().withAlpha(OPACITY_40), activeSnapSideColor = colorScheme.primary.toArgb(), - inactiveStrokeColor = colorScheme.outlineVariant.toArgb(), + inactiveStrokeColor = colorScheme.outlineVariant.toArgb().withAlpha(OPACITY_60), activeStrokeColor = colorScheme.primary.toArgb(), inactiveBackgroundColor = menuBackgroundColor, activeBackgroundColor = colorScheme.primary.toArgb().withAlpha(OPACITY_12) @@ -753,7 +752,8 @@ class MaximizeMenu( val activeStrokeAndFill = colorScheme.primary.toArgb() val activeBackground = colorScheme.primary.toArgb().withAlpha(OPACITY_12) val activeDrawable = createMaximizeOrImmersiveButtonDrawable( - strokeAndFillColor = activeStrokeAndFill, + strokeColor = activeStrokeAndFill, + fillColor = activeStrokeAndFill, backgroundColor = activeBackground, // Add a mask with the menu background's color because the active background color is // semi transparent, otherwise the transparency will reveal the stroke/fill color @@ -770,7 +770,8 @@ class MaximizeMenu( addState( StateSet.WILD_CARD, createMaximizeOrImmersiveButtonDrawable( - strokeAndFillColor = colorScheme.outlineVariant.toArgb(), + strokeColor = colorScheme.outlineVariant.toArgb().withAlpha(OPACITY_60), + fillColor = colorScheme.outlineVariant.toArgb(), backgroundColor = colorScheme.surfaceContainerLow.toArgb(), backgroundMask = null, // not needed because the bg color is fully opaque fillPadding = fillPadding, @@ -780,7 +781,8 @@ class MaximizeMenu( } private fun createMaximizeOrImmersiveButtonDrawable( - @ColorInt strokeAndFillColor: Int, + @ColorInt strokeColor: Int, + @ColorInt fillColor: Int, @ColorInt backgroundColor: Int, @ColorInt backgroundMask: Int?, fillPadding: Rect, @@ -794,7 +796,7 @@ class MaximizeMenu( null /* inset */, null /* innerRadii */ ) - paint.color = strokeAndFillColor + paint.color = strokeColor paint.style = Paint.Style.FILL }) // Second layer, a mask for the next (background) layer if needed because of @@ -829,7 +831,7 @@ class MaximizeMenu( null /* inset */, null /* innerRadii */ ) - paint.color = strokeAndFillColor + paint.color = fillColor paint.style = Paint.Style.FILL }) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ThemeUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ThemeUtils.kt index f7cfbfa88485..c5057aa3cc18 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ThemeUtils.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/ThemeUtils.kt @@ -52,6 +52,7 @@ const val OPACITY_12 = 31 const val OPACITY_15 = 38 const val OPACITY_40 = 102 const val OPACITY_55 = 140 +const val OPACITY_60 = 153 const val OPACITY_65 = 166 /** diff --git a/libs/WindowManager/Shell/tests/OWNERS b/libs/WindowManager/Shell/tests/OWNERS index 65e50f86e8fe..19829e7e5677 100644 --- a/libs/WindowManager/Shell/tests/OWNERS +++ b/libs/WindowManager/Shell/tests/OWNERS @@ -6,6 +6,7 @@ pablogamito@google.com lbill@google.com madym@google.com hwwang@google.com +gabiyev@google.com chenghsiuchang@google.com atsjenk@google.com jorgegil@google.com diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/MinimizeAutoPipAppWindowTest.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/MinimizeAutoPipAppWindowTest.kt new file mode 100644 index 000000000000..48befc06b9c5 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/MinimizeAutoPipAppWindowTest.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.functional + +import android.platform.test.annotations.Postsubmit +import com.android.wm.shell.scenarios.MinimizeAutoPipAppWindow +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +/* Functional test for [MinimizeAutoPipAppWindow]. */ +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +class MinimizeAutoPipAppWindowTest : MinimizeAutoPipAppWindow() diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithDrag.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithDrag.kt index 0f546cdf97c5..8d04749d76a5 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithDrag.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithDrag.kt @@ -49,7 +49,8 @@ constructor( @Test open fun enterDesktopWithDrag() { - testApp.enterDesktopModeWithDrag(wmHelper, device) + // By default this method uses drag to desktop + testApp.enterDesktopMode(wmHelper, device) } @After diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MinimizeAutoPipAppWindow.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MinimizeAutoPipAppWindow.kt new file mode 100644 index 000000000000..d6c3266e915c --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MinimizeAutoPipAppWindow.kt @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.scenarios + +import android.app.Instrumentation +import android.tools.NavBar +import android.tools.Rotation +import android.tools.traces.parsers.WindowManagerStateHelper +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.helpers.DesktopModeAppHelper +import com.android.server.wm.flicker.helpers.PipAppHelper +import com.android.server.wm.flicker.helpers.SimpleAppHelper +import com.android.window.flags.Flags +import com.android.wm.shell.Utils +import org.junit.After +import org.junit.Assume +import org.junit.Before +import org.junit.Ignore +import org.junit.Rule +import org.junit.Test + +/** Base scenario test for minimizing the app entering pip on leave automatically */ +@Ignore("Test Base Class") +abstract class MinimizeAutoPipAppWindow { + private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + private val tapl = LauncherInstrumentation() + private val wmHelper = WindowManagerStateHelper(instrumentation) + private val device = UiDevice.getInstance(instrumentation) + private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation)) + private val pipApp = PipAppHelper(instrumentation) + private val pipAppDesktopMode = DesktopModeAppHelper(pipApp) + + @Rule + @JvmField + val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, Rotation.ROTATION_0) + + @Before + fun setup() { + Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) + Assume.assumeTrue(Flags.enableMinimizeButton()) + testApp.enterDesktopMode(wmHelper, device) + pipApp.launchViaIntent(wmHelper) + pipApp.enableAutoEnterForPipActivity() + } + + @Test + open fun minimizePipAppWindow() { + pipAppDesktopMode.minimizeDesktopApp(wmHelper, device, isPip = true) + } + + @After + fun teardown() { + pipApp.exit(wmHelper) + testApp.exit(wmHelper) + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MinimizeWindowOnAppOpen.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MinimizeWindowOnAppOpen.kt index 7987f7ec59fa..a246326f1137 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MinimizeWindowOnAppOpen.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MinimizeWindowOnAppOpen.kt @@ -23,12 +23,10 @@ import androidx.test.platform.app.InstrumentationRegistry import androidx.test.uiautomator.UiDevice import com.android.launcher3.tapl.LauncherInstrumentation import com.android.server.wm.flicker.helpers.DesktopModeAppHelper -import com.android.server.wm.flicker.helpers.ImeAppHelper -import com.android.server.wm.flicker.helpers.LetterboxAppHelper import com.android.server.wm.flicker.helpers.MailAppHelper -import com.android.server.wm.flicker.helpers.NewTasksAppHelper import com.android.server.wm.flicker.helpers.SimpleAppHelper import com.android.window.flags.Flags +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import org.junit.After import org.junit.Assume import org.junit.Before @@ -51,32 +49,30 @@ open class MinimizeWindowOnAppOpen() private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation)) private val mailApp = DesktopModeAppHelper(MailAppHelper(instrumentation)) - private val newTasksApp = DesktopModeAppHelper(NewTasksAppHelper(instrumentation)) - private val imeApp = DesktopModeAppHelper(ImeAppHelper(instrumentation)) - private val letterboxAppHelper = DesktopModeAppHelper(LetterboxAppHelper(instrumentation)) + + private val maxNum = DesktopModeStatus.getMaxTaskLimit(instrumentation.context) @Before fun setup() { Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) + Assume.assumeTrue(maxNum > 0) testApp.enterDesktopMode(wmHelper, device) - mailApp.launchViaIntent(wmHelper) - newTasksApp.launchViaIntent(wmHelper) - imeApp.launchViaIntent(wmHelper) + // Launch new [maxNum-1] tasks, which ends up opening [maxNum] tasks in total. + for (i in 1..maxNum - 1) { + mailApp.launchViaIntent(wmHelper) + } } @Test open fun openAppToMinimizeWindow() { - // Launch a new app while 4 apps are already open on desktop. This should result in the - // first app we opened to be minimized. - letterboxAppHelper.launchViaIntent(wmHelper) + // Launch a new tasks, which ends up opening [maxNum]+1 tasks in total. This should + // result in the first app we opened to be minimized. + mailApp.launchViaIntent(wmHelper) } @After fun teardown() { testApp.exit(wmHelper) mailApp.exit(wmHelper) - newTasksApp.exit(wmHelper) - imeApp.exit(wmHelper) - letterboxAppHelper.exit(wmHelper) } } diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/Android.bp b/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/Android.bp deleted file mode 100644 index 85e6a8d1d865..000000000000 --- a/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/Android.bp +++ /dev/null @@ -1,38 +0,0 @@ -// -// Copyright (C) 2020 The Android Open Source Project -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT 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 { - // 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: "WMShellFlickerTestsMediaProjection", - defaults: ["WMShellFlickerTestsDefault"], - manifest: "AndroidManifest.xml", - test_config_template: "AndroidTestTemplate.xml", - srcs: ["src/**/*.kt"], - static_libs: [ - "WMShellFlickerTestsBase", - "WMShellScenariosMediaProjection", - "WMShellTestUtils", - ], - data: ["trace_config/*"], -} diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/AndroidManifest.xml b/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/AndroidManifest.xml deleted file mode 100644 index 74b0daf3a2aa..000000000000 --- a/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/AndroidManifest.xml +++ /dev/null @@ -1,85 +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. - --> - -<manifest xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:tools="http://schemas.android.com/tools" - package="com.android.wm.shell.flicker"> - - <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29"/> - <!-- Read and write traces from external storage --> - <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> - <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> - <!-- Allow the test to write directly to /sdcard/ --> - <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" /> - <!-- Write secure settings --> - <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" /> - <!-- Capture screen contents --> - <uses-permission android:name="android.permission.ACCESS_SURFACE_FLINGER" /> - <!-- Enable / Disable tracing !--> - <uses-permission android:name="android.permission.DUMP" /> - <!-- Run layers trace --> - <uses-permission android:name="android.permission.HARDWARE_TEST"/> - <!-- Capture screen recording --> - <uses-permission android:name="android.permission.CAPTURE_VIDEO_OUTPUT"/> - <!-- Workaround grant runtime permission exception from b/152733071 --> - <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS"/> - <uses-permission android:name="android.permission.READ_LOGS"/> - <!-- Force-stop test apps --> - <uses-permission android:name="android.permission.FORCE_STOP_PACKAGES"/> - <!-- Control test app's media session --> - <uses-permission android:name="android.permission.MEDIA_CONTENT_CONTROL"/> - <!-- ATM.removeRootTasksWithActivityTypes() --> - <uses-permission android:name="android.permission.MANAGE_ACTIVITY_TASKS" /> - <!-- Enable bubble notification--> - <uses-permission android:name="android.permission.STATUS_BAR_SERVICE" /> - <!-- Allow the test to connect to perfetto trace processor --> - <uses-permission android:name="android.permission.INTERNET"/> - <uses-permission android:name="android.permission.READ_COMPAT_CHANGE_CONFIG" /> - <uses-permission android:name="android.permission.MANAGE_MEDIA_PROJECTION" /> - - <!-- Allow the test to write directly to /sdcard/ and connect to trace processor --> - <application android:requestLegacyExternalStorage="true" - android:networkSecurityConfig="@xml/network_security_config" - android:largeHeap="true"> - <uses-library android:name="android.test.runner"/> - - <service android:name=".NotificationListener" - android:exported="true" - android:label="WMShellTestsNotificationListenerService" - android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"> - <intent-filter> - <action android:name="android.service.notification.NotificationListenerService" /> - </intent-filter> - </service> - - <service android:name="com.android.wm.shell.flicker.utils.MediaProjectionService" - android:foregroundServiceType="mediaProjection" - android:label="WMShellTestsMediaProjectionService" - android:enabled="true"> - </service> - - <!-- (b/197936012) Remove startup provider due to test timeout issue --> - <provider - android:name="androidx.startup.InitializationProvider" - android:authorities="${applicationId}.androidx-startup" - tools:node="remove" /> - </application> - - <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" - android:targetPackage="com.android.wm.shell.flicker" - android:label="WindowManager Shell Flicker Tests"> - </instrumentation> -</manifest> diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/AndroidTestTemplate.xml deleted file mode 100644 index 40dbbac32c7f..000000000000 --- a/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/AndroidTestTemplate.xml +++ /dev/null @@ -1,97 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ Copyright (C) 2023 The Android Open Source Project - ~ - ~ Licensed under the Apache License, Version 2.0 (the "License"); - ~ you may not use this file except in compliance with the License. - ~ You may obtain a copy of the License at - ~ - ~ http://www.apache.org/licenses/LICENSE-2.0 - ~ - ~ Unless required by applicable law or agreed to in writing, software - ~ distributed under the License is distributed on an "AS IS" BASIS, - ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - ~ See the License for the specific language governing permissions and - ~ limitations under the License. - --> -<configuration description="Runs WindowManager Shell Flicker Tests {MODULE}"> - <option name="test-tag" value="FlickerTests"/> - <!-- Needed for storing the perfetto trace files in the sdcard/test_results--> - <option name="isolated-storage" value="false"/> - - <target_preparer class="com.android.tradefed.targetprep.DeviceSetup"> - <!-- disable DeprecatedTargetSdk warning --> - <option name="run-command" value="setprop debug.wm.disable_deprecated_target_sdk_dialog 1"/> - <!-- keeps the screen on during tests --> - <option name="screen-always-on" value="on"/> - <!-- prevents the phone from restarting --> - <option name="force-skip-system-props" value="true"/> - <!-- set WM tracing verbose level to all --> - <option name="run-command" value="cmd window tracing level all"/> - <!-- set WM tracing to frame (avoid incomplete states) --> - <option name="run-command" value="cmd window tracing frame"/> - <!-- disable betterbug as it's log collection dialogues cause flakes in e2e tests --> - <option name="run-command" value="pm disable com.google.android.internal.betterbug"/> - <!-- ensure lock screen mode is swipe --> - <option name="run-command" value="locksettings set-disabled false"/> - <!-- restart launcher to activate TAPL --> - <option name="run-command" - value="setprop ro.test_harness 1 ; am force-stop com.google.android.apps.nexuslauncher"/> - <!-- Increase trace size: 20mb for WM and 80mb for SF --> - <option name="run-command" value="cmd window tracing size 20480"/> - <option name="run-command" value="su root service call SurfaceFlinger 1029 i32 81920"/> - </target_preparer> - <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer"> - <option name="test-user-token" value="%TEST_USER%"/> - <option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/> - <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> - <option name="run-command" value="settings put system show_touches 1"/> - <option name="run-command" value="settings put system pointer_location 1"/> - <option name="teardown-command" - value="settings delete secure show_ime_with_hard_keyboard"/> - <option name="teardown-command" value="settings delete system show_touches"/> - <option name="teardown-command" value="settings delete system pointer_location"/> - <option name="teardown-command" - value="cmd overlay enable com.android.internal.systemui.navbar.gestural"/> - </target_preparer> - <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> - <option name="cleanup-apks" value="true"/> - <option name="test-file-name" value="{MODULE}.apk"/> - <option name="test-file-name" value="FlickerTestApp.apk"/> - </target_preparer> - - <!-- Needed for pushing the trace config file --> - <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/> - <target_preparer class="com.android.tradefed.targetprep.PushFilePreparer"> - <option name="push-file" - key="trace_config.textproto" - value="/data/misc/perfetto-traces/trace_config.textproto" - /> - <!--Install the content provider automatically when we push some file in sdcard folder.--> - <!--Needed to avoid the installation during the test suite.--> - <option name="push-file" key="trace_config.textproto" value="/sdcard/sample.textproto"/> - </target_preparer> - <test class="com.android.tradefed.testtype.AndroidJUnitTest"> - <option name="package" value="{PACKAGE}"/> - <option name="shell-timeout" value="6600s"/> - <option name="test-timeout" value="6000s"/> - <option name="hidden-api-checks" value="false"/> - <option name="device-listeners" value="android.device.collectors.PerfettoListener"/> - <!-- PerfettoListener related arguments --> - <option name="instrumentation-arg" key="perfetto_config_text_proto" value="true"/> - <option name="instrumentation-arg" - key="perfetto_config_file" - value="trace_config.textproto" - /> - <option name="instrumentation-arg" key="per_run" value="true"/> - <option name="instrumentation-arg" key="perfetto_persist_pid_track" value="true"/> - </test> - <!-- Needed for pulling the collected trace config on to the host --> - <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector"> - <option name="pull-pattern-keys" value="perfetto_file_path"/> - <option name="directory-keys" - value="/data/user/0/com.android.wm.shell.flicker/files"/> - <option name="collect-on-run-ended-only" value="true"/> - <option name="clean-up" value="true"/> - </metrics_collector> -</configuration> diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/trace_config/trace_config.textproto b/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/trace_config/trace_config.textproto deleted file mode 100644 index 9f2e49755fec..000000000000 --- a/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/trace_config/trace_config.textproto +++ /dev/null @@ -1,71 +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. - -# proto-message: TraceConfig - -# Enable periodic flushing of the trace buffer into the output file. -write_into_file: true - -# Writes the userspace buffer into the file every 1s. -file_write_period_ms: 2500 - -# See b/126487238 - we need to guarantee ordering of events. -flush_period_ms: 30000 - -# The trace buffers needs to be big enough to hold |file_write_period_ms| of -# trace data. The trace buffer sizing depends on the number of trace categories -# enabled and the device activity. - -# RSS events -buffers: { - size_kb: 63488 - fill_policy: RING_BUFFER -} - -data_sources { - config { - name: "linux.process_stats" - target_buffer: 0 - # polled per-process memory counters and process/thread names. - # If you don't want the polled counters, remove the "process_stats_config" - # section, but keep the data source itself as it still provides on-demand - # thread/process naming for ftrace data below. - process_stats_config { - scan_all_processes_on_start: true - } - } -} - -data_sources: { - config { - name: "linux.ftrace" - ftrace_config { - ftrace_events: "ftrace/print" - ftrace_events: "task/task_newtask" - ftrace_events: "task/task_rename" - atrace_categories: "ss" - atrace_categories: "wm" - atrace_categories: "am" - atrace_categories: "aidl" - atrace_categories: "input" - atrace_categories: "binder_driver" - atrace_categories: "sched_process_exit" - atrace_apps: "com.android.server.wm.flicker.testapp" - atrace_apps: "com.android.systemui" - atrace_apps: "com.android.wm.shell.flicker.service" - atrace_apps: "com.google.android.apps.nexuslauncher" - } - } -} - diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/functional/StartAppMediaProjectionFromSplitScreenTest.kt b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/functional/StartAppMediaProjectionFromSplitScreenTest.kt new file mode 100644 index 000000000000..2b9772d9cbdd --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/functional/StartAppMediaProjectionFromSplitScreenTest.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.functional + +import android.platform.test.annotations.Postsubmit +import com.android.wm.shell.scenarios.StartAppMediaProjectionFromSplitScreen +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +/* Functional test for [StartAppMediaProjectionFromSplitScreen]. */ +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +class StartAppMediaProjectionFromSplitScreenTest() : StartAppMediaProjectionFromSplitScreen()
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/functional/StartAppMediaProjectionInSplitScreenTest.kt b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/functional/StartAppMediaProjectionInSplitScreenTest.kt new file mode 100644 index 000000000000..e92297b48166 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/functional/StartAppMediaProjectionInSplitScreenTest.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.functional + +import android.platform.test.annotations.Postsubmit +import com.android.wm.shell.scenarios.StartAppMediaProjectionInSplitScreen +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +/* Functional test for [StartAppMediaProjectionInSplitScreen]. */ +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +class StartAppMediaProjectionInSplitScreenTest() : StartAppMediaProjectionInSplitScreen()
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/functional/StartAppMediaProjectionTest.kt b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/functional/StartAppMediaProjectionTest.kt new file mode 100644 index 000000000000..3f8107592667 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/functional/StartAppMediaProjectionTest.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.functional + +import android.platform.test.annotations.Postsubmit +import com.android.wm.shell.scenarios.StartAppMediaProjection +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +/* Functional test for [StartAppMediaProjection]. */ +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +class StartAppMediaProjectionTest() : StartAppMediaProjection()
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/functional/StartAppMediaProjectionWithExtraIntentTest.kt b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/functional/StartAppMediaProjectionWithExtraIntentTest.kt new file mode 100644 index 000000000000..1975cc7f86d2 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/functional/StartAppMediaProjectionWithExtraIntentTest.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.functional + +import android.platform.test.annotations.Postsubmit +import com.android.wm.shell.scenarios.StartAppMediaProjectionWithExtraIntent +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +/* Functional test for [StartAppMediaProjectionWithExtraIntent]. */ +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +class StartAppMediaProjectionWithExtraIntentTest : StartAppMediaProjectionWithExtraIntent()
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/functional/StartRecentAppMediaProjectionFromSplitScreenTest.kt b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/functional/StartRecentAppMediaProjectionFromSplitScreenTest.kt new file mode 100644 index 000000000000..943033c1819c --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/functional/StartRecentAppMediaProjectionFromSplitScreenTest.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.functional + +import android.platform.test.annotations.Postsubmit +import com.android.wm.shell.scenarios.StartRecentAppMediaProjectionFromSplitScreen +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +/* Functional test for [StartRecentAppMediaProjectionFromSplitScreen]. */ +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +class StartRecentAppMediaProjectionFromSplitScreenTest() : StartRecentAppMediaProjectionFromSplitScreen()
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/functional/StartRecentAppMediaProjectionInSplitScreenTest.kt b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/functional/StartRecentAppMediaProjectionInSplitScreenTest.kt new file mode 100644 index 000000000000..6facfd5b0063 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/functional/StartRecentAppMediaProjectionInSplitScreenTest.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.functional + +import android.platform.test.annotations.Postsubmit +import com.android.wm.shell.scenarios.StartRecentAppMediaProjectionInSplitScreen +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +/* Functional test for [StartRecentAppMediaProjectionInSplitScreen]. */ +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +class StartRecentAppMediaProjectionInSplitScreenTest() : StartRecentAppMediaProjectionInSplitScreen()
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/functional/StartRecentAppMediaProjectionTest.kt b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/functional/StartRecentAppMediaProjectionTest.kt new file mode 100644 index 000000000000..bab09052715a --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/functional/StartRecentAppMediaProjectionTest.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.functional + +import android.platform.test.annotations.Postsubmit +import com.android.wm.shell.scenarios.StartRecentAppMediaProjection +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +/* Functional test for [StartRecentAppMediaProjection]. */ +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +class StartRecentAppMediaProjectionTest() : StartRecentAppMediaProjection()
\ No newline at end of file diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjection.kt b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjection.kt new file mode 100644 index 000000000000..fe2c57801f72 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjection.kt @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.scenarios + +import android.app.Instrumentation +import android.platform.test.annotations.Postsubmit +import android.tools.NavBar +import android.tools.Rotation +import android.tools.device.apphelpers.CalculatorAppHelper +import android.tools.traces.parsers.WindowManagerStateHelper +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.helpers.StartMediaProjectionAppHelper +import com.android.wm.shell.Utils +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +/** + * Test scenario which requests an a single-app MediaProjection session. + * + * This is for testing that the requested app is opened as expected upon selecting it from the app + * selector, so capture can proceed as expected. + */ +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +open class StartAppMediaProjection { + + val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + val tapl = LauncherInstrumentation() + val wmHelper = WindowManagerStateHelper(instrumentation) + val device = UiDevice.getInstance(instrumentation) + + private val initialRotation = Rotation.ROTATION_0 + private val targetApp = CalculatorAppHelper(instrumentation) + private val testApp = StartMediaProjectionAppHelper(instrumentation) + + @Rule + @JvmField + val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, initialRotation) + + @Before + fun setup() { + tapl.setEnableRotation(true) + tapl.setExpectedRotation(initialRotation.value) + testApp.launchViaIntent(wmHelper) + } + + @Test + open fun startMediaProjection() { + testApp.startSingleAppMediaProjection(wmHelper, targetApp) + } + + @After + fun teardown() { + testApp.exit(wmHelper) + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionFromSplitScreen.kt b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionFromSplitScreen.kt new file mode 100644 index 000000000000..3beece8c38b8 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionFromSplitScreen.kt @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.scenarios + +import android.app.Instrumentation +import android.platform.test.annotations.Postsubmit +import android.tools.NavBar +import android.tools.Rotation +import android.tools.device.apphelpers.CalculatorAppHelper +import android.tools.traces.parsers.WindowManagerStateHelper +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.helpers.SimpleAppHelper +import com.android.server.wm.flicker.helpers.StartMediaProjectionAppHelper +import com.android.wm.shell.Utils +import com.android.wm.shell.flicker.utils.SplitScreenUtils +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +/** + * Test scenario which requests an a single-app MediaProjection session, while the HOST app is in + * split screen + * + * This is for testing that the requested app is opened as expected upon selecting it from the app + * selector, so capture can proceed as expected. + */ +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +open class StartAppMediaProjectionFromSplitScreen { + + val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + val tapl = LauncherInstrumentation() + val wmHelper = WindowManagerStateHelper(instrumentation) + val device = UiDevice.getInstance(instrumentation) + + private val initialRotation = Rotation.ROTATION_0 + private val targetApp = CalculatorAppHelper(instrumentation) + private val simpleApp = SimpleAppHelper(instrumentation) + private val testApp = StartMediaProjectionAppHelper(instrumentation) + + @Rule + @JvmField + val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, initialRotation) + + @Before + fun setup() { + tapl.workspace.switchToOverview().dismissAllTasks() + + tapl.setEnableRotation(true) + tapl.setExpectedRotation(initialRotation.value) + SplitScreenUtils.enterSplit(wmHelper, tapl, device, simpleApp, testApp, initialRotation) + SplitScreenUtils.waitForSplitComplete(wmHelper, simpleApp, testApp) + } + + @Test + open fun startMediaProjection() { + testApp.startSingleAppMediaProjection(wmHelper, targetApp) + } + + @After + fun teardown() { + testApp.exit(wmHelper) + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionInSplitScreen.kt b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionInSplitScreen.kt new file mode 100644 index 000000000000..d3186ae88081 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionInSplitScreen.kt @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.scenarios + +import android.app.Instrumentation +import android.platform.test.annotations.Postsubmit +import android.tools.NavBar +import android.tools.Rotation +import android.tools.device.apphelpers.CalculatorAppHelper +import android.tools.traces.parsers.WindowManagerStateHelper +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.helpers.StartMediaProjectionAppHelper +import com.android.wm.shell.Utils +import com.android.wm.shell.flicker.utils.SplitScreenUtils +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +/** + * Test scenario which requests an a single-app MediaProjection session, while the TARGET app is in + * split screen (next to the host app) + * + * This is for testing that the split pair isn't broken, and capture still proceeds as expected + */ +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +open class StartAppMediaProjectionInSplitScreen { + + val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + val tapl = LauncherInstrumentation() + val wmHelper = WindowManagerStateHelper(instrumentation) + val device = UiDevice.getInstance(instrumentation) + + private val initialRotation = Rotation.ROTATION_0 + private val targetApp = CalculatorAppHelper(instrumentation) + private val testApp = StartMediaProjectionAppHelper(instrumentation) + + @Rule + @JvmField + val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, initialRotation) + + @Before + fun setup() { + tapl.workspace.switchToOverview().dismissAllTasks() + tapl.setEnableRotation(true) + tapl.setExpectedRotation(initialRotation.value) + SplitScreenUtils.enterSplit(wmHelper, tapl, device, targetApp, testApp, initialRotation) + } + + @Test + open fun startMediaProjection() { + testApp.startSingleAppMediaProjection(wmHelper, targetApp) + + wmHelper + .StateSyncBuilder() + .withAppTransitionIdle() + .withWindowSurfaceAppeared(targetApp) + .withWindowSurfaceAppeared(testApp) + .waitForAndVerify() + } + + @After + fun teardown() { + testApp.exit(wmHelper) + targetApp.exit(wmHelper) + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionWithExtraIntent.kt b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionWithExtraIntent.kt new file mode 100644 index 000000000000..0b2a1ca23cdb --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartAppMediaProjectionWithExtraIntent.kt @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.scenarios + +import android.app.Instrumentation +import android.platform.test.annotations.Postsubmit +import android.tools.NavBar +import android.tools.Rotation +import android.tools.device.apphelpers.CalculatorAppHelper +import android.tools.traces.parsers.WindowManagerStateHelper +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.helpers.StartMediaProjectionAppHelper +import com.android.wm.shell.Utils +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +/** + * Test scenario which requests an a single-app MediaProjection session but launches an intent to + * return to the home screen, before the intent for opening the requested app to capture. + * + * This is for testing that even if a different intent interrupts the process the launching the + * requested capture target, the MediaProjection process isn't interrupted and the device is still + * interactive. + */ +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +open class StartAppMediaProjectionWithExtraIntent { + + val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + val tapl = LauncherInstrumentation() + val wmHelper = WindowManagerStateHelper(instrumentation) + val device = UiDevice.getInstance(instrumentation) + + private val initialRotation = Rotation.ROTATION_0 + private val targetApp = CalculatorAppHelper(instrumentation) + private val testApp = StartMediaProjectionAppHelper(instrumentation) + + @Rule + @JvmField + val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, initialRotation) + + @Before + fun setup() { + tapl.setEnableRotation(true) + testApp.launchViaIntent(wmHelper) + } + + @Test + open fun startMediaProjection() { + testApp.startSingleAppMediaProjectionWithExtraIntent(wmHelper, targetApp) + + // Check we can still interact with device after + tapl.workspace.switchToAllApps().getAppIcon(targetApp.appName).launch(targetApp.packageName) + + wmHelper + .StateSyncBuilder() + .withAppTransitionIdle() + .withWindowSurfaceAppeared(targetApp) + .waitForAndVerify() + } + + @After + fun teardown() { + testApp.exit(wmHelper) + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartRecentAppMediaProjection.kt b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartRecentAppMediaProjection.kt new file mode 100644 index 000000000000..30e0e4aa490a --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartRecentAppMediaProjection.kt @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.scenarios + +import android.app.Instrumentation +import android.platform.test.annotations.Postsubmit +import android.tools.NavBar +import android.tools.Rotation +import android.tools.device.apphelpers.CalculatorAppHelper +import android.tools.traces.parsers.WindowManagerStateHelper +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.helpers.StartMediaProjectionAppHelper +import com.android.wm.shell.Utils +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +/** + * Test scenario which requests an a single-app MediaProjection session from recents. + * + * This is for testing that the app is started from recents and capture proceeds as expected. + */ +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +open class StartRecentAppMediaProjection { + + val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + val tapl = LauncherInstrumentation() + val wmHelper = WindowManagerStateHelper(instrumentation) + val device = UiDevice.getInstance(instrumentation) + + private val initialRotation = Rotation.ROTATION_0 + private val targetApp = CalculatorAppHelper(instrumentation) + private val testApp = StartMediaProjectionAppHelper(instrumentation) + + @Rule + @JvmField + val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, initialRotation) + + @Before + fun setup() { + tapl.setEnableRotation(true) + tapl.setExpectedRotation(initialRotation.value) + targetApp.open() + testApp.launchViaIntent(wmHelper) + } + + @Test + open fun startMediaProjection() { + testApp.startSingleAppMediaProjectionFromRecents(wmHelper, targetApp) + } + + @After + fun teardown() { + testApp.exit(wmHelper) + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartRecentAppMediaProjectionFromSplitScreen.kt b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartRecentAppMediaProjectionFromSplitScreen.kt new file mode 100644 index 000000000000..f1dcf1fab140 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartRecentAppMediaProjectionFromSplitScreen.kt @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.scenarios + +import android.app.Instrumentation +import android.platform.test.annotations.Postsubmit +import android.tools.NavBar +import android.tools.Rotation +import android.tools.device.apphelpers.CalculatorAppHelper +import android.tools.traces.parsers.WindowManagerStateHelper +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.helpers.SimpleAppHelper +import com.android.server.wm.flicker.helpers.StartMediaProjectionAppHelper +import com.android.wm.shell.Utils +import com.android.wm.shell.flicker.utils.SplitScreenUtils +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +/** + * Test scenario which requests an a single-app MediaProjection session from recents while the HOST + * app is in split screen. + * + * This is for testing that the split pair isn't broken, and capture still proceeds as expected + */ +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +open class StartRecentAppMediaProjectionFromSplitScreen { + + val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + val tapl = LauncherInstrumentation() + val wmHelper = WindowManagerStateHelper(instrumentation) + val device = UiDevice.getInstance(instrumentation) + + private val initialRotation = Rotation.ROTATION_0 + private val simpleApp = SimpleAppHelper(instrumentation) + private val targetApp = CalculatorAppHelper(instrumentation) + private val testApp = StartMediaProjectionAppHelper(instrumentation) + + @Rule + @JvmField + val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, initialRotation) + + @Before + fun setup() { + tapl.workspace.switchToOverview().dismissAllTasks() + tapl.setEnableRotation(true) + tapl.setExpectedRotation(initialRotation.value) + targetApp.open() + SplitScreenUtils.enterSplit(wmHelper, tapl, device, simpleApp, testApp, initialRotation) + } + + @Test + open fun startMediaProjection() { + // The app we want to open for PSS will be the second item in the carousel, + // because the first will be the app open in split screen alongside the MediaProjection app + testApp.startSingleAppMediaProjectionFromRecents(wmHelper, targetApp, recentTasksIndex = 1) + } + + @After + fun teardown() { + testApp.exit(wmHelper) + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartRecentAppMediaProjectionInSplitScreen.kt b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartRecentAppMediaProjectionInSplitScreen.kt new file mode 100644 index 000000000000..0a6992f9a152 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/mediaprojection/scenarios/src/com/android/wm/shell/scenarios/StartRecentAppMediaProjectionInSplitScreen.kt @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.scenarios + +import android.app.Instrumentation +import android.platform.test.annotations.Postsubmit +import android.tools.NavBar +import android.tools.Rotation +import android.tools.device.apphelpers.CalculatorAppHelper +import android.tools.traces.parsers.WindowManagerStateHelper +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.helpers.StartMediaProjectionAppHelper +import com.android.wm.shell.Utils +import com.android.wm.shell.flicker.utils.SplitScreenUtils +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +/** + * Test scenario which requests an a single-app MediaProjection session from recents while the + * TARGET app is in split screen (with host app). + * + * This is for testing that the split pair isn't broken, and capture still proceeds as expected + */ +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +open class StartRecentAppMediaProjectionInSplitScreen { + + val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + val tapl = LauncherInstrumentation() + val wmHelper = WindowManagerStateHelper(instrumentation) + val device = UiDevice.getInstance(instrumentation) + + private val initialRotation = Rotation.ROTATION_0 + private val targetApp = CalculatorAppHelper(instrumentation) + private val testApp = StartMediaProjectionAppHelper(instrumentation) + + @Rule + @JvmField + val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, initialRotation) + + @Before + fun setup() { + tapl.workspace.switchToOverview().dismissAllTasks() + tapl.setEnableRotation(true) + tapl.setExpectedRotation(initialRotation.value) + SplitScreenUtils.enterSplit(wmHelper, tapl, device, targetApp, testApp, initialRotation) + } + + @Test + open fun startMediaProjection() { + testApp.startSingleAppMediaProjectionFromRecents(wmHelper, targetApp) + + wmHelper + .StateSyncBuilder() + .withAppTransitionIdle() + .withWindowSurfaceAppeared(targetApp) + .withWindowSurfaceAppeared(testApp) + .waitForAndVerify() + } + + @After + fun teardown() { + testApp.exit(wmHelper) + targetApp.exit(wmHelper) + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/MediaProjectionUtils.kt b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/MediaProjectionUtils.kt index f9706969ff11..8c2bdad364fc 100644 --- a/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/MediaProjectionUtils.kt +++ b/libs/WindowManager/Shell/tests/flicker/src/com/android/wm/shell/flicker/utils/MediaProjectionUtils.kt @@ -17,7 +17,11 @@ package com.android.wm.shell.flicker.utils object MediaProjectionUtils { - const val REQUEST_CODE: Int = 99 + // Request code for the normal media projection request + const val REQUEST_CODE_NORMAL: Int = 11 + // Request code for the media projection request which will include an extra intent to open + // home screen before starting requested app + const val REQUEST_CODE_EXTRA_INTENT: Int = 12 const val MSG_START_FOREGROUND_DONE: Int = 1 const val MSG_SERVICE_DESTROYED: Int = 2 const val EXTRA_MESSENGER: String = "messenger" diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java index 6fa37885b724..ce640b5e5195 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java @@ -47,6 +47,7 @@ import android.view.WindowManager; import androidx.test.filters.SmallTest; +import com.android.internal.logging.testing.UiEventLoggerFake; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.bubbles.BubbleData.TimeSource; import com.android.wm.shell.common.ShellExecutor; @@ -102,6 +103,7 @@ public class BubbleDataTest extends ShellTestCase { private BubbleData mBubbleData; private TestableBubblePositioner mPositioner; + private UiEventLoggerFake mUiEventLogger; @Mock private TimeSource mTimeSource; @@ -112,8 +114,6 @@ public class BubbleDataTest extends ShellTestCase { @Mock private PendingIntent mDeleteIntent; @Mock - private BubbleLogger mBubbleLogger; - @Mock private BubbleEducationController mEducationController; @Mock private ShellExecutor mMainExecutor; @@ -196,10 +196,12 @@ public class BubbleDataTest extends ShellTestCase { mock(Icon.class), mMainExecutor, mBgExecutor); + mUiEventLogger = new UiEventLoggerFake(); + mPositioner = new TestableBubblePositioner(mContext, mContext.getSystemService(WindowManager.class)); - mBubbleData = new BubbleData(getContext(), mBubbleLogger, mPositioner, mEducationController, - mMainExecutor, mBgExecutor); + mBubbleData = new BubbleData(getContext(), new BubbleLogger(mUiEventLogger), mPositioner, + mEducationController, mMainExecutor, mBgExecutor); // Used by BubbleData to set lastAccessedTime when(mTimeSource.currentTimeMillis()).thenReturn(1000L); @@ -297,6 +299,82 @@ public class BubbleDataTest extends ShellTestCase { } @Test + public void testRemoveBubbleFromBubbleBar_notifCancelled_logEvent() { + mPositioner.setShowingInBubbleBar(true); + + sendUpdatedEntryAtTime(mEntryA1, 1000); + mBubbleData.setListener(mListener); + + mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_NOTIF_CANCEL); + assertThat(mUiEventLogger.numLogs()).isEqualTo(1); + assertThat(mUiEventLogger.eventId(0)).isEqualTo( + BubbleLogger.Event.BUBBLE_BAR_BUBBLE_REMOVED_CANCELED.getId()); + } + + @Test + public void testRemoveBubbleFromBubbleBar_taskFinished_logEvent() { + mPositioner.setShowingInBubbleBar(true); + + sendUpdatedEntryAtTime(mEntryA1, 1000); + mBubbleData.setListener(mListener); + + mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_TASK_FINISHED); + assertThat(mUiEventLogger.numLogs()).isEqualTo(1); + assertThat(mUiEventLogger.eventId(0)).isEqualTo( + BubbleLogger.Event.BUBBLE_BAR_BUBBLE_ACTIVITY_FINISH.getId()); + } + + @Test + public void testRemoveBubbleFromBubbleBar_notifBlocked_logEvent() { + mPositioner.setShowingInBubbleBar(true); + + sendUpdatedEntryAtTime(mEntryA1, 1000); + mBubbleData.setListener(mListener); + + mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_BLOCKED); + assertThat(mUiEventLogger.numLogs()).isEqualTo(1); + assertThat(mUiEventLogger.eventId(0)).isEqualTo( + BubbleLogger.Event.BUBBLE_BAR_BUBBLE_REMOVED_BLOCKED.getId()); + } + + @Test + public void testRemoveBubbleFromBubbleBar_noLongerBubble_logEvent() { + mPositioner.setShowingInBubbleBar(true); + + sendUpdatedEntryAtTime(mEntryA1, 1000); + mBubbleData.setListener(mListener); + + mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_NO_LONGER_BUBBLE); + assertThat(mUiEventLogger.numLogs()).isEqualTo(1); + assertThat(mUiEventLogger.eventId(0)).isEqualTo( + BubbleLogger.Event.BUBBLE_BAR_BUBBLE_REMOVED_BLOCKED.getId()); + } + + @Test + public void testRemoveBubbleFromBubbleBar_addToOverflow_logEvent() { + mPositioner.setShowingInBubbleBar(true); + + sendUpdatedEntryAtTime(mEntryA1, 1000); + mBubbleData.setListener(mListener); + + mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_AGED); + assertThat(mUiEventLogger.numLogs()).isEqualTo(1); + assertThat(mUiEventLogger.eventId(0)).isEqualTo( + BubbleLogger.Event.BUBBLE_BAR_OVERFLOW_ADD_AGED.getId()); + } + + @Test + public void testRemoveBubble_notifCancelled_noLog() { + mPositioner.setShowingInBubbleBar(false); + + sendUpdatedEntryAtTime(mEntryA1, 1000); + mBubbleData.setListener(mListener); + + mBubbleData.dismissBubbleWithKey(mEntryA1.getKey(), Bubbles.DISMISS_BLOCKED); + assertThat(mUiEventLogger.numLogs()).isEqualTo(0); + } + + @Test public void ifSuppress_hideFlyout() { // Setup mBubbleData.setListener(mListener); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index 936835c34c04..5df395754c7a 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -116,6 +116,7 @@ import com.android.wm.shell.recents.RecentsTransitionStateListener import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.UNKNOWN import com.android.wm.shell.shared.split.SplitScreenConstants +import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_INDEX_UNDEFINED import com.android.wm.shell.splitscreen.SplitScreenController import com.android.wm.shell.sysui.ShellCommandHandler import com.android.wm.shell.sysui.ShellController @@ -3158,7 +3159,7 @@ class DesktopTasksControllerTest : ShellTestCase() { runOpenNewWindow(task) verify(splitScreenController) .startIntent(any(), anyInt(), any(), any(), - optionsCaptor.capture(), anyOrNull(), eq(true) + optionsCaptor.capture(), anyOrNull(), eq(true), eq(SPLIT_INDEX_UNDEFINED) ) assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode) .isEqualTo(WINDOWING_MODE_MULTI_WINDOW) @@ -3174,7 +3175,7 @@ class DesktopTasksControllerTest : ShellTestCase() { verify(splitScreenController) .startIntent( any(), anyInt(), any(), any(), - optionsCaptor.capture(), anyOrNull(), eq(true) + optionsCaptor.capture(), anyOrNull(), eq(true), eq(SPLIT_INDEX_UNDEFINED) ) assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode) .isEqualTo(WINDOWING_MODE_MULTI_WINDOW) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/SplitDragPolicyTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/SplitDragPolicyTest.java index 2cfce6933e1b..0cf15baf30b0 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/SplitDragPolicyTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/SplitDragPolicyTest.java @@ -24,6 +24,7 @@ import static android.content.ClipDescription.MIMETYPE_APPLICATION_SHORTCUT; import static android.content.ClipDescription.MIMETYPE_APPLICATION_TASK; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_INDEX_UNDEFINED; import static com.android.wm.shell.draganddrop.DragTestUtils.createAppClipData; import static com.android.wm.shell.draganddrop.DragTestUtils.createIntentClipData; import static com.android.wm.shell.draganddrop.DragTestUtils.createTaskInfo; @@ -226,7 +227,7 @@ public class SplitDragPolicyTest extends ShellTestCase { mPolicy.onDropped(filterTargetByType(targets, TYPE_FULLSCREEN), null /* hideTaskToken */); verify(mFullscreenStarter).startIntent(any(), anyInt(), any(), - eq(SPLIT_POSITION_UNDEFINED), any(), any()); + eq(SPLIT_POSITION_UNDEFINED), any(), any(), eq(SPLIT_INDEX_UNDEFINED)); } private void dragOverFullscreenApp_expectSplitScreenTargets(ClipData data) { @@ -241,12 +242,12 @@ public class SplitDragPolicyTest extends ShellTestCase { mPolicy.onDropped(filterTargetByType(targets, TYPE_SPLIT_LEFT), null /* hideTaskToken */); verify(mSplitScreenStarter).startIntent(any(), anyInt(), any(), - eq(SPLIT_POSITION_TOP_OR_LEFT), any(), any()); + eq(SPLIT_POSITION_TOP_OR_LEFT), any(), any(), eq(SPLIT_INDEX_UNDEFINED)); reset(mSplitScreenStarter); mPolicy.onDropped(filterTargetByType(targets, TYPE_SPLIT_RIGHT), null /* hideTaskToken */); verify(mSplitScreenStarter).startIntent(any(), anyInt(), any(), - eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), any(), any()); + eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), any(), any(), eq(SPLIT_INDEX_UNDEFINED)); } private void dragOverFullscreenAppPhone_expectVerticalSplitScreenTargets(ClipData data) { @@ -261,13 +262,13 @@ public class SplitDragPolicyTest extends ShellTestCase { mPolicy.onDropped(filterTargetByType(targets, TYPE_SPLIT_TOP), null /* hideTaskToken */); verify(mSplitScreenStarter).startIntent(any(), anyInt(), any(), - eq(SPLIT_POSITION_TOP_OR_LEFT), any(), any()); + eq(SPLIT_POSITION_TOP_OR_LEFT), any(), any(), eq(SPLIT_INDEX_UNDEFINED)); reset(mSplitScreenStarter); mPolicy.onDropped(filterTargetByType(targets, TYPE_SPLIT_BOTTOM), null /* hideTaskToken */); verify(mSplitScreenStarter).startIntent(any(), anyInt(), any(), - eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), any(), any()); + eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), any(), any(), eq(SPLIT_INDEX_UNDEFINED)); } @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java index 5f58265b45f5..289fd2d838fd 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java @@ -36,6 +36,8 @@ import android.content.ComponentName; import android.content.pm.ActivityInfo; import android.graphics.Rect; import android.os.RemoteException; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.util.Rational; @@ -46,6 +48,7 @@ import android.window.WindowContainerToken; import androidx.test.filters.SmallTest; +import com.android.wm.shell.Flags; import com.android.wm.shell.MockSurfaceControlHelper; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; @@ -67,6 +70,8 @@ import com.android.wm.shell.pip.phone.PhonePipMenuController; import com.android.wm.shell.splitscreen.SplitScreenController; import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentMatchers; @@ -81,7 +86,13 @@ import java.util.Optional; @SmallTest @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper +@DisableFlags(Flags.FLAG_ENABLE_PIP2) public class PipTaskOrganizerTest extends ShellTestCase { + @ClassRule + public static final SetFlagsRule.ClassRule mClassRule = new SetFlagsRule.ClassRule(); + @Rule + public final SetFlagsRule mSetFlagsRule = mClassRule.createSetFlagsRule(); + private PipTaskOrganizer mPipTaskOrganizer; @Mock private DisplayController mMockDisplayController; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java index 96003515a485..b123f4dfac9e 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java @@ -42,11 +42,14 @@ import android.graphics.Rect; import android.os.Bundle; import android.os.Handler; import android.os.RemoteException; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import androidx.test.filters.SmallTest; +import com.android.wm.shell.Flags; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.WindowManagerShellWrapper; import com.android.wm.shell.common.DisplayController; @@ -74,6 +77,8 @@ import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -88,7 +93,11 @@ import java.util.Set; @SmallTest @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper +@DisableFlags(Flags.FLAG_ENABLE_PIP2) public class PipControllerTest extends ShellTestCase { + @ClassRule public static final SetFlagsRule.ClassRule mClassRule = new SetFlagsRule.ClassRule(); + @Rule public final SetFlagsRule mSetFlagsRule = mClassRule.createSetFlagsRule(); + private PipController mPipController; private ShellInit mShellInit; private ShellController mShellController; 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 966651f19711..72a7a3f5ec99 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 @@ -23,6 +23,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; import static android.content.Intent.FLAG_ACTIVITY_NO_USER_ACTION; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_INDEX_0; import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; @@ -200,10 +201,11 @@ public class SplitScreenControllerTests extends ShellTestCase { PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE); mSplitScreenController.startIntent(pendingIntent, mContext.getUserId(), null, - SPLIT_POSITION_TOP_OR_LEFT, null /* options */, null /* hideTaskToken */); + SPLIT_POSITION_TOP_OR_LEFT, null /* options */, null /* hideTaskToken */, + SPLIT_INDEX_0); verify(mStageCoordinator).startIntent(eq(pendingIntent), mIntentCaptor.capture(), - eq(SPLIT_POSITION_TOP_OR_LEFT), isNull(), isNull()); + eq(SPLIT_POSITION_TOP_OR_LEFT), isNull(), isNull(), eq(SPLIT_INDEX_0)); assertEquals(FLAG_ACTIVITY_NO_USER_ACTION, mIntentCaptor.getValue().getFlags() & FLAG_ACTIVITY_NO_USER_ACTION); } @@ -220,10 +222,11 @@ public class SplitScreenControllerTests extends ShellTestCase { doReturn(topRunningTask).when(mRecentTasks).getTopRunningTask(any()); mSplitScreenController.startIntent(pendingIntent, mContext.getUserId(), null, - SPLIT_POSITION_TOP_OR_LEFT, null /* options */, null /* hideTaskToken */); + SPLIT_POSITION_TOP_OR_LEFT, null /* options */, null /* hideTaskToken */, + SPLIT_INDEX_0); verify(mStageCoordinator).startIntent(eq(pendingIntent), mIntentCaptor.capture(), - eq(SPLIT_POSITION_TOP_OR_LEFT), isNull(), isNull()); + eq(SPLIT_POSITION_TOP_OR_LEFT), isNull(), isNull(), eq(SPLIT_INDEX_0)); assertEquals(FLAG_ACTIVITY_MULTIPLE_TASK, mIntentCaptor.getValue().getFlags() & FLAG_ACTIVITY_MULTIPLE_TASK); } @@ -243,10 +246,11 @@ public class SplitScreenControllerTests extends ShellTestCase { doReturn(sameTaskInfo).when(mRecentTasks).findTaskInBackground(any(), anyInt(), any()); mSplitScreenController.startIntent(pendingIntent, mContext.getUserId(), null, - SPLIT_POSITION_TOP_OR_LEFT, null /* options */, null /* hideTaskToken */); + SPLIT_POSITION_TOP_OR_LEFT, null /* options */, null /* hideTaskToken */, + SPLIT_INDEX_0); verify(mStageCoordinator).startTask(anyInt(), eq(SPLIT_POSITION_TOP_OR_LEFT), - isNull(), isNull()); + isNull(), isNull(), eq(SPLIT_INDEX_0)); verify(mMultiInstanceHelper, never()).supportsMultiInstanceSplit(any()); verify(mStageCoordinator, never()).switchSplitPosition(any()); } @@ -269,10 +273,11 @@ public class SplitScreenControllerTests extends ShellTestCase { .findTaskInBackground(any(), anyInt(), any()); mSplitScreenController.startIntent(pendingIntent, mContext.getUserId(), null, - SPLIT_POSITION_TOP_OR_LEFT, null /* options */, null /* hideTaskToken */); + SPLIT_POSITION_TOP_OR_LEFT, null /* options */, null /* hideTaskToken */, + SPLIT_INDEX_0); verify(mMultiInstanceHelper, never()).supportsMultiInstanceSplit(any()); verify(mStageCoordinator).startTask(anyInt(), eq(SPLIT_POSITION_TOP_OR_LEFT), - isNull(), isNull()); + isNull(), isNull(), eq(SPLIT_INDEX_0)); } @Test @@ -289,7 +294,8 @@ public class SplitScreenControllerTests extends ShellTestCase { SPLIT_POSITION_BOTTOM_OR_RIGHT); mSplitScreenController.startIntent(pendingIntent, mContext.getUserId(), null, - SPLIT_POSITION_TOP_OR_LEFT, null /* options */, null /* hideTaskToken */); + SPLIT_POSITION_TOP_OR_LEFT, null /* options */, null /* hideTaskToken */, + SPLIT_INDEX_0); verify(mStageCoordinator).switchSplitPosition(anyString()); } @@ -301,7 +307,7 @@ public class SplitScreenControllerTests extends ShellTestCase { PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE); mSplitScreenController.startIntent(pendingIntent, mContext.getUserId(), null, SPLIT_POSITION_TOP_OR_LEFT, null /* options */, null /* hideTaskToken */, - true /* forceLaunchNewTask */); + true /* forceLaunchNewTask */, SPLIT_INDEX_0); verify(mRecentTasks, never()).findTaskInBackground(any(), anyInt(), any()); } @@ -312,7 +318,7 @@ public class SplitScreenControllerTests extends ShellTestCase { PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE); mSplitScreenController.startIntent(pendingIntent, mContext.getUserId(), null, SPLIT_POSITION_TOP_OR_LEFT, null /* options */, null /* hideTaskToken */, - false /* forceLaunchNewTask */); + false /* forceLaunchNewTask */, SPLIT_INDEX_0); verify(mRecentTasks).findTaskInBackground(any(), anyInt(), any()); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java index 66dcef6f14cc..d13a8888edc7 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java @@ -53,8 +53,8 @@ public class SplitTestUtils { doReturn(dividerBounds).when(out).getDividerBounds(); doReturn(dividerBounds).when(out).getRefDividerBounds(); doReturn(leash).when(out).getDividerLeash(); - doReturn(bounds1).when(out).getBounds1(); - doReturn(bounds2).when(out).getBounds2(); + doReturn(bounds1).when(out).getTopLeftBounds(); + doReturn(bounds2).when(out).getBottomRightBounds(); return out; } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java index ce3944a5855e..e32cf3899a03 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java @@ -27,6 +27,7 @@ import static android.view.WindowManager.TRANSIT_TO_FRONT; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_CHILDREN_TASKS_REPARENT; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER; +import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_MAIN; import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_SIDE; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_APP_DOES_NOT_SUPPORT_MULTIWINDOW; import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_DRAG_DIVIDER; @@ -133,11 +134,11 @@ public class SplitTransitionTests extends ShellTestCase { mSplitLayout = SplitTestUtils.createMockSplitLayout(); mMainStage = spy(new StageTaskListener(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mock( StageTaskListener.StageListenerCallbacks.class), mSyncQueue, - mIconProvider, Optional.of(mWindowDecorViewModel))); + mIconProvider, Optional.of(mWindowDecorViewModel), STAGE_TYPE_MAIN)); mMainStage.onTaskAppeared(new TestRunningTaskInfoBuilder().build(), createMockSurface()); mSideStage = spy(new StageTaskListener(mContext, mTaskOrganizer, DEFAULT_DISPLAY, mock( StageTaskListener.StageListenerCallbacks.class), mSyncQueue, - mIconProvider, Optional.of(mWindowDecorViewModel))); + mIconProvider, Optional.of(mWindowDecorViewModel), STAGE_TYPE_SIDE)); mSideStage.onTaskAppeared(new TestRunningTaskInfoBuilder().build(), createMockSurface()); mStageCoordinator = new SplitTestUtils.TestStageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue, mTaskOrganizer, mMainStage, mSideStage, mDisplayController, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java index a252a9db7095..4f6f3c69aa3a 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java @@ -19,6 +19,7 @@ package com.android.wm.shell.splitscreen; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.view.Display.DEFAULT_DISPLAY; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_INDEX_UNDEFINED; import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_UNDEFINED; @@ -140,8 +141,8 @@ public class StageCoordinatorTests extends ShellTestCase { Optional.empty())); mDividerLeash = new SurfaceControl.Builder().setName("fakeDivider").build(); - when(mSplitLayout.getBounds1()).thenReturn(mBounds1); - when(mSplitLayout.getBounds2()).thenReturn(mBounds2); + when(mSplitLayout.getTopLeftBounds()).thenReturn(mBounds1); + when(mSplitLayout.getBottomRightBounds()).thenReturn(mBounds2); when(mSplitLayout.getRootBounds()).thenReturn(mRootBounds); when(mSplitLayout.isLeftRightSplit()).thenReturn(false); when(mSplitLayout.applyTaskChanges(any(), any(), any())).thenReturn(true); @@ -165,8 +166,9 @@ public class StageCoordinatorTests extends ShellTestCase { final WindowContainerTransaction wct = spy(new WindowContainerTransaction()); mStageCoordinator.moveToStage(task, SPLIT_POSITION_BOTTOM_OR_RIGHT, wct); + // TODO(b/349828130) Address this once we remove index_undefined called verify(mStageCoordinator).prepareEnterSplitScreen(eq(wct), eq(task), - eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), eq(false)); + eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), eq(false), eq(SPLIT_INDEX_UNDEFINED)); verify(mMainStage).reparentTopTask(eq(wct)); assertEquals(SPLIT_POSITION_BOTTOM_OR_RIGHT, mStageCoordinator.getSideStagePosition()); assertEquals(SPLIT_POSITION_TOP_OR_LEFT, mStageCoordinator.getMainStagePosition()); @@ -183,8 +185,9 @@ public class StageCoordinatorTests extends ShellTestCase { final WindowContainerTransaction wct = new WindowContainerTransaction(); mStageCoordinator.moveToStage(task, SPLIT_POSITION_BOTTOM_OR_RIGHT, wct); + // TODO(b/349828130) Address this once we remove index_undefined called verify(mStageCoordinator).prepareEnterSplitScreen(eq(wct), eq(task), - eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), eq(false)); + eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), eq(false), eq(SPLIT_INDEX_UNDEFINED)); assertEquals(SPLIT_POSITION_BOTTOM_OR_RIGHT, mStageCoordinator.getMainStagePosition()); assertEquals(SPLIT_POSITION_TOP_OR_LEFT, mStageCoordinator.getSideStagePosition()); } @@ -195,8 +198,9 @@ public class StageCoordinatorTests extends ShellTestCase { final WindowContainerTransaction wct = new WindowContainerTransaction(); mStageCoordinator.moveToStage(task, SPLIT_POSITION_BOTTOM_OR_RIGHT, wct); + // TODO(b/349828130) Address this once we remove index_undefined called verify(mStageCoordinator).prepareEnterSplitScreen(eq(wct), eq(task), - eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), eq(false)); + eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), eq(false), eq(SPLIT_INDEX_UNDEFINED)); assertEquals(SPLIT_POSITION_BOTTOM_OR_RIGHT, mStageCoordinator.getSideStagePosition()); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java index 7144a1e038f9..fe91440b106f 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageTaskListenerTests.java @@ -19,6 +19,8 @@ package com.android.wm.shell.splitscreen; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.view.Display.DEFAULT_DISPLAY; +import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED; + import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertFalse; @@ -93,7 +95,8 @@ public final class StageTaskListenerTests extends ShellTestCase { mCallbacks, mSyncQueue, mIconProvider, - Optional.of(mWindowDecorViewModel)); + Optional.of(mWindowDecorViewModel), + STAGE_TYPE_UNDEFINED); mRootTask = new TestRunningTaskInfoBuilder().build(); mRootTask.parentTaskId = INVALID_TASK_ID; mSurfaceControl = new SurfaceControl.Builder().setName("test").build(); diff --git a/libs/hwui/DeviceInfo.h b/libs/hwui/DeviceInfo.h index fb58a69747b3..b72e066e64ae 100644 --- a/libs/hwui/DeviceInfo.h +++ b/libs/hwui/DeviceInfo.h @@ -84,6 +84,7 @@ public: // this value is only valid after the GPU has been initialized and there is a valid graphics // context or if you are using the HWUI_NULL_GPU int maxTextureSize() const; + bool hasMaxTextureSize() const { return mMaxTextureSize > 0; } sk_sp<SkColorSpace> getWideColorSpace() const { return mWideColorSpace; } SkColorType getWideColorType() { static std::once_flag kFlag; diff --git a/libs/hwui/RenderNode.cpp b/libs/hwui/RenderNode.cpp index 2c23864317a4..4801bd1038a3 100644 --- a/libs/hwui/RenderNode.cpp +++ b/libs/hwui/RenderNode.cpp @@ -186,7 +186,7 @@ void RenderNode::pushLayerUpdate(TreeInfo& info) { // If we are not a layer OR we cannot be rendered (eg, view was detached) // we need to destroy any Layers we may have had previously if (CC_LIKELY(layerType != LayerType::RenderLayer) || CC_UNLIKELY(!isRenderable()) || - CC_UNLIKELY(properties().getWidth() == 0) || CC_UNLIKELY(properties().getHeight() == 0) || + CC_UNLIKELY(properties().getWidth() <= 0) || CC_UNLIKELY(properties().getHeight() <= 0) || CC_UNLIKELY(!properties().fitsOnLayer())) { if (CC_UNLIKELY(hasLayer())) { this->setLayerSurface(nullptr); diff --git a/libs/hwui/RenderProperties.h b/libs/hwui/RenderProperties.h index b1ad8b2eb1b9..4dc57004e401 100644 --- a/libs/hwui/RenderProperties.h +++ b/libs/hwui/RenderProperties.h @@ -545,7 +545,8 @@ public: bool fitsOnLayer() const { const DeviceInfo* deviceInfo = DeviceInfo::get(); return mPrimitiveFields.mWidth <= deviceInfo->maxTextureSize() && - mPrimitiveFields.mHeight <= deviceInfo->maxTextureSize(); + mPrimitiveFields.mHeight <= deviceInfo->maxTextureSize() && + mPrimitiveFields.mWidth > 0 && mPrimitiveFields.mHeight > 0; } bool promotedToLayer() const { diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp index 8ec04304a808..b36b8be10779 100644 --- a/libs/hwui/renderthread/CanvasContext.cpp +++ b/libs/hwui/renderthread/CanvasContext.cpp @@ -418,6 +418,11 @@ void CanvasContext::prepareTree(TreeInfo& info, int64_t* uiFrameInfo, int64_t sy RenderNode* target) { mRenderThread.removeFrameCallback(this); + // Make sure we have a valid device info + if (!DeviceInfo::get()->hasMaxTextureSize()) { + (void)mRenderThread.requireGrContext(); + } + // If the previous frame was dropped we don't need to hold onto it, so // just keep using the previous frame's structure instead const auto reason = wasSkipped(mCurrentFrameInfo); diff --git a/libs/hwui/tests/unit/RenderPropertiesTests.cpp b/libs/hwui/tests/unit/RenderPropertiesTests.cpp index 3e8e0576bf49..6ec042cf23b0 100644 --- a/libs/hwui/tests/unit/RenderPropertiesTests.cpp +++ b/libs/hwui/tests/unit/RenderPropertiesTests.cpp @@ -40,7 +40,11 @@ TEST(RenderProperties, layerValidity) { props.setLeftTopRightBottom(0, 0, maxTextureSize + 1, maxTextureSize + 1); ASSERT_FALSE(props.fitsOnLayer()); - // Too small, but still 'fits'. Not fitting is an error case, so don't report empty as such. + // Too small, we can't create a layer for a 0 width or height props.setLeftTopRightBottom(0, 0, 100, 0); - ASSERT_TRUE(props.fitsOnLayer()); + ASSERT_FALSE(props.fitsOnLayer()); + + // Can't create a negative-sized layer + props.setLeftTopRightBottom(0, 0, -100, 300); + ASSERT_FALSE(props.fitsOnLayer()); } diff --git a/media/java/android/media/AudioDevicePort.java b/media/java/android/media/AudioDevicePort.java index f5913c763b82..4b3962e6dd74 100644 --- a/media/java/android/media/AudioDevicePort.java +++ b/media/java/android/media/AudioDevicePort.java @@ -22,6 +22,7 @@ import android.os.Build; import com.android.aconfig.annotations.VisibleForTesting; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -68,14 +69,14 @@ public class AudioDevicePort extends AudioPort { return new AudioDevicePort( new AudioHandle(/* id= */ 0), /* name= */ "testAudioDevicePort", - /* profiles= */ null, + /* profiles= */ new ArrayList<>(), /* gains= */ null, /* type= */ AudioManager.DEVICE_OUT_SPEAKER, /* address= */ "testAddress", /* speakerLayoutChannelMask= */ speakerLayoutChannelMask, /* encapsulationModes= */ null, /* encapsulationMetadataTypes= */ null, - /* descriptors= */ null); + /* descriptors= */ new ArrayList<>()); } private final int mType; diff --git a/media/java/android/media/MediaCodecInfo.java b/media/java/android/media/MediaCodecInfo.java index 96edd63a9b12..782db358bf9f 100644 --- a/media/java/android/media/MediaCodecInfo.java +++ b/media/java/android/media/MediaCodecInfo.java @@ -1876,6 +1876,8 @@ public final class MediaCodecInfo { * Codecs with this security model is not included in * {@link MediaCodecList#REGULAR_CODECS}, but included in * {@link MediaCodecList#ALL_CODECS}. + * + * @hide */ @FlaggedApi(FLAG_IN_PROCESS_SW_AUDIO_CODEC) public static final int SECURITY_MODEL_TRUSTED_CONTENT_ONLY = 2; diff --git a/media/java/android/media/MediaFormat.java b/media/java/android/media/MediaFormat.java index bd65b2ecb76a..bc09aee9ac11 100644 --- a/media/java/android/media/MediaFormat.java +++ b/media/java/android/media/MediaFormat.java @@ -1748,6 +1748,7 @@ public final class MediaFormat { (1 << MediaCodecInfo.SECURITY_MODEL_MEMORY_SAFE); /** * Flag for {@link MediaCodecInfo#SECURITY_MODEL_TRUSTED_CONTENT_ONLY}. + * @hide */ @FlaggedApi(FLAG_IN_PROCESS_SW_AUDIO_CODEC) public static final int FLAG_SECURITY_MODEL_TRUSTED_CONTENT_ONLY = @@ -1759,8 +1760,7 @@ public final class MediaFormat { * The associated value is a flag of the following values: * {@link FLAG_SECURITY_MODEL_SANDBOXED}, * {@link FLAG_SECURITY_MODEL_MEMORY_SAFE}, - * {@link FLAG_SECURITY_MODEL_TRUSTED_CONTENT_ONLY}. The default value is - * {@link FLAG_SECURITY_MODEL_SANDBOXED}. + * The default value is {@link FLAG_SECURITY_MODEL_SANDBOXED}. * <p> * When passed to {@link MediaCodecList#findDecoderForFormat} or * {@link MediaCodecList#findEncoderForFormat}, MediaCodecList filters diff --git a/media/java/android/media/quality/IMediaQualityManager.aidl b/media/java/android/media/quality/IMediaQualityManager.aidl index aaedf21999f7..1c85c7b97a6e 100644 --- a/media/java/android/media/quality/IMediaQualityManager.aidl +++ b/media/java/android/media/quality/IMediaQualityManager.aidl @@ -21,6 +21,7 @@ import android.media.quality.IAmbientBacklightCallback; import android.media.quality.IPictureProfileCallback; import android.media.quality.ISoundProfileCallback; import android.media.quality.ParamCapability; +import android.media.quality.PictureProfileHandle; import android.media.quality.PictureProfile; import android.media.quality.SoundProfile; @@ -38,6 +39,7 @@ interface IMediaQualityManager { List<String> getPictureProfilePackageNames(); List<String> getPictureProfileAllowList(); void setPictureProfileAllowList(in List<String> packages); + PictureProfileHandle getPictureProfileHandle(in String id); SoundProfile createSoundProfile(in SoundProfile pp); void updateSoundProfile(in String id, in SoundProfile pp); diff --git a/media/java/android/media/quality/MediaQualityManager.java b/media/java/android/media/quality/MediaQualityManager.java index 28fe9b6c8112..43e884a8271e 100644 --- a/media/java/android/media/quality/MediaQualityManager.java +++ b/media/java/android/media/quality/MediaQualityManager.java @@ -271,6 +271,17 @@ public final class MediaQualityManager { } } + /** + * Gets picture profile handle by profile ID. + * @hide + */ + public PictureProfileHandle getPictureProfileHandle(String id) { + try { + return mService.getPictureProfileHandle(id); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } /** * Creates a picture profile and store it in the system. diff --git a/media/java/android/media/tv/tuner/frontend/FrontendStatus.java b/media/java/android/media/tv/tuner/frontend/FrontendStatus.java index 898a8bf02edb..4de71235df8c 100644 --- a/media/java/android/media/tv/tuner/frontend/FrontendStatus.java +++ b/media/java/android/media/tv/tuner/frontend/FrontendStatus.java @@ -63,7 +63,7 @@ public class FrontendStatus { FRONTEND_STATUS_TYPE_DVBT_CELL_IDS, FRONTEND_STATUS_TYPE_ATSC3_ALL_PLP_INFO, FRONTEND_STATUS_TYPE_IPTV_CONTENT_URL, FRONTEND_STATUS_TYPE_IPTV_PACKETS_LOST, FRONTEND_STATUS_TYPE_IPTV_PACKETS_RECEIVED, FRONTEND_STATUS_TYPE_IPTV_WORST_JITTER_MS, - FRONTEND_STATUS_TYPE_IPTV_AVERAGE_JITTER_MS, FRONTEND_STATUS_TYPE_STANDARD_EXT}) + FRONTEND_STATUS_TYPE_IPTV_AVERAGE_JITTER_MS, FRONTEND_STATUS_TYPE_STANDARD_EXTENSION}) @Retention(RetentionPolicy.SOURCE) public @interface FrontendStatusType {} @@ -317,7 +317,7 @@ public class FrontendStatus { * Standard extension. */ @FlaggedApi(Flags.FLAG_TUNER_W_APIS) - public static final int FRONTEND_STATUS_TYPE_STANDARD_EXT = + public static final int FRONTEND_STATUS_TYPE_STANDARD_EXTENSION = android.hardware.tv.tuner.FrontendStatusType.STANDARD_EXT; /** @hide */ @@ -567,7 +567,7 @@ public class FrontendStatus { private Long mIptvPacketsReceived; private Integer mIptvWorstJitterMs; private Integer mIptvAverageJitterMs; - private StandardExt mStandardExt; + private StandardExtension mStandardExtension; // Constructed and fields set by JNI code. private FrontendStatus() { @@ -1298,12 +1298,12 @@ public class FrontendStatus { */ @NonNull @FlaggedApi(Flags.FLAG_TUNER_W_APIS) - public StandardExt getStandardExt() { + public StandardExtension getStandardExtension() { TunerVersionChecker.checkHigherOrEqualVersionTo( - TunerVersionChecker.TUNER_VERSION_4_0, "StandardExt status"); - if (mStandardExt == null) { - throw new IllegalStateException("StandardExt status is empty"); + TunerVersionChecker.TUNER_VERSION_4_0, "StandardExtension status"); + if (mStandardExtension == null) { + throw new IllegalStateException("StandardExtension status is empty"); } - return mStandardExt; + return mStandardExtension; } } diff --git a/media/java/android/media/tv/tuner/frontend/StandardExt.java b/media/java/android/media/tv/tuner/frontend/StandardExtension.java index 490727278b46..8bff3dd99cdb 100644 --- a/media/java/android/media/tv/tuner/frontend/StandardExt.java +++ b/media/java/android/media/tv/tuner/frontend/StandardExtension.java @@ -29,16 +29,16 @@ import android.media.tv.flags.Flags; */ @SystemApi @FlaggedApi(Flags.FLAG_TUNER_W_APIS) -public final class StandardExt { - private final int mDvbsStandardExt; - private final int mDvbtStandardExt; +public final class StandardExtension { + private final int mDvbsStandardExtension; + private final int mDvbtStandardExtension; /** * Private constructor called by JNI only. */ - private StandardExt(int dvbsStandardExt, int dvbtStandardExt) { - mDvbsStandardExt = dvbsStandardExt; - mDvbtStandardExt = dvbtStandardExt; + private StandardExtension(int dvbsStandardExtension, int dvbtStandardExtension) { + mDvbsStandardExtension = dvbsStandardExtension; + mDvbtStandardExtension = dvbtStandardExtension; } /** @@ -50,11 +50,11 @@ public final class StandardExt { * @see android.media.tv.tuner.frontend.DvbsFrontendSettings */ @DvbsFrontendSettings.Standard - public int getDvbsStandardExt() { - if (mDvbsStandardExt == FrontendDvbsStandard.UNDEFINED) { + public int getDvbsStandardExtension() { + if (mDvbsStandardExtension == FrontendDvbsStandard.UNDEFINED) { throw new IllegalStateException("No DVB-S standard transition"); } - return mDvbsStandardExt; + return mDvbsStandardExtension; } /** @@ -66,10 +66,10 @@ public final class StandardExt { * @see android.media.tv.tuner.frontend.DvbtFrontendSettings */ @DvbtFrontendSettings.Standard - public int getDvbtStandardExt() { - if (mDvbtStandardExt == FrontendDvbtStandard.UNDEFINED) { + public int getDvbtStandardExtension() { + if (mDvbtStandardExtension == FrontendDvbtStandard.UNDEFINED) { throw new IllegalStateException("No DVB-T standard transition"); } - return mDvbtStandardExt; + return mDvbtStandardExtension; } } diff --git a/media/jni/android_media_tv_Tuner.cpp b/media/jni/android_media_tv_Tuner.cpp index 80ca4f239a26..2fe069af638a 100644 --- a/media/jni/android_media_tv_Tuner.cpp +++ b/media/jni/android_media_tv_Tuner.cpp @@ -2940,10 +2940,10 @@ jobject JTuner::getFrontendStatus(jintArray types) { break; } case FrontendStatus::Tag::standardExt: { - jfieldID field = env->GetFieldID(clazz, "mStandardExt", - "Landroid/media/tv/tuner/frontend/StandardExt;"); + jfieldID field = env->GetFieldID(clazz, "mStandardExtension", + "Landroid/media/tv/tuner/frontend/StandardExtension;"); ScopedLocalRef standardExtClazz(env, - env->FindClass("android/media/tv/tuner/frontend/StandardExt")); + env->FindClass("android/media/tv/tuner/frontend/StandardExtension")); jmethodID initStandardExt = env->GetMethodID(standardExtClazz.get(), "<init>", "(II)V"); diff --git a/native/android/libandroid.map.txt b/native/android/libandroid.map.txt index 2d1fbf9e7f66..6b41ddde5cc8 100644 --- a/native/android/libandroid.map.txt +++ b/native/android/libandroid.map.txt @@ -363,6 +363,7 @@ LIBANDROID { APerformanceHint_reportActualWorkDuration2; # introduced=VanillaIceCream APerformanceHint_notifyWorkloadIncrease; # introduced=36 APerformanceHint_notifyWorkloadReset; # introduced=36 + APerformanceHint_borrowSessionFromJava; # introduced=36 AWorkDuration_create; # introduced=VanillaIceCream AWorkDuration_release; # introduced=VanillaIceCream AWorkDuration_setWorkPeriodStartTimestampNanos; # introduced=VanillaIceCream @@ -383,6 +384,8 @@ LIBANDROID_PLATFORM { APerformanceHint_setUseFMQForTesting; APerformanceHint_getRateLimiterPropertiesForTesting; APerformanceHint_setUseNewLoadHintBehaviorForTesting; + APerformanceHint_closeSessionFromJava; + APerformanceHint_createSessionFromJava; extern "C++" { ASurfaceControl_registerSurfaceStatsListener*; ASurfaceControl_unregisterSurfaceStatsListener*; diff --git a/native/android/performance_hint.cpp b/native/android/performance_hint.cpp index e2fa94dd39bb..bc1945e37072 100644 --- a/native/android/performance_hint.cpp +++ b/native/android/performance_hint.cpp @@ -36,6 +36,7 @@ #include <cutils/trace.h> #include <fmq/AidlMessageQueue.h> #include <inttypes.h> +#include <jni_wrappers.h> #include <performance_hint_private.h> #include <utils/SystemClock.h> @@ -137,10 +138,14 @@ public: APerformanceHintSession* createSession(const int32_t* threadIds, size_t size, int64_t initialTargetWorkDurationNanos, - hal::SessionTag tag = hal::SessionTag::APP); + hal::SessionTag tag = hal::SessionTag::APP, + bool isJava = false); + APerformanceHintSession* getSessionFromJava(JNIEnv* _Nonnull env, jobject _Nonnull sessionObj); + int64_t getPreferredRateNanos() const; FMQWrapper& getFMQWrapper(); bool canSendLoadHints(std::vector<hal::SessionHint>& hints, int64_t now) REQUIRES(sHintMutex); + void initJava(JNIEnv* _Nonnull env); private: // Necessary to create an empty binder object @@ -161,13 +166,16 @@ private: FMQWrapper mFMQWrapper; double mHintBudget = kMaxLoadHintsPerInterval; int64_t mLastBudgetReplenish = 0; + bool mJavaInitialized = false; + jclass mJavaSessionClazz; + jfieldID mJavaSessionNativePtr; }; struct APerformanceHintSession { public: APerformanceHintSession(std::shared_ptr<IHintManager> hintManager, std::shared_ptr<IHintSession> session, int64_t preferredRateNanos, - int64_t targetDurationNanos, + int64_t targetDurationNanos, bool isJava, std::optional<hal::SessionConfig> sessionConfig); APerformanceHintSession() = delete; ~APerformanceHintSession(); @@ -181,6 +189,7 @@ public: int getThreadIds(int32_t* const threadIds, size_t* size); int setPreferPowerEfficiency(bool enabled); int reportActualWorkDuration(AWorkDuration* workDuration); + bool isJava(); private: friend struct APerformanceHintManager; @@ -203,6 +212,8 @@ private: std::vector<int64_t> mLastHintSentTimestamp GUARDED_BY(sHintMutex); // Cached samples std::vector<hal::WorkDuration> mActualWorkDurations GUARDED_BY(sHintMutex); + // Is this session backing an SDK wrapper object + const bool mIsJava; std::string mSessionName; static int64_t sIDCounter GUARDED_BY(sHintMutex); // The most recent set of thread IDs @@ -299,7 +310,7 @@ bool APerformanceHintManager::canSendLoadHints(std::vector<hal::SessionHint>& hi APerformanceHintSession* APerformanceHintManager::createSession( const int32_t* threadIds, size_t size, int64_t initialTargetWorkDurationNanos, - hal::SessionTag tag) { + hal::SessionTag tag, bool isJava) { std::vector<int32_t> tids(threadIds, threadIds + size); std::shared_ptr<IHintSession> session; ndk::ScopedAStatus ret; @@ -312,7 +323,7 @@ APerformanceHintSession* APerformanceHintManager::createSession( return nullptr; } auto out = new APerformanceHintSession(mHintManager, std::move(session), mPreferredRateNanos, - initialTargetWorkDurationNanos, + initialTargetWorkDurationNanos, isJava, sessionConfig.id == -1 ? std::nullopt : std::make_optional<hal::SessionConfig>( @@ -324,6 +335,18 @@ APerformanceHintSession* APerformanceHintManager::createSession( return out; } +APerformanceHintSession* APerformanceHintManager::getSessionFromJava(JNIEnv* env, + jobject sessionObj) { + initJava(env); + LOG_ALWAYS_FATAL_IF(!env->IsInstanceOf(sessionObj, mJavaSessionClazz), + "Wrong java type passed to APerformanceHint_getSessionFromJava"); + APerformanceHintSession* out = reinterpret_cast<APerformanceHintSession*>( + env->GetLongField(sessionObj, mJavaSessionNativePtr)); + LOG_ALWAYS_FATAL_IF(out == nullptr, "Java-wrapped native hint session is nullptr"); + LOG_ALWAYS_FATAL_IF(!out->isJava(), "Unmanaged native hint session returned from Java SDK"); + return out; +} + int64_t APerformanceHintManager::getPreferredRateNanos() const { return mPreferredRateNanos; } @@ -332,13 +355,23 @@ FMQWrapper& APerformanceHintManager::getFMQWrapper() { return mFMQWrapper; } +void APerformanceHintManager::initJava(JNIEnv* _Nonnull env) { + if (mJavaInitialized) { + return; + } + jclass sessionClazz = FindClassOrDie(env, "android/os/PerformanceHintManager$Session"); + mJavaSessionClazz = MakeGlobalRefOrDie(env, sessionClazz); + mJavaSessionNativePtr = GetFieldIDOrDie(env, mJavaSessionClazz, "mNativeSessionPtr", "J"); + mJavaInitialized = true; +} + // ===================================== APerformanceHintSession implementation constexpr int kNumEnums = enum_size<hal::SessionHint>(); APerformanceHintSession::APerformanceHintSession(std::shared_ptr<IHintManager> hintManager, std::shared_ptr<IHintSession> session, int64_t preferredRateNanos, - int64_t targetDurationNanos, + int64_t targetDurationNanos, bool isJava, std::optional<hal::SessionConfig> sessionConfig) : mHintManager(hintManager), mHintSession(std::move(session)), @@ -347,6 +380,7 @@ APerformanceHintSession::APerformanceHintSession(std::shared_ptr<IHintManager> h mFirstTargetMetTimestamp(0), mLastTargetMetTimestamp(0), mLastHintSentTimestamp(std::vector<int64_t>(kNumEnums, 0)), + mIsJava(isJava), mSessionConfig(sessionConfig) { if (sessionConfig->id > INT32_MAX) { ALOGE("Session ID too large, must fit 32-bit integer"); @@ -401,6 +435,10 @@ int APerformanceHintSession::reportActualWorkDuration(int64_t actualDurationNano return reportActualWorkDurationInternal(static_cast<AWorkDuration*>(&workDuration)); } +bool APerformanceHintSession::isJava() { + return mIsJava; +} + int APerformanceHintSession::sendHints(std::vector<hal::SessionHint>& hints, int64_t now, const char*) { std::scoped_lock lock(sHintMutex); @@ -826,6 +864,22 @@ APerformanceHintSession* APerformanceHint_createSessionInternal( static_cast<hal::SessionTag>(tag)); } +APerformanceHintSession* APerformanceHint_createSessionFromJava( + APerformanceHintManager* manager, const int32_t* threadIds, size_t size, + int64_t initialTargetWorkDurationNanos) { + VALIDATE_PTR(manager) + VALIDATE_PTR(threadIds) + return manager->createSession(threadIds, size, initialTargetWorkDurationNanos, + hal::SessionTag::APP, true); +} + +APerformanceHintSession* APerformanceHint_borrowSessionFromJava(JNIEnv* env, + jobject sessionObj) { + VALIDATE_PTR(env) + VALIDATE_PTR(sessionObj) + return APerformanceHintManager::getInstance()->getSessionFromJava(env, sessionObj); +} + int64_t APerformanceHint_getPreferredUpdateRateNanos(APerformanceHintManager* manager) { VALIDATE_PTR(manager) return manager->getPreferredRateNanos(); @@ -846,6 +900,16 @@ int APerformanceHint_reportActualWorkDuration(APerformanceHintSession* session, void APerformanceHint_closeSession(APerformanceHintSession* session) { VALIDATE_PTR(session) + if (session->isJava()) { + LOG_ALWAYS_FATAL("%s: Java-owned PerformanceHintSession cannot be closed in native", + __FUNCTION__); + return; + } + delete session; +} + +void APerformanceHint_closeSessionFromJava(APerformanceHintSession* session) { + VALIDATE_PTR(session) delete session; } diff --git a/nfc/api/current.txt b/nfc/api/current.txt index 008120429c40..9a7a39f213ee 100644 --- a/nfc/api/current.txt +++ b/nfc/api/current.txt @@ -81,11 +81,14 @@ package android.nfc { method @FlaggedApi("android.nfc.enable_nfc_reader_option") public boolean isReaderOptionSupported(); method public boolean isSecureNfcEnabled(); method public boolean isSecureNfcSupported(); + method @FlaggedApi("android.nfc.nfc_check_tag_intent_preference") public boolean isTagIntentAllowed(); + method @FlaggedApi("android.nfc.nfc_check_tag_intent_preference") public boolean isTagIntentAppPreferenceSupported(); method @FlaggedApi("android.nfc.enable_nfc_charging") public boolean isWlcEnabled(); method @FlaggedApi("android.nfc.enable_nfc_set_discovery_tech") public void resetDiscoveryTechnology(@NonNull android.app.Activity); method @FlaggedApi("android.nfc.enable_nfc_set_discovery_tech") public void setDiscoveryTechnology(@NonNull android.app.Activity, int, int); method @FlaggedApi("android.nfc.nfc_observe_mode") public boolean setObserveModeEnabled(boolean); field public static final String ACTION_ADAPTER_STATE_CHANGED = "android.nfc.action.ADAPTER_STATE_CHANGED"; + field @FlaggedApi("android.nfc.nfc_check_tag_intent_preference") public static final String ACTION_CHANGE_TAG_INTENT_PREFERENCE = "android.nfc.action.CHANGE_TAG_INTENT_PREFERENCE"; field public static final String ACTION_NDEF_DISCOVERED = "android.nfc.action.NDEF_DISCOVERED"; field @RequiresPermission(android.Manifest.permission.NFC_PREFERRED_PAYMENT_INFO) public static final String ACTION_PREFERRED_PAYMENT_CHANGED = "android.nfc.action.PREFERRED_PAYMENT_CHANGED"; field public static final String ACTION_TAG_DISCOVERED = "android.nfc.action.TAG_DISCOVERED"; diff --git a/nfc/api/system-current.txt b/nfc/api/system-current.txt index f587660cae5b..15814edcd86a 100644 --- a/nfc/api/system-current.txt +++ b/nfc/api/system-current.txt @@ -11,7 +11,6 @@ package android.nfc { method @NonNull @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public java.util.Map<java.lang.String,java.lang.Boolean> getTagIntentAppPreferenceForUser(int); method @RequiresPermission(android.Manifest.permission.NFC_SET_CONTROLLER_ALWAYS_ON) public boolean isControllerAlwaysOn(); method @RequiresPermission(android.Manifest.permission.NFC_SET_CONTROLLER_ALWAYS_ON) public boolean isControllerAlwaysOnSupported(); - method @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean isTagIntentAppPreferenceSupported(); method @RequiresPermission(android.Manifest.permission.NFC_SET_CONTROLLER_ALWAYS_ON) public void registerControllerAlwaysOnListener(@NonNull java.util.concurrent.Executor, @NonNull android.nfc.NfcAdapter.ControllerAlwaysOnListener); method @FlaggedApi("android.nfc.nfc_vendor_cmd") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void registerNfcVendorNciCallback(@NonNull java.util.concurrent.Executor, @NonNull android.nfc.NfcAdapter.NfcVendorNciCallback); method @FlaggedApi("android.nfc.enable_nfc_charging") public void registerWlcStateListener(@NonNull java.util.concurrent.Executor, @NonNull android.nfc.NfcAdapter.WlcStateListener); @@ -97,6 +96,7 @@ package android.nfc { method public void onDisableRequested(@NonNull java.util.function.Consumer<java.lang.Boolean>); method public void onDisableStarted(); method public void onEeListenActivated(boolean); + method public void onEeUpdated(); method public void onEnableFinished(int); method public void onEnableRequested(@NonNull java.util.function.Consumer<java.lang.Boolean>); method public void onEnableStarted(); diff --git a/nfc/java/android/nfc/INfcAdapter.aidl b/nfc/java/android/nfc/INfcAdapter.aidl index 31514a09adad..a08b55fe86b8 100644 --- a/nfc/java/android/nfc/INfcAdapter.aidl +++ b/nfc/java/android/nfc/INfcAdapter.aidl @@ -121,4 +121,5 @@ interface INfcAdapter List<Entry> getRoutingTableEntryList(); void indicateDataMigration(boolean inProgress, String pkg); int commitRouting(); + boolean isTagIntentAllowed(in String pkg, in int Userid); } diff --git a/nfc/java/android/nfc/INfcOemExtensionCallback.aidl b/nfc/java/android/nfc/INfcOemExtensionCallback.aidl index 1a21c0bae413..e5eac0b4d6fd 100644 --- a/nfc/java/android/nfc/INfcOemExtensionCallback.aidl +++ b/nfc/java/android/nfc/INfcOemExtensionCallback.aidl @@ -48,6 +48,7 @@ interface INfcOemExtensionCallback { void onRfFieldActivated(boolean isActivated); void onRfDiscoveryStarted(boolean isDiscoveryStarted); void onEeListenActivated(boolean isActivated); + void onEeUpdated(); void onGetOemAppSearchIntent(in List<String> firstPackage, in ResultReceiver intentConsumer); void onNdefMessage(in Tag tag, in NdefMessage message, in ResultReceiver hasOemExecutableContent); void onLaunchHceAppChooserActivity(in String selectedAid, in List<ApduServiceInfo> services, in ComponentName failedComponent, in String category); diff --git a/nfc/java/android/nfc/NfcAdapter.java b/nfc/java/android/nfc/NfcAdapter.java index c5d8191b22e6..056844f38f3c 100644 --- a/nfc/java/android/nfc/NfcAdapter.java +++ b/nfc/java/android/nfc/NfcAdapter.java @@ -49,6 +49,7 @@ import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.RemoteException; +import android.os.UserHandle; import android.util.Log; import java.io.IOException; @@ -2505,22 +2506,22 @@ public final class NfcAdapter { } /** - * Checks if the device supports Tag application preference. + * Checks if the device supports Tag Intent App Preference functionality. + * + * When supported, {@link #ACTION_NDEF_DISCOVERED}, {@link #ACTION_TECH_DISCOVERED} or + * {@link #ACTION_TAG_DISCOVERED} will not be dispatched to an Activity if + * {@link isTagIntentAllowed} returns {@code false}. * * @return {@code true} if the device supports Tag application preference, {@code false} * otherwise * @throws UnsupportedOperationException if FEATURE_NFC is unavailable - * - * @hide */ - @SystemApi - @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) + @FlaggedApi(Flags.FLAG_NFC_CHECK_TAG_INTENT_PREFERENCE) public boolean isTagIntentAppPreferenceSupported() { if (!sHasNfcFeature) { throw new UnsupportedOperationException(); } return callServiceReturn(() -> sService.isTagIntentAppPreferenceSupported(), false); - } /** @@ -2895,4 +2896,42 @@ public final class NfcAdapter { } return mNfcOemExtension; } + + /** + * Activity action: Bring up the settings page that allows the user to enable or disable tag + * intent reception for apps. + * + * <p>This will direct user to the settings page shows a list that asks users whether + * they want to allow or disallow the package to start an activity when a tag is discovered. + * + */ + @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION) + @FlaggedApi(Flags.FLAG_NFC_CHECK_TAG_INTENT_PREFERENCE) + public static final String ACTION_CHANGE_TAG_INTENT_PREFERENCE = + "android.nfc.action.CHANGE_TAG_INTENT_PREFERENCE"; + + /** + * Checks whether the user has disabled the calling app from receiving NFC tag intents. + * + * <p>This method checks whether the caller package name is either not present in the user + * disabled list or is explicitly allowed by the user. + * + * @return {@code true} if an app is either not present in the list or is added to the list + * with the flag set to {@code true}. Otherwise, it returns {@code false}. + * It also returns {@code true} if {@link isTagIntentAppPreferenceSupported} returns + * {@code false}. + * + * @throws UnsupportedOperationException if FEATURE_NFC is unavailable. + */ + @FlaggedApi(Flags.FLAG_NFC_CHECK_TAG_INTENT_PREFERENCE) + public boolean isTagIntentAllowed() { + if (!sHasNfcFeature) { + throw new UnsupportedOperationException(); + } + if (!isTagIntentAppPreferenceSupported()) { + return true; + } + return callServiceReturn(() -> sService.isTagIntentAllowed(mContext.getPackageName(), + UserHandle.myUserId()), false); + } } diff --git a/nfc/java/android/nfc/NfcOemExtension.java b/nfc/java/android/nfc/NfcOemExtension.java index 326ca6449c53..9ed678fe6014 100644 --- a/nfc/java/android/nfc/NfcOemExtension.java +++ b/nfc/java/android/nfc/NfcOemExtension.java @@ -367,6 +367,15 @@ public final class NfcOemExtension { void onEeListenActivated(boolean isActivated); /** + * Notifies that some NFCEE (NFC Execution Environment) has been updated. + * + * <p> This indicates that some applet has been installed/updated/removed in + * one of the NFCEE's. + * </p> + */ + void onEeUpdated(); + + /** * Gets the intent to find the OEM package in the OEM App market. If the consumer returns * {@code null} or a timeout occurs, the intent from the first available package will be * used instead. @@ -830,6 +839,12 @@ public final class NfcOemExtension { } @Override + public void onEeUpdated() throws RemoteException { + mCallbackMap.forEach((cb, ex) -> + handleVoidCallback(null, (Object input) -> cb.onEeUpdated(), ex)); + } + + @Override public void onStateUpdated(int state) throws RemoteException { mCallbackMap.forEach((cb, ex) -> handleVoidCallback(state, cb::onStateUpdated, ex)); diff --git a/nfc/java/android/nfc/flags.aconfig b/nfc/java/android/nfc/flags.aconfig index 8a37aa28cf9d..ee287aba709f 100644 --- a/nfc/java/android/nfc/flags.aconfig +++ b/nfc/java/android/nfc/flags.aconfig @@ -181,3 +181,11 @@ flag { description: "Enable set service enabled for category other" bug: "338157113" } + +flag { + name: "nfc_check_tag_intent_preference" + is_exported: true + namespace: "nfc" + description: "App can check its tag intent preference status" + bug: "335916336" +} diff --git a/packages/CrashRecovery/services/module/java/com/android/server/PackageWatchdog.java b/packages/CrashRecovery/services/module/java/com/android/server/PackageWatchdog.java index fbf51fd7cca7..8c15b093e48e 100644 --- a/packages/CrashRecovery/services/module/java/com/android/server/PackageWatchdog.java +++ b/packages/CrashRecovery/services/module/java/com/android/server/PackageWatchdog.java @@ -218,7 +218,7 @@ public class PackageWatchdog { @GuardedBy("sPackageWatchdogLock") private static PackageWatchdog sPackageWatchdog; - private final Object mLock = new Object(); + private static final Object sLock = new Object(); // System server context private final Context mContext; // Handler to run short running tasks @@ -228,7 +228,7 @@ public class PackageWatchdog { // Contains (observer-name -> observer-handle) that have ever been registered from // previous boots. Observers with all packages expired are periodically pruned. // It is saved to disk on system shutdown and repouplated on startup so it survives reboots. - @GuardedBy("mLock") + @GuardedBy("sLock") private final ArrayMap<String, ObserverInternal> mAllObservers = new ArrayMap<>(); // File containing the XML data of monitored packages /data/system/package-watchdog.xml private final AtomicFile mPolicyFile; @@ -244,26 +244,26 @@ public class PackageWatchdog { private final Set<String> mPackagesExemptFromImpactLevelThreshold = new ArraySet<>(); // The set of packages that have been synced with the ExplicitHealthCheckController - @GuardedBy("mLock") + @GuardedBy("sLock") private Set<String> mRequestedHealthCheckPackages = new ArraySet<>(); - @GuardedBy("mLock") + @GuardedBy("sLock") private boolean mIsPackagesReady; // Flag to control whether explicit health checks are supported or not - @GuardedBy("mLock") + @GuardedBy("sLock") private boolean mIsHealthCheckEnabled = DEFAULT_EXPLICIT_HEALTH_CHECK_ENABLED; - @GuardedBy("mLock") + @GuardedBy("sLock") private int mTriggerFailureDurationMs = DEFAULT_TRIGGER_FAILURE_DURATION_MS; - @GuardedBy("mLock") + @GuardedBy("sLock") private int mTriggerFailureCount = DEFAULT_TRIGGER_FAILURE_COUNT; // SystemClock#uptimeMillis when we last executed #syncState // 0 if no prune is scheduled. - @GuardedBy("mLock") + @GuardedBy("sLock") private long mUptimeAtLastStateSync; // If true, sync explicit health check packages with the ExplicitHealthCheckController. - @GuardedBy("mLock") + @GuardedBy("sLock") private boolean mSyncRequired = false; - @GuardedBy("mLock") + @GuardedBy("sLock") private long mLastMitigation = -1000000; @FunctionalInterface @@ -303,7 +303,11 @@ public class PackageWatchdog { sPackageWatchdog = this; } - /** Creates or gets singleton instance of PackageWatchdog. */ + /** + * Creates or gets singleton instance of PackageWatchdog. + * + * @param context The system server context. + */ public static @NonNull PackageWatchdog getInstance(@NonNull Context context) { synchronized (sPackageWatchdogLock) { if (sPackageWatchdog == null) { @@ -319,7 +323,7 @@ public class PackageWatchdog { * @hide */ public void onPackagesReady() { - synchronized (mLock) { + synchronized (sLock) { mIsPackagesReady = true; mHealthCheckController.setCallbacks(packageName -> onHealthCheckPassed(packageName), packages -> onSupportedPackages(packages), @@ -338,7 +342,7 @@ public class PackageWatchdog { * @hide */ public void registerHealthObserver(PackageHealthObserver observer) { - synchronized (mLock) { + synchronized (sLock) { ObserverInternal internalObserver = mAllObservers.get(observer.getUniqueIdentifier()); if (internalObserver != null) { internalObserver.registeredObserver = observer; @@ -405,7 +409,7 @@ public class PackageWatchdog { mLongTaskHandler.post(() -> { syncState("observing new packages"); - synchronized (mLock) { + synchronized (sLock) { ObserverInternal oldObserver = mAllObservers.get(observer.getUniqueIdentifier()); if (oldObserver == null) { Slog.d(TAG, observer.getUniqueIdentifier() + " started monitoring health " @@ -437,7 +441,7 @@ public class PackageWatchdog { */ public void unregisterHealthObserver(PackageHealthObserver observer) { mLongTaskHandler.post(() -> { - synchronized (mLock) { + synchronized (sLock) { mAllObservers.remove(observer.getUniqueIdentifier()); } syncState("unregistering observer: " + observer.getUniqueIdentifier()); @@ -458,7 +462,7 @@ public class PackageWatchdog { Slog.w(TAG, "Could not resolve a list of failing packages"); return; } - synchronized (mLock) { + synchronized (sLock) { final long now = mSystemClock.uptimeMillis(); if (Flags.recoverabilityDetection()) { if (now >= mLastMitigation @@ -469,7 +473,7 @@ public class PackageWatchdog { } } mLongTaskHandler.post(() -> { - synchronized (mLock) { + synchronized (sLock) { if (mAllObservers.isEmpty()) { return; } @@ -569,7 +573,7 @@ public class PackageWatchdog { int currentObserverImpact, int mitigationCount) { if (allowMitigations(currentObserverImpact, versionedPackage)) { - synchronized (mLock) { + synchronized (sLock) { mLastMitigation = mSystemClock.uptimeMillis(); } currentObserverToNotify.onExecuteHealthCheckMitigation(versionedPackage, failureReason, @@ -599,7 +603,7 @@ public class PackageWatchdog { */ @SuppressWarnings("GuardedBy") public void noteBoot() { - synchronized (mLock) { + synchronized (sLock) { // if boot count has reached threshold, start mitigation. // We wait until threshold number of restarts only for the first time. Perform // mitigations for every restart after that. @@ -652,7 +656,7 @@ public class PackageWatchdog { // This currently adds about 7ms extra to shutdown thread /** @hide Writes the package information to file during shutdown. */ public void writeNow() { - synchronized (mLock) { + synchronized (sLock) { // Must only run synchronous tasks as this runs on the ShutdownThread and no other // thread is guaranteed to run during shutdown. if (!mAllObservers.isEmpty()) { @@ -671,7 +675,7 @@ public class PackageWatchdog { * passed and the health check service is stopped. */ private void setExplicitHealthCheckEnabled(boolean enabled) { - synchronized (mLock) { + synchronized (sLock) { mIsHealthCheckEnabled = enabled; mHealthCheckController.setEnabled(enabled); mSyncRequired = true; @@ -841,7 +845,10 @@ public class PackageWatchdog { /** * Returns {@code true} if this observer wishes to observe the given package, {@code false} - * otherwise + * otherwise. + * Any failing package can be passed on to the observer. Currently the packages that have + * ANRs and perform {@link android.service.watchdog.ExplicitHealthCheckService} are being + * passed to observers in these API. * * <p> A persistent observer may choose to start observing certain failing packages, even if * it has not explicitly asked to watch the package with {@link #startObservingHealth}. @@ -853,14 +860,14 @@ public class PackageWatchdog { @VisibleForTesting long getTriggerFailureCount() { - synchronized (mLock) { + synchronized (sLock) { return mTriggerFailureCount; } } @VisibleForTesting long getTriggerFailureDurationMs() { - synchronized (mLock) { + synchronized (sLock) { return mTriggerFailureDurationMs; } } @@ -881,7 +888,7 @@ public class PackageWatchdog { */ private void syncRequests() { boolean syncRequired = false; - synchronized (mLock) { + synchronized (sLock) { if (mIsPackagesReady) { Set<String> packages = getPackagesPendingHealthChecksLocked(); if (mSyncRequired || !packages.equals(mRequestedHealthCheckPackages) @@ -918,7 +925,7 @@ public class PackageWatchdog { Slog.i(TAG, "Health check passed for package: " + packageName); boolean isStateChanged = false; - synchronized (mLock) { + synchronized (sLock) { for (int observerIdx = 0; observerIdx < mAllObservers.size(); observerIdx++) { ObserverInternal observer = mAllObservers.valueAt(observerIdx); MonitoredPackage monitoredPackage = observer.getMonitoredPackage(packageName); @@ -946,7 +953,7 @@ public class PackageWatchdog { supportedPackageTimeouts.put(info.getPackageName(), info.getHealthCheckTimeoutMillis()); } - synchronized (mLock) { + synchronized (sLock) { Slog.d(TAG, "Received supported packages " + supportedPackages); Iterator<ObserverInternal> oit = mAllObservers.values().iterator(); while (oit.hasNext()) { @@ -977,13 +984,13 @@ public class PackageWatchdog { } private void onSyncRequestNotified() { - synchronized (mLock) { + synchronized (sLock) { mSyncRequired = true; syncRequestsAsync(); } } - @GuardedBy("mLock") + @GuardedBy("sLock") private Set<String> getPackagesPendingHealthChecksLocked() { Set<String> packages = new ArraySet<>(); Iterator<ObserverInternal> oit = mAllObservers.values().iterator(); @@ -1009,7 +1016,7 @@ public class PackageWatchdog { * health check service and schedules the next state sync. */ private void syncState(String reason) { - synchronized (mLock) { + synchronized (sLock) { Slog.i(TAG, "Syncing state, reason: " + reason); pruneObserversLocked(); @@ -1025,7 +1032,7 @@ public class PackageWatchdog { syncState("scheduled"); } - @GuardedBy("mLock") + @GuardedBy("sLock") private void scheduleNextSyncStateLocked() { long durationMs = getNextStateSyncMillisLocked(); mShortTaskHandler.removeCallbacks(mSyncStateWithScheduledReason); @@ -1043,7 +1050,7 @@ public class PackageWatchdog { * * @returns Long#MAX_VALUE if there are no observed packages. */ - @GuardedBy("mLock") + @GuardedBy("sLock") private long getNextStateSyncMillisLocked() { long shortestDurationMs = Long.MAX_VALUE; for (int oIndex = 0; oIndex < mAllObservers.size(); oIndex++) { @@ -1064,7 +1071,7 @@ public class PackageWatchdog { * Removes {@code elapsedMs} milliseconds from all durations on monitored packages * and updates other internal state. */ - @GuardedBy("mLock") + @GuardedBy("sLock") private void pruneObserversLocked() { long elapsedMs = mUptimeAtLastStateSync == 0 ? 0 : mSystemClock.uptimeMillis() - mUptimeAtLastStateSync; @@ -1092,7 +1099,7 @@ public class PackageWatchdog { private void onHealthCheckFailed(ObserverInternal observer, Set<MonitoredPackage> failedPackages) { mLongTaskHandler.post(() -> { - synchronized (mLock) { + synchronized (sLock) { PackageHealthObserver registeredObserver = observer.registeredObserver; if (registeredObserver != null) { Iterator<MonitoredPackage> it = failedPackages.iterator(); @@ -1201,7 +1208,7 @@ public class PackageWatchdog { */ @VisibleForTesting void updateConfigs() { - synchronized (mLock) { + synchronized (sLock) { mTriggerFailureCount = DeviceConfig.getInt( DeviceConfig.NAMESPACE_ROLLBACK, PROPERTY_WATCHDOG_TRIGGER_FAILURE_COUNT, @@ -1230,7 +1237,7 @@ public class PackageWatchdog { */ private boolean saveToFile() { Slog.i(TAG, "Saving observer state to file"); - synchronized (mLock) { + synchronized (sLock) { FileOutputStream stream; try { stream = mPolicyFile.startWrite(); @@ -1297,20 +1304,38 @@ public class PackageWatchdog { /** Dump status of every observer in mAllObservers. */ public void dump(@NonNull PrintWriter pw) { - if (Flags.synchronousRebootInRescueParty() && RescueParty.isRecoveryTriggeredReboot()) { + if (Flags.synchronousRebootInRescueParty() && isRecoveryTriggeredReboot()) { dumpInternal(pw); } else { - synchronized (mLock) { + synchronized (sLock) { dumpInternal(pw); } } } + /** + * Check if we're currently attempting to reboot during mitigation. This method must return + * true if triggered reboot early during a boot loop, since the device will not be fully booted + * at this time. + * @hide + */ + public static boolean isRecoveryTriggeredReboot() { + return isFactoryResetPropertySet() || isRebootPropertySet(); + } + + private static boolean isFactoryResetPropertySet() { + return CrashRecoveryProperties.attemptingFactoryReset().orElse(false); + } + + private static boolean isRebootPropertySet() { + return CrashRecoveryProperties.attemptingReboot().orElse(false); + } + private void dumpInternal(@NonNull PrintWriter pw) { IndentingPrintWriter ipw = new IndentingPrintWriter(pw, " "); ipw.println("Package Watchdog status"); ipw.increaseIndent(); - synchronized (mLock) { + synchronized (sLock) { for (String observerName : mAllObservers.keySet()) { ipw.println("Observer name: " + observerName); ipw.increaseIndent(); @@ -1324,7 +1349,7 @@ public class PackageWatchdog { } @VisibleForTesting - @GuardedBy("mLock") + @GuardedBy("sLock") void registerObserverInternal(ObserverInternal observerInternal) { mAllObservers.put(observerInternal.name, observerInternal); } @@ -1333,15 +1358,15 @@ public class PackageWatchdog { * Represents an observer monitoring a set of packages along with the failure thresholds for * each package. * - * <p> Note, the PackageWatchdog#mLock must always be held when reading or writing + * <p> Note, the PackageWatchdog#sLock must always be held when reading or writing * instances of this class. */ static class ObserverInternal { public final String name; - @GuardedBy("mLock") + @GuardedBy("sLock") private final ArrayMap<String, MonitoredPackage> mPackages = new ArrayMap<>(); @Nullable - @GuardedBy("mLock") + @GuardedBy("sLock") public PackageHealthObserver registeredObserver; private int mMitigationCount; @@ -1359,7 +1384,7 @@ public class PackageWatchdog { * Writes important {@link MonitoredPackage} details for this observer to file. * Does not persist any package failure thresholds. */ - @GuardedBy("mLock") + @GuardedBy("sLock") public boolean writeLocked(XmlSerializer out) { try { out.startTag(null, TAG_OBSERVER); @@ -1387,7 +1412,7 @@ public class PackageWatchdog { mMitigationCount = mitigationCount; } - @GuardedBy("mLock") + @GuardedBy("sLock") public void updatePackagesLocked(List<MonitoredPackage> packages) { for (int pIndex = 0; pIndex < packages.size(); pIndex++) { MonitoredPackage p = packages.get(pIndex); @@ -1410,7 +1435,7 @@ public class PackageWatchdog { * health check passing, or an empty list if no package expired for which an explicit health * check was still pending */ - @GuardedBy("mLock") + @GuardedBy("sLock") private Set<MonitoredPackage> prunePackagesLocked(long elapsedMs) { Set<MonitoredPackage> failedPackages = new ArraySet<>(); Iterator<MonitoredPackage> it = mPackages.values().iterator(); @@ -1435,7 +1460,7 @@ public class PackageWatchdog { * @returns {@code true} if failure threshold is exceeded, {@code false} otherwise * @hide */ - @GuardedBy("mLock") + @GuardedBy("sLock") public boolean onPackageFailureLocked(String packageName) { if (getMonitoredPackage(packageName) == null && registeredObserver.isPersistent() && registeredObserver.mayObservePackage(packageName)) { @@ -1454,7 +1479,7 @@ public class PackageWatchdog { * * @return a mapping of package names to {@link MonitoredPackage} objects. */ - @GuardedBy("mLock") + @GuardedBy("sLock") public ArrayMap<String, MonitoredPackage> getMonitoredPackages() { return mPackages; } @@ -1467,7 +1492,7 @@ public class PackageWatchdog { * @return the {@link MonitoredPackage} object associated with the package name if one * exists, {@code null} otherwise. */ - @GuardedBy("mLock") + @GuardedBy("sLock") @Nullable public MonitoredPackage getMonitoredPackage(String packageName) { return mPackages.get(packageName); @@ -1478,7 +1503,7 @@ public class PackageWatchdog { * * @param p: the {@link MonitoredPackage} to store. */ - @GuardedBy("mLock") + @GuardedBy("sLock") public void putMonitoredPackage(MonitoredPackage p) { mPackages.put(p.getName(), p); } @@ -1601,17 +1626,17 @@ public class PackageWatchdog { * Represents a package and its health check state along with the time * it should be monitored for. * - * <p> Note, the PackageWatchdog#mLock must always be held when reading or writing + * <p> Note, the PackageWatchdog#sLock must always be held when reading or writing * instances of this class. */ class MonitoredPackage { private final String mPackageName; // Times when package failures happen sorted in ascending order - @GuardedBy("mLock") + @GuardedBy("sLock") private final LongArrayQueue mFailureHistory = new LongArrayQueue(); // Times when an observer was called to mitigate this package's failure. Sorted in // ascending order. - @GuardedBy("mLock") + @GuardedBy("sLock") private final LongArrayQueue mMitigationCalls; // One of STATE_[ACTIVE|INACTIVE|PASSED|FAILED]. Updated on construction and after // methods that could change the health check state: handleElapsedTimeLocked and @@ -1620,17 +1645,17 @@ public class PackageWatchdog { // Whether an explicit health check has passed. // This value in addition with mHealthCheckDurationMs determines the health check state // of the package, see #getHealthCheckStateLocked - @GuardedBy("mLock") + @GuardedBy("sLock") private boolean mHasPassedHealthCheck; // System uptime duration to monitor package. - @GuardedBy("mLock") + @GuardedBy("sLock") private long mDurationMs; // System uptime duration to check the result of an explicit health check // Initially, MAX_VALUE until we get a value from the health check service // and request health checks. // This value in addition with mHasPassedHealthCheck determines the health check state // of the package, see #getHealthCheckStateLocked - @GuardedBy("mLock") + @GuardedBy("sLock") private long mHealthCheckDurationMs = Long.MAX_VALUE; MonitoredPackage(String packageName, long durationMs, @@ -1647,7 +1672,7 @@ public class PackageWatchdog { /** Writes the salient fields to disk using {@code out}. * @hide */ - @GuardedBy("mLock") + @GuardedBy("sLock") public void writeLocked(XmlSerializer out) throws IOException { out.startTag(null, TAG_PACKAGE); out.attribute(null, ATTR_NAME, getName()); @@ -1665,7 +1690,7 @@ public class PackageWatchdog { * * @return {@code true} if failure count exceeds a threshold, {@code false} otherwise */ - @GuardedBy("mLock") + @GuardedBy("sLock") public boolean onFailureLocked() { // Sliding window algorithm: find out if there exists a window containing failures >= // mTriggerFailureCount. @@ -1685,7 +1710,7 @@ public class PackageWatchdog { /** * Notes the timestamp of a mitigation call into the observer. */ - @GuardedBy("mLock") + @GuardedBy("sLock") public void noteMitigationCallLocked() { mMitigationCalls.addLast(mSystemClock.uptimeMillis()); } @@ -1696,7 +1721,7 @@ public class PackageWatchdog { * * @return the number of mitigation calls made in the de-escalation window. */ - @GuardedBy("mLock") + @GuardedBy("sLock") public int getMitigationCountLocked() { try { final long now = mSystemClock.uptimeMillis(); @@ -1716,7 +1741,7 @@ public class PackageWatchdog { * * @return a LongArrayQueue of the mitigation calls relative to the current system uptime. */ - @GuardedBy("mLock") + @GuardedBy("sLock") public LongArrayQueue normalizeMitigationCalls() { LongArrayQueue normalized = new LongArrayQueue(); final long now = mSystemClock.uptimeMillis(); @@ -1731,7 +1756,7 @@ public class PackageWatchdog { * * @return the new health check state */ - @GuardedBy("mLock") + @GuardedBy("sLock") public int setHealthCheckActiveLocked(long initialHealthCheckDurationMs) { if (initialHealthCheckDurationMs <= 0) { Slog.wtf(TAG, "Cannot set non-positive health check duration " @@ -1751,7 +1776,7 @@ public class PackageWatchdog { * * @return the new health check state */ - @GuardedBy("mLock") + @GuardedBy("sLock") public int handleElapsedTimeLocked(long elapsedMs) { if (elapsedMs <= 0) { Slog.w(TAG, "Cannot handle non-positive elapsed time for package " + getName()); @@ -1769,7 +1794,7 @@ public class PackageWatchdog { } /** Explicitly update the monitoring duration of the package. */ - @GuardedBy("mLock") + @GuardedBy("sLock") public void updateHealthCheckDuration(long newDurationMs) { mDurationMs = newDurationMs; } @@ -1780,7 +1805,7 @@ public class PackageWatchdog { * * @return the new {@link HealthCheckState health check state} */ - @GuardedBy("mLock") + @GuardedBy("sLock") @HealthCheckState public int tryPassHealthCheckLocked() { if (mHealthCheckState != HealthCheckState.FAILED) { @@ -1799,7 +1824,7 @@ public class PackageWatchdog { /** * Returns the current {@link HealthCheckState health check state}. */ - @GuardedBy("mLock") + @GuardedBy("sLock") @HealthCheckState public int getHealthCheckStateLocked() { return mHealthCheckState; @@ -1810,7 +1835,7 @@ public class PackageWatchdog { * * @return the duration or {@link Long#MAX_VALUE} if the package should not be scheduled */ - @GuardedBy("mLock") + @GuardedBy("sLock") public long getShortestScheduleDurationMsLocked() { // Consider health check duration only if #isPendingHealthChecksLocked is true return Math.min(toPositive(mDurationMs), @@ -1822,7 +1847,7 @@ public class PackageWatchdog { * Returns {@code true} if the total duration left to monitor the package is less than or * equal to 0 {@code false} otherwise. */ - @GuardedBy("mLock") + @GuardedBy("sLock") public boolean isExpiredLocked() { return mDurationMs <= 0; } @@ -1831,7 +1856,7 @@ public class PackageWatchdog { * Returns {@code true} if the package, {@link #getName} is expecting health check results * {@code false} otherwise. */ - @GuardedBy("mLock") + @GuardedBy("sLock") public boolean isPendingHealthChecksLocked() { return mHealthCheckState == HealthCheckState.ACTIVE || mHealthCheckState == HealthCheckState.INACTIVE; @@ -1843,7 +1868,7 @@ public class PackageWatchdog { * * @return the new {@link HealthCheckState health check state} */ - @GuardedBy("mLock") + @GuardedBy("sLock") @HealthCheckState private int updateHealthCheckStateLocked() { int oldState = mHealthCheckState; @@ -1898,7 +1923,7 @@ public class PackageWatchdog { } } - @GuardedBy("mLock") + @GuardedBy("sLock") @SuppressWarnings("GuardedBy") void saveAllObserversBootMitigationCountToMetadata(String filePath) { HashMap<String, Integer> bootMitigationCounts = new HashMap<>(); @@ -2001,7 +2026,7 @@ public class PackageWatchdog { /** Increments the boot counter, and returns whether the device is bootlooping. */ - @GuardedBy("mLock") + @GuardedBy("sLock") public boolean incrementAndTest() { if (Flags.recoverabilityDetection()) { readAllObserversBootMitigationCountIfNecessary(METADATA_FILE); @@ -2042,7 +2067,7 @@ public class PackageWatchdog { } } - @GuardedBy("mLock") + @GuardedBy("sLock") private boolean performedMitigationsDuringWindow() { for (ObserverInternal observerInternal: mAllObservers.values()) { if (observerInternal.getBootMitigationCount() > 0) { @@ -2052,7 +2077,7 @@ public class PackageWatchdog { return false; } - @GuardedBy("mLock") + @GuardedBy("sLock") private void resetAllObserversBootMitigationCount() { for (int i = 0; i < mAllObservers.size(); i++) { final ObserverInternal observer = mAllObservers.valueAt(i); @@ -2061,7 +2086,7 @@ public class PackageWatchdog { saveAllObserversBootMitigationCountToMetadata(METADATA_FILE); } - @GuardedBy("mLock") + @GuardedBy("sLock") @SuppressWarnings("GuardedBy") void readAllObserversBootMitigationCountIfNecessary(String filePath) { File metadataFile = new File(filePath); diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/ModifierExt.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/ModifierExt.kt index e883a4a55af9..25bb46c837f6 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/ModifierExt.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/compose/ModifierExt.kt @@ -25,3 +25,36 @@ fun Modifier.contentDescription(contentDescription: String?) = if (contentDescription != null) this.semantics { this.contentDescription = contentDescription } else this + +/** + * Concatenates this modifier with another if `condition` is true. + * + * This method allows inline conditional addition of modifiers to a modifier chain. Instead of + * writing + * + * ``` + * val aModifier = Modifier.a() + * val bModifier = if(condition) aModifier.b() else aModifier + * Composable(modifier = bModifier) + * ``` + * + * You can instead write + * + * ``` + * Composable(modifier = Modifier.a().thenIf(condition){ + * Modifier.b() + * } + * ``` + * + * This makes the modifier chain easier to read. + * + * Note that unlike the non-factory version, the conditional modifier is recreated each time, and + * may never be created at all. + * + * @param condition Whether or not to apply the modifiers. + * @param factory Creates the modifier to concatenate with the current one. + * @return a Modifier representing this modifier followed by other in sequence. + * @see Modifier.then + */ +inline fun Modifier.thenIf(condition: Boolean, crossinline factory: () -> Modifier): Modifier = + if (condition) this.then(factory()) else this diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt index acb96be64a34..b1bb79d61b03 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/preference/BaseLayout.kt @@ -36,12 +36,13 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.min +import com.android.settingslib.spa.framework.compose.thenIf import com.android.settingslib.spa.framework.theme.SettingsDimension import com.android.settingslib.spa.framework.theme.SettingsOpacity.alphaForEnabled import com.android.settingslib.spa.framework.theme.SettingsShape import com.android.settingslib.spa.framework.theme.SettingsTheme import com.android.settingslib.spa.framework.theme.isSpaExpressiveEnabled +import com.android.settingslib.spa.widget.ui.LocalIsInCategory import com.android.settingslib.spa.widget.ui.SettingsTitle @Composable @@ -57,18 +58,18 @@ internal fun BaseLayout( paddingVertical: Dp = SettingsDimension.itemPaddingVertical, widget: @Composable () -> Unit = {}, ) { + val surfaceBright = MaterialTheme.colorScheme.surfaceBright Row( modifier = modifier .fillMaxWidth() .semantics(mergeDescendants = true) {} - .then( - if (isSpaExpressiveEnabled) - Modifier.heightIn(min = SettingsDimension.preferenceMinHeight) - .clip(SettingsShape.CornerExtraSmall) - .background(MaterialTheme.colorScheme.surfaceBright) - else Modifier - ) + .thenIf(isSpaExpressiveEnabled) { + Modifier.heightIn(min = SettingsDimension.preferenceMinHeight) + } + .thenIf(isSpaExpressiveEnabled && LocalIsInCategory.current) { + Modifier.clip(SettingsShape.CornerExtraSmall).background(surfaceBright) + } .padding(end = paddingEnd), verticalAlignment = Alignment.CenterVertically, ) { diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt index 28b2b4ab1662..96d2abb70391 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt @@ -31,6 +31,8 @@ import androidx.compose.material.icons.outlined.TouchApp import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -99,7 +101,7 @@ fun Category(title: String? = null, content: @Composable ColumnScope.() -> Unit) verticalArrangement = if (isSpaExpressiveEnabled) Arrangement.spacedBy(SettingsDimension.paddingTiny) else Arrangement.Top, - content = content, + content = { CompositionLocalProvider(LocalIsInCategory provides true) { content() } }, ) } } @@ -109,15 +111,14 @@ fun Category(title: String? = null, content: @Composable ColumnScope.() -> Unit) * * @param list The list of items to display. * @param entry The entry for each list item according to its index in list. - * @param key Optional. The key for each item in list to provide unique item identifiers, making - * the list more efficient. - * @param title Optional. Category title for each item or each group of items in the list. It - * should be decided by the index. + * @param key Optional. The key for each item in list to provide unique item identifiers, making the + * list more efficient. + * @param title Optional. Category title for each item or each group of items in the list. It should + * be decided by the index. * @param bottomPadding Optional. Bottom outside padding of the category. * @param state Optional. State of LazyList. * @param content Optional. Content to be shown at the top of the category. */ - @Composable fun LazyCategory( list: List<Any>, @@ -144,17 +145,19 @@ fun LazyCategory( verticalArrangement = Arrangement.spacedBy(SettingsDimension.paddingTiny), state = state, ) { - item { content() } + item { CompositionLocalProvider(LocalIsInCategory provides true) { content() } } items(count = list.size, key = key) { title?.invoke(it)?.let { title -> CategoryTitle(title) } - val entryPreference = entry(it) - entryPreference() + CompositionLocalProvider(LocalIsInCategory provides true) { entry(it)() } } } } } +/** LocalIsInCategory containing the if the current composable is in a category. */ +internal val LocalIsInCategory = compositionLocalOf { false } + @Preview @Composable private fun CategoryPreview() { diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java index d0827b30efc9..4eb0567c67d9 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java @@ -133,8 +133,9 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> * If an ACTION_UUID intent comes in within * MAX_UUID_DELAY_FOR_AUTO_CONNECT milliseconds, we will try auto-connect * again with the new UUIDs + * The value is reset if a manual disconnection happens. */ - private long mConnectAttempted; + private long mConnectAttempted = -1; // Active device state private boolean mIsActiveDeviceA2dp = false; @@ -369,6 +370,7 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> } public void disconnect() { + mConnectAttempted = -1; synchronized (mProfileLock) { if (getGroupId() != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) { for (CachedBluetoothDevice member : getMemberDevice()) { @@ -983,15 +985,19 @@ public class CachedBluetoothDevice implements Comparable<CachedBluetoothDevice> } if (BluetoothUtils.D) { - Log.d(TAG, "onUuidChanged: Time since last connect=" - + (SystemClock.elapsedRealtime() - mConnectAttempted)); + long lastConnectAttempted = mConnectAttempted == -1 ? 0 : mConnectAttempted; + Log.d( + TAG, + "onUuidChanged: Time since last connect/manual disconnect=" + + (SystemClock.elapsedRealtime() - lastConnectAttempted)); } /* * If a connect was attempted earlier without any UUID, we will do the connect now. * Otherwise, allow the connect on UUID change. */ - if ((mConnectAttempted + timeout) > SystemClock.elapsedRealtime()) { + if (mConnectAttempted != -1 + && (mConnectAttempted + timeout) > SystemClock.elapsedRealtime()) { Log.d(TAG, "onUuidChanged: triggering connectDevice"); connectDevice(); } diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java index c90ba8249d54..32d4580f67ec 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java @@ -464,5 +464,14 @@ public class GlobalSettingsValidators { )); VALIDATORS.put(Global.HEARING_DEVICE_LOCAL_AMBIENT_VOLUME, ANY_STRING_VALIDATOR); VALIDATORS.put(Global.HEARING_DEVICE_LOCAL_NOTIFICATION, ANY_STRING_VALIDATOR); + VALIDATORS.put( + Global.Wearable.WEAR_SYSTEM_STATUS_TRAY_CONFIGURATION, + new DiscreteValueValidator( + new String[] { + String.valueOf( + Global.Wearable.STATUS_TRAY_CONFIGURATION_DEFAULT), + String.valueOf( + Global.Wearable.STATUS_TRAY_CONFIGURATION_SYSTEM_HIDDEN) + })); } } diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java index 603a91195d04..03fea37deaf5 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java @@ -2434,28 +2434,40 @@ public class SettingsProvider extends ContentProvider { context.checkCallingOrSelfPermission( Manifest.permission.WRITE_DEVICE_CONFIG) == PackageManager.PERMISSION_GRANTED; - boolean isRoot = Binder.getCallingUid() == Process.ROOT_UID; + // Only the shell user and tests request the allowlist permission; this is used to force + // the WRITE_ALLOWLISTED_DEVICE_CONFIG path to log any flags that need to be allowlisted. + boolean isRestrictedShell = android.security.Flags.protectDeviceConfigFlags() + && hasAllowlistPermission; - if (isRoot) { - return; - } - - if (hasWritePermission) { + if (!isRestrictedShell && hasWritePermission) { assertCallingUserDenyList(flags); } else if (hasAllowlistPermission) { for (String flag : flags) { boolean namespaceAllowed = false; - for (String allowlistedPrefix : WritableNamespacePrefixes.ALLOWLIST) { - if (flag.startsWith(allowlistedPrefix)) { + if (isRestrictedShell) { + int delimiterIndex = flag.indexOf("/"); + String flagNamespace; + if (delimiterIndex != -1) { + flagNamespace = flag.substring(0, delimiterIndex); + } else { + flagNamespace = flag; + } + if (WritableNamespaces.ALLOWLIST.contains(flagNamespace)) { namespaceAllowed = true; - break; + } + } else { + for (String allowlistedPrefix : WritableNamespacePrefixes.ALLOWLIST) { + if (flag.startsWith(allowlistedPrefix)) { + namespaceAllowed = true; + break; + } } } if (!namespaceAllowed && !DeviceConfig.getAdbWritableFlags().contains(flag)) { - throw new SecurityException("Permission denial for flag '" - + flag - + "'; allowlist permission granted, but must add flag to the allowlist."); + Slog.wtf(LOG_TAG, "Permission denial for flag '" + flag + + "'; allowlist permission granted, but must add flag to the " + + "allowlist"); } } assertCallingUserDenyList(flags); diff --git a/packages/SettingsProvider/src/com/android/providers/settings/WritableNamespaces.java b/packages/SettingsProvider/src/com/android/providers/settings/WritableNamespaces.java new file mode 100644 index 000000000000..d835c5f5c179 --- /dev/null +++ b/packages/SettingsProvider/src/com/android/providers/settings/WritableNamespaces.java @@ -0,0 +1,38 @@ +/* + * 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.providers.settings; + +import android.util.ArraySet; + +import java.util.Arrays; +import java.util.Set; + +/** + * Contains the list of namespaces in which any flag can be written by adb without root + * permissions. + * <p> + * A security review is required for any namespace that's added to this list. To add to + * the list, create a change and tag the OWNER. In the commit message, include a + * description of the flag's functionality, and a justification for why it needs to be + * allowlisted. + */ +final class WritableNamespaces { + public static final Set<String> ALLOWLIST = + new ArraySet<String>(Arrays.asList( + "exo" + )); +} diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java index 9de7faf04df6..a62b7fd3db81 100644 --- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java +++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java @@ -635,7 +635,8 @@ public class SettingsBackupTest { Settings.Global.Wearable.WEAR_MEDIA_SESSIONS_PACKAGE, Settings.Global.Wearable.WEAR_POWER_ANOMALY_SERVICE_ENABLED, Settings.Global.Wearable.CONNECTIVITY_KEEP_DATA_ON, - Settings.Global.Wearable.PHONE_SWITCHING_REQUEST_SOURCE); + Settings.Global.Wearable.PHONE_SWITCHING_REQUEST_SOURCE, + Settings.Global.Wearable.WEAR_SYSTEM_STATUS_TRAY_CONFIGURATION); private static final Set<String> BACKUP_DENY_LIST_SECURE_SETTINGS = newHashSet( diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index 526320debb1a..fef0f8c7857a 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -242,6 +242,8 @@ <uses-permission android:name="android.permission.READ_LOWPAN_CREDENTIAL" /> <uses-permission android:name="android.permission.BLUETOOTH_STACK" /> <uses-permission android:name="android.permission.GET_ACCOUNTS" /> + <uses-permission android:name="android.permission.COPY_ACCOUNTS" /> + <uses-permission android:name="android.permission.REMOVE_ACCOUNTS" /> <uses-permission android:name="android.permission.RETRIEVE_WINDOW_TOKEN" /> <uses-permission android:name="android.permission.FRAME_STATS" /> <uses-permission android:name="android.permission.BIND_APPWIDGET" /> @@ -746,6 +748,9 @@ <!-- Permission required for ATS test - CarDevicePolicyManagerTest --> <uses-permission android:name="android.permission.LOCK_DEVICE" /> + <!-- Permission required for AuthenticationPolicyManagerTest --> + <uses-permission android:name="android.permission.MANAGE_SECURE_LOCK_DEVICE" /> + <!-- Permissions required for CTS test - CtsSafetyCenterTestCases --> <uses-permission android:name="android.permission.SEND_SAFETY_CENTER_UPDATE" /> <uses-permission android:name="android.permission.READ_SAFETY_CENTER_STATUS" /> diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 67666c3db81f..c82c63c7c78b 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -1798,3 +1798,17 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "gsf_bouncer" + namespace: "systemui" + description: "Applies GSF font styles to Bouncer surfaces." + bug: "379364381" +} + +flag { + name: "gsf_quick_settings" + namespace: "systemui" + description: "Applies GSF font styles to Quick Settings surfaces." + bug: "379364381" +} 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 9390664d1283..597cbf24729b 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 @@ -31,6 +31,7 @@ import androidx.compose.ui.viewinterop.AndroidView import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.SceneScope import com.android.systemui.biometrics.AuthController +import com.android.systemui.customization.R as customR import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.flags.FeatureFlagsClassic import com.android.systemui.flags.Flags @@ -147,7 +148,7 @@ constructor( } else { val scaleFactor = authController.scaleFactor val bottomPaddingPx = - context.resources.getDimensionPixelSize(R.dimen.lock_icon_margin_bottom) + context.resources.getDimensionPixelSize(customR.dimen.lock_icon_margin_bottom) val heightPx = windowViewBounds.bottom.toFloat() Pair( 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 160326792f81..0db545f262a9 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 @@ -519,8 +519,19 @@ internal class MultiPointerDraggableNode( // we intercept an ongoing swipe transition (i.e. startDragImmediately() returned // true). if (overSlop == 0f) { - val delta = (drag.position - consumablePointer.position).toFloat() - check(delta != 0f) { "delta is equal to 0" } + // If the user drags in the opposite direction, the delta becomes zero because + // we return to the original point. Therefore, we should use the previous event + // to calculate the direction. + val delta = (drag.position - drag.previousPosition).toFloat() + check(delta != 0f) { + buildString { + append("delta is equal to 0 ") + append("touchSlop ${currentValueOf(LocalViewConfiguration).touchSlop} ") + append("consumablePointer.position ${consumablePointer.position} ") + append("drag.position ${drag.position} ") + append("drag.previousPosition ${drag.previousPosition}") + } + } overSlop = delta.sign } drag diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt index b3fd097946d0..4bccac1e3ba0 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt @@ -430,14 +430,6 @@ internal class MutableSceneTransitionLayoutStateImpl( check(transitionStates.size == 1) check(transitionStates[0] is TransitionState.Idle) transitionStates = listOf(transition) - } else if (currentState == transition.replacedTransition) { - // Replace the transition. - transitionStates = - transitionStates.subList(0, transitionStates.lastIndex) + transition - - // Make sure it is removed from the finishedTransitions set if it was already - // finished. - finishedTransitions.remove(currentState) } else { // Append the new transition. transitionStates = transitionStates + transition diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/InterruptionHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/InterruptionHandlerTest.kt index b87cc5c88335..3622369b8ff9 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/InterruptionHandlerTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/InterruptionHandlerTest.kt @@ -132,6 +132,9 @@ class InterruptionHandlerTest { assertThat(state.currentTransitions) .comparingElementsUsing(FromToCurrentTriple) .containsExactly( + // Initial transition, A => B. + Triple(SceneA, SceneB, SceneB), + // Initial transition reversed, B back to A. Triple(SceneA, SceneB, SceneA), diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt index 5ec74f8d2260..85d8b60d61fb 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MultiPointerDraggableTest.kt @@ -593,7 +593,7 @@ class MultiPointerDraggableTest { } } - fun continueDraggingDown() { + fun dragDown() { rule.onRoot().performTouchInput { moveBy(Offset(0f, touchSlop)) } } @@ -603,11 +603,78 @@ class MultiPointerDraggableTest { assertThat(started).isFalse() swipeConsume = true - continueDraggingDown() + // Drag in same direction + dragDown() assertThat(capturedChange).isNotNull() capturedChange = null - continueDraggingDown() + dragDown() + assertThat(capturedChange).isNull() + + assertThat(started).isTrue() + } + + @Test + fun multiPointerSwipeDetectorInteractionZeroOffsetFromStartPosition() { + val size = 200f + val middle = Offset(size / 2f, size / 2f) + + var started = false + + var capturedChange: PointerInputChange? = null + var swipeConsume = false + + var touchSlop = 0f + rule.setContent { + touchSlop = LocalViewConfiguration.current.touchSlop + Box( + Modifier.size(with(LocalDensity.current) { Size(size, size).toDpSize() }) + .nestedScrollDispatcher() + .multiPointerDraggable( + orientation = Orientation.Vertical, + startDragImmediately = { false }, + swipeDetector = + object : SwipeDetector { + override fun detectSwipe(change: PointerInputChange): Boolean { + capturedChange = change + return swipeConsume + } + }, + onDragStarted = { _, _ -> + started = true + SimpleDragController( + onDrag = { /* do nothing */ }, + onStop = { /* do nothing */ }, + ) + }, + dispatcher = defaultDispatcher, + ) + ) {} + } + + fun startDraggingDown() { + rule.onRoot().performTouchInput { + down(middle) + moveBy(Offset(0f, touchSlop)) + } + } + + fun dragUp() { + rule.onRoot().performTouchInput { moveBy(Offset(0f, -touchSlop)) } + } + + startDraggingDown() + assertThat(capturedChange).isNotNull() + capturedChange = null + assertThat(started).isFalse() + + swipeConsume = true + // Drag in the opposite direction + dragUp() + assertThat(capturedChange).isNotNull() + capturedChange = null + + dragUp() assertThat(capturedChange).isNull() assertThat(started).isTrue() diff --git a/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/res/xml/network_security_config.xml b/packages/SystemUI/customization/res/values-land/dimens.xml index 4bd9ca049f55..50f220c882bd 100644 --- a/libs/WindowManager/Shell/tests/e2e/mediaprojection/flicker-service/res/xml/network_security_config.xml +++ b/packages/SystemUI/customization/res/values-land/dimens.xml @@ -1,6 +1,5 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- - ~ Copyright (C) 2023 The Android Open Source Project +<?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. @@ -15,8 +14,6 @@ ~ limitations under the License. --> -<network-security-config> - <domain-config cleartextTrafficPermitted="true"> - <domain includeSubdomains="true">localhost</domain> - </domain-config> -</network-security-config> +<resources> + <dimen name="lock_icon_margin_bottom">24dp</dimen> +</resources>
\ No newline at end of file diff --git a/packages/SystemUI/customization/res/values-sw600dp-land/dimens.xml b/packages/SystemUI/customization/res/values-sw600dp-land/dimens.xml index 18073adf9e7a..876028195ff3 100644 --- a/packages/SystemUI/customization/res/values-sw600dp-land/dimens.xml +++ b/packages/SystemUI/customization/res/values-sw600dp-land/dimens.xml @@ -17,4 +17,5 @@ <resources> <dimen name="keyguard_smartspace_top_offset">0dp</dimen> <dimen name="status_view_margin_horizontal">8dp</dimen> + <dimen name="lock_icon_margin_bottom">60dp</dimen> </resources>
\ No newline at end of file diff --git a/packages/SystemUI/customization/res/values/dimens.xml b/packages/SystemUI/customization/res/values/dimens.xml index 041ae62670e5..21b4c7165226 100644 --- a/packages/SystemUI/customization/res/values/dimens.xml +++ b/packages/SystemUI/customization/res/values/dimens.xml @@ -40,4 +40,5 @@ <dimen name="date_weather_view_height">24dp</dimen> <dimen name="enhanced_smartspace_height">104dp</dimen> <dimen name="status_view_margin_horizontal">0dp</dimen> + <dimen name="lock_icon_margin_bottom">74dp</dimen> </resources>
\ No newline at end of file diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractorTest.kt index 13f2c7212e36..4e64c50a3253 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractorTest.kt @@ -17,22 +17,23 @@ package com.android.systemui.biometrics.domain.interactor import android.graphics.Rect -import android.hardware.fingerprint.FingerprintManager import android.view.MotionEvent import android.view.Surface import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.AuthController +import com.android.systemui.biometrics.authController import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.user.domain.interactor.SelectedUserInteractor +import com.android.systemui.kosmos.testScope +import com.android.systemui.res.R +import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest -import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -44,29 +45,25 @@ import org.mockito.Mockito.verify import org.mockito.Mockito.`when` as whenever import org.mockito.junit.MockitoJUnit +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class UdfpsOverlayInteractorTest : SysuiTestCase() { @JvmField @Rule var mockitoRule = MockitoJUnit.rule() - private lateinit var testScope: TestScope + private val kosmos = testKosmos() - @Mock private lateinit var fingerprintManager: FingerprintManager - @Mock private lateinit var authController: AuthController + private val testScope: TestScope = kosmos.testScope + + private val authController: AuthController = kosmos.authController @Captor private lateinit var authControllerCallback: ArgumentCaptor<AuthController.Callback> @Mock private lateinit var udfpsOverlayParams: UdfpsOverlayParams @Mock private lateinit var overlayBounds: Rect - @Mock private lateinit var selectedUserInteractor: SelectedUserInteractor private lateinit var underTest: UdfpsOverlayInteractor - @Before - fun setUp() { - testScope = TestScope(StandardTestDispatcher()) - } - @Test fun testShouldInterceptTouch() = testScope.runTest { @@ -107,15 +104,26 @@ class UdfpsOverlayInteractorTest : SysuiTestCase() { assertThat(udfpsOverlayParams()).isEqualTo(firstParams) } + @Test + fun testPaddingIsNeverNegative() = + testScope.runTest { + context.orCreateTestableResources.addOverride(R.dimen.pixel_pitch, 60.0f) + createUdfpsOverlayInteractor() + val padding by collectLastValue(underTest.iconPadding) + runCurrent() + + verify(authController).addCallback(authControllerCallback.capture()) + + // Simulate the first empty udfps overlay params value. + authControllerCallback.value.onUdfpsLocationChanged(UdfpsOverlayParams()) + runCurrent() + + assertThat(padding).isEqualTo(0) + context.orCreateTestableResources.removeOverride(R.dimen.pixel_pitch) + } + private fun createUdfpsOverlayInteractor() { - underTest = - UdfpsOverlayInteractor( - context, - authController, - selectedUserInteractor, - fingerprintManager, - testScope.backgroundScope - ) + underTest = kosmos.udfpsOverlayInteractor testScope.runCurrent() } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/development/data/repository/DevelopmentSettingRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/development/data/repository/DevelopmentSettingRepositoryTest.kt new file mode 100644 index 000000000000..e17b66e90c2d --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/development/data/repository/DevelopmentSettingRepositoryTest.kt @@ -0,0 +1,167 @@ +/* + * 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.development.data.repository + +import android.content.pm.UserInfo +import android.os.Build +import android.os.UserHandle +import android.os.UserManager +import android.os.userManager +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.kosmos.Kosmos +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.android.systemui.util.settings.fakeGlobalSettings +import com.google.common.truth.Truth.assertThat +import kotlin.test.Test +import kotlinx.coroutines.test.runTest +import org.junit.runner.RunWith +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.stub + +@RunWith(AndroidJUnit4::class) +@SmallTest +class DevelopmentSettingRepositoryTest : SysuiTestCase() { + + private val kosmos = testKosmos() + + private val underTest = kosmos.developmentSettingRepository + + @Test + fun nonAdminUser_unrestricted_neverDevelopmentEnabled() = + with(kosmos) { + testScope.runTest { + val userInfo = nonAdminUserInfo + val settingEnabled by + collectLastValue(underTest.isDevelopmentSettingEnabled(userInfo)) + + setUserRestriction(userInfo.userHandle, restricted = false) + + assertThat(settingEnabled).isFalse() + + setSettingValue(false) + assertThat(settingEnabled).isFalse() + + setSettingValue(true) + assertThat(settingEnabled).isFalse() + } + } + + @Test + fun nonAdminUser_restricted_neverDevelopmentEnabled() = + with(kosmos) { + testScope.runTest { + val userInfo = nonAdminUserInfo + val settingEnabled by + collectLastValue(underTest.isDevelopmentSettingEnabled(userInfo)) + + setUserRestriction(userInfo.userHandle, restricted = true) + + assertThat(settingEnabled).isFalse() + + setSettingValue(false) + assertThat(settingEnabled).isFalse() + + setSettingValue(true) + assertThat(settingEnabled).isFalse() + } + } + + @Test + fun adminUser_unrestricted_defaultValueOfSetting() = + with(kosmos) { + testScope.runTest { + val userInfo = adminUserInfo + val settingEnabled by + collectLastValue(underTest.isDevelopmentSettingEnabled(userInfo)) + + setUserRestriction(userInfo.userHandle, restricted = false) + + val defaultValue = Build.TYPE == "eng" + + assertThat(settingEnabled).isEqualTo(defaultValue) + } + } + + @Test + fun adminUser_unrestricted_enabledTracksSetting() = + with(kosmos) { + testScope.runTest { + val userInfo = adminUserInfo + val settingEnabled by + collectLastValue(underTest.isDevelopmentSettingEnabled(userInfo)) + + setUserRestriction(userInfo.userHandle, restricted = false) + + setSettingValue(false) + assertThat(settingEnabled).isFalse() + + setSettingValue(true) + assertThat(settingEnabled).isTrue() + } + } + + @Test + fun adminUser_restricted_neverDevelopmentEnabled() = + with(kosmos) { + testScope.runTest { + val userInfo = adminUserInfo + val settingEnabled by + collectLastValue(underTest.isDevelopmentSettingEnabled(userInfo)) + + setUserRestriction(userInfo.userHandle, restricted = true) + + assertThat(settingEnabled).isFalse() + + setSettingValue(false) + assertThat(settingEnabled).isFalse() + + setSettingValue(true) + assertThat(settingEnabled).isFalse() + } + } + + private companion object { + const val USER_RESTRICTION = UserManager.DISALLOW_DEBUGGING_FEATURES + const val SETTING_NAME = Settings.Global.DEVELOPMENT_SETTINGS_ENABLED + + val adminUserInfo = + UserInfo( + /* id= */ 10, + /* name= */ "", + /* flags */ UserInfo.FLAG_ADMIN or UserInfo.FLAG_FULL, + ) + val nonAdminUserInfo = + UserInfo(/* id= */ 11, /* name= */ "", /* flags */ UserInfo.FLAG_FULL) + + fun Kosmos.setUserRestriction(userHandle: UserHandle, restricted: Boolean) { + userManager.stub { + on { hasUserRestrictionForUser(eq(USER_RESTRICTION), eq(userHandle)) } doReturn + restricted + } + } + + fun Kosmos.setSettingValue(enabled: Boolean) { + fakeGlobalSettings.putInt(SETTING_NAME, if (enabled) 1 else 0) + } + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/development/domain/interactor/BuildNumberInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/development/domain/interactor/BuildNumberInteractorTest.kt new file mode 100644 index 000000000000..f29dabe98664 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/development/domain/interactor/BuildNumberInteractorTest.kt @@ -0,0 +1,138 @@ +/* + * 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.development.domain.interactor + +import android.content.ClipData +import android.content.ClipDescription +import android.content.clipboardManager +import android.content.pm.UserInfo +import android.content.res.mainResources +import android.os.Build +import android.provider.Settings +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.internal.R +import com.android.systemui.SysuiTestCase +import com.android.systemui.development.shared.model.BuildNumber +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runCurrent +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.android.systemui.user.data.repository.fakeUserRepository +import com.android.systemui.util.settings.fakeGlobalSettings +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.verify + +@RunWith(AndroidJUnit4::class) +@SmallTest +class BuildNumberInteractorTest : SysuiTestCase() { + + private val kosmos = + testKosmos().apply { + fakeUserRepository.setUserInfos(listOf(adminUserInfo, nonAdminUserInfo)) + } + + private val expectedBuildNumber = + BuildNumber( + kosmos.mainResources.getString( + R.string.bugreport_status, + Build.VERSION.RELEASE_OR_CODENAME, + Build.ID, + ) + ) + + private val clipLabel = + kosmos.mainResources.getString( + com.android.systemui.res.R.string.build_number_clip_data_label + ) + + private val underTest = kosmos.buildNumberInteractor + + @Test + fun nonAdminUser_settingEnabled_buildNumberNull() = + with(kosmos) { + testScope.runTest { + val buildNumber by collectLastValue(underTest.buildNumber) + + fakeUserRepository.setSelectedUserInfo(nonAdminUserInfo) + setSettingValue(true) + + assertThat(buildNumber).isNull() + } + } + + @Test + fun adminUser_buildNumberCorrect_onlyWhenSettingEnabled() = + with(kosmos) { + testScope.runTest { + val buildNumber by collectLastValue(underTest.buildNumber) + + fakeUserRepository.setSelectedUserInfo(adminUserInfo) + + setSettingValue(false) + assertThat(buildNumber).isNull() + + setSettingValue(true) + assertThat(buildNumber).isEqualTo(expectedBuildNumber) + } + } + + @Test + fun copyToClipboard() = + with(kosmos) { + testScope.runTest { + fakeUserRepository.setSelectedUserInfo(adminUserInfo) + + underTest.copyBuildNumber() + runCurrent() + + val argumentCaptor = argumentCaptor<ClipData>() + + verify(clipboardManager).setPrimaryClip(argumentCaptor.capture()) + + with(argumentCaptor.firstValue) { + assertThat(description.label).isEqualTo(clipLabel) + assertThat(description.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN)) + .isTrue() + assertThat(itemCount).isEqualTo(1) + assertThat(getItemAt(0).text).isEqualTo(expectedBuildNumber.value) + } + } + } + + private companion object { + const val SETTING_NAME = Settings.Global.DEVELOPMENT_SETTINGS_ENABLED + + val adminUserInfo = + UserInfo( + /* id= */ 10, + /* name= */ "", + /* flags */ UserInfo.FLAG_ADMIN or UserInfo.FLAG_FULL, + ) + val nonAdminUserInfo = + UserInfo(/* id= */ 11, /* name= */ "", /* flags */ UserInfo.FLAG_FULL) + + fun Kosmos.setSettingValue(enabled: Boolean) { + fakeGlobalSettings.putInt(SETTING_NAME, if (enabled) 1 else 0) + } + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt index f0d79bb83652..47cba0723804 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt @@ -568,6 +568,41 @@ class DeviceUnlockedInteractorTest : SysuiTestCase() { assertThat(isUnlocked).isFalse() } + @Test + fun lockNow() = + testScope.runTest { + setLockAfterScreenTimeout(5000) + val isUnlocked by collectLastValue(underTest.deviceUnlockStatus.map { it.isUnlocked }) + unlockDevice() + assertThat(isUnlocked).isTrue() + + underTest.lockNow() + runCurrent() + + assertThat(isUnlocked).isFalse() + } + + @Test + fun deviceUnlockStatus_isResetToFalse_whenDeviceGoesToSleep_fromSleepButton() = + testScope.runTest { + setLockAfterScreenTimeout(5000) + kosmos.fakeAuthenticationRepository.powerButtonInstantlyLocks = false + val deviceUnlockStatus by collectLastValue(underTest.deviceUnlockStatus) + + kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( + SuccessFingerprintAuthenticationStatus(0, true) + ) + runCurrent() + assertThat(deviceUnlockStatus?.isUnlocked).isTrue() + + kosmos.powerInteractor.setAsleepForTest( + sleepReason = PowerManager.GO_TO_SLEEP_REASON_SLEEP_BUTTON + ) + runCurrent() + + assertThat(deviceUnlockStatus?.isUnlocked).isFalse() + } + private fun TestScope.unlockDevice() { val deviceUnlockStatus by collectLastValue(underTest.deviceUnlockStatus) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/ui/viewmodel/UdfpsAccessibilityOverlayViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/ui/viewmodel/UdfpsAccessibilityOverlayViewModelTest.kt index c39c3fe5f527..2d54337def13 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/ui/viewmodel/UdfpsAccessibilityOverlayViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/ui/viewmodel/UdfpsAccessibilityOverlayViewModelTest.kt @@ -36,6 +36,7 @@ import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep import com.android.systemui.keyguard.ui.viewmodel.fakeDeviceEntryIconViewModelTransition import com.android.systemui.kosmos.testScope +import com.android.systemui.res.R import com.android.systemui.shade.shadeTestUtil import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat @@ -43,6 +44,7 @@ import kotlin.test.Test import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest +import org.junit.After import org.junit.Before import org.junit.runner.RunWith import platform.test.runner.parameterized.ParameterizedAndroidJunit4 @@ -84,6 +86,12 @@ class UdfpsAccessibilityOverlayViewModelTest(flags: FlagsParameterization) : Sys @Before fun setup() { underTest = kosmos.deviceEntryUdfpsAccessibilityOverlayViewModel + overrideResource(R.integer.udfps_padding_debounce_duration, 0) + } + + @After + fun teardown() { + mContext.orCreateTestableResources.removeOverride(R.integer.udfps_padding_debounce_duration) } @Test @@ -118,6 +126,7 @@ class UdfpsAccessibilityOverlayViewModelTest(flags: FlagsParameterization) : Sys runCurrent() assertThat(visible).isFalse() } + fun fpNotRunning_overlayNotVisible() = testScope.runTest { val visible by collectLastValue(underTest.visible) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepositoryTest.kt index cc4c7c419a11..27b8c59a076d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepositoryTest.kt @@ -18,13 +18,17 @@ package com.android.systemui.keyboard.shortcut.data.repository import android.content.Context import android.content.Context.INPUT_SERVICE -import android.hardware.input.InputGestureData -import android.hardware.input.InputGestureData.createKeyTrigger -import android.hardware.input.KeyGestureEvent import android.hardware.input.fakeInputManager import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags -import android.view.KeyEvent +import android.view.KeyEvent.KEYCODE_SLASH +import android.view.KeyEvent.META_ALT_ON +import android.view.KeyEvent.META_CAPS_LOCK_ON +import android.view.KeyEvent.META_CTRL_ON +import android.view.KeyEvent.META_FUNCTION_ON +import android.view.KeyEvent.META_META_ON +import android.view.KeyEvent.META_SHIFT_ON +import android.view.KeyEvent.META_SYM_ON import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.hardware.input.Flags.FLAG_ENABLE_CUSTOMIZABLE_INPUT_GESTURES @@ -32,17 +36,14 @@ import com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyboard.shortcut.customShortcutCategoriesRepository -import com.android.systemui.keyboard.shortcut.shared.model.Shortcut -import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategory -import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType -import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.AppCategories -import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.MultiTasking -import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.System -import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCommand +import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.allCustomizableInputGesturesWithSimpleShortcutCombinations +import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.customizableInputGestureWithUnknownKeyGestureType +import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.expectedShortcutCategoriesWithSimpleShortcutCombination +import com.android.systemui.keyboard.shortcut.shared.model.KeyCombination import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey -import com.android.systemui.keyboard.shortcut.shared.model.ShortcutSubCategory import com.android.systemui.keyboard.shortcut.shortcutHelperTestHelper import com.android.systemui.kosmos.testScope +import com.android.systemui.res.R import com.android.systemui.settings.FakeUserTracker import com.android.systemui.settings.userTracker import com.android.systemui.testKosmos @@ -83,7 +84,7 @@ class CustomShortcutCategoriesRepositoryTest : SysuiTestCase() { whenever( fakeInputManager.inputManager.getCustomInputGestures(/* filter= */ anyOrNull()) ) - .thenReturn(customizableInputGesturesWithSimpleShortcutCombinations) + .thenReturn(allCustomizableInputGesturesWithSimpleShortcutCombinations) helper.toggle(deviceId = 123) val categories by collectLastValue(repo.categories) @@ -100,7 +101,7 @@ class CustomShortcutCategoriesRepositoryTest : SysuiTestCase() { whenever( fakeInputManager.inputManager.getCustomInputGestures(/* filter= */ anyOrNull()) ) - .thenReturn(customizableInputGesturesWithSimpleShortcutCombinations) + .thenReturn(allCustomizableInputGesturesWithSimpleShortcutCombinations) helper.toggle(deviceId = 123) val categories by collectLastValue(repo.categories) @@ -125,167 +126,73 @@ class CustomShortcutCategoriesRepositoryTest : SysuiTestCase() { } } - private fun simpleInputGestureData( - keyCode: Int = KeyEvent.KEYCODE_A, - modifiers: Int = KeyEvent.META_CTRL_ON or KeyEvent.META_ALT_ON, - keyGestureType: Int, - ): InputGestureData { - val builder = InputGestureData.Builder() - builder.setKeyGestureType(keyGestureType) - builder.setTrigger(createKeyTrigger(keyCode, modifiers)) - return builder.build() + @Test + fun pressedKeys_isEmptyByDefault() { + testScope.runTest { + val pressedKeys by collectLastValue(repo.pressedKeys) + assertThat(pressedKeys).isEmpty() + + helper.toggle(deviceId = 123) + assertThat(pressedKeys).isEmpty() + } } - private fun simpleShortcutCategory( - category: ShortcutCategoryType, - subcategoryLabel: String, - shortcutLabel: String, - ): ShortcutCategory { - return ShortcutCategory( - type = category, - subCategories = - listOf( - ShortcutSubCategory( - label = subcategoryLabel, - shortcuts = listOf(simpleShortcut(shortcutLabel)), - ) - ), - ) + @Test + fun pressedKeys_recognizesAllSupportedModifiers() { + testScope.runTest { + helper.toggle(deviceId = 123) + val pressedKeys by collectLastValue(repo.pressedKeys) + repo.updateUserKeyCombination( + KeyCombination(modifiers = allSupportedModifiers, keyCode = null) + ) + + assertThat(pressedKeys) + .containsExactly( + ShortcutKey.Icon.ResIdIcon(R.drawable.ic_ksh_key_meta), + ShortcutKey.Text("Ctrl"), + ShortcutKey.Text("Fn"), + ShortcutKey.Text("Shift"), + ShortcutKey.Text("Alt"), + ShortcutKey.Text("Sym"), + ) + } } - private fun simpleShortcut(label: String) = - Shortcut( - label = label, - commands = - listOf( - ShortcutCommand( - isCustom = true, - keys = - listOf( - ShortcutKey.Text("Ctrl"), - ShortcutKey.Text("Alt"), - ShortcutKey.Text("A"), - ), - ) - ), - ) + @Test + fun pressedKeys_ignoresUnsupportedModifiers() { + testScope.runTest { + helper.toggle(deviceId = 123) + val pressedKeys by collectLastValue(repo.pressedKeys) + repo.updateUserKeyCombination( + KeyCombination(modifiers = META_CAPS_LOCK_ON, keyCode = null) + ) - private val customizableInputGestureWithUnknownKeyGestureType = - // These key gesture events are currently not supported by shortcut helper customizer - listOf( - simpleInputGestureData( - keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS - ), - simpleInputGestureData(keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_MEDIA_KEY), - ) + assertThat(pressedKeys).isEmpty() + } + } - private val expectedShortcutCategoriesWithSimpleShortcutCombination = - listOf( - simpleShortcutCategory(System, "System apps", "Open assistant"), - simpleShortcutCategory(System, "System controls", "Go to home screen"), - simpleShortcutCategory(System, "System apps", "Open settings"), - simpleShortcutCategory(System, "System controls", "Lock screen"), - simpleShortcutCategory(System, "System controls", "View notifications"), - simpleShortcutCategory(System, "System apps", "Take a note"), - simpleShortcutCategory(System, "System controls", "Take screenshot"), - simpleShortcutCategory(System, "System controls", "Go back"), - simpleShortcutCategory( - MultiTasking, - "Split screen", - "Switch from split screen to full screen", - ), - simpleShortcutCategory( - MultiTasking, - "Split screen", - "Use split screen with current app on the left", - ), - simpleShortcutCategory( - MultiTasking, - "Split screen", - "Switch to app on left or above while using split screen", - ), - simpleShortcutCategory( - MultiTasking, - "Split screen", - "Use split screen with current app on the right", - ), - simpleShortcutCategory( - MultiTasking, - "Split screen", - "Switch to app on right or below while using split screen", - ), - simpleShortcutCategory(System, "System controls", "Show shortcuts"), - simpleShortcutCategory(System, "System controls", "View recent apps"), - simpleShortcutCategory(AppCategories, "Applications", "Calculator"), - simpleShortcutCategory(AppCategories, "Applications", "Calendar"), - simpleShortcutCategory(AppCategories, "Applications", "Browser"), - simpleShortcutCategory(AppCategories, "Applications", "Contacts"), - simpleShortcutCategory(AppCategories, "Applications", "Email"), - simpleShortcutCategory(AppCategories, "Applications", "Maps"), - simpleShortcutCategory(AppCategories, "Applications", "SMS"), - simpleShortcutCategory(MultiTasking, "Recent apps", "Cycle forward through recent apps"), - ) + @Test + fun pressedKeys_assertCorrectConversion() { + testScope.runTest { + helper.toggle(deviceId = 123) + val pressedKeys by collectLastValue(repo.pressedKeys) + repo.updateUserKeyCombination( + KeyCombination(modifiers = META_META_ON, keyCode = KEYCODE_SLASH) + ) + + assertThat(pressedKeys) + .containsExactly( + ShortcutKey.Icon.ResIdIcon(R.drawable.ic_ksh_key_meta), + ShortcutKey.Text("/"), + ) + } + } - private val customizableInputGesturesWithSimpleShortcutCombinations = - listOf( - simpleInputGestureData( - keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT - ), - simpleInputGestureData(keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_HOME), - simpleInputGestureData( - keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS - ), - simpleInputGestureData(keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_LOCK_SCREEN), - simpleInputGestureData( - keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL - ), - simpleInputGestureData(keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_NOTES), - simpleInputGestureData( - keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT - ), - simpleInputGestureData(keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_BACK), - simpleInputGestureData( - keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION - ), - simpleInputGestureData( - keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT - ), - simpleInputGestureData( - keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_LEFT - ), - simpleInputGestureData( - keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT - ), - simpleInputGestureData( - keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_RIGHT - ), - simpleInputGestureData( - keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER - ), - simpleInputGestureData(keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS), - simpleInputGestureData( - keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALCULATOR - ), - simpleInputGestureData( - keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALENDAR - ), - simpleInputGestureData( - keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_BROWSER - ), - simpleInputGestureData( - keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CONTACTS - ), - simpleInputGestureData( - keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_EMAIL - ), - simpleInputGestureData( - keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MAPS - ), - simpleInputGestureData( - keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MESSAGING - ), - simpleInputGestureData( - keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER - ), - ) + private val allSupportedModifiers = + META_META_ON or + META_CTRL_ON or + META_FUNCTION_ON or + META_SHIFT_ON or + META_ALT_ON or + META_SYM_ON } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt index c9c39b3ebf66..a1e7ef4ac5a3 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt @@ -16,11 +16,20 @@ package com.android.systemui.keyboard.shortcut.data.source +import android.hardware.input.InputGestureData +import android.hardware.input.InputGestureData.createKeyTrigger +import android.hardware.input.KeyGestureEvent import android.view.KeyEvent import android.view.KeyboardShortcutGroup import android.view.KeyboardShortcutInfo +import com.android.systemui.keyboard.shortcut.shared.model.Shortcut import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategory import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType +import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.AppCategories +import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.MultiTasking +import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.System +import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCommand +import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey import com.android.systemui.keyboard.shortcut.shared.model.ShortcutSubCategory import com.android.systemui.keyboard.shortcut.shared.model.shortcut import com.android.systemui.res.R @@ -64,6 +73,13 @@ object TestShortcuts { } } + private val goHomeShortcutInfo = + KeyboardShortcutInfo( + /* label = */ "Go to home screen", + /* keycode = */ KeyEvent.KEYCODE_B, + /* modifiers = */ KeyEvent.META_CTRL_ON or KeyEvent.META_ALT_ON, + ) + private val standardShortcutInfo1 = KeyboardShortcutInfo( /* label = */ "Standard shortcut 1", @@ -79,6 +95,16 @@ object TestShortcuts { } } + private val customGoHomeShortcut = + shortcut("Go to home screen") { + command { + key("Ctrl") + key("Alt") + key("A") + isCustom(true) + } + } + private val standardShortcutInfo2 = KeyboardShortcutInfo( /* label = */ "Standard shortcut 2", @@ -123,35 +149,38 @@ object TestShortcuts { listOf( shortcutInfoWithRepeatedLabel, shortcutInfoWithRepeatedLabelAlternate, - shortcutInfoWithRepeatedLabelSecondAlternate - ) + shortcutInfoWithRepeatedLabelSecondAlternate, + ), ) private val subCategoryWithGroupedRepeatedShortcutLabels = ShortcutSubCategory( label = groupWithRepeatedShortcutLabels.label!!.toString(), - shortcuts = listOf(shortcutWithGroupedRepeatedLabel) + shortcuts = listOf(shortcutWithGroupedRepeatedLabel), ) private val groupWithStandardShortcutInfo = KeyboardShortcutGroup("Standard group", listOf(standardShortcutInfo1)) + val groupWithGoHomeShortcutInfo = + KeyboardShortcutGroup("System controls", listOf(goHomeShortcutInfo)) + private val subCategoryWithStandardShortcut = ShortcutSubCategory( label = groupWithStandardShortcutInfo.label!!.toString(), - shortcuts = listOf(standardShortcut1) + shortcuts = listOf(standardShortcut1), ) private val groupWithOnlyUnsupportedModifierShortcut = KeyboardShortcutGroup( "Group with unsupported modifiers", - listOf(shortcutInfoWithUnsupportedModifiers) + listOf(shortcutInfoWithUnsupportedModifiers), ) private val groupWithSupportedAndUnsupportedModifierShortcut = KeyboardShortcutGroup( "Group with mix of supported and not supported modifiers", - listOf(standardShortcutInfo3, shortcutInfoWithUnsupportedModifiers) + listOf(standardShortcutInfo3, shortcutInfoWithUnsupportedModifiers), ) private val switchToNextLanguageShortcut = @@ -174,19 +203,19 @@ object TestShortcuts { private val subCategoryForInputLanguageSwitchShortcuts = ShortcutSubCategory( "Input", - listOf(switchToNextLanguageShortcut, switchToPreviousLanguageShortcut) + listOf(switchToNextLanguageShortcut, switchToPreviousLanguageShortcut), ) private val subCategoryWithUnsupportedShortcutsRemoved = ShortcutSubCategory( groupWithSupportedAndUnsupportedModifierShortcut.label!!.toString(), - listOf(standardShortcut3) + listOf(standardShortcut3), ) private val standardGroup1 = KeyboardShortcutGroup( "Standard group 1", - listOf(standardShortcutInfo1, standardShortcutInfo2, standardShortcutInfo3) + listOf(standardShortcutInfo1, standardShortcutInfo2, standardShortcutInfo3), ) private val standardPackageName1 = "standard.app.group1" @@ -194,38 +223,63 @@ object TestShortcuts { private val standardAppGroup1 = KeyboardShortcutGroup( "Standard app group 1", - listOf(standardShortcutInfo1, standardShortcutInfo2, standardShortcutInfo3) + listOf(standardShortcutInfo1, standardShortcutInfo2, standardShortcutInfo3), ) .apply { packageName = standardPackageName1 } + private val standardSystemAppSubcategoryWithCustomHomeShortcut = + ShortcutSubCategory("System controls", listOf(customGoHomeShortcut)) + private val standardSubCategory1 = ShortcutSubCategory( standardGroup1.label!!.toString(), - listOf(standardShortcut1, standardShortcut2, standardShortcut3) + listOf(standardShortcut1, standardShortcut2, standardShortcut3), ) private val standardGroup2 = KeyboardShortcutGroup( "Standard group 2", - listOf(standardShortcutInfo3, standardShortcutInfo2, standardShortcutInfo1) + listOf(standardShortcutInfo3, standardShortcutInfo2, standardShortcutInfo1), ) private val standardSubCategory2 = ShortcutSubCategory( standardGroup2.label!!.toString(), - listOf(standardShortcut3, standardShortcut2, standardShortcut1) + listOf(standardShortcut3, standardShortcut2, standardShortcut1), ) private val standardGroup3 = KeyboardShortcutGroup( "Standard group 3", - listOf(standardShortcutInfo2, standardShortcutInfo1) + listOf(standardShortcutInfo2, standardShortcutInfo1), ) private val standardSubCategory3 = ShortcutSubCategory( standardGroup3.label!!.toString(), - listOf(standardShortcut2, standardShortcut1) + listOf(standardShortcut2, standardShortcut1), + ) + + private val systemSubCategoryWithGoHomeShortcuts = + ShortcutSubCategory( + label = "System controls", + shortcuts = + listOf( + shortcut("Go to home screen") { + command { + key("Ctrl") + key("Alt") + key("B") + } + command { + key("Ctrl") + key("Alt") + key("A") + isCustom(true) + } + } + ), ) + val imeGroups = listOf(standardGroup1, standardGroup2, standardGroup3) val imeCategory = ShortcutCategory( @@ -235,8 +289,8 @@ object TestShortcuts { subCategoryForInputLanguageSwitchShortcuts, standardSubCategory1, standardSubCategory2, - standardSubCategory3 - ) + standardSubCategory3, + ), ) val currentAppGroups = listOf(standardAppGroup1) @@ -245,15 +299,33 @@ object TestShortcuts { val systemGroups = listOf(standardGroup3, standardGroup2, standardGroup1) val systemCategory = ShortcutCategory( - type = ShortcutCategoryType.System, - subCategories = listOf(standardSubCategory3, standardSubCategory2, standardSubCategory1) + type = System, + subCategories = listOf(standardSubCategory3, standardSubCategory2, standardSubCategory1), + ) + + val systemCategoryWithMergedGoHomeShortcut = + ShortcutCategory( + type = System, + subCategories = listOf(systemSubCategoryWithGoHomeShortcuts), + ) + + val systemCategoryWithCustomHomeShortcut = + ShortcutCategory( + type = System, + subCategories = + listOf( + standardSubCategory3, + standardSubCategory2, + standardSubCategory1, + standardSystemAppSubcategoryWithCustomHomeShortcut, + ), ) val multitaskingGroups = listOf(standardGroup2, standardGroup1) val multitaskingCategory = ShortcutCategory( - type = ShortcutCategoryType.MultiTasking, - subCategories = listOf(standardSubCategory2, standardSubCategory1) + type = MultiTasking, + subCategories = listOf(standardSubCategory2, standardSubCategory1), ) val groupsWithDuplicateShortcutLabels = @@ -266,14 +338,14 @@ object TestShortcuts { listOf( subCategoryForInputLanguageSwitchShortcuts, subCategoryWithGroupedRepeatedShortcutLabels, - subCategoryWithStandardShortcut + subCategoryWithStandardShortcut, ) val groupsWithUnsupportedModifier = listOf( groupWithStandardShortcutInfo, groupWithOnlyUnsupportedModifierShortcut, - groupWithSupportedAndUnsupportedModifierShortcut + groupWithSupportedAndUnsupportedModifierShortcut, ) val subCategoriesWithUnsupportedModifiersRemoved = @@ -283,8 +355,174 @@ object TestShortcuts { listOf( subCategoryForInputLanguageSwitchShortcuts, subCategoryWithStandardShortcut, - subCategoryWithUnsupportedShortcutsRemoved + subCategoryWithUnsupportedShortcutsRemoved, ) val groupsWithOnlyUnsupportedModifiers = listOf(groupWithOnlyUnsupportedModifierShortcut) + + private fun simpleInputGestureData( + keyCode: Int = KeyEvent.KEYCODE_A, + modifiers: Int = KeyEvent.META_CTRL_ON or KeyEvent.META_ALT_ON, + keyGestureType: Int, + ): InputGestureData { + val builder = InputGestureData.Builder() + builder.setKeyGestureType(keyGestureType) + builder.setTrigger(createKeyTrigger(keyCode, modifiers)) + return builder.build() + } + + private fun simpleShortcutCategory( + category: ShortcutCategoryType, + subcategoryLabel: String, + shortcutLabel: String, + ): ShortcutCategory { + return ShortcutCategory( + type = category, + subCategories = + listOf( + ShortcutSubCategory( + label = subcategoryLabel, + shortcuts = listOf(simpleShortcut(shortcutLabel)), + ) + ), + ) + } + + private fun simpleShortcut(label: String) = + Shortcut( + label = label, + commands = + listOf( + ShortcutCommand( + isCustom = true, + keys = + listOf( + ShortcutKey.Text("Ctrl"), + ShortcutKey.Text("Alt"), + ShortcutKey.Text("A"), + ), + ) + ), + ) + + val customizableInputGestureWithUnknownKeyGestureType = + // These key gesture events are currently not supported by shortcut helper customizer + listOf( + simpleInputGestureData( + keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_GLOBAL_ACTIONS + ), + simpleInputGestureData(keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_MEDIA_KEY), + ) + + val expectedShortcutCategoriesWithSimpleShortcutCombination = + listOf( + simpleShortcutCategory(System, "System apps", "Open assistant"), + simpleShortcutCategory(System, "System controls", "Go to home screen"), + simpleShortcutCategory(System, "System apps", "Open settings"), + simpleShortcutCategory(System, "System controls", "Lock screen"), + simpleShortcutCategory(System, "System controls", "View notifications"), + simpleShortcutCategory(System, "System apps", "Take a note"), + simpleShortcutCategory(System, "System controls", "Take screenshot"), + simpleShortcutCategory(System, "System controls", "Go back"), + simpleShortcutCategory( + MultiTasking, + "Split screen", + "Switch from split screen to full screen", + ), + simpleShortcutCategory( + MultiTasking, + "Split screen", + "Use split screen with current app on the left", + ), + simpleShortcutCategory( + MultiTasking, + "Split screen", + "Switch to app on left or above while using split screen", + ), + simpleShortcutCategory( + MultiTasking, + "Split screen", + "Use split screen with current app on the right", + ), + simpleShortcutCategory( + MultiTasking, + "Split screen", + "Switch to app on right or below while using split screen", + ), + simpleShortcutCategory(System, "System controls", "Show shortcuts"), + simpleShortcutCategory(System, "System controls", "View recent apps"), + simpleShortcutCategory(AppCategories, "Applications", "Calculator"), + simpleShortcutCategory(AppCategories, "Applications", "Calendar"), + simpleShortcutCategory(AppCategories, "Applications", "Browser"), + simpleShortcutCategory(AppCategories, "Applications", "Contacts"), + simpleShortcutCategory(AppCategories, "Applications", "Email"), + simpleShortcutCategory(AppCategories, "Applications", "Maps"), + simpleShortcutCategory(AppCategories, "Applications", "SMS"), + simpleShortcutCategory(MultiTasking, "Recent apps", "Cycle forward through recent apps"), + ) + val customInputGestureTypeHome = + simpleInputGestureData(keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_HOME) + + val allCustomizableInputGesturesWithSimpleShortcutCombinations = + listOf( + simpleInputGestureData( + keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT + ), + simpleInputGestureData(keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_HOME), + simpleInputGestureData( + keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS + ), + simpleInputGestureData(keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_LOCK_SCREEN), + simpleInputGestureData( + keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL + ), + simpleInputGestureData(keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_NOTES), + simpleInputGestureData( + keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT + ), + simpleInputGestureData(keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_BACK), + simpleInputGestureData( + keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION + ), + simpleInputGestureData( + keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT + ), + simpleInputGestureData( + keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_LEFT + ), + simpleInputGestureData( + keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT + ), + simpleInputGestureData( + keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_RIGHT + ), + simpleInputGestureData( + keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER + ), + simpleInputGestureData(keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS), + simpleInputGestureData( + keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALCULATOR + ), + simpleInputGestureData( + keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CALENDAR + ), + simpleInputGestureData( + keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_BROWSER + ), + simpleInputGestureData( + keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_CONTACTS + ), + simpleInputGestureData( + keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_EMAIL + ), + simpleInputGestureData( + keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MAPS + ), + simpleInputGestureData( + keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MESSAGING + ), + simpleInputGestureData( + keyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER + ), + ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractorTest.kt index 57c8b444b922..f7c77017e239 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractorTest.kt @@ -16,12 +16,24 @@ package com.android.systemui.keyboard.shortcut.domain.interactor +import android.content.Context +import android.content.Context.INPUT_SERVICE +import android.hardware.input.InputGestureData +import android.hardware.input.fakeInputManager +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyboard.shortcut.data.source.FakeKeyboardShortcutGroupsSource import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts +import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.allCustomizableInputGesturesWithSimpleShortcutCombinations +import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.customInputGestureTypeHome +import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.groupWithGoHomeShortcutInfo +import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.systemCategoryWithCustomHomeShortcut +import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.systemCategoryWithMergedGoHomeShortcut import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategory import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.InputMethodEditor import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.MultiTasking @@ -34,6 +46,8 @@ import com.android.systemui.keyboard.shortcut.shortcutHelperSystemShortcutsSourc import com.android.systemui.keyboard.shortcut.shortcutHelperTestHelper import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope +import com.android.systemui.settings.FakeUserTracker +import com.android.systemui.settings.userTracker import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -42,13 +56,18 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidJUnit4::class) class ShortcutHelperCategoriesInteractorTest : SysuiTestCase() { + private val mockUserContext: Context = mock() private val systemShortcutsSource = FakeKeyboardShortcutGroupsSource() private val multitaskingShortcutsSource = FakeKeyboardShortcutGroupsSource() + @OptIn(ExperimentalCoroutinesApi::class) private val kosmos = testKosmos().also { @@ -57,17 +76,23 @@ class ShortcutHelperCategoriesInteractorTest : SysuiTestCase() { it.shortcutHelperMultiTaskingShortcutsSource = multitaskingShortcutsSource it.shortcutHelperAppCategoriesShortcutsSource = FakeKeyboardShortcutGroupsSource() it.shortcutHelperCurrentAppShortcutsSource = FakeKeyboardShortcutGroupsSource() + it.userTracker = FakeUserTracker(onCreateCurrentUserContext = { mockUserContext }) } + private val fakeInputManager = kosmos.fakeInputManager private val testScope = kosmos.testScope - private val interactor = kosmos.shortcutHelperCategoriesInteractor + private lateinit var interactor: ShortcutHelperCategoriesInteractor private val helper = kosmos.shortcutHelperTestHelper + private val inter by lazy { kosmos.shortcutHelperCategoriesInteractor } @Before fun setShortcuts() { + interactor = kosmos.shortcutHelperCategoriesInteractor helper.setImeShortcuts(TestShortcuts.imeGroups) systemShortcutsSource.setGroups(TestShortcuts.systemGroups) multitaskingShortcutsSource.setGroups(TestShortcuts.multitaskingGroups) + whenever(mockUserContext.getSystemService(INPUT_SERVICE)) + .thenReturn(fakeInputManager.inputManager) } @Test @@ -120,7 +145,7 @@ class ShortcutHelperCategoriesInteractorTest : SysuiTestCase() { ShortcutCategory( type = InputMethodEditor, subCategories = - TestShortcuts.imeSubCategoriesWithGroupedDuplicatedShortcutLabels + TestShortcuts.imeSubCategoriesWithGroupedDuplicatedShortcutLabels, ), ) .inOrder() @@ -140,7 +165,7 @@ class ShortcutHelperCategoriesInteractorTest : SysuiTestCase() { ShortcutCategory( type = System, subCategories = - TestShortcuts.subCategoriesWithGroupedDuplicatedShortcutLabels + TestShortcuts.subCategoriesWithGroupedDuplicatedShortcutLabels, ), TestShortcuts.multitaskingCategory, TestShortcuts.imeCategory, @@ -163,7 +188,7 @@ class ShortcutHelperCategoriesInteractorTest : SysuiTestCase() { ShortcutCategory( type = MultiTasking, subCategories = - TestShortcuts.subCategoriesWithGroupedDuplicatedShortcutLabels + TestShortcuts.subCategoriesWithGroupedDuplicatedShortcutLabels, ), TestShortcuts.imeCategory, ) @@ -185,7 +210,7 @@ class ShortcutHelperCategoriesInteractorTest : SysuiTestCase() { ShortcutCategory( type = InputMethodEditor, subCategories = - TestShortcuts.imeSubCategoriesWithUnsupportedModifiersRemoved + TestShortcuts.imeSubCategoriesWithUnsupportedModifiersRemoved, ), ) .inOrder() @@ -203,7 +228,7 @@ class ShortcutHelperCategoriesInteractorTest : SysuiTestCase() { .containsExactly( ShortcutCategory( type = System, - subCategories = TestShortcuts.subCategoriesWithUnsupportedModifiersRemoved + subCategories = TestShortcuts.subCategoriesWithUnsupportedModifiersRemoved, ), TestShortcuts.multitaskingCategory, TestShortcuts.imeCategory, @@ -224,7 +249,7 @@ class ShortcutHelperCategoriesInteractorTest : SysuiTestCase() { TestShortcuts.systemCategory, ShortcutCategory( type = MultiTasking, - subCategories = TestShortcuts.subCategoriesWithUnsupportedModifiersRemoved + subCategories = TestShortcuts.subCategoriesWithUnsupportedModifiersRemoved, ), TestShortcuts.imeCategory, ) @@ -240,10 +265,7 @@ class ShortcutHelperCategoriesInteractorTest : SysuiTestCase() { helper.showFromActivity() assertThat(categories) - .containsExactly( - TestShortcuts.multitaskingCategory, - TestShortcuts.imeCategory, - ) + .containsExactly(TestShortcuts.multitaskingCategory, TestShortcuts.imeCategory) .inOrder() } @@ -256,10 +278,63 @@ class ShortcutHelperCategoriesInteractorTest : SysuiTestCase() { helper.showFromActivity() assertThat(categories) + .containsExactly(TestShortcuts.systemCategory, TestShortcuts.imeCategory) + .inOrder() + } + + @Test + @DisableFlags(Flags.FLAG_KEYBOARD_SHORTCUT_HELPER_SHORTCUT_CUSTOMIZER) + fun categories_excludesCustomShortcutsWhenFlagIsOff() { + testScope.runTest { + setCustomInputGestures(allCustomizableInputGesturesWithSimpleShortcutCombinations) + helper.showFromActivity() + val categories by collectLastValue(interactor.shortcutCategories) + assertThat(categories) .containsExactly( TestShortcuts.systemCategory, + TestShortcuts.multitaskingCategory, + TestShortcuts.imeCategory, + ) + } + } + + @Test + @EnableFlags(Flags.FLAG_KEYBOARD_SHORTCUT_HELPER_SHORTCUT_CUSTOMIZER) + fun categories_includesCustomShortcutsWhenFlagIsOn() { + testScope.runTest { + setCustomInputGestures(listOf(customInputGestureTypeHome)) + helper.showFromActivity() + val categories by collectLastValue(interactor.shortcutCategories) + assertThat(categories) + .containsExactly( + systemCategoryWithCustomHomeShortcut, + TestShortcuts.multitaskingCategory, + TestShortcuts.imeCategory, + ) + } + } + + @Test + @EnableFlags(Flags.FLAG_KEYBOARD_SHORTCUT_HELPER_SHORTCUT_CUSTOMIZER) + fun categories_correctlyMergesDefaultAndCustomShortcutsOfSameType() { + testScope.runTest { + setCustomInputGestures(listOf(customInputGestureTypeHome)) + systemShortcutsSource.setGroups(groupWithGoHomeShortcutInfo) + helper.showFromActivity() + + val categories by collectLastValue(interactor.shortcutCategories) + + assertThat(categories) + .containsExactly( + systemCategoryWithMergedGoHomeShortcut, + TestShortcuts.multitaskingCategory, TestShortcuts.imeCategory, ) - .inOrder() } + } + + private fun setCustomInputGestures(customInputGestures: List<InputGestureData>) { + whenever(fakeInputManager.inputManager.getCustomInputGestures(/* filter= */ anyOrNull())) + .thenReturn(customInputGestures) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperDialogStarterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperDialogStarterTest.kt index 1580ea5c1272..000024f9b814 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperDialogStarterTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/ui/ShortcutHelperDialogStarterTest.kt @@ -16,6 +16,9 @@ package com.android.systemui.keyboard.shortcut.ui +import android.content.Context +import android.content.Context.INPUT_SERVICE +import android.hardware.input.fakeInputManager import androidx.test.annotation.UiThreadTest import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -36,6 +39,8 @@ import com.android.systemui.kosmos.testCase import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.activityStarter +import com.android.systemui.settings.FakeUserTracker +import com.android.systemui.settings.userTracker import com.android.systemui.statusbar.phone.systemUIDialogFactory import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -44,6 +49,8 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @@ -52,7 +59,7 @@ class ShortcutHelperDialogStarterTest : SysuiTestCase() { private val fakeSystemSource = FakeKeyboardShortcutGroupsSource() private val fakeMultiTaskingSource = FakeKeyboardShortcutGroupsSource() - + private val mockUserContext: Context = mock() private val kosmos = Kosmos().also { it.testCase = this @@ -62,8 +69,10 @@ class ShortcutHelperDialogStarterTest : SysuiTestCase() { it.shortcutHelperAppCategoriesShortcutsSource = FakeKeyboardShortcutGroupsSource() it.shortcutHelperInputShortcutsSource = FakeKeyboardShortcutGroupsSource() it.shortcutHelperCurrentAppShortcutsSource = FakeKeyboardShortcutGroupsSource() + it.userTracker = FakeUserTracker(onCreateCurrentUserContext = { mockUserContext }) } + private val inputManager = kosmos.fakeInputManager.inputManager private val testScope = kosmos.testScope private val testHelper = kosmos.shortcutHelperTestHelper private val dialogFactory = kosmos.systemUIDialogFactory @@ -85,6 +94,7 @@ class ShortcutHelperDialogStarterTest : SysuiTestCase() { fun setUp() { fakeSystemSource.setGroups(TestShortcuts.systemGroups) fakeMultiTaskingSource.setGroups(TestShortcuts.multitaskingGroups) + whenever(mockUserContext.getSystemService(INPUT_SERVICE)).thenReturn(inputManager) } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/binder/WindowManagerLockscreenVisibilityManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/binder/WindowManagerLockscreenVisibilityManagerTest.kt index 92764ae94271..74a0bafda931 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/binder/WindowManagerLockscreenVisibilityManagerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/binder/WindowManagerLockscreenVisibilityManagerTest.kt @@ -17,6 +17,10 @@ package com.android.systemui.keyguard.ui.binder import android.app.IActivityTaskManager +import android.platform.test.annotations.RequiresFlagsDisabled +import android.platform.test.annotations.RequiresFlagsEnabled +import android.platform.test.flag.junit.CheckFlagsRule +import android.platform.test.flag.junit.DeviceFlagsValueProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -25,8 +29,10 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardDismissTransition import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.time.FakeSystemClock +import com.android.window.flags.Flags import com.android.wm.shell.keyguard.KeyguardTransitions import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.eq @@ -41,6 +47,9 @@ import org.mockito.kotlin.any @RunWith(AndroidJUnit4::class) @kotlinx.coroutines.ExperimentalCoroutinesApi class WindowManagerLockscreenVisibilityManagerTest : SysuiTestCase() { + + @get:Rule val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() + private lateinit var underTest: WindowManagerLockscreenVisibilityManager private lateinit var executor: FakeExecutor @@ -68,32 +77,62 @@ class WindowManagerLockscreenVisibilityManagerTest : SysuiTestCase() { } @Test - fun testLockscreenVisible_andAodVisible() { + @RequiresFlagsDisabled(Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING) + fun testLockscreenVisible_andAodVisible_without_keyguard_shell_transitions() { underTest.setLockscreenShown(true) - underTest.setAodVisible(true) - verify(activityTaskManagerService).setLockScreenShown(true, false) + underTest.setAodVisible(true) verify(activityTaskManagerService).setLockScreenShown(true, true) + verifyNoMoreInteractions(activityTaskManagerService) } @Test - fun testGoingAway_whenLockscreenVisible_thenSurfaceMadeVisible() { + @RequiresFlagsEnabled(Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING) + fun testLockscreenVisible_andAodVisible_with_keyguard_shell_transitions() { underTest.setLockscreenShown(true) + verify(keyguardTransitions).startKeyguardTransition(true, false) underTest.setAodVisible(true) + verify(keyguardTransitions).startKeyguardTransition(true, true) + verifyNoMoreInteractions(keyguardTransitions) + } + + @Test + @RequiresFlagsDisabled(Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING) + fun testGoingAway_whenLockscreenVisible_thenSurfaceMadeVisible_without_keyguard_shell_transitions() { + underTest.setLockscreenShown(true) verify(activityTaskManagerService).setLockScreenShown(true, false) + underTest.setAodVisible(true) verify(activityTaskManagerService).setLockScreenShown(true, true) + verifyNoMoreInteractions(activityTaskManagerService) underTest.setSurfaceBehindVisibility(true) - verify(activityTaskManagerService).keyguardGoingAway(anyInt()) + verifyNoMoreInteractions(activityTaskManagerService) } @Test - fun testSurfaceVisible_whenLockscreenNotShowing_doesNotTriggerGoingAway() { + @RequiresFlagsEnabled(Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING) + fun testGoingAway_whenLockscreenVisible_thenSurfaceMadeVisible_with_keyguard_shell_transitions() { + underTest.setLockscreenShown(true) + verify(keyguardTransitions).startKeyguardTransition(true, false) + underTest.setAodVisible(true) + verify(keyguardTransitions).startKeyguardTransition(true, true) + + verifyNoMoreInteractions(keyguardTransitions) + + underTest.setSurfaceBehindVisibility(true) + verify(keyguardTransitions).startKeyguardTransition(false, false) + + verifyNoMoreInteractions(keyguardTransitions) + } + + @Test + @RequiresFlagsDisabled(Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING) + fun testSurfaceVisible_whenLockscreenNotShowing_doesNotTriggerGoingAway_without_keyguard_shell_transitions() { underTest.setLockscreenShown(false) underTest.setAodVisible(false) @@ -106,7 +145,22 @@ class WindowManagerLockscreenVisibilityManagerTest : SysuiTestCase() { } @Test - fun testAodVisible_noLockscreenShownCallYet_doesNotShowLockscreenUntilLater() { + @RequiresFlagsEnabled(Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING) + fun testSurfaceVisible_whenLockscreenNotShowing_doesNotTriggerGoingAway_with_keyguard_shell_transitions() { + underTest.setLockscreenShown(false) + underTest.setAodVisible(false) + + verify(keyguardTransitions).startKeyguardTransition(false, false) + verifyNoMoreInteractions(keyguardTransitions) + + underTest.setSurfaceBehindVisibility(true) + + verifyNoMoreInteractions(keyguardTransitions) + } + + @Test + @RequiresFlagsDisabled(Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING) + fun testAodVisible_noLockscreenShownCallYet_doesNotShowLockscreenUntilLater_without_keyguard_shell_transitions() { underTest.setAodVisible(false) verifyNoMoreInteractions(activityTaskManagerService) @@ -116,7 +170,19 @@ class WindowManagerLockscreenVisibilityManagerTest : SysuiTestCase() { } @Test - fun setSurfaceBehindVisibility_goesAwayFirst_andIgnoresSecondCall() { + @RequiresFlagsEnabled(Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING) + fun testAodVisible_noLockscreenShownCallYet_doesNotShowLockscreenUntilLater_with_keyguard_shell_transitions() { + underTest.setAodVisible(false) + verifyNoMoreInteractions(keyguardTransitions) + + underTest.setLockscreenShown(true) + verify(keyguardTransitions).startKeyguardTransition(true, false) + verifyNoMoreInteractions(activityTaskManagerService) + } + + @Test + @RequiresFlagsDisabled(Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING) + fun setSurfaceBehindVisibility_goesAwayFirst_andIgnoresSecondCall_without_keyguard_shell_transitions() { underTest.setLockscreenShown(true) underTest.setSurfaceBehindVisibility(true) verify(activityTaskManagerService).keyguardGoingAway(0) @@ -126,8 +192,27 @@ class WindowManagerLockscreenVisibilityManagerTest : SysuiTestCase() { } @Test - fun setSurfaceBehindVisibility_falseSetsLockscreenVisibility() { + @RequiresFlagsEnabled(Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING) + fun setSurfaceBehindVisibility_goesAwayFirst_andIgnoresSecondCall_with_keyguard_shell_transitions() { + underTest.setLockscreenShown(true) + underTest.setSurfaceBehindVisibility(true) + verify(keyguardTransitions).startKeyguardTransition(false, false) + + underTest.setSurfaceBehindVisibility(true) + verifyNoMoreInteractions(keyguardTransitions) + } + + @Test + @RequiresFlagsDisabled(Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING) + fun setSurfaceBehindVisibility_falseSetsLockscreenVisibility_without_keyguard_shell_transitions() { underTest.setSurfaceBehindVisibility(false) verify(activityTaskManagerService).setLockScreenShown(eq(true), any()) } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_ENSURE_KEYGUARD_DOES_TRANSITION_STARTING) + fun setSurfaceBehindVisibility_falseSetsLockscreenVisibility_with_keyguard_shell_transitions() { + underTest.setSurfaceBehindVisibility(false) + verify(keyguardTransitions).startKeyguardTransition(eq(true), any()) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSectionTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSectionTest.kt index d94c97af6f14..c0db95f9e5d2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSectionTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/view/layout/sections/SmartspaceSectionTest.kt @@ -34,6 +34,7 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteract import com.android.systemui.keyguard.domain.interactor.KeyguardSmartspaceInteractor import com.android.systemui.keyguard.ui.viewmodel.KeyguardClockViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel +import com.android.systemui.res.R import com.android.systemui.shared.R as sharedR import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController import com.android.systemui.util.mockito.any @@ -68,6 +69,7 @@ class SmartspaceSectionTest : SysuiTestCase() { private val clockShouldBeCentered = MutableStateFlow(false) private val hasCustomWeatherDataDisplay = MutableStateFlow(false) private val isWeatherVisibleFlow = MutableStateFlow(false) + private val isShadeLayoutWide = MutableStateFlow(false) @Before fun setup() { @@ -80,7 +82,7 @@ class SmartspaceSectionTest : SysuiTestCase() { keyguardSmartspaceInteractor, lockscreenSmartspaceController, keyguardUnlockAnimationController, - blueprintInteractor + blueprintInteractor, ) constraintLayout = ConstraintLayout(mContext) whenever(lockscreenSmartspaceController.buildAndConnectView(any())) @@ -93,6 +95,7 @@ class SmartspaceSectionTest : SysuiTestCase() { whenever(keyguardClockViewModel.clockShouldBeCentered).thenReturn(clockShouldBeCentered) whenever(keyguardSmartspaceViewModel.isSmartspaceEnabled).thenReturn(true) whenever(keyguardSmartspaceViewModel.isWeatherVisible).thenReturn(isWeatherVisibleFlow) + whenever(keyguardSmartspaceViewModel.isShadeLayoutWide).thenReturn(isShadeLayoutWide) constraintSet = ConstraintSet() } @@ -125,6 +128,26 @@ class SmartspaceSectionTest : SysuiTestCase() { } @Test + fun testConstraintsWhenShadeLayoutIsNotWide() { + underTest.addViews(constraintLayout) + underTest.applyConstraints(constraintSet) + + val smartspaceConstraints = constraintSet.getConstraint(smartspaceView.id) + assertThat(smartspaceConstraints.layout.endToEnd).isEqualTo(ConstraintSet.PARENT_ID) + } + + @Test + fun testConstraintsWhenShadeLayoutIsWide() { + isShadeLayoutWide.value = true + + underTest.addViews(constraintLayout) + underTest.applyConstraints(constraintSet) + + val smartspaceConstraints = constraintSet.getConstraint(smartspaceView.id) + assertThat(smartspaceConstraints.layout.endToEnd).isEqualTo(R.id.split_shade_guideline) + } + + @Test fun testConstraintsWhenNotHasCustomWeatherDataDisplay() { whenever(keyguardSmartspaceViewModel.isDateWeatherDecoupled).thenReturn(true) underTest.addViews(constraintLayout) @@ -160,6 +183,7 @@ class SmartspaceSectionTest : SysuiTestCase() { assertThat(constraintSet.getVisibility(weatherView.id)).isEqualTo(GONE) assertThat(constraintSet.getVisibility(dateView.id)).isEqualTo(VISIBLE) } + @Test fun testCustomDateWeatherVisibility() { hasCustomWeatherDataDisplay.value = true diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryForegroundViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryForegroundViewModelTest.kt index b0959e4eea0b..d42b538cf355 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryForegroundViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryForegroundViewModelTest.kt @@ -27,10 +27,13 @@ import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepos import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.kosmos.testScope +import com.android.systemui.res.R import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -43,6 +46,16 @@ class DeviceEntryForegroundViewModelTest : SysuiTestCase() { private val underTest: DeviceEntryForegroundViewModel = kosmos.deviceEntryForegroundIconViewModel + @Before + fun setup() { + context.orCreateTestableResources.addOverride(R.integer.udfps_padding_debounce_duration, 0) + } + + @After + fun teardown() { + context.orCreateTestableResources.removeOverride(R.integer.udfps_padding_debounce_duration) + } + @Test fun aodIconColorWhite() = testScope.runTest { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModelTest.kt index 0c3fcb3ef759..adce9d65cbe0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModelTest.kt @@ -26,6 +26,7 @@ import com.android.systemui.keyguard.data.repository.keyguardSmartspaceRepositor import com.android.systemui.keyguard.shared.model.ClockSize import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.clocks.ClockController +import com.android.systemui.shade.data.repository.shadeRepository import com.android.systemui.testKosmos import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat @@ -96,4 +97,26 @@ class KeyguardSmartspaceViewModelTest : SysuiTestCase() { assertThat(isWeatherVisible).isEqualTo(false) } + + @Test + fun isShadeLayoutWide_withConfigTrue_true() = + with(kosmos) { + testScope.runTest { + val isShadeLayoutWide by collectLastValue(underTest.isShadeLayoutWide) + shadeRepository.setShadeLayoutWide(true) + + assertThat(isShadeLayoutWide).isTrue() + } + } + + @Test + fun isShadeLayoutWide_withConfigFalse_false() = + with(kosmos) { + testScope.runTest { + val isShadeLayoutWide by collectLastValue(underTest.isShadeLayoutWide) + shadeRepository.setShadeLayoutWide(false) + + assertThat(isShadeLayoutWide).isFalse() + } + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/FakeNoteTaskBubbleController.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/FakeNoteTaskBubbleController.kt index 450aadd70171..ebc00c3897cb 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/FakeNoteTaskBubbleController.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/FakeNoteTaskBubbleController.kt @@ -20,6 +20,7 @@ import android.content.Context import android.content.Intent import android.graphics.drawable.Icon import android.os.UserHandle +import com.android.wm.shell.bubbles.Bubble import com.android.wm.shell.bubbles.Bubbles import java.util.Optional import kotlinx.coroutines.CoroutineDispatcher @@ -33,14 +34,29 @@ import kotlinx.coroutines.CoroutineDispatcher class FakeNoteTaskBubbleController( unUsed1: Context, unsUsed2: CoroutineDispatcher, - private val optionalBubbles: Optional<Bubbles> + private val optionalBubbles: Optional<Bubbles>, ) : NoteTaskBubblesController(unUsed1, unsUsed2) { override suspend fun areBubblesAvailable() = optionalBubbles.isPresent - override suspend fun showOrHideAppBubble(intent: Intent, userHandle: UserHandle, icon: Icon) { + override suspend fun showOrHideAppBubble( + intent: Intent, + userHandle: UserHandle, + icon: Icon, + bubbleExpandBehavior: NoteTaskBubbleExpandBehavior, + ) { optionalBubbles.ifPresentOrElse( - { bubbles -> bubbles.showOrHideAppBubble(intent, userHandle, icon) }, - { throw IllegalAccessException() } + { bubbles -> + if ( + bubbleExpandBehavior == NoteTaskBubbleExpandBehavior.KEEP_IF_EXPANDED && + bubbles.isBubbleExpanded( + Bubble.getAppBubbleKeyForApp(intent.`package`, userHandle) + ) + ) { + return@ifPresentOrElse + } + bubbles.showOrHideAppBubble(intent, userHandle, icon) + }, + { throw IllegalAccessException() }, ) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/NoteTaskBubblesServiceTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/NoteTaskBubblesServiceTest.kt index 9ef6b9c13315..e55d6ad6c5a0 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/NoteTaskBubblesServiceTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/NoteTaskBubblesServiceTest.kt @@ -21,9 +21,9 @@ import android.graphics.drawable.Icon import android.os.UserHandle import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.systemui.res.R import com.android.systemui.SysuiTestCase import com.android.systemui.notetask.NoteTaskBubblesController.NoteTaskBubblesService +import com.android.systemui.res.R import com.android.wm.shell.bubbles.Bubbles import com.google.common.truth.Truth.assertThat import java.util.Optional @@ -33,6 +33,9 @@ import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations +import org.mockito.kotlin.any +import org.mockito.kotlin.never +import org.mockito.kotlin.whenever /** atest SystemUITests:NoteTaskBubblesServiceTest */ @SmallTest @@ -61,12 +64,40 @@ internal class NoteTaskBubblesServiceTest : SysuiTestCase() { } @Test - fun showOrHideAppBubble() { + fun showOrHideAppBubble_defaultExpandBehavior_shouldCallBubblesApi() { val intent = Intent() val user = UserHandle.SYSTEM val icon = Icon.createWithResource(context, R.drawable.ic_note_task_shortcut_widget) + val bubbleExpandBehavior = NoteTaskBubbleExpandBehavior.DEFAULT + whenever(bubbles.isBubbleExpanded(any())).thenReturn(false) + + createServiceBinder().showOrHideAppBubble(intent, user, icon, bubbleExpandBehavior) + + verify(bubbles).showOrHideAppBubble(intent, user, icon) + } + + @Test + fun showOrHideAppBubble_keepIfExpanded_bubbleShown_shouldNotCallBubblesApi() { + val intent = Intent().apply { setPackage("test") } + val user = UserHandle.SYSTEM + val icon = Icon.createWithResource(context, R.drawable.ic_note_task_shortcut_widget) + val bubbleExpandBehavior = NoteTaskBubbleExpandBehavior.KEEP_IF_EXPANDED + whenever(bubbles.isBubbleExpanded(any())).thenReturn(true) + + createServiceBinder().showOrHideAppBubble(intent, user, icon, bubbleExpandBehavior) + + verify(bubbles, never()).showOrHideAppBubble(intent, user, icon) + } + + @Test + fun showOrHideAppBubble_keepIfExpanded_bubbleNotShown_shouldCallBubblesApi() { + val intent = Intent().apply { setPackage("test") } + val user = UserHandle.SYSTEM + val icon = Icon.createWithResource(context, R.drawable.ic_note_task_shortcut_widget) + val bubbleExpandBehavior = NoteTaskBubbleExpandBehavior.KEEP_IF_EXPANDED + whenever(bubbles.isBubbleExpanded(any())).thenReturn(false) - createServiceBinder().showOrHideAppBubble(intent, user, icon) + createServiceBinder().showOrHideAppBubble(intent, user, icon, bubbleExpandBehavior) verify(bubbles).showOrHideAppBubble(intent, user, icon) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/NoteTaskInfoTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/NoteTaskInfoTest.kt index 8f4078b88fc0..d3578fb9f69b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/NoteTaskInfoTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/notetask/NoteTaskInfoTest.kt @@ -19,6 +19,7 @@ import android.os.UserHandle import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.notetask.NoteTaskEntryPoint.QS_NOTES_TILE import com.android.systemui.notetask.NoteTaskEntryPoint.WIDGET_PICKER_SHORTCUT_IN_MULTI_WINDOW_MODE import com.google.common.truth.Truth.assertThat import org.junit.Test @@ -44,10 +45,19 @@ internal class NoteTaskInfoTest : SysuiTestCase() { } @Test - fun launchMode_keyguardUnlocked_launchModeAppBubble() { + fun launchMode_keyguardUnlocked_launchModeAppBubble_withDefaultExpandBehavior() { val underTest = DEFAULT_INFO.copy(isKeyguardLocked = false) - assertThat(underTest.launchMode).isEqualTo(NoteTaskLaunchMode.AppBubble) + assertThat(underTest.launchMode) + .isEqualTo(NoteTaskLaunchMode.AppBubble(NoteTaskBubbleExpandBehavior.DEFAULT)) + } + + @Test + fun launchMode_keyguardUnlocked_qsTileEntryPoint_launchModeAppBubble_withKeepIfExpandedExpandBehavior() { + val underTest = DEFAULT_INFO.copy(isKeyguardLocked = false, entryPoint = QS_NOTES_TILE) + + assertThat(underTest.launchMode) + .isEqualTo(NoteTaskLaunchMode.AppBubble(NoteTaskBubbleExpandBehavior.KEEP_IF_EXPANDED)) } private companion object { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java index 83f95eaf4cda..e9633f49f76d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSPanelControllerBaseTest.java @@ -21,9 +21,6 @@ import static com.android.systemui.flags.SceneContainerFlagParameterizationKt.pa import static com.google.common.truth.Truth.assertThat; -import static kotlinx.coroutines.flow.FlowKt.asStateFlow; -import static kotlinx.coroutines.flow.StateFlowKt.MutableStateFlow; - import static org.junit.Assert.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; @@ -39,6 +36,9 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static kotlinx.coroutines.flow.FlowKt.asStateFlow; +import static kotlinx.coroutines.flow.StateFlowKt.MutableStateFlow; + import android.content.res.Configuration; import android.content.res.Resources; import android.platform.test.annotations.DisableFlags; @@ -70,9 +70,6 @@ import com.android.systemui.scene.shared.flag.SceneContainerFlag; import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController; import com.android.systemui.util.animation.DisappearParameters; -import kotlinx.coroutines.flow.MutableStateFlow; -import kotlinx.coroutines.flow.StateFlow; - import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -88,6 +85,8 @@ import java.util.List; import javax.inject.Provider; +import kotlinx.coroutines.flow.MutableStateFlow; +import kotlinx.coroutines.flow.StateFlow; import platform.test.runner.parameterized.ParameterizedAndroidJunit4; import platform.test.runner.parameterized.Parameters; @@ -232,6 +231,7 @@ public class QSPanelControllerBaseTest extends SysuiTestCase { @After public void tearDown() { + mController.destroy(); disallowTestableLooperAsMainThread(); } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSPanelControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSPanelControllerTest.kt index 02c5b5ad214c..96f6a622e2f3 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSPanelControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSPanelControllerTest.kt @@ -1,10 +1,10 @@ package com.android.systemui.qs import android.content.res.Configuration -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.SmallTest import android.testing.TestableResources import android.view.ContextThemeWrapper +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest import com.android.internal.logging.MetricsLogger import com.android.internal.logging.UiEventLogger import com.android.systemui.SysuiTestCase @@ -37,8 +37,8 @@ import org.mockito.Mockito.any import org.mockito.Mockito.never import org.mockito.Mockito.reset import org.mockito.Mockito.verify -import org.mockito.MockitoAnnotations import org.mockito.Mockito.`when` as whenever +import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidJUnit4::class) @@ -81,37 +81,39 @@ class QSPanelControllerTest : SysuiTestCase() { setShouldUseSplitShade(false) whenever(qsPanel.resources).thenReturn(testableResources.resources) whenever(qsPanel.context) - .thenReturn( ContextThemeWrapper(context, R.style.Theme_SystemUI_QuickSettings)) + .thenReturn(ContextThemeWrapper(context, R.style.Theme_SystemUI_QuickSettings)) whenever(qsPanel.getOrCreateTileLayout()).thenReturn(pagedTileLayout) whenever(statusBarKeyguardViewManager.isPrimaryBouncerInTransit()).thenReturn(false) whenever(qsPanel.setListening(anyBoolean())).then { whenever(qsPanel.isListening).thenReturn(it.getArgument(0)) } - controller = QSPanelController( - qsPanel, - tunerService, - qsHost, - qsCustomizerController, - /* usingMediaPlayer= */ usingMediaPlayer, - mediaHost, - qsTileRevealControllerFactory, - dumpManager, - metricsLogger, - uiEventLogger, - qsLogger, - brightnessControllerFactory, - brightnessSliderFactory, - falsingManager, - statusBarKeyguardViewManager, - ResourcesSplitShadeStateController(), - longPressEffectProvider, - mediaCarouselInteractor, - ) + controller = + QSPanelController( + qsPanel, + tunerService, + qsHost, + qsCustomizerController, + /* usingMediaPlayer= */ usingMediaPlayer, + mediaHost, + qsTileRevealControllerFactory, + dumpManager, + metricsLogger, + uiEventLogger, + qsLogger, + brightnessControllerFactory, + brightnessSliderFactory, + falsingManager, + statusBarKeyguardViewManager, + ResourcesSplitShadeStateController(), + longPressEffectProvider, + mediaCarouselInteractor, + ) } @After fun tearDown() { + controller.destroy() reset(mediaHost) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt index 369bb228494c..7880aceb53be 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QuickQSPanelControllerTest.kt @@ -34,6 +34,7 @@ import com.android.systemui.qs.logging.QSLogger import com.android.systemui.res.R import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController import com.android.systemui.util.leak.RotationUtils +import javax.inject.Provider import org.junit.After import org.junit.Before import org.junit.Test @@ -45,9 +46,8 @@ import org.mockito.Mockito.any import org.mockito.Mockito.reset import org.mockito.Mockito.times import org.mockito.Mockito.verify -import org.mockito.MockitoAnnotations -import javax.inject.Provider import org.mockito.Mockito.`when` as whenever +import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidJUnit4::class) @@ -108,6 +108,7 @@ class QuickQSPanelControllerTest : SysuiTestCase() { @After fun tearDown() { controller.onViewDetached() + controller.destroy() } @Test @@ -184,7 +185,7 @@ class QuickQSPanelControllerTest : SysuiTestCase() { dumpManager, ResourcesSplitShadeStateController(), longPressEffectProvider, - mediaCarouselInteractor + mediaCarouselInteractor, ) { private var rotation = RotationUtils.ROTATION_NONE diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt index 2e074da02103..48deee531bd9 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt @@ -2532,6 +2532,64 @@ class SceneContainerStartableTest : SysuiTestCase() { assertThat(isAlternateBouncerVisible).isFalse() } + @Test + fun replacesLockscreenSceneOnBackStack_whenFaceUnlocked_fromShade_noAlternateBouncer() = + testScope.runTest { + val transitionState = + prepareState( + isDeviceUnlocked = false, + initialSceneKey = Scenes.Lockscreen, + authenticationMethod = AuthenticationMethodModel.Pin, + ) + underTest.start() + + val isUnlocked by + collectLastValue( + kosmos.deviceUnlockedInteractor.deviceUnlockStatus.map { it.isUnlocked } + ) + val currentScene by collectLastValue(sceneInteractor.currentScene) + val backStack by collectLastValue(sceneBackInteractor.backStack) + val isAlternateBouncerVisible by + collectLastValue(kosmos.alternateBouncerInteractor.isVisible) + assertThat(isUnlocked).isFalse() + assertThat(currentScene).isEqualTo(Scenes.Lockscreen) + assertThat(isAlternateBouncerVisible).isFalse() + + // Change to shade. + sceneInteractor.changeScene(Scenes.Shade, "") + transitionState.value = ObservableTransitionState.Idle(Scenes.Shade) + runCurrent() + assertThat(isUnlocked).isFalse() + assertThat(currentScene).isEqualTo(Scenes.Shade) + assertThat(backStack?.asIterable()?.first()).isEqualTo(Scenes.Lockscreen) + assertThat(isAlternateBouncerVisible).isFalse() + + // Show the alternate bouncer. + kosmos.alternateBouncerInteractor.forceShow() + kosmos.sysuiStatusBarStateController.leaveOpen = true // leave shade open + runCurrent() + assertThat(isUnlocked).isFalse() + assertThat(currentScene).isEqualTo(Scenes.Shade) + assertThat(backStack?.asIterable()?.first()).isEqualTo(Scenes.Lockscreen) + assertThat(isAlternateBouncerVisible).isTrue() + + // Simulate race condition by hiding the alternate bouncer *before* the face unlock: + kosmos.alternateBouncerInteractor.hide() + runCurrent() + assertThat(isUnlocked).isFalse() + assertThat(currentScene).isEqualTo(Scenes.Shade) + assertThat(backStack?.asIterable()?.first()).isEqualTo(Scenes.Lockscreen) + assertThat(isAlternateBouncerVisible).isFalse() + + // Trigger a face unlock. + updateFaceAuthStatus(isSuccess = true) + runCurrent() + assertThat(isUnlocked).isTrue() + assertThat(currentScene).isEqualTo(Scenes.Shade) + assertThat(backStack?.asIterable()?.first()).isEqualTo(Scenes.Gone) + assertThat(isAlternateBouncerVisible).isFalse() + } + private fun TestScope.emulateSceneTransition( transitionStateFlow: MutableStateFlow<ObservableTransitionState>, toScene: SceneKey, @@ -2768,15 +2826,16 @@ class SceneContainerStartableTest : SysuiTestCase() { } private fun updateFaceAuthStatus(isSuccess: Boolean) { - if (isSuccess) { - kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus( - SuccessFaceAuthenticationStatus( - successResult = Mockito.mock(FaceManager.AuthenticationResult::class.java) - ) - ) - } else { - kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus( - FailedFaceAuthenticationStatus() + with(kosmos.fakeDeviceEntryFaceAuthRepository) { + isAuthenticated.value = isSuccess + setAuthenticationStatus( + if (isSuccess) { + SuccessFaceAuthenticationStatus( + successResult = Mockito.mock(FaceManager.AuthenticationResult::class.java) + ) + } else { + FailedFaceAuthenticationStatus() + } ) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarIconViewTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarIconViewTest.java index fb7252b24295..60a185537b0d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarIconViewTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarIconViewTest.java @@ -16,8 +16,6 @@ package com.android.systemui.statusbar; -import static android.app.Notification.CATEGORY_CALL; - import static com.google.common.truth.Truth.assertThat; import static junit.framework.Assert.assertEquals; @@ -199,19 +197,6 @@ public class StatusBarIconViewTest extends SysuiTestCase { } @Test - public void testContentDescForNotification_noNotifContent() { - Notification n = new Notification.Builder(mContext, "test") - .setSmallIcon(0) - .setContentTitle("hello") - .setCategory(CATEGORY_CALL) - .build(); - assertThat(NotificationContentDescription.contentDescForNotification(mContext, n) - .toString()).startsWith("com.android.systemui.tests notification"); - assertThat(NotificationContentDescription.contentDescForNotification(mContext, n) - .toString()).doesNotContain("hello"); - } - - @Test @EnableFlags({Flags.FLAG_MODES_UI, Flags.FLAG_MODES_UI_ICONS}) public void setIcon_withPreloaded_usesPreloaded() { Icon mockIcon = mock(Icon.class); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSlidersInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSlidersInteractorTest.kt index 7c5a48728a11..3f995c69c32f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSlidersInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSlidersInteractorTest.kt @@ -155,6 +155,30 @@ class VolumeDialogSlidersInteractorTest : SysuiTestCase() { } } + @Test + fun activeStreamChanges_showBoth() { + with(kosmos) { + testScope.runTest { + runCurrent() + fakeVolumeDialogController.updateState { + activeStream = AudioManager.STREAM_SYSTEM + states.put(AudioManager.STREAM_MUSIC, buildStreamState()) + states.put(AudioManager.STREAM_SYSTEM, buildStreamState()) + } + val slidersModel by collectLastValue(underTest.sliders) + runCurrent() + + fakeVolumeDialogController.updateState { activeStream = AudioManager.STREAM_MUSIC } + runCurrent() + + assertThat(slidersModel!!.slider) + .isEqualTo(VolumeDialogSliderType.Stream(AudioManager.STREAM_MUSIC)) + assertThat(slidersModel!!.floatingSliders) + .containsExactly(VolumeDialogSliderType.Stream(AudioManager.STREAM_SYSTEM)) + } + } + } + private fun buildStreamState( build: VolumeDialogController.StreamState.() -> Unit = {} ): VolumeDialogController.StreamState { diff --git a/packages/SystemUI/res/values-land/dimens.xml b/packages/SystemUI/res/values-land/dimens.xml index 235015b5286f..70ae5c1832af 100644 --- a/packages/SystemUI/res/values-land/dimens.xml +++ b/packages/SystemUI/res/values-land/dimens.xml @@ -91,7 +91,6 @@ <dimen name="notification_blocker_channel_list_height">128dp</dimen> <dimen name="keyguard_indication_margin_bottom">8dp</dimen> - <dimen name="lock_icon_margin_bottom">24dp</dimen> <!-- Keyboard shortcuts helper --> <dimen name="ksh_container_horizontal_margin">48dp</dimen> diff --git a/packages/SystemUI/res/values-sw600dp-land/dimens.xml b/packages/SystemUI/res/values-sw600dp-land/dimens.xml index 75bee9f9266a..055c3a641d1b 100644 --- a/packages/SystemUI/res/values-sw600dp-land/dimens.xml +++ b/packages/SystemUI/res/values-sw600dp-land/dimens.xml @@ -20,7 +20,6 @@ <!-- keyguard--> <dimen name="keyguard_indication_margin_bottom">25dp</dimen> <dimen name="ambient_indication_margin_bottom">115dp</dimen> - <dimen name="lock_icon_margin_bottom">60dp</dimen> <!-- margin from keyguard status bar to clock. For split shade it should be keyguard_split_shade_top_margin - status_bar_header_height_keyguard = 8dp --> <dimen name="keyguard_clock_top_margin">8dp</dimen> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 580f6d302b37..8cab15506d20 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -948,7 +948,6 @@ <dimen name="keyguard_translate_distance_on_swipe_up">-200dp</dimen> <dimen name="keyguard_indication_margin_bottom">32dp</dimen> - <dimen name="lock_icon_margin_bottom">74dp</dimen> <dimen name="ambient_indication_margin_bottom">71dp</dimen> @@ -2057,6 +2056,9 @@ <!-- UDFPS view attributes --> <!-- UDFPS icon size in microns/um --> <dimen name="udfps_icon_size" format="float">6000</dimen> + <!-- Limits the updates to at most one update per debounce duration to avoid too many + updates due to quick changes to padding. --> + <integer name="udfps_padding_debounce_duration">100</integer> <!-- Microns/ums (1000 um = 1mm) per pixel for the given device. If unspecified, UI that relies on this value will not be sized correctly. --> <item name="pixel_pitch" format="float" type="dimen">-1</item> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 245ba0aca876..c3f4222a5eb8 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -3214,6 +3214,9 @@ <!-- Text to display when copying the build number off QS [CHAR LIMIT=NONE]--> <string name="build_number_copy_toast">Build number copied to clipboard.</string> + <!-- Text for accessibility action for copying content to clipboard [CHAR LIMIT=NONE]--> + <string name="copy_to_clipboard_a11y_action">copy to clipboard.</string> + <!-- Status for conversation without interaction data [CHAR LIMIT=120] --> <string name="basic_status">Open conversation</string> <!--Title text for Conversation widget set up screen [CHAR LIMIT=180] --> diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractor.kt index abbbd730c47e..976329580c60 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/UdfpsOverlayInteractor.kt @@ -36,8 +36,10 @@ 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.map import kotlinx.coroutines.flow.stateIn +import kotlin.math.max /** Encapsulates business logic for interacting with the UDFPS overlay. */ @SysUISingleton @@ -124,8 +126,9 @@ constructor( udfpsOverlayParams.map { params -> val sensorWidth = params.nativeSensorBounds.right - params.nativeSensorBounds.left val nativePadding = (sensorWidth - iconSize) / 2 - (nativePadding * params.scaleFactor).toInt() - } + // padding can be negative when udfpsOverlayParams has not been initialized yet. + max(0, (nativePadding * params.scaleFactor).toInt()) + }.distinctUntilChanged() companion object { private const val TAG = "UdfpsOverlayInteractor" diff --git a/packages/SystemUI/src/com/android/systemui/development/data/repository/DevelopmentSettingRepository.kt b/packages/SystemUI/src/com/android/systemui/development/data/repository/DevelopmentSettingRepository.kt new file mode 100644 index 000000000000..a8fa9797ebfd --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/development/data/repository/DevelopmentSettingRepository.kt @@ -0,0 +1,79 @@ +/* + * 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.development.data.repository + +import android.content.pm.UserInfo +import android.os.Build +import android.os.UserManager +import android.provider.Settings +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.util.kotlin.emitOnStart +import com.android.systemui.util.settings.GlobalSettings +import com.android.systemui.util.settings.SettingsProxyExt.observerFlow +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext + +@SysUISingleton +class DevelopmentSettingRepository +@Inject +constructor( + private val globalSettings: GlobalSettings, + private val userManager: UserManager, + @Background private val backgroundDispatcher: CoroutineDispatcher, +) { + private val settingFlow = globalSettings.observerFlow(SETTING) + + /** + * Indicates whether development settings is enabled for this user. The conditions are: + * * Setting is enabled (defaults to true in eng builds) + * * User is an admin + * * User is not restricted from Debugging features. + */ + fun isDevelopmentSettingEnabled(userInfo: UserInfo): Flow<Boolean> { + return settingFlow + .emitOnStart() + .map { checkDevelopmentSettingEnabled(userInfo) } + .flowOn(backgroundDispatcher) + } + + private suspend fun checkDevelopmentSettingEnabled(userInfo: UserInfo): Boolean { + val hasUserRestriction = + withContext(backgroundDispatcher) { + userManager.hasUserRestrictionForUser( + UserManager.DISALLOW_DEBUGGING_FEATURES, + userInfo.userHandle, + ) + } + val isSettingEnabled = + withContext(backgroundDispatcher) { + globalSettings.getInt(SETTING, DEFAULT_ENABLED) != 0 + } + val isAdmin = userInfo.isAdmin + return isAdmin && !hasUserRestriction && isSettingEnabled + } + + private companion object { + val DEFAULT_ENABLED = if (Build.TYPE == "eng") 1 else 0 + + const val SETTING = Settings.Global.DEVELOPMENT_SETTINGS_ENABLED + } +} diff --git a/packages/SystemUI/src/com/android/systemui/development/domain/interactor/BuildNumberInteractor.kt b/packages/SystemUI/src/com/android/systemui/development/domain/interactor/BuildNumberInteractor.kt new file mode 100644 index 000000000000..4d786fe76bc1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/development/domain/interactor/BuildNumberInteractor.kt @@ -0,0 +1,89 @@ +/* + * 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.development.domain.interactor + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.res.Resources +import android.os.Build +import android.os.UserHandle +import com.android.internal.R as InternalR +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.development.data.repository.DevelopmentSettingRepository +import com.android.systemui.development.shared.model.BuildNumber +import com.android.systemui.res.R as SystemUIR +import com.android.systemui.user.data.repository.UserRepository +import com.android.systemui.user.utils.UserScopedService +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flatMapConcat +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext + +@OptIn(ExperimentalCoroutinesApi::class) +@SysUISingleton +class BuildNumberInteractor +@Inject +constructor( + repository: DevelopmentSettingRepository, + @Main resources: Resources, + private val userRepository: UserRepository, + private val clipboardManagerProvider: UserScopedService<ClipboardManager>, + @Background private val backgroundDispatcher: CoroutineDispatcher, +) { + + /** + * Build number, or `null` if Development Settings is not enabled for the current user. + * + * @see DevelopmentSettingRepository.isDevelopmentSettingEnabled + */ + val buildNumber: Flow<BuildNumber?> = + userRepository.selectedUserInfo + .flatMapConcat { userInfo -> repository.isDevelopmentSettingEnabled(userInfo) } + .map { enabled -> buildText.takeIf { enabled } } + + private val buildText = + BuildNumber( + resources.getString( + InternalR.string.bugreport_status, + Build.VERSION.RELEASE_OR_CODENAME, + Build.ID, + ) + ) + + private val clipLabel = resources.getString(SystemUIR.string.build_number_clip_data_label) + + private val currentUserHandle: UserHandle + get() = userRepository.getSelectedUserInfo().userHandle + + /** + * Copy to the clipboard the build number for the current user. + * + * This can be performed regardless of the current user having Development Settings enabled + */ + suspend fun copyBuildNumber() { + withContext(backgroundDispatcher) { + clipboardManagerProvider + .forUser(currentUserHandle) + .setPrimaryClip(ClipData.newPlainText(clipLabel, buildText.value)) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/development/shared/model/BuildNumber.kt b/packages/SystemUI/src/com/android/systemui/development/shared/model/BuildNumber.kt new file mode 100644 index 000000000000..5bd713fc4840 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/development/shared/model/BuildNumber.kt @@ -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 com.android.systemui.development.shared.model + +@JvmInline value class BuildNumber(val value: String) diff --git a/packages/SystemUI/src/com/android/systemui/development/ui/compose/BuildNumber.kt b/packages/SystemUI/src/com/android/systemui/development/ui/compose/BuildNumber.kt new file mode 100644 index 000000000000..72e1cede0002 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/development/ui/compose/BuildNumber.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.development.ui.compose + +import androidx.compose.foundation.basicMarquee +import androidx.compose.foundation.focusable +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.wrapContentWidth +import androidx.compose.material3.Text +import androidx.compose.material3.minimumInteractiveComponentSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.onLongClick +import androidx.compose.ui.semantics.semantics +import com.android.systemui.communal.ui.compose.extensions.detectLongPressGesture +import com.android.systemui.development.ui.viewmodel.BuildNumberViewModel +import com.android.systemui.lifecycle.rememberViewModel +import com.android.systemui.res.R + +@Composable +fun BuildNumber( + viewModelFactory: BuildNumberViewModel.Factory, + textColor: Color, + modifier: Modifier = Modifier, +) { + val viewModel = rememberViewModel(traceName = "BuildNumber") { viewModelFactory.create() } + + val buildNumber = viewModel.buildNumber + + if (buildNumber != null) { + val haptics = LocalHapticFeedback.current + val copyToClipboardActionLabel = stringResource(id = R.string.copy_to_clipboard_a11y_action) + + Text( + text = buildNumber.value, + modifier = + modifier + .focusable() + .wrapContentWidth() + // Using this instead of combinedClickable because this node should not support + // single click + .pointerInput(Unit) { + detectLongPressGesture { + haptics.performHapticFeedback(HapticFeedbackType.LongPress) + viewModel.onBuildNumberLongPress() + } + } + .semantics { + onLongClick(copyToClipboardActionLabel) { + viewModel.onBuildNumberLongPress() + true + } + } + .basicMarquee(iterations = 1, initialDelayMillis = 2000) + .minimumInteractiveComponentSize(), + color = textColor, + maxLines = 1, + ) + } else { + Spacer(modifier) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/development/ui/viewmodel/BuildNumberViewModel.kt b/packages/SystemUI/src/com/android/systemui/development/ui/viewmodel/BuildNumberViewModel.kt new file mode 100644 index 000000000000..68c51ea80ffd --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/development/ui/viewmodel/BuildNumberViewModel.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.development.ui.viewmodel + +import androidx.compose.runtime.getValue +import com.android.systemui.development.domain.interactor.BuildNumberInteractor +import com.android.systemui.development.shared.model.BuildNumber +import com.android.systemui.lifecycle.ExclusiveActivatable +import com.android.systemui.lifecycle.Hydrator +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch + +/** View model for UI that (optionally) shows the build number and copies it on long press. */ +class BuildNumberViewModel +@AssistedInject +constructor(private val buildNumberInteractor: BuildNumberInteractor) : ExclusiveActivatable() { + + private val hydrator = Hydrator("BuildNumberViewModel") + + private val copyRequests = Channel<Unit>() + + val buildNumber: BuildNumber? by + hydrator.hydratedStateOf( + traceName = "buildNumber", + initialValue = null, + source = buildNumberInteractor.buildNumber, + ) + + fun onBuildNumberLongPress() { + copyRequests.trySend(Unit) + } + + override suspend fun onActivated(): Nothing { + coroutineScope { + launch { hydrator.activate() } + launch { + copyRequests.receiveAsFlow().collect { buildNumberInteractor.copyBuildNumber() } + } + awaitCancellation() + } + } + + @AssistedFactory + interface Factory { + fun create(): BuildNumberViewModel + } +} 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 1a5654144b65..400d09742d13 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 @@ -16,6 +16,7 @@ package com.android.systemui.deviceentry.domain.interactor +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.internal.policy.IKeyguardDismissCallback import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor import com.android.systemui.authentication.shared.model.AuthenticationMethodModel @@ -43,7 +44,6 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn -import com.android.app.tracing.coroutines.launchTraced as launch /** * Hosts application business logic related to device entry. @@ -174,6 +174,14 @@ constructor( } /** + * Whether lockscreen bypass is enabled. When enabled, the lockscreen will be automatically + * dismissed once the authentication challenge is completed. For example, completing a biometric + * authentication challenge via face unlock or fingerprint sensor can automatically bypass the + * lockscreen. + */ + val isBypassEnabled: StateFlow<Boolean> = repository.isBypassEnabled + + /** * Attempt to enter the device and dismiss the lockscreen. If authentication is required to * unlock the device it will transition to bouncer. * @@ -238,11 +246,8 @@ constructor( isLockscreenEnabled() } - /** - * Whether lockscreen bypass is enabled. When enabled, the lockscreen will be automatically - * dismissed once the authentication challenge is completed. For example, completing a biometric - * authentication challenge via face unlock or fingerprint sensor can automatically bypass the - * lockscreen. - */ - val isBypassEnabled: StateFlow<Boolean> = repository.isBypassEnabled + /** Locks the device instantly. */ + fun lockNow() { + deviceUnlockedInteractor.lockNow() + } } diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt index 35eed5e6a6d9..7d684cab39f7 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractor.kt @@ -43,6 +43,7 @@ import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow @@ -57,6 +58,7 @@ import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.launch @OptIn(ExperimentalCoroutinesApi::class) @@ -178,6 +180,8 @@ constructor( val deviceUnlockStatus: StateFlow<DeviceUnlockStatus> = repository.deviceUnlockStatus.asStateFlow() + private val lockNowRequests = Channel<Unit>() + override suspend fun onActivated(): Nothing { authenticationInteractor.authenticationMethod.collectLatest { authMethod -> if (!authMethod.isSecure) { @@ -196,6 +200,11 @@ constructor( awaitCancellation() } + /** Locks the device instantly. */ + fun lockNow() { + lockNowRequests.trySend(Unit) + } + private suspend fun handleLockAndUnlockEvents() { try { Log.d(TAG, "started watching for lock and unlock events") @@ -225,10 +234,12 @@ constructor( .map { (isAsleep, lastSleepReason) -> if (isAsleep) { if ( - lastSleepReason == WakeSleepReason.POWER_BUTTON && + (lastSleepReason == WakeSleepReason.POWER_BUTTON) && authenticationInteractor.getPowerButtonInstantlyLocks() ) { LockImmediately("locked instantly from power button") + } else if (lastSleepReason == WakeSleepReason.SLEEP_BUTTON) { + LockImmediately("locked instantly from sleep button") } else { LockWithDelay("entering sleep") } @@ -256,6 +267,7 @@ constructor( emptyFlow() } }, + lockNowRequests.receiveAsFlow().map { LockImmediately("lockNow") }, ) .collectLatest(::onLockEvent) } diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperModule.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperModule.kt index 7b3380a6a608..1af7340ad7b4 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperModule.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ShortcutHelperModule.kt @@ -18,6 +18,9 @@ package com.android.systemui.keyboard.shortcut import com.android.systemui.CoreStartable import com.android.systemui.Flags.keyboardShortcutHelperRewrite +import com.android.systemui.keyboard.shortcut.data.repository.CustomShortcutCategoriesRepository +import com.android.systemui.keyboard.shortcut.data.repository.DefaultShortcutCategoriesRepository +import com.android.systemui.keyboard.shortcut.data.repository.ShortcutCategoriesRepository import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperStateRepository import com.android.systemui.keyboard.shortcut.data.source.AppCategoriesShortcutsSource import com.android.systemui.keyboard.shortcut.data.source.CurrentAppShortcutsSource @@ -27,6 +30,8 @@ import com.android.systemui.keyboard.shortcut.data.source.MultitaskingShortcutsS import com.android.systemui.keyboard.shortcut.data.source.SystemShortcutsSource import com.android.systemui.keyboard.shortcut.qualifiers.AppCategoriesShortcuts import com.android.systemui.keyboard.shortcut.qualifiers.CurrentAppShortcuts +import com.android.systemui.keyboard.shortcut.qualifiers.CustomShortcutCategories +import com.android.systemui.keyboard.shortcut.qualifiers.DefaultShortcutCategories import com.android.systemui.keyboard.shortcut.qualifiers.InputShortcuts import com.android.systemui.keyboard.shortcut.qualifiers.MultitaskingShortcuts import com.android.systemui.keyboard.shortcut.qualifiers.SystemShortcuts @@ -63,6 +68,18 @@ interface ShortcutHelperModule { impl: AppCategoriesShortcutsSource ): KeyboardShortcutGroupsSource + @Binds + @DefaultShortcutCategories + fun defaultShortcutCategoriesRepository( + impl: DefaultShortcutCategoriesRepository + ): ShortcutCategoriesRepository + + @Binds + @CustomShortcutCategories + fun customShortcutCategoriesRepository( + impl: CustomShortcutCategoriesRepository + ): ShortcutCategoriesRepository + companion object { @Provides @IntoMap diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepository.kt index ec1d358b6bd2..da5590ae27fa 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepository.kt @@ -23,19 +23,24 @@ import android.hardware.input.InputGestureData.KeyTrigger import android.hardware.input.InputManager import android.hardware.input.InputSettings import android.hardware.input.KeyGestureEvent +import com.android.systemui.Flags.shortcutHelperKeyGlyph import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.keyboard.shortcut.data.model.InternalKeyboardShortcutGroup import com.android.systemui.keyboard.shortcut.data.model.InternalKeyboardShortcutInfo +import com.android.systemui.keyboard.shortcut.shared.model.KeyCombination import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategory import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState.Active +import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey import com.android.systemui.settings.UserTracker import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.withContext @@ -60,6 +65,8 @@ constructor( private val inputManager: InputManager get() = userContext.getSystemService(INPUT_SERVICE) as InputManager + private val _selectedKeyCombination = MutableStateFlow<KeyCombination?>(null) + private val activeInputDevice = stateRepository.state.map { if (it is Active) { @@ -69,6 +76,41 @@ constructor( } } + val pressedKeys = + _selectedKeyCombination + .combine(activeInputDevice) { keyCombination, inputDevice -> + if (inputDevice == null || keyCombination == null) { + return@combine emptyList() + } else { + val keyGlyphMap = + if (shortcutHelperKeyGlyph()) { + inputManager.getKeyGlyphMap(inputDevice.id) + } else null + val modifiers = + shortcutCategoriesUtils.toShortcutModifierKeys( + keyCombination.modifiers, + keyGlyphMap, + ) + val triggerKey = + keyCombination.keyCode?.let { + shortcutCategoriesUtils.toShortcutKey( + keyGlyphMap, + inputDevice.keyCharacterMap, + keyCode = it, + ) + } + val keys = mutableListOf<ShortcutKey>() + modifiers?.let { keys += it } + triggerKey?.let { keys += it } + return@combine keys + } + } + .stateIn( + scope = backgroundScope, + started = SharingStarted.Lazily, + initialValue = emptyList(), + ) + override val categories: Flow<List<ShortcutCategory>> = activeInputDevice .map { inputDevice -> @@ -104,6 +146,10 @@ constructor( started = SharingStarted.Lazily, ) + fun updateUserKeyCombination(keyCombination: KeyCombination?) { + _selectedKeyCombination.value = keyCombination + } + private fun toInternalGroupSources( inputGestures: List<InputGestureData> ): List<InternalGroupsSource> { @@ -148,7 +194,7 @@ constructor( private fun fetchGroupLabelByGestureType( @KeyGestureEvent.KeyGestureType keyGestureType: Int ): String? { - InputGestures.gestureToInternalKeyboardShortcutGroupLabelMap[keyGestureType]?.let { + InputGestures.gestureToInternalKeyboardShortcutGroupLabelResIdMap[keyGestureType]?.let { return context.getString(it) } ?: return null } @@ -156,7 +202,7 @@ constructor( private fun fetchShortcutInfoLabelByGestureType( @KeyGestureEvent.KeyGestureType keyGestureType: Int ): String? { - InputGestures.gestureToInternalKeyboardShortcutInfoLabelMap[keyGestureType]?.let { + InputGestures.gestureToInternalKeyboardShortcutInfoLabelResIdMap[keyGestureType]?.let { return context.getString(it) } ?: return null } diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestures.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestures.kt index 90be9888e622..7bb294df98cd 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestures.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestures.kt @@ -81,7 +81,7 @@ object InputGestures { KEY_GESTURE_TYPE_LAUNCH_DEFAULT_MESSAGING to AppCategories, ) - val gestureToInternalKeyboardShortcutGroupLabelMap = + val gestureToInternalKeyboardShortcutGroupLabelResIdMap = mapOf( // System Category KEY_GESTURE_TYPE_HOME to R.string.shortcut_helper_category_system_controls, @@ -129,7 +129,7 @@ object InputGestures { R.string.keyboard_shortcut_group_applications, ) - val gestureToInternalKeyboardShortcutInfoLabelMap = + val gestureToInternalKeyboardShortcutInfoLabelResIdMap = mapOf( // System Category KEY_GESTURE_TYPE_HOME to R.string.group_system_access_home_screen, diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutCategoriesUtils.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutCategoriesUtils.kt index 899fd15d6115..3988d1f155bd 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutCategoriesUtils.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutCategoriesUtils.kt @@ -24,6 +24,7 @@ import android.util.Log import android.view.InputDevice import android.view.KeyCharacterMap import android.view.KeyEvent +import android.view.KeyEvent.META_META_ON import com.android.systemui.Flags.shortcutHelperKeyGlyph import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.keyboard.shortcut.data.model.InternalKeyboardShortcutGroup @@ -92,7 +93,7 @@ constructor( } .filter { it.shortcuts.isNotEmpty() } return if (subCategories.isEmpty()) { - Log.wtf(TAG, "Empty sub categories after converting $shortcutGroups") + Log.w(TAG, "Empty sub categories after converting $shortcutGroups") null } else { ShortcutCategory(type, subCategories) @@ -161,7 +162,7 @@ constructor( } if (remainingModifiers != 0) { // There is a remaining modifier we don't support - Log.wtf(TAG, "Unsupported modifiers remaining: $remainingModifiers") + Log.w(TAG, "Unsupported modifiers remaining: $remainingModifiers") return null } if (info.keycode != 0 || info.baseCharacter > Char.MIN_VALUE) { @@ -170,21 +171,32 @@ constructor( ?: return null } if (keys.isEmpty()) { - Log.wtf(TAG, "No keys for $info") + Log.w(TAG, "No keys for $info") return null } return ShortcutCommand(keys = keys, isCustom = info.isCustomShortcut) } + fun toShortcutModifierKeys(modifiers: Int, keyGlyphMap: KeyGlyphMap?): List<ShortcutKey>? { + val keys: MutableList<ShortcutKey> = mutableListOf() + var remainingModifiers = modifiers + SUPPORTED_MODIFIERS.forEach { supportedModifier -> + if ((supportedModifier and remainingModifiers) != 0) { + keys += toShortcutModifierKey(keyGlyphMap, supportedModifier) ?: return null + remainingModifiers = remainingModifiers and supportedModifier.inv() + } + } + return keys + } + private fun toShortcutModifierKey(keyGlyphMap: KeyGlyphMap?, modifierMask: Int): ShortcutKey? { val modifierDrawable = keyGlyphMap?.getDrawableForModifierState(context, modifierMask) if (modifierDrawable != null) { return ShortcutKey.Icon.DrawableIcon(drawable = modifierDrawable) } - val iconResId = ShortcutHelperKeys.keyIcons[modifierMask] - if (iconResId != null) { - return ShortcutKey.Icon.ResIdIcon(iconResId) + if (modifierMask == META_META_ON) { + return ShortcutKey.Icon.ResIdIcon(ShortcutHelperKeys.metaModifierIconResId) } val modifierLabel = ShortcutHelperKeys.modifierLabels[modifierMask] @@ -195,7 +207,7 @@ constructor( return null } - private fun toShortcutKey( + fun toShortcutKey( keyGlyphMap: KeyGlyphMap?, keyCharacterMap: KeyCharacterMap, keyCode: Int, @@ -222,7 +234,7 @@ constructor( if (displayLabelCharacter.code != 0) { return ShortcutKey.Text(displayLabelCharacter.toString()) } - Log.wtf(TAG, "Couldn't find label or icon for key: $keyCode") + Log.w(TAG, "Couldn't find label or icon for key: $keyCode") return null } diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperKeys.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperKeys.kt index 288efa275219..e47b33f8c37c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperKeys.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperKeys.kt @@ -116,9 +116,10 @@ import com.android.systemui.res.R object ShortcutHelperKeys { + val metaModifierIconResId = R.drawable.ic_ksh_key_meta + val keyIcons = mapOf( - META_META_ON to R.drawable.ic_ksh_key_meta, KEYCODE_BACK to R.drawable.ic_arrow_back_2, KEYCODE_HOME to R.drawable.ic_radio_button_unchecked, KEYCODE_RECENT_APPS to R.drawable.ic_check_box_outline_blank, diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutCustomizationInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutCustomizationInteractor.kt index 85d22144f201..aad55dc11c16 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutCustomizationInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutCustomizationInteractor.kt @@ -16,13 +16,22 @@ package com.android.systemui.keyboard.shortcut.domain.interactor -import android.view.KeyEvent.META_META_ON +import com.android.systemui.keyboard.shortcut.data.repository.CustomShortcutCategoriesRepository import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperKeys +import com.android.systemui.keyboard.shortcut.shared.model.KeyCombination import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey import javax.inject.Inject -class ShortcutCustomizationInteractor @Inject constructor() { +class ShortcutCustomizationInteractor +@Inject +constructor(private val customShortcutRepository: CustomShortcutCategoriesRepository) { + val pressedKeys = customShortcutRepository.pressedKeys + + fun updateUserSelectedKeyCombination(keyCombination: KeyCombination?) { + customShortcutRepository.updateUserKeyCombination(keyCombination) + } + fun getDefaultCustomShortcutModifierKey(): ShortcutKey.Icon.ResIdIcon { - return ShortcutKey.Icon.ResIdIcon(ShortcutHelperKeys.keyIcons[META_META_ON]!!) + return ShortcutKey.Icon.ResIdIcon(ShortcutHelperKeys.metaModifierIconResId) } } diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractor.kt index 39fc27d35082..0381eaec5026 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/domain/interactor/ShortcutHelperCategoriesInteractor.kt @@ -16,37 +16,66 @@ package com.android.systemui.keyboard.shortcut.domain.interactor +import com.android.systemui.Flags.keyboardShortcutHelperShortcutCustomizer import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.keyboard.shortcut.data.repository.DefaultShortcutCategoriesRepository +import com.android.systemui.keyboard.shortcut.data.repository.ShortcutCategoriesRepository +import com.android.systemui.keyboard.shortcut.qualifiers.CustomShortcutCategories +import com.android.systemui.keyboard.shortcut.qualifiers.DefaultShortcutCategories import com.android.systemui.keyboard.shortcut.shared.model.Shortcut import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategory import com.android.systemui.keyboard.shortcut.shared.model.ShortcutSubCategory +import dagger.Lazy import javax.inject.Inject import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOf @SysUISingleton class ShortcutHelperCategoriesInteractor @Inject -constructor(categoriesRepository: DefaultShortcutCategoriesRepository) { - +constructor( + @DefaultShortcutCategories defaultCategoriesRepository: ShortcutCategoriesRepository, + @CustomShortcutCategories customCategoriesRepositoryLazy: Lazy<ShortcutCategoriesRepository>, +) { val shortcutCategories: Flow<List<ShortcutCategory>> = - categoriesRepository.categories.map { categories -> - categories.map { category -> groupSubCategoriesInCategory(category) } + defaultCategoriesRepository.categories.combine( + if (keyboardShortcutHelperShortcutCustomizer()) { + customCategoriesRepositoryLazy.get().categories + } else { + flowOf(emptyList()) + } + ) { defaultShortcutCategories, customShortcutCategories -> + groupCategories(defaultShortcutCategories + customShortcutCategories) } - private fun groupSubCategoriesInCategory(shortcutCategory: ShortcutCategory): ShortcutCategory { - val subCategoriesWithGroupedShortcuts = - shortcutCategory.subCategories.map { + private fun groupCategories( + shortcutCategories: List<ShortcutCategory> + ): List<ShortcutCategory> { + return shortcutCategories + .groupBy { it.type } + .entries + .map { (categoryType, groupedCategories) -> + ShortcutCategory( + type = categoryType, + subCategories = + groupSubCategories(groupedCategories.flatMap { it.subCategories }), + ) + } + } + + private fun groupSubCategories( + subCategories: List<ShortcutSubCategory> + ): List<ShortcutSubCategory> { + return subCategories + .groupBy { it.label } + .entries + .map { (label, groupedSubcategories) -> ShortcutSubCategory( - label = it.label, - shortcuts = groupShortcutsInSubcategory(it.shortcuts), + label = label, + shortcuts = + groupShortcutsInSubcategory(groupedSubcategories.flatMap { it.shortcuts }), ) } - return ShortcutCategory( - type = shortcutCategory.type, - subCategories = subCategoriesWithGroupedShortcuts, - ) } private fun groupShortcutsInSubcategory(shortcuts: List<Shortcut>) = diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/qualifiers/CustomShortcutCategories.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/qualifiers/CustomShortcutCategories.kt new file mode 100644 index 000000000000..8acb9eebbd4d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/qualifiers/CustomShortcutCategories.kt @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyboard.shortcut.qualifiers + +import javax.inject.Qualifier + +@Qualifier annotation class CustomShortcutCategories diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/qualifiers/DefaultShortcutCategories.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/qualifiers/DefaultShortcutCategories.kt new file mode 100644 index 000000000000..f94e10d17964 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/qualifiers/DefaultShortcutCategories.kt @@ -0,0 +1,21 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyboard.shortcut.qualifiers + +import javax.inject.Qualifier + +@Qualifier annotation class DefaultShortcutCategories diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/KeyCombination.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/KeyCombination.kt new file mode 100644 index 000000000000..5e4031b29502 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/KeyCombination.kt @@ -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 com.android.systemui.keyboard.shortcut.shared.model + +data class KeyCombination(val modifiers: Int, val keyCode: Int?) diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModel.kt index 912bfe99cea8..08fd0af81006 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModel.kt @@ -123,7 +123,7 @@ constructor( userContext.packageManager.getApplicationIcon(type.packageName) IconSource(painter = DrawablePainter(drawable = iconDrawable)) } catch (e: NameNotFoundException) { - Log.wtf( + Log.w( "ShortcutHelperViewModel", "Package not found when retrieving icon for ${type.packageName}", ) @@ -153,7 +153,7 @@ constructor( packageManagerForUser.getApplicationInfo(type.packageName, /* flags= */ 0) return packageManagerForUser.getApplicationLabel(currentAppInfo).toString() } catch (e: NameNotFoundException) { - Log.wtf( + Log.w( "ShortcutHelperViewModel", "Package Not found when retrieving Label for ${type.packageName}", ) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java index 32c2bc7c8620..7097c1d2fc4e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java @@ -662,7 +662,9 @@ public class KeyguardService extends Service { trace("doKeyguardTimeout"); checkPermission(); - if (KeyguardWmStateRefactor.isEnabled()) { + if (SceneContainerFlag.isEnabled()) { + mDeviceEntryInteractorLazy.get().lockNow(); + } else if (KeyguardWmStateRefactor.isEnabled()) { mKeyguardLockWhileAwakeInteractor.onKeyguardServiceDoKeyguardTimeout(options); } 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 b51bb7b229ee..aa7eb2933992 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 @@ -28,6 +28,7 @@ import androidx.annotation.VisibleForTesting import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import com.android.systemui.biometrics.AuthController +import com.android.systemui.customization.R as customR import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags @@ -115,7 +116,7 @@ constructor( val scaleFactor: Float = authController.scaleFactor val mBottomPaddingPx = - context.resources.getDimensionPixelSize(R.dimen.lock_icon_margin_bottom) + context.resources.getDimensionPixelSize(customR.dimen.lock_icon_margin_bottom) val bounds = windowManager.currentWindowMetrics.bounds var widthPixels = bounds.right.toFloat() if (featureFlags.isEnabled(Flags.LOCKSCREEN_ENABLE_LANDSCAPE)) { 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 7ad2ec5e3bf8..d54d411b7de6 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 @@ -124,7 +124,7 @@ constructor( ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START, - horizontalPaddingStart + horizontalPaddingStart, ) // migrate addSmartspaceView from KeyguardClockSwitchController @@ -135,15 +135,15 @@ constructor( ConstraintSet.START, ConstraintSet.PARENT_ID, ConstraintSet.START, - horizontalPaddingStart + horizontalPaddingStart, ) connect( sharedR.id.bc_smartspace_view, ConstraintSet.END, - if (keyguardClockViewModel.clockShouldBeCentered.value) ConstraintSet.PARENT_ID - else R.id.split_shade_guideline, + if (keyguardSmartspaceViewModel.isShadeLayoutWide.value) R.id.split_shade_guideline + else ConstraintSet.PARENT_ID, ConstraintSet.END, - horizontalPaddingEnd + horizontalPaddingEnd, ) if (keyguardClockViewModel.hasCustomWeatherDataDisplay.value) { @@ -152,7 +152,7 @@ constructor( sharedR.id.date_smartspace_view, ConstraintSet.BOTTOM, sharedR.id.bc_smartspace_view, - ConstraintSet.TOP + ConstraintSet.TOP, ) } else { clear(sharedR.id.date_smartspace_view, ConstraintSet.BOTTOM) @@ -160,13 +160,13 @@ constructor( sharedR.id.date_smartspace_view, ConstraintSet.TOP, customR.id.lockscreen_clock_view, - ConstraintSet.BOTTOM + ConstraintSet.BOTTOM, ) connect( sharedR.id.bc_smartspace_view, ConstraintSet.TOP, sharedR.id.date_smartspace_view, - ConstraintSet.BOTTOM + ConstraintSet.BOTTOM, ) } @@ -174,10 +174,7 @@ constructor( R.id.smart_space_barrier_bottom, Barrier.BOTTOM, 0, - *intArrayOf( - sharedR.id.bc_smartspace_view, - sharedR.id.date_smartspace_view, - ) + *intArrayOf(sharedR.id.bc_smartspace_view, sharedR.id.date_smartspace_view), ) } updateVisibility(constraintSet) @@ -212,7 +209,7 @@ constructor( setVisibility(sharedR.id.weather_smartspace_view, weatherVisibility) setAlpha( sharedR.id.weather_smartspace_view, - if (weatherVisibility == View.VISIBLE) 1f else 0f + if (weatherVisibility == View.VISIBLE) 1f else 0f, ) val dateVisibility = if (keyguardClockViewModel.hasCustomWeatherDataDisplay.value) ConstraintSet.GONE @@ -220,7 +217,7 @@ constructor( setVisibility(sharedR.id.date_smartspace_view, dateVisibility) setAlpha( sharedR.id.date_smartspace_view, - if (dateVisibility == ConstraintSet.VISIBLE) 1f else 0f + if (dateVisibility == ConstraintSet.VISIBLE) 1f else 0f, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryForegroundViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryForegroundViewModel.kt index 5065fcbbac93..19652525bee0 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryForegroundViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DeviceEntryForegroundViewModel.kt @@ -31,8 +31,10 @@ import com.android.systemui.shade.ShadeDisplayAware import javax.inject.Inject import kotlin.math.roundToInt import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf @@ -40,6 +42,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart /** Models the UI state for the device entry icon foreground view (displayed icon). */ +@OptIn(FlowPreview::class) @ExperimentalCoroutinesApi @SysUISingleton class DeviceEntryForegroundViewModel @@ -97,7 +100,7 @@ constructor( private val padding: Flow<Int> = deviceEntryUdfpsInteractor.isUdfpsSupported.flatMapLatest { udfpsSupported -> if (udfpsSupported) { - udfpsOverlayInteractor.iconPadding + udfpsOverlayInteractor.iconPadding.debounce(udfpsPaddingDebounceDuration.toLong()) } else { configurationInteractor.scaleForResolution.map { scale -> (context.resources.getDimensionPixelSize(R.dimen.lock_icon_padding) * scale) @@ -120,6 +123,9 @@ constructor( ) } + private val udfpsPaddingDebounceDuration: Int + get() = context.resources.getInteger(R.integer.udfps_padding_debounce_duration) + data class ForegroundIconViewModel( val type: DeviceEntryIconView.IconType, val useAodVariant: Boolean, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModel.kt index de0927ec27cb..3266dc45427a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModel.kt @@ -22,6 +22,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.domain.interactor.KeyguardSmartspaceInteractor import com.android.systemui.res.R +import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -39,6 +40,7 @@ constructor( smartspaceController: LockscreenSmartspaceController, keyguardClockViewModel: KeyguardClockViewModel, smartspaceInteractor: KeyguardSmartspaceInteractor, + shadeInteractor: ShadeInteractor, ) { /** Whether the smartspace section is available in the build. */ val isSmartspaceEnabled: Boolean = smartspaceController.isEnabled @@ -89,6 +91,8 @@ constructor( /* trigger clock and smartspace constraints change when smartspace appears */ val bcSmartspaceVisibility: StateFlow<Int> = smartspaceInteractor.bcSmartspaceVisibility + val isShadeLayoutWide: StateFlow<Boolean> = shadeInteractor.isShadeLayoutWide + companion object { fun getSmartspaceStartMargin(context: Context): Int { return context.resources.getDimensionPixelSize(R.dimen.below_clock_padding_start) + diff --git a/packages/SystemUI/src/com/android/systemui/notetask/INoteTaskBubblesService.aidl b/packages/SystemUI/src/com/android/systemui/notetask/INoteTaskBubblesService.aidl index 3e947d9d2b16..7803f229dda7 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/INoteTaskBubblesService.aidl +++ b/packages/SystemUI/src/com/android/systemui/notetask/INoteTaskBubblesService.aidl @@ -16,6 +16,7 @@ package com.android.systemui.notetask; +import com.android.systemui.notetask.NoteTaskBubbleExpandBehavior; import android.content.Intent; import android.graphics.drawable.Icon; import android.os.UserHandle; @@ -25,5 +26,6 @@ interface INoteTaskBubblesService { boolean areBubblesAvailable(); - void showOrHideAppBubble(in Intent intent, in UserHandle userHandle, in Icon icon); + void showOrHideAppBubble(in Intent intent, in UserHandle userHandle, in Icon icon, + in NoteTaskBubbleExpandBehavior bubbleExpandBehavior); } diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskBubbleExpandBehavior.aidl b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskBubbleExpandBehavior.aidl new file mode 100644 index 000000000000..86a06a568a4b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskBubbleExpandBehavior.aidl @@ -0,0 +1,3 @@ +package com.android.systemui.notetask; + +parcelable NoteTaskBubbleExpandBehavior;
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskBubbleExpandBehavior.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskBubbleExpandBehavior.kt new file mode 100644 index 000000000000..63b38a1b73f9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskBubbleExpandBehavior.kt @@ -0,0 +1,50 @@ +/* + * 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.notetask + +import android.os.Parcel +import android.os.Parcelable + +enum class NoteTaskBubbleExpandBehavior : Parcelable { + /** + * The default bubble expand behavior for note task bubble: The bubble will collapse if there is + * already an expanded bubble, The bubble will expand if there is a collapsed bubble. + */ + DEFAULT, + /** + * The special bubble expand behavior for note task bubble: The bubble will stay expanded, not + * collapse, if there is already an expanded bubble, The bubble will expand if there is a + * collapsed bubble. + */ + KEEP_IF_EXPANDED; + + override fun describeContents(): Int { + return 0 + } + + override fun writeToParcel(dest: Parcel, flags: Int) { + dest.writeString(name) + } + + companion object CREATOR : Parcelable.Creator<NoteTaskBubbleExpandBehavior> { + override fun createFromParcel(parcel: Parcel?): NoteTaskBubbleExpandBehavior { + return parcel?.readString()?.let { valueOf(it) } ?: DEFAULT + } + + override fun newArray(size: Int) = arrayOfNulls<NoteTaskBubbleExpandBehavior>(size) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskBubblesController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskBubblesController.kt index ec205f87d9fa..169285f6742e 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskBubblesController.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskBubblesController.kt @@ -27,6 +27,7 @@ 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.DebugLogger.debugLog +import com.android.wm.shell.bubbles.Bubble import com.android.wm.shell.bubbles.Bubbles import java.util.Optional import javax.inject.Inject @@ -48,7 +49,7 @@ open class NoteTaskBubblesController @Inject constructor( @Application private val context: Context, - @Background private val bgDispatcher: CoroutineDispatcher + @Background private val bgDispatcher: CoroutineDispatcher, ) { private val serviceConnector: ServiceConnector<INoteTaskBubblesService> = @@ -57,7 +58,7 @@ constructor( Intent(context, NoteTaskBubblesService::class.java), Context.BIND_AUTO_CREATE or Context.BIND_WAIVE_PRIORITY or Context.BIND_NOT_VISIBLE, UserHandle.USER_SYSTEM, - INoteTaskBubblesService.Stub::asInterface + INoteTaskBubblesService.Stub::asInterface, ) /** Returns whether notes app bubble is supported. */ @@ -79,11 +80,12 @@ constructor( open suspend fun showOrHideAppBubble( intent: Intent, userHandle: UserHandle, - icon: Icon + icon: Icon, + bubbleExpandBehavior: NoteTaskBubbleExpandBehavior, ) { withContext(bgDispatcher) { serviceConnector - .post { it.showOrHideAppBubble(intent, userHandle, icon) } + .post { it.showOrHideAppBubble(intent, userHandle, icon, bubbleExpandBehavior) } .whenComplete { _, error -> if (error != null) { debugLog(error = error) { @@ -120,16 +122,28 @@ constructor( override fun showOrHideAppBubble( intent: Intent, userHandle: UserHandle, - icon: Icon + icon: Icon, + bubbleExpandBehavior: NoteTaskBubbleExpandBehavior, ) { mOptionalBubbles.ifPresentOrElse( - { bubbles -> bubbles.showOrHideAppBubble(intent, userHandle, icon) }, + { bubbles -> + if ( + bubbleExpandBehavior == + NoteTaskBubbleExpandBehavior.KEEP_IF_EXPANDED && + bubbles.isBubbleExpanded( + Bubble.getAppBubbleKeyForApp(intent.`package`, userHandle) + ) + ) { + return@ifPresentOrElse + } + bubbles.showOrHideAppBubble(intent, userHandle, icon) + }, { debugLog { "Failed to show or hide bubble for intent $intent," + "user $user, and icon $icon as bubble is empty." } - } + }, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt index 1fa5baaa21ae..a615963ed2ca 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskController.kt @@ -84,7 +84,7 @@ constructor( private val userTracker: UserTracker, private val secureSettings: SecureSettings, @Application private val applicationScope: CoroutineScope, - @Background private val bgCoroutineContext: CoroutineContext + @Background private val bgCoroutineContext: CoroutineContext, ) { @VisibleForTesting val infoReference = AtomicReference<NoteTaskInfo?>() @@ -98,7 +98,7 @@ constructor( if (key != Bubble.getAppBubbleKeyForApp(info.packageName, info.user)) return // Safe guard mechanism, this callback should only be called for app bubbles. - if (info.launchMode != NoteTaskLaunchMode.AppBubble) return + if (info.launchMode !is NoteTaskLaunchMode.AppBubble) return if (isExpanding) { debugLog { "onBubbleExpandChanged - expanding: $info" } @@ -117,10 +117,8 @@ constructor( } else { getUserForHandlingNotesTaking(entryPoint) } - activityContext.startActivityAsUser( - createNotesRoleHolderSettingsIntent(), - user - ) + + activityContext.startActivityAsUser(createNotesRoleHolderSettingsIntent(), user) } /** @@ -140,8 +138,7 @@ constructor( entryPoint == QUICK_AFFORDANCE -> { userTracker.userProfiles .firstOrNull { userManager.isManagedProfile(it.id) } - ?.userHandle - ?: userTracker.userHandle + ?.userHandle ?: userTracker.userHandle } // On work profile devices, SysUI always run in the main user. else -> userTracker.userHandle @@ -158,19 +155,14 @@ constructor( * * That will let users open other apps in full screen, and take contextual notes. */ - fun showNoteTask( - entryPoint: NoteTaskEntryPoint, - ) { + fun showNoteTask(entryPoint: NoteTaskEntryPoint) { if (!isEnabled) return showNoteTaskAsUser(entryPoint, getUserForHandlingNotesTaking(entryPoint)) } /** A variant of [showNoteTask] which launches note task in the given [user]. */ - fun showNoteTaskAsUser( - entryPoint: NoteTaskEntryPoint, - user: UserHandle, - ) { + fun showNoteTaskAsUser(entryPoint: NoteTaskEntryPoint, user: UserHandle) { if (!isEnabled) return applicationScope.launch("$TAG#showNoteTaskAsUser") { @@ -178,10 +170,7 @@ constructor( } } - private suspend fun awaitShowNoteTaskAsUser( - entryPoint: NoteTaskEntryPoint, - user: UserHandle, - ) { + private suspend fun awaitShowNoteTaskAsUser(entryPoint: NoteTaskEntryPoint, user: UserHandle) { if (!isEnabled) return if (!noteTaskBubblesController.areBubblesAvailable()) { @@ -222,7 +211,13 @@ constructor( val intent = createNoteTaskIntent(info) val icon = Icon.createWithResource(context, R.drawable.ic_note_task_shortcut_widget) - noteTaskBubblesController.showOrHideAppBubble(intent, user, icon) + noteTaskBubblesController.showOrHideAppBubble( + intent, + user, + icon, + info.launchMode.bubbleExpandBehavior, + ) + // App bubble logging happens on `onBubbleExpandChanged`. debugLog { "onShowNoteTask - opened as app bubble: $info" } } @@ -399,8 +394,8 @@ constructor( const val EXTRA_SHORTCUT_BADGE_OVERRIDE_PACKAGE = "extra_shortcut_badge_override_package" /** Returns notes role holder settings intent. */ - fun createNotesRoleHolderSettingsIntent() = Intent(Intent.ACTION_MANAGE_DEFAULT_APP). - putExtra(Intent.EXTRA_ROLE_NAME, ROLE_NOTES) + fun createNotesRoleHolderSettingsIntent() = + Intent(Intent.ACTION_MANAGE_DEFAULT_APP).putExtra(Intent.EXTRA_ROLE_NAME, ROLE_NOTES) } } diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInfo.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInfo.kt index 269eb870686c..8319e07525d8 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInfo.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInfo.kt @@ -31,6 +31,10 @@ data class NoteTaskInfo( if (isKeyguardLocked || entryPoint == WIDGET_PICKER_SHORTCUT_IN_MULTI_WINDOW_MODE) { NoteTaskLaunchMode.Activity } else { - NoteTaskLaunchMode.AppBubble + if (entryPoint == NoteTaskEntryPoint.QS_NOTES_TILE) { + NoteTaskLaunchMode.AppBubble(NoteTaskBubbleExpandBehavior.KEEP_IF_EXPANDED) + } else { + NoteTaskLaunchMode.AppBubble(NoteTaskBubbleExpandBehavior.DEFAULT) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskLaunchMode.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskLaunchMode.kt index 836e103f4d69..6c85f20f2bce 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskLaunchMode.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskLaunchMode.kt @@ -26,7 +26,8 @@ import com.android.wm.shell.bubbles.Bubbles sealed class NoteTaskLaunchMode { /** @see Bubbles.showOrHideAppBubble */ - object AppBubble : NoteTaskLaunchMode() + data class AppBubble(val bubbleExpandBehavior: NoteTaskBubbleExpandBehavior) : + NoteTaskLaunchMode() /** @see Context.startActivity */ object Activity : NoteTaskLaunchMode() diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt index a1c5c9c682c3..5d5465633f9f 100644 --- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt +++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskModule.kt @@ -41,6 +41,7 @@ import com.android.systemui.qs.tiles.impl.notes.domain.model.NotesTileModel import com.android.systemui.qs.tiles.viewmodel.QSTileConfig import com.android.systemui.qs.tiles.viewmodel.QSTileUIConfig import com.android.systemui.qs.tiles.viewmodel.QSTileViewModel +import com.android.systemui.qs.tiles.viewmodel.StubQSTileViewModel import com.android.systemui.res.R import dagger.Binds import dagger.Module @@ -106,12 +107,14 @@ interface NoteTaskModule { stateInteractor: NotesTileDataInteractor, userActionInteractor: NotesTileUserActionInteractor, ): QSTileViewModel = - factory.create( - TileSpec.create(NOTES_TILE_SPEC), - userActionInteractor, - stateInteractor, - mapper, - ) + if (com.android.systemui.Flags.qsNewTilesFuture()) + factory.create( + TileSpec.create(NOTES_TILE_SPEC), + userActionInteractor, + stateInteractor, + mapper, + ) + else StubQSTileViewModel @Provides @IntoMap @@ -120,10 +123,10 @@ interface NoteTaskModule { QSTileConfig( tileSpec = TileSpec.create(NOTES_TILE_SPEC), uiConfig = - QSTileUIConfig.Resource( - iconRes = R.drawable.ic_qs_notes, - labelRes = R.string.quick_settings_notes_label, - ), + QSTileUIConfig.Resource( + iconRes = R.drawable.ic_qs_notes, + labelRes = R.string.quick_settings_notes_label, + ), instanceId = uiEventLogger.getNewInstanceId(), category = TileCategory.UTILITIES, ) diff --git a/packages/SystemUI/src/com/android/systemui/power/shared/model/WakeSleepReason.kt b/packages/SystemUI/src/com/android/systemui/power/shared/model/WakeSleepReason.kt index 776a8f47f056..c57b53bab442 100644 --- a/packages/SystemUI/src/com/android/systemui/power/shared/model/WakeSleepReason.kt +++ b/packages/SystemUI/src/com/android/systemui/power/shared/model/WakeSleepReason.kt @@ -26,6 +26,9 @@ enum class WakeSleepReason( /** The physical power button was pressed to wake up or sleep the device. */ POWER_BUTTON(isTouch = false, PowerManager.WAKE_REASON_POWER_BUTTON), + /** The sleep button was pressed to sleep the device. */ + SLEEP_BUTTON(isTouch = false, PowerManager.GO_TO_SLEEP_REASON_SLEEP_BUTTON), + /** The user has tapped or double tapped to wake the screen. */ TAP(isTouch = true, PowerManager.WAKE_REASON_TAP), @@ -78,6 +81,7 @@ enum class WakeSleepReason( fun fromPowerManagerSleepReason(reason: Int): WakeSleepReason { return when (reason) { PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON -> POWER_BUTTON + PowerManager.GO_TO_SLEEP_REASON_SLEEP_BUTTON -> SLEEP_BUTTON PowerManager.GO_TO_SLEEP_REASON_TIMEOUT -> TIMEOUT PowerManager.GO_TO_SLEEP_REASON_DEVICE_FOLD -> FOLD else -> OTHER diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSHostAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/QSHostAdapter.kt index 8b0694219630..0d464f5a0936 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSHostAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/QSHostAdapter.kt @@ -28,6 +28,7 @@ import com.android.systemui.qs.pipeline.data.repository.TileSpecRepository.Compa import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor import com.android.systemui.qs.pipeline.shared.QSPipelineFlagsRepository import com.android.systemui.qs.pipeline.shared.TileSpec +import com.android.systemui.shade.ShadeDisplayAware import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job @@ -48,7 +49,7 @@ class QSHostAdapter @Inject constructor( private val interactor: CurrentTilesInteractor, - private val context: Context, + @ShadeDisplayAware private val context: Context, private val tileServiceRequestControllerBuilder: TileServiceRequestController.Builder, @Application private val scope: CoroutineScope, dumpManager: DumpManager, diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java index 85bcc25e140c..afb852ae824c 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java @@ -50,6 +50,7 @@ import com.android.systemui.util.kotlin.JavaAdapterKt; import kotlin.Unit; import kotlin.jvm.functions.Function1; +import kotlinx.coroutines.DisposableHandle; import kotlinx.coroutines.flow.StateFlow; import java.io.PrintWriter; @@ -107,6 +108,8 @@ public abstract class QSPanelControllerBase<T extends QSPanel> extends ViewContr setLayoutForMediaInScene(); }; + private DisposableHandle mJavaAdapterDisposableHandle; + private boolean mLastListening; @VisibleForTesting @@ -221,6 +224,9 @@ public abstract class QSPanelControllerBase<T extends QSPanel> extends ViewContr mView.removeTile(record); } mRecords.clear(); + if (mJavaAdapterDisposableHandle != null) { + mJavaAdapterDisposableHandle.dispose(); + } } @Override @@ -255,7 +261,7 @@ public abstract class QSPanelControllerBase<T extends QSPanel> extends ViewContr } private void registerForMediaInteractorChanges() { - JavaAdapterKt.collectFlow( + mJavaAdapterDisposableHandle = JavaAdapterKt.collectFlow( mView, getMediaVisibleFlow(), mMediaOrRecommendationVisibleConsumer diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java index d38f8492c883..88f0318c2fbf 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSSecurityFooterUtils.java @@ -87,6 +87,7 @@ import com.android.systemui.qs.footer.domain.model.SecurityButtonConfig; import com.android.systemui.res.R; import com.android.systemui.security.data.model.SecurityModel; import com.android.systemui.settings.UserTracker; +import com.android.systemui.shade.ShadeDisplayAware; import com.android.systemui.statusbar.phone.SystemUIDialog; import com.android.systemui.statusbar.policy.SecurityController; @@ -177,7 +178,7 @@ public class QSSecurityFooterUtils implements DialogInterface.OnClickListener { @Inject QSSecurityFooterUtils( - @Application Context context, DevicePolicyManager devicePolicyManager, + @ShadeDisplayAware Context context, DevicePolicyManager devicePolicyManager, UserTracker userTracker, @Main Handler mainHandler, ActivityStarter activityStarter, SecurityController securityController, @Background Looper bgLooper, DialogTransitionAnimator dialogTransitionAnimator) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/dagger/QSFragmentComposeModule.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/dagger/QSFragmentComposeModule.kt index 676f6a426264..2ec729223a8d 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/dagger/QSFragmentComposeModule.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/dagger/QSFragmentComposeModule.kt @@ -20,6 +20,7 @@ import android.content.Context import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.qs.flags.QSComposeFragment +import com.android.systemui.shade.ShadeDisplayAware import com.android.systemui.util.Utils import dagger.Module import dagger.Provides @@ -34,7 +35,7 @@ interface QSFragmentComposeModule { @Provides @SysUISingleton @Named(QS_USING_MEDIA_PLAYER) - fun providesUsingMedia(@Application context: Context): Boolean { + fun providesUsingMedia(@ShadeDisplayAware context: Context): Boolean { return QSComposeFragment.isEnabled && Utils.useQsMediaPlayer(context) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java b/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java index 89f85ab14dd6..15e34990e111 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java +++ b/packages/SystemUI/src/com/android/systemui/qs/customize/TileQueryHelper.java @@ -43,6 +43,7 @@ import com.android.systemui.qs.external.CustomTile; import com.android.systemui.qs.tileimpl.QSTileImpl.DrawableIcon; import com.android.systemui.res.R; import com.android.systemui.settings.UserTracker; +import com.android.systemui.shade.ShadeDisplayAware; import java.util.ArrayList; import java.util.Arrays; @@ -69,7 +70,7 @@ public class TileQueryHelper { @Inject public TileQueryHelper( - Context context, + @ShadeDisplayAware Context context, UserTracker userTracker, @Main Executor mainExecutor, @Background Executor bgExecutor diff --git a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt index 564bc78a3f98..8ef637545e69 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/footer/ui/viewmodel/FooterActionsViewModel.kt @@ -37,6 +37,7 @@ import com.android.systemui.qs.footer.data.model.UserSwitcherStatusModel import com.android.systemui.qs.footer.domain.interactor.FooterActionsInteractor import com.android.systemui.qs.footer.domain.model.SecurityButtonConfig import com.android.systemui.res.R +import com.android.systemui.shade.ShadeDisplayAware import com.android.systemui.util.icuMessageFormat import javax.inject.Inject import javax.inject.Named @@ -112,7 +113,7 @@ class FooterActionsViewModel( class Factory @Inject constructor( - @Application private val context: Context, + @ShadeDisplayAware private val context: Context, private val falsingManager: FalsingManager, private val footerActionsInteractor: FooterActionsInteractor, private val globalActionsDialogLiteProvider: Provider<GlobalActionsDialogLite>, @@ -175,7 +176,7 @@ class FooterActionsViewModel( } fun FooterActionsViewModel( - @Application appContext: Context, + @ShadeDisplayAware appContext: Context, footerActionsInteractor: FooterActionsInteractor, falsingManager: FalsingManager, globalActionsDialogLite: GlobalActionsDialogLite, diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt index 4e094cc77eae..789fdebc36eb 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/PaginatedGridLayout.kt @@ -16,12 +16,17 @@ package com.android.systemui.qs.panels.ui.compose -import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Arrangement.spacedBy import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.requiredHeight +import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.layout.wrapContentWidth import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.RoundedCornerShape @@ -39,16 +44,17 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.android.compose.animation.scene.SceneScope import com.android.compose.modifiers.padding import com.android.systemui.compose.modifiers.sysuiResTag +import com.android.systemui.development.ui.compose.BuildNumber +import com.android.systemui.development.ui.viewmodel.BuildNumberViewModel import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.qs.panels.dagger.PaginatedBaseLayoutType -import com.android.systemui.qs.panels.ui.compose.PaginatedGridLayout.Dimensions.FooterHeight -import com.android.systemui.qs.panels.ui.compose.PaginatedGridLayout.Dimensions.InterPageSpacing +import com.android.systemui.qs.panels.ui.compose.Dimensions.FooterHeight +import com.android.systemui.qs.panels.ui.compose.Dimensions.InterPageSpacing import com.android.systemui.qs.panels.ui.viewmodel.PaginatedGridViewModel import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel import com.android.systemui.qs.ui.compose.borderOnFocus @@ -121,38 +127,78 @@ constructor( TileGrid(tiles = page, modifier = Modifier, editModeStart = {}) } } - // Use requiredHeight so it won't be squished if the view doesn't quite fit. As this is - // expected to be inside a scrollable container, this should not be an issue. - Box(modifier = Modifier.requiredHeight(FooterHeight).fillMaxWidth()) { - PagerDots( - pagerState = pagerState, - activeColor = MaterialTheme.colorScheme.primary, - nonActiveColor = MaterialTheme.colorScheme.surfaceVariant, - modifier = Modifier.align(Alignment.Center), - ) - CompositionLocalProvider(value = LocalContentColor provides Color.White) { - IconButton( - onClick = editModeStart, - shape = RoundedCornerShape(CornerSize(28.dp)), - modifier = - Modifier.align(Alignment.CenterEnd) - .borderOnFocus( - color = MaterialTheme.colorScheme.secondary, - cornerSize = CornerSize(FooterHeight / 2), - ), - ) { - Icon( - imageVector = Icons.Default.Edit, - contentDescription = stringResource(id = R.string.qs_edit), + FooterBar( + buildNumberViewModelFactory = viewModel.buildNumberViewModelFactory, + pagerState = pagerState, + editModeStart = editModeStart, + ) + } + } +} + +private object Dimensions { + val FooterHeight = 48.dp + val InterPageSpacing = 16.dp +} + +@Composable +private fun FooterBar( + buildNumberViewModelFactory: BuildNumberViewModel.Factory, + pagerState: PagerState, + editModeStart: () -> Unit, +) { + // Use requiredHeight so it won't be squished if the view doesn't quite fit. As this is + // expected to be inside a scrollable container, this should not be an issue. + // Also, we construct the layout this way to do the following: + // * PagerDots is centered in the row, taking as much space as it needs. + // * On the start side, we place the BuildNumber, taking as much space as it needs, but + // constrained by the available space left over after PagerDots + // * On the end side, we place the edit mode button, with the same constraints as for + // BuildNumber (but it will usually fit, as it's just a square button). + Row( + modifier = Modifier.requiredHeight(FooterHeight).fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = spacedBy(8.dp), + ) { + Row(Modifier.weight(1f)) { + BuildNumber( + viewModelFactory = buildNumberViewModelFactory, + textColor = MaterialTheme.colorScheme.onSurface, + modifier = + Modifier.borderOnFocus( + color = MaterialTheme.colorScheme.secondary, + cornerSize = CornerSize(1.dp), ) - } + .wrapContentSize(), + ) + Spacer(modifier = Modifier.weight(1f)) + } + PagerDots( + pagerState = pagerState, + activeColor = MaterialTheme.colorScheme.primary, + nonActiveColor = MaterialTheme.colorScheme.surfaceVariant, + modifier = Modifier.wrapContentWidth(), + ) + Row(Modifier.weight(1f)) { + Spacer(modifier = Modifier.weight(1f)) + CompositionLocalProvider( + value = LocalContentColor provides MaterialTheme.colorScheme.onSurface + ) { + IconButton( + onClick = editModeStart, + shape = RoundedCornerShape(CornerSize(28.dp)), + modifier = + Modifier.borderOnFocus( + color = MaterialTheme.colorScheme.secondary, + cornerSize = CornerSize(FooterHeight / 2), + ), + ) { + Icon( + imageVector = Icons.Default.Edit, + contentDescription = stringResource(id = R.string.qs_edit), + ) } } } } - - private object Dimensions { - val FooterHeight = 48.dp - val InterPageSpacing = 16.dp - } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt index cb57c6710553..0a80a19871fd 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/Tile.kt @@ -126,7 +126,7 @@ fun Tile( val currentBounceableInfo by rememberUpdatedState(bounceableInfo) val resources = resources() val uiState = remember(state, resources) { state.toUiState(resources) } - val colors = TileDefaults.getColorForState(uiState) + val colors = TileDefaults.getColorForState(uiState, iconOnly) val hapticsViewModel: TileHapticsViewModel? = rememberViewModel(traceName = "TileHapticsViewModel") { tileHapticsViewModelFactoryProvider.getHapticsViewModelFactory()?.create(tile) @@ -365,22 +365,24 @@ private object TileDefaults { ) @Composable - fun getColorForState(uiState: TileUiState): TileColors { + fun getColorForState(uiState: TileUiState, iconOnly: Boolean): TileColors { return when (uiState.state) { STATE_ACTIVE -> { - if (uiState.handlesSecondaryClick) { + if (uiState.handlesSecondaryClick && !iconOnly) { activeDualTargetTileColors() } else { activeTileColors() } } + STATE_INACTIVE -> { - if (uiState.handlesSecondaryClick) { + if (uiState.handlesSecondaryClick && !iconOnly) { inactiveDualTargetTileColors() } else { inactiveTileColors() } } + else -> unavailableTileColors() } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModel.kt index 4e34e73654fc..faab6960a99c 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/EditModeViewModel.kt @@ -56,7 +56,7 @@ constructor( private val tilesAvailabilityInteractor: TilesAvailabilityInteractor, private val minTilesInteractor: MinimumTilesInteractor, @ShadeDisplayAware private val configurationInteractor: ConfigurationInteractor, - @Application private val applicationContext: Context, + @ShadeDisplayAware private val context: Context, @Named("Default") private val defaultGridLayout: GridLayout, @Application private val applicationScope: CoroutineScope, gridLayoutTypeInteractor: GridLayoutTypeInteractor, @@ -140,7 +140,7 @@ constructor( .combine(configurationInteractor.onAnyConfigurationChange.emitOnStart()) { tiles, _ -> - tiles.fastMap { it.load(applicationContext) } + tiles.fastMap { it.load(context) } } } else { emptyFlow() diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModel.kt index e5607eb6e620..bff330b98fda 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModel.kt @@ -17,6 +17,7 @@ package com.android.systemui.qs.panels.ui.viewmodel import androidx.compose.runtime.getValue +import com.android.systemui.development.ui.viewmodel.BuildNumberViewModel import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.Hydrator import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager.Companion.LOCATION_QS @@ -34,6 +35,7 @@ constructor( columnsWithMediaViewModelFactory: QSColumnsViewModel.Factory, paginatedGridInteractor: PaginatedGridInteractor, inFirstPageViewModel: InFirstPageViewModel, + val buildNumberViewModelFactory: BuildNumberViewModel.Factory, ) : IconTilesViewModel by iconTilesViewModel, ExclusiveActivatable() { private val hydrator = Hydrator("PaginatedGridViewModel") 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 c5b27376a82a..41cdefdb2396 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 @@ -30,8 +30,8 @@ import androidx.annotation.GuardedBy import com.android.systemui.common.data.repository.PackageChangeRepository import com.android.systemui.common.shared.model.PackageChangeModel import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.shade.ShadeDisplayAware import com.android.systemui.util.kotlin.isComponentActuallyEnabled import javax.inject.Inject import kotlinx.coroutines.CoroutineScope @@ -54,7 +54,7 @@ interface InstalledTilesComponentRepository { class InstalledTilesComponentRepositoryImpl @Inject constructor( - @Application private val applicationContext: Context, + @ShadeDisplayAware private val context: Context, @Background private val backgroundScope: CoroutineScope, private val packageChangeRepository: PackageChangeRepository ) : InstalledTilesComponentRepository { @@ -77,10 +77,10 @@ constructor( * context. */ val packageManager = - if (applicationContext.userId == userId) { - applicationContext.packageManager + if (context.userId == userId) { + context.packageManager } else { - applicationContext + context .createContextAsUser( UserHandle.of(userId), /* flags */ 0, diff --git a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/NightDisplayAutoAddable.kt b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/NightDisplayAutoAddable.kt index 31ea734fb842..e9c91ca0db12 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/NightDisplayAutoAddable.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/pipeline/domain/autoaddable/NightDisplayAutoAddable.kt @@ -27,6 +27,7 @@ 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 com.android.systemui.qs.tiles.NightDisplayTile +import com.android.systemui.shade.ShadeDisplayAware import javax.inject.Inject import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow @@ -42,7 +43,7 @@ class NightDisplayAutoAddable @Inject constructor( private val nightDisplayListenerBuilder: NightDisplayListenerModule.Builder, - context: Context, + @ShadeDisplayAware context: Context, ) : AutoAddable { private val enabled = ColorDisplayManager.isNightDisplayAvailable(context) diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java index 6b654beea149..fed8b60a653f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/UserDetailView.java @@ -39,6 +39,7 @@ import com.android.systemui.qs.PseudoGridView; import com.android.systemui.qs.QSUserSwitcherEvent; import com.android.systemui.qs.user.UserSwitchDialogController; import com.android.systemui.res.R; +import com.android.systemui.shade.ShadeDisplayAware; import com.android.systemui.statusbar.phone.SystemUIDialog; import com.android.systemui.statusbar.policy.BaseUserSwitcherAdapter; import com.android.systemui.statusbar.policy.UserSwitcherController; @@ -95,7 +96,7 @@ public class UserDetailView extends PseudoGridView { } @Inject - public Adapter(Context context, UserSwitcherController controller, + public Adapter(@ShadeDisplayAware Context context, UserSwitcherController controller, UiEventLogger uiEventLogger, FalsingManager falsingManager) { super(controller); mContext = context; diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/DisabledByPolicyInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/DisabledByPolicyInteractor.kt index 87b89ea6810a..45775272e01f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/DisabledByPolicyInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/base/interactor/DisabledByPolicyInteractor.kt @@ -27,6 +27,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.plugins.ActivityStarter import com.android.systemui.qs.tiles.base.interactor.DisabledByPolicyInteractor.PolicyResult +import com.android.systemui.shade.ShadeDisplayAware import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext @@ -70,7 +71,7 @@ interface DisabledByPolicyInteractor { class DisabledByPolicyInteractorImpl @Inject constructor( - private val context: Context, + @ShadeDisplayAware private val context: Context, private val activityStarter: ActivityStarter, private val restrictedLockProxy: RestrictedLockProxy, @Background private val backgroundDispatcher: CoroutineDispatcher, @@ -105,7 +106,7 @@ constructor( /** Mockable proxy for [RestrictedLockUtilsInternal] static methods. */ @VisibleForTesting -class RestrictedLockProxy @Inject constructor(private val context: Context) { +class RestrictedLockProxy @Inject constructor(@ShadeDisplayAware private val context: Context) { @WorkerThread fun hasBaseUserRestriction(userId: Int, userRestriction: String?): Boolean = diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/di/QSTilesModule.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/di/QSTilesModule.kt index b766ee0d4333..222fa3efbe94 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/di/QSTilesModule.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/di/QSTilesModule.kt @@ -28,6 +28,7 @@ import com.android.systemui.qs.tiles.viewmodel.QSTileConfig import com.android.systemui.qs.tiles.viewmodel.QSTileConfigProvider import com.android.systemui.qs.tiles.viewmodel.QSTileConfigProviderImpl import com.android.systemui.qs.tiles.viewmodel.QSTileViewModel +import com.android.systemui.shade.ShadeDisplayAware import dagger.Binds import dagger.Module import dagger.Provides @@ -66,6 +67,6 @@ interface QSTilesModule { companion object { - @Provides fun provideTilesTheme(context: Context): Theme = context.theme + @Provides fun provideTilesTheme(@ShadeDisplayAware context: Context): Theme = context.theme } } 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 8f6c4e743269..244f024625db 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 @@ -87,6 +87,7 @@ import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.Flags; import com.android.systemui.plugins.ActivityStarter; import com.android.systemui.res.R; +import com.android.systemui.shade.ShadeDisplayAware; import com.android.systemui.statusbar.connectivity.AccessPointController; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.systemui.statusbar.policy.LocationController; @@ -240,7 +241,7 @@ public class InternetDialogController implements AccessPointController.AccessPoi } @Inject - public InternetDialogController(@NonNull Context context, UiEventLogger uiEventLogger, + public InternetDialogController(@ShadeDisplayAware Context context, UiEventLogger uiEventLogger, ActivityStarter starter, AccessPointController accessPointController, SubscriptionManager subscriptionManager, TelephonyManager telephonyManager, @Nullable WifiManager wifiManager, ConnectivityManager connectivityManager, diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegate.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegate.java index 89b9eee52f2a..0ab533bb9838 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegate.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/InternetDialogDelegate.java @@ -71,6 +71,7 @@ import com.android.systemui.animation.DialogTransitionAnimator; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.res.R; +import com.android.systemui.shade.ShadeDisplayAware; import com.android.systemui.statusbar.phone.SystemUIDialog; import com.android.systemui.statusbar.policy.KeyguardStateController; import com.android.wifitrackerlib.WifiEntry; @@ -190,7 +191,7 @@ public class InternetDialogDelegate implements @AssistedInject public InternetDialogDelegate( - Context context, + @ShadeDisplayAware Context context, InternetDialogManager internetDialogManager, InternetDialogController internetDialogController, @Assisted(CAN_CONFIG_MOBILE_DATA) boolean canConfigMobileData, diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTileDefaultsRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTileDefaultsRepository.kt index 1546ec2c54bd..32fb1d18724d 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTileDefaultsRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTileDefaultsRepository.kt @@ -26,6 +26,7 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.qs.tiles.impl.custom.data.entity.CustomTileDefaults import com.android.systemui.qs.tiles.impl.di.QSTileScope +import com.android.systemui.shade.ShadeDisplayAware import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope @@ -68,7 +69,7 @@ interface CustomTileDefaultsRepository { class CustomTileDefaultsRepositoryImpl @Inject constructor( - private val context: Context, + @ShadeDisplayAware private val context: Context, @Application applicationScope: CoroutineScope, @Background private val backgroundDispatcher: CoroutineDispatcher, ) : CustomTileDefaultsRepository { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTilePackageUpdatesRepository.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTilePackageUpdatesRepository.kt index 0ebd6f2b4ac3..cd4938f01b63 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTilePackageUpdatesRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/data/repository/CustomTilePackageUpdatesRepository.kt @@ -28,6 +28,7 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.qs.pipeline.shared.TileSpec import com.android.systemui.qs.tiles.impl.di.QSTileScope +import com.android.systemui.shade.ShadeDisplayAware import javax.inject.Inject import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope @@ -51,7 +52,7 @@ class CustomTilePackageUpdatesRepositoryImpl @Inject constructor( private val tileSpec: TileSpec.CustomTileSpec, - @Application private val context: Context, + @ShadeDisplayAware private val context: Context, @QSTileScope private val tileScope: CoroutineScope, @Background private val backgroundCoroutineContext: CoroutineContext, ) : CustomTilePackageUpdatesRepository { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/CustomTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/CustomTileMapper.kt index 60aa4ea4759f..c446865f31af 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/CustomTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/CustomTileMapper.kt @@ -30,12 +30,16 @@ import com.android.systemui.qs.tiles.base.interactor.QSTileDataToStateMapper import com.android.systemui.qs.tiles.impl.custom.domain.entity.CustomTileDataModel import com.android.systemui.qs.tiles.viewmodel.QSTileConfig import com.android.systemui.qs.tiles.viewmodel.QSTileState +import com.android.systemui.shade.ShadeDisplayAware import javax.inject.Inject @SysUISingleton class CustomTileMapper @Inject -constructor(private val context: Context, private val uriGrantsManager: IUriGrantsManager) : +constructor( + @ShadeDisplayAware private val context: Context, + private val uriGrantsManager: IUriGrantsManager +) : QSTileDataToStateMapper<CustomTileDataModel> { override fun map(config: QSTileConfig, data: CustomTileDataModel): QSTileState { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileUserActionInteractor.kt index af2bb9d0d2f7..1153b5c67261 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileUserActionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileUserActionInteractor.kt @@ -42,6 +42,7 @@ import com.android.systemui.qs.tiles.impl.custom.domain.entity.CustomTileDataMod import com.android.systemui.qs.tiles.impl.di.QSTileScope import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction import com.android.systemui.settings.DisplayTracker +import com.android.systemui.shade.ShadeDisplayAware import java.util.concurrent.atomic.AtomicReference import javax.inject.Inject import kotlin.coroutines.CoroutineContext @@ -51,7 +52,7 @@ import kotlinx.coroutines.withContext class CustomTileUserActionInteractor @Inject constructor( - private val context: Context, + @ShadeDisplayAware private val context: Context, private val tileSpec: TileSpec, private val qsTileLogger: QSTileLogger, private val windowManager: IWindowManager, diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt index fc945851cdad..1a6876d0b765 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/InternetTileMapper.kt @@ -30,6 +30,7 @@ import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileMode import com.android.systemui.qs.tiles.viewmodel.QSTileConfig import com.android.systemui.qs.tiles.viewmodel.QSTileState import com.android.systemui.res.R +import com.android.systemui.shade.ShadeDisplayAware import com.android.systemui.statusbar.pipeline.shared.ui.model.InternetTileIconModel import javax.inject.Inject @@ -37,9 +38,9 @@ import javax.inject.Inject class InternetTileMapper @Inject constructor( - @Main private val resources: Resources, + @ShadeDisplayAware private val resources: Resources, private val theme: Resources.Theme, - private val context: Context, + @ShadeDisplayAware private val context: Context, @Main private val handler: Handler, ) : QSTileDataToStateMapper<InternetTileModel> { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt index 6fe3979fa446..6d10843decc0 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt @@ -28,6 +28,7 @@ import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor import com.android.systemui.qs.tiles.impl.internet.domain.model.InternetTileModel import com.android.systemui.res.R +import com.android.systemui.shade.ShadeDisplayAware import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository import com.android.systemui.statusbar.pipeline.ethernet.domain.EthernetInteractor import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor @@ -54,7 +55,7 @@ import kotlinx.coroutines.flow.stateIn class InternetTileDataInteractor @Inject constructor( - private val context: Context, + @ShadeDisplayAware private val context: Context, @Application private val scope: CoroutineScope, airplaneModeRepository: AirplaneModeRepository, private val connectivityRepository: ConnectivityRepository, diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt index 3e44258229f9..9b2880b6d47f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/domain/interactor/ModesTileDataInteractor.kt @@ -28,6 +28,7 @@ import com.android.systemui.qs.tiles.ModesTile import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor import com.android.systemui.qs.tiles.impl.modes.domain.model.ModesTileModel +import com.android.systemui.shade.ShadeDisplayAware import com.android.systemui.statusbar.policy.domain.interactor.ZenModeInteractor import com.android.systemui.statusbar.policy.domain.model.ActiveZenModes import com.android.systemui.statusbar.policy.domain.model.ZenModeInfo @@ -42,7 +43,7 @@ import kotlinx.coroutines.flow.map class ModesTileDataInteractor @Inject constructor( - val context: Context, + @ShadeDisplayAware val context: Context, val zenModeInteractor: ZenModeInteractor, @Background val bgDispatcher: CoroutineDispatcher, ) : QSTileDataInteractor<ModesTileModel> { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/interactor/DataSaverTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/interactor/DataSaverTileUserActionInteractor.kt index 252e3f84df6c..05bdf0a92679 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/interactor/DataSaverTileUserActionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/saver/domain/interactor/DataSaverTileUserActionInteractor.kt @@ -32,6 +32,7 @@ import com.android.systemui.qs.tiles.impl.saver.domain.DataSaverDialogDelegate import com.android.systemui.qs.tiles.impl.saver.domain.model.DataSaverTileModel import com.android.systemui.qs.tiles.viewmodel.QSTileUserAction import com.android.systemui.settings.UserFileManager +import com.android.systemui.shade.ShadeDisplayAware import com.android.systemui.statusbar.phone.SystemUIDialog import com.android.systemui.statusbar.policy.DataSaverController import javax.inject.Inject @@ -42,7 +43,7 @@ import kotlinx.coroutines.withContext class DataSaverTileUserActionInteractor @Inject constructor( - @Application private val context: Context, + @ShadeDisplayAware private val context: Context, @Main private val coroutineContext: CoroutineContext, @Background private val backgroundContext: CoroutineContext, private val dataSaverController: DataSaverController, diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/interactor/UiModeNightTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/interactor/UiModeNightTileDataInteractor.kt index c928e8af17fc..7af3576d8cd9 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/interactor/UiModeNightTileDataInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/uimodenight/domain/interactor/UiModeNightTileDataInteractor.kt @@ -25,6 +25,7 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.qs.tiles.base.interactor.DataUpdateTrigger import com.android.systemui.qs.tiles.base.interactor.QSTileDataInteractor import com.android.systemui.qs.tiles.impl.uimodenight.domain.model.UiModeNightTileModel +import com.android.systemui.shade.ShadeDisplayAware import com.android.systemui.statusbar.policy.BatteryController import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.LocationController @@ -38,7 +39,7 @@ import kotlinx.coroutines.flow.flowOf class UiModeNightTileDataInteractor @Inject constructor( - @Application private val context: Context, + @ShadeDisplayAware private val context: Context, private val configurationController: ConfigurationController, private val uiModeManager: UiModeManager, private val batteryController: BatteryController, diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt index 35b1b9636263..ab3862b75ee8 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/viewmodel/QSTileViewModelAdapter.kt @@ -81,7 +81,7 @@ constructor( // additional // guidance on how to auto add your tile throw UnsupportedOperationException( - "Turning on tile is not supported now" + "Turning on tile is not supported now. Tile spec: $tileSpec" ) } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt index 9125d7e8bb0e..066fe1b0d896 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt @@ -430,8 +430,13 @@ constructor( "mechanism: ${deviceUnlockStatus.deviceUnlockSource}" else -> null } - // Not on lockscreen or bouncer, so remain in the current scene. - else -> null + // Not on lockscreen or bouncer, so remain in the current scene but since + // unlocked, replace the Lockscreen scene from the bottom of the navigation + // back stack with the Gone scene. + else -> { + replaceLockscreenSceneOnBackStack() + null + } } } .collect { (targetSceneKey, loggingReason) -> @@ -440,17 +445,19 @@ constructor( } } - /** If the [Scenes.Lockscreen] is on the backstack, replaces it with [Scenes.Gone]. */ + /** + * If the [Scenes.Lockscreen] is on the bottom of the navigation backstack, replaces it with + * [Scenes.Gone]. + */ private fun replaceLockscreenSceneOnBackStack() { sceneBackInteractor.updateBackStack { stack -> val list = stack.asIterable().toMutableList() - check(list.last() == Scenes.Lockscreen) { - "The bottommost/last SceneKey of the back stack isn't" + - " the Lockscreen scene like expected. The back" + - " stack is $stack." + if (list.lastOrNull() == Scenes.Lockscreen) { + list[list.size - 1] = Scenes.Gone + sceneStackOf(*list.toTypedArray()) + } else { + stack } - list[list.size - 1] = Scenes.Gone - sceneStackOf(*list.toTypedArray()) } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt index 2dcb706234b8..d0c033bb10b0 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImpl.kt @@ -834,6 +834,15 @@ constructor( public?.let { it.layoutInflaterFactory = provider.provide(row, FLAG_CONTENT_VIEW_PUBLIC) } + if (android.app.Flags.notificationsRedesignAppIcons()) { + normalGroupHeader?.let { + it.layoutInflaterFactory = provider.provide(row, FLAG_GROUP_SUMMARY_HEADER) + } + minimizedGroupHeader?.let { + it.layoutInflaterFactory = + provider.provide(row, FLAG_LOW_PRIORITY_GROUP_SUMMARY_HEADER) + } + } return this } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java index 7ef1e416aa19..5837a498e44f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ManagedProfileControllerImpl.java @@ -31,6 +31,7 @@ import java.util.ArrayList; import java.util.LinkedList; import java.util.List; import java.util.concurrent.Executor; +import java.util.function.Consumer; import javax.inject.Inject; @@ -63,17 +64,21 @@ public class ManagedProfileControllerImpl implements ManagedProfileController { @Override public void addCallback(@NonNull Callback callback) { - mCallbacks.add(callback); - if (mCallbacks.size() == 1) { - setListening(true); + synchronized (mCallbacks) { + mCallbacks.add(callback); + if (mCallbacks.size() == 1) { + setListening(true); + } + callback.onManagedProfileChanged(); } - callback.onManagedProfileChanged(); } @Override public void removeCallback(@NonNull Callback callback) { - if (mCallbacks.remove(callback) && mCallbacks.size() == 0) { - setListening(false); + synchronized (mCallbacks) { + if (mCallbacks.remove(callback) && mCallbacks.size() == 0) { + setListening(false); + } } } @@ -109,10 +114,7 @@ public class ManagedProfileControllerImpl implements ManagedProfileController { } private void notifyManagedProfileRemoved() { - ArrayList<Callback> copy = new ArrayList<>(mCallbacks); - for (Callback callback : copy) { - callback.onManagedProfileRemoved(); - } + notifyCallbacks(Callback::onManagedProfileRemoved); } public boolean hasActiveProfile() { @@ -136,6 +138,16 @@ public class ManagedProfileControllerImpl implements ManagedProfileController { } } + private void notifyCallbacks(Consumer<Callback> method) { + ArrayList<Callback> copy; + synchronized (mCallbacks) { + copy = new ArrayList<>(mCallbacks); + } + for (Callback callback : copy) { + method.accept(callback); + } + } + private void setListening(boolean listening) { if (mListening == listening) { return; @@ -154,19 +166,13 @@ public class ManagedProfileControllerImpl implements ManagedProfileController { @Override public void onUserChanged(int newUser, @NonNull Context userContext) { reloadManagedProfiles(); - ArrayList<Callback> copy = new ArrayList<>(mCallbacks); - for (Callback callback : copy) { - callback.onManagedProfileChanged(); - } + notifyCallbacks(Callback::onManagedProfileChanged); } @Override public void onProfilesChanged(@NonNull List<UserInfo> profiles) { reloadManagedProfiles(); - ArrayList<Callback> copy = new ArrayList<>(mCallbacks); - for (Callback callback : copy) { - callback.onManagedProfileChanged(); - } + notifyCallbacks(Callback::onManagedProfileChanged); } } } diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt index d371acf86a28..66a900bd72d8 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/TutorialSelectionScreen.kt @@ -18,6 +18,7 @@ package com.android.systemui.touchpad.tutorial.ui.composable import android.content.res.Configuration import androidx.compose.foundation.background +import androidx.compose.foundation.focusable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -36,8 +37,12 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.pointer.pointerInteropFilter @@ -49,6 +54,7 @@ import com.android.systemui.inputdevice.tutorial.ui.composable.DoneButton import com.android.systemui.res.R import com.android.systemui.touchpad.tutorial.ui.gesture.isFourFingerTouchpadSwipe import com.android.systemui.touchpad.tutorial.ui.gesture.isThreeFingerTouchpadSwipe +import com.android.systemui.touchpad.tutorial.ui.viewmodel.Screen @Composable fun TutorialSelectionScreen( @@ -56,6 +62,7 @@ fun TutorialSelectionScreen( onHomeTutorialClicked: () -> Unit, onRecentAppsTutorialClicked: () -> Unit, onDoneButtonClicked: () -> Unit, + lastSelectedScreen: Screen, ) { Column( verticalArrangement = Arrangement.Center, @@ -80,6 +87,7 @@ fun TutorialSelectionScreen( onHomeTutorialClicked = onHomeTutorialClicked, onRecentAppsTutorialClicked = onRecentAppsTutorialClicked, modifier = Modifier.weight(1f).padding(60.dp), + lastSelectedScreen, ) } else -> { @@ -88,6 +96,7 @@ fun TutorialSelectionScreen( onHomeTutorialClicked = onHomeTutorialClicked, onRecentAppsTutorialClicked = onRecentAppsTutorialClicked, modifier = Modifier.weight(1f).padding(60.dp), + lastSelectedScreen, ) } } @@ -105,6 +114,7 @@ private fun HorizontalSelectionButtons( onHomeTutorialClicked: () -> Unit, onRecentAppsTutorialClicked: () -> Unit, modifier: Modifier = Modifier, + lastSelectedScreen: Screen, ) { Row( horizontalArrangement = Arrangement.spacedBy(20.dp), @@ -116,6 +126,7 @@ private fun HorizontalSelectionButtons( onHomeTutorialClicked, onRecentAppsTutorialClicked, modifier = Modifier.weight(1f).fillMaxSize(), + lastSelectedScreen, ) } } @@ -126,6 +137,7 @@ private fun VerticalSelectionButtons( onHomeTutorialClicked: () -> Unit, onRecentAppsTutorialClicked: () -> Unit, modifier: Modifier = Modifier, + lastSelectedScreen: Screen, ) { Column( verticalArrangement = Arrangement.spacedBy(20.dp), @@ -137,6 +149,7 @@ private fun VerticalSelectionButtons( onHomeTutorialClicked, onRecentAppsTutorialClicked, modifier = Modifier.weight(1f).fillMaxSize(), + lastSelectedScreen, ) } } @@ -147,14 +160,26 @@ private fun ThreeTutorialButtons( onHomeTutorialClicked: () -> Unit, onRecentAppsTutorialClicked: () -> Unit, modifier: Modifier = Modifier, + lastSelectedScreen: Screen, ) { + val homeFocusRequester = remember { FocusRequester() } + val backFocusRequester = remember { FocusRequester() } + val recentAppsFocusRequester = remember { FocusRequester() } + LaunchedEffect(Unit) { + when (lastSelectedScreen) { + Screen.HOME_GESTURE -> homeFocusRequester.requestFocus() + Screen.BACK_GESTURE -> backFocusRequester.requestFocus() + Screen.RECENT_APPS_GESTURE -> recentAppsFocusRequester.requestFocus() + else -> {} // No-Op. + } + } TutorialButton( text = stringResource(R.string.touchpad_tutorial_home_gesture_button), icon = ImageVector.vectorResource(id = R.drawable.touchpad_tutorial_home_icon), iconColor = MaterialTheme.colorScheme.onPrimary, onClick = onHomeTutorialClicked, backgroundColor = MaterialTheme.colorScheme.primary, - modifier = modifier, + modifier = modifier.focusRequester(homeFocusRequester).focusable(), ) TutorialButton( text = stringResource(R.string.touchpad_tutorial_back_gesture_button), @@ -162,7 +187,7 @@ private fun ThreeTutorialButtons( iconColor = MaterialTheme.colorScheme.onTertiary, onClick = onBackTutorialClicked, backgroundColor = MaterialTheme.colorScheme.tertiary, - modifier = modifier, + modifier = modifier.focusRequester(backFocusRequester).focusable(), ) TutorialButton( text = stringResource(R.string.touchpad_tutorial_recent_apps_gesture_button), @@ -170,7 +195,7 @@ private fun ThreeTutorialButtons( iconColor = MaterialTheme.colorScheme.onSecondary, onClick = onRecentAppsTutorialClicked, backgroundColor = MaterialTheme.colorScheme.secondary, - modifier = modifier, + modifier = modifier.focusRequester(recentAppsFocusRequester).focusable(), ) } diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt index e1f7bd59005c..6662fc5c127c 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt @@ -24,12 +24,16 @@ import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.lifecycle.Lifecycle.State.STARTED import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.compose.theme.PlatformTheme import com.android.systemui.inputdevice.tutorial.InputDeviceTutorialLogger import com.android.systemui.inputdevice.tutorial.InputDeviceTutorialLogger.TutorialContext import com.android.systemui.inputdevice.tutorial.KeyboardTouchpadTutorialMetricsLogger +import com.android.systemui.res.R import com.android.systemui.touchpad.tutorial.ui.composable.BackGestureTutorialScreen import com.android.systemui.touchpad.tutorial.ui.composable.HomeGestureTutorialScreen import com.android.systemui.touchpad.tutorial.ui.composable.RecentAppsGestureTutorialScreen @@ -54,6 +58,7 @@ constructor( override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() + setTitle(getString(R.string.launch_touchpad_tutorial_notification_content)) setContent { PlatformTheme { TouchpadTutorialScreen(vm, closeTutorial = ::finishTutorial) } } @@ -82,13 +87,24 @@ constructor( @Composable fun TouchpadTutorialScreen(vm: TouchpadTutorialViewModel, closeTutorial: () -> Unit) { val activeScreen by vm.screen.collectAsStateWithLifecycle(STARTED) + var lastSelectedScreen by remember { mutableStateOf(TUTORIAL_SELECTION) } when (activeScreen) { TUTORIAL_SELECTION -> TutorialSelectionScreen( - onBackTutorialClicked = { vm.goTo(BACK_GESTURE) }, - onHomeTutorialClicked = { vm.goTo(HOME_GESTURE) }, - onRecentAppsTutorialClicked = { vm.goTo(RECENT_APPS_GESTURE) }, + onBackTutorialClicked = { + lastSelectedScreen = BACK_GESTURE + vm.goTo(BACK_GESTURE) + }, + onHomeTutorialClicked = { + lastSelectedScreen = HOME_GESTURE + vm.goTo(HOME_GESTURE) + }, + onRecentAppsTutorialClicked = { + lastSelectedScreen = RECENT_APPS_GESTURE + vm.goTo(RECENT_APPS_GESTURE) + }, onDoneButtonClicked = closeTutorial, + lastSelectedScreen, ) BACK_GESTURE -> BackGestureTutorialScreen( diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSlidersInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSlidersInteractor.kt index 66a598b7cf1b..c904ac5e3ae1 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSlidersInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSlidersInteractor.kt @@ -35,6 +35,7 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.runningReduce import kotlinx.coroutines.flow.stateIn private const val DEFAULT_STREAM = AudioManager.STREAM_MUSIC @@ -54,13 +55,17 @@ constructor( volumeDialogStateInteractor.volumeDialogState .filter { it.streamModels.isNotEmpty() } .map { stateModel -> - stateModel.streamModels.values - .filter { streamModel -> shouldShowSliders(stateModel, streamModel) } - .sortedWith(streamsSorter) + val sliderTypes = + stateModel.streamModels.values + .filter { streamModel -> shouldShowSliders(stateModel, streamModel) } + .sortedWith(streamsSorter) + .map { model -> model.toType() } + LinkedHashSet(sliderTypes) } - .map { models -> - val sliderTypes: List<VolumeDialogSliderType> = - models.map { model -> model.toType() } + .runningReduce { sliderTypes, newSliderTypes -> + newSliderTypes.apply { addAll(sliderTypes) } + } + .map { sliderTypes -> VolumeDialogSlidersModel( slider = sliderTypes.first(), floatingSliders = sliderTypes.drop(1), diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupHelperTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupHelperTest.kt index 6e9b24f1433e..4ca84c58f6d9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupHelperTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupHelperTest.kt @@ -37,7 +37,6 @@ import com.google.common.truth.Truth.assertThat import java.io.File import java.io.FileInputStream import java.io.FileOutputStream -import org.junit.After import org.junit.Before import org.junit.Rule import org.junit.Test @@ -63,22 +62,18 @@ class CommunalBackupHelperTest : SysuiTestCase() { Room.inMemoryDatabaseBuilder(context, CommunalDatabase::class.java) .allowMainThreadQueries() .build() + onTeardown { database.close() } CommunalDatabase.setInstance(database) dao = database.communalWidgetDao() backupUtils = CommunalBackupUtils(context) backupDataFile = File(context.cacheDir, "backup_data_file") + onTeardown { backupDataFile.delete() } underTest = CommunalBackupHelper(UserHandle.SYSTEM, backupUtils) } - @After - fun teardown() { - backupDataFile.delete() - database.close() - } - @Test @EnableFlags(Flags.FLAG_COMMUNAL_HUB) fun backupAndRestoreCommunalHub() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt index f8d848139039..a9b6dd16cd95 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModelTest.kt @@ -26,6 +26,7 @@ import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey import com.android.systemui.keyboard.shortcut.shortcutCustomizationViewModelFactory import com.android.systemui.keyboard.shortcut.ui.model.ShortcutCustomizationUiState import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testCase import com.android.systemui.kosmos.testScope import com.android.systemui.res.R import com.google.common.truth.Truth.assertThat @@ -37,7 +38,7 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class ShortcutCustomizationViewModelTest : SysuiTestCase() { - private val kosmos = Kosmos() + private val kosmos = Kosmos().also { it.testCase = this } private val testScope = kosmos.testScope private val viewModel = kosmos.shortcutCustomizationViewModelFactory.create() diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.kt index 7383faf2fd78..feae901114e5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutHelperViewModelTest.kt @@ -278,7 +278,11 @@ class ShortcutHelperViewModelTest : SysuiTestCase() { testScope.runTest { fakeSystemSource.setGroups( groupWithShortcutLabels("first Foo shortcut1", "first bar shortcut1"), - groupWithShortcutLabels("second foO shortcut2", "second bar shortcut2"), + groupWithShortcutLabels( + "second foO shortcut2", + "second bar shortcut2", + groupLabel = SECOND_SIMPLE_GROUP_LABEL, + ), ) fakeMultiTaskingSource.setGroups( groupWithShortcutLabels("third FoO shortcut1", "third bar shortcut1") @@ -298,7 +302,10 @@ class ShortcutHelperViewModelTest : SysuiTestCase() { ShortcutCategory( System, subCategoryWithShortcutLabels("first Foo shortcut1"), - subCategoryWithShortcutLabels("second foO shortcut2"), + subCategoryWithShortcutLabels( + "second foO shortcut2", + subCategoryLabel = SECOND_SIMPLE_GROUP_LABEL, + ), ), ), ShortcutCategoryUi( @@ -380,16 +387,23 @@ class ShortcutHelperViewModelTest : SysuiTestCase() { assertThat(activeUiState.defaultSelectedCategory).isInstanceOf(CurrentApp::class.java) } - private fun groupWithShortcutLabels(vararg shortcutLabels: String) = - KeyboardShortcutGroup(SIMPLE_GROUP_LABEL, shortcutLabels.map { simpleShortcutInfo(it) }) - .apply { packageName = "test.package.name" } + private fun groupWithShortcutLabels( + vararg shortcutLabels: String, + groupLabel: String = FIRST_SIMPLE_GROUP_LABEL, + ) = + KeyboardShortcutGroup(groupLabel, shortcutLabels.map { simpleShortcutInfo(it) }).apply { + packageName = "test.package.name" + } private fun simpleShortcutInfo(label: String) = KeyboardShortcutInfo(label, KeyEvent.KEYCODE_A, KeyEvent.META_CTRL_ON) - private fun subCategoryWithShortcutLabels(vararg shortcutLabels: String) = + private fun subCategoryWithShortcutLabels( + vararg shortcutLabels: String, + subCategoryLabel: String = FIRST_SIMPLE_GROUP_LABEL, + ) = ShortcutSubCategory( - label = SIMPLE_GROUP_LABEL, + label = subCategoryLabel, shortcuts = shortcutLabels.map { simpleShortcut(it) }, ) @@ -402,6 +416,7 @@ class ShortcutHelperViewModelTest : SysuiTestCase() { } companion object { - private const val SIMPLE_GROUP_LABEL = "simple group" + private const val FIRST_SIMPLE_GROUP_LABEL = "simple group 1" + private const val SECOND_SIMPLE_GROUP_LABEL = "simple group 2" } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java index 856333ea724e..aad8b4ba1191 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java @@ -2707,6 +2707,44 @@ public class BubblesTest extends SysuiTestCase { eq(BubbleLogger.Event.BUBBLE_BAR_EXPANDED)); } + @EnableFlags(FLAG_ENABLE_BUBBLE_BAR) + @Test + public void testEventLogging_bubbleBar_openOverflow() { + mBubbleProperties.mIsBubbleBarEnabled = true; + mPositioner.setIsLargeScreen(true); + FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener(); + mBubbleController.registerBubbleStateListener(bubbleStateListener); + + mEntryListener.onEntryAdded(mRow); + + clearInvocations(mBubbleLogger); + mBubbleController.expandStackAndSelectBubbleFromLauncher(BubbleOverflow.KEY, 0); + verify(mBubbleLogger).log(BubbleLogger.Event.BUBBLE_BAR_OVERFLOW_SELECTED); + verifyNoMoreInteractions(mBubbleLogger); + } + + @EnableFlags(FLAG_ENABLE_BUBBLE_BAR) + @Test + public void testEventLogging_bubbleBar_fromOverflowToBar() { + mBubbleProperties.mIsBubbleBarEnabled = true; + mPositioner.setIsLargeScreen(true); + FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener(); + mBubbleController.registerBubbleStateListener(bubbleStateListener); + + mEntryListener.onEntryAdded(mRow); + + // Dismiss the bubble so it's in the overflow + mBubbleController.removeBubble( + mRow.getKey(), Bubbles.DISMISS_USER_GESTURE); + Bubble overflowBubble = mBubbleData.getOverflowBubbleWithKey(mRow.getKey()); + assertThat(overflowBubble).isNotNull(); + + // Promote overflow bubble and check that it is logged + mBubbleController.promoteBubbleFromOverflow(overflowBubble); + verify(mBubbleLogger).log(eqBubbleWithKey(overflowBubble.getKey()), + eq(BubbleLogger.Event.BUBBLE_BAR_OVERFLOW_REMOVE_BACK_TO_BAR)); + } + /** Creates a bubble using the userId and package. */ private Bubble createBubble(int userId, String pkg) { final UserHandle userHandle = new UserHandle(userId); diff --git a/packages/SystemUI/tests/utils/src/android/content/ClipboardManagerKosmos.kt b/packages/SystemUI/tests/utils/src/android/content/ClipboardManagerKosmos.kt new file mode 100644 index 000000000000..379c00842b62 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/android/content/ClipboardManagerKosmos.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content + +import com.android.systemui.kosmos.Kosmos +import org.mockito.kotlin.mock + +val Kosmos.clipboardManager by Kosmos.Fixture { mock<ClipboardManager>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/development/data/repository/DevelopmentSettingRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/development/data/repository/DevelopmentSettingRepositoryKosmos.kt new file mode 100644 index 000000000000..3ce119576096 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/development/data/repository/DevelopmentSettingRepositoryKosmos.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.development.data.repository + +import android.os.userManager +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.util.settings.fakeGlobalSettings + +val Kosmos.developmentSettingRepository by + Kosmos.Fixture { DevelopmentSettingRepository(fakeGlobalSettings, userManager, testDispatcher) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/development/domain/interactor/BuildNumberInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/development/domain/interactor/BuildNumberInteractorKosmos.kt new file mode 100644 index 000000000000..aa4dd18a6cba --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/development/domain/interactor/BuildNumberInteractorKosmos.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.development.domain.interactor + +import android.content.clipboardManager +import android.content.res.mainResources +import com.android.systemui.development.data.repository.developmentSettingRepository +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.user.data.repository.userRepository + +val Kosmos.buildNumberInteractor by + Kosmos.Fixture { + BuildNumberInteractor( + developmentSettingRepository, + mainResources, + userRepository, + { clipboardManager }, + testDispatcher, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/development/ui/viewmodel/BuildNumberViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/development/ui/viewmodel/BuildNumberViewModelKosmos.kt new file mode 100644 index 000000000000..c827311a6ac3 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/development/ui/viewmodel/BuildNumberViewModelKosmos.kt @@ -0,0 +1,29 @@ +/* + * 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.development.ui.viewmodel + +import com.android.systemui.development.domain.interactor.buildNumberInteractor +import com.android.systemui.kosmos.Kosmos + +val Kosmos.buildNumberViewModelFactory by + Kosmos.Fixture { + object : BuildNumberViewModel.Factory { + override fun create(): BuildNumberViewModel { + return BuildNumberViewModel(buildNumberInteractor) + } + } + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt index 903bc8ebf42d..9cb15c5b816d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt @@ -136,7 +136,11 @@ val Kosmos.shortcutHelperStateInteractor by } val Kosmos.shortcutHelperCategoriesInteractor by - Kosmos.Fixture { ShortcutHelperCategoriesInteractor(defaultShortcutCategoriesRepository) } + Kosmos.Fixture { + ShortcutHelperCategoriesInteractor(defaultShortcutCategoriesRepository) { + customShortcutCategoriesRepository + } + } val Kosmos.shortcutHelperViewModel by Kosmos.Fixture { @@ -162,7 +166,8 @@ val Kosmos.shortcutCustomizationDialogStarterFactory by } } -val Kosmos.shortcutCustomizationInteractor by Kosmos.Fixture { ShortcutCustomizationInteractor() } +val Kosmos.shortcutCustomizationInteractor by + Kosmos.Fixture { ShortcutCustomizationInteractor(customShortcutCategoriesRepository) } val Kosmos.shortcutCustomizationViewModelFactory by Kosmos.Fixture { diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModelKosmos.kt index d33d594d9e8a..76e2cc8b7bd0 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardSmartspaceViewModelKosmos.kt @@ -19,6 +19,7 @@ package com.android.systemui.keyguard.ui.viewmodel import com.android.systemui.keyguard.domain.interactor.keyguardSmartspaceInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.util.mockito.mock val Kosmos.keyguardSmartspaceViewModel by @@ -28,5 +29,6 @@ val Kosmos.keyguardSmartspaceViewModel by smartspaceController = mock(), keyguardClockViewModel = keyguardClockViewModel, smartspaceInteractor = keyguardSmartspaceInteractor, + shadeInteractor = shadeInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModelKosmos.kt index 0e5edb75846d..2e80293eafff 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/PaginatedGridViewModelKosmos.kt @@ -16,6 +16,7 @@ package com.android.systemui.qs.panels.ui.viewmodel +import com.android.systemui.development.ui.viewmodel.buildNumberViewModelFactory import com.android.systemui.kosmos.Kosmos import com.android.systemui.qs.panels.domain.interactor.paginatedGridInteractor @@ -26,5 +27,6 @@ val Kosmos.paginatedGridViewModel by qsColumnsViewModelFactory, paginatedGridInteractor, inFirstPageViewModel, + buildNumberViewModelFactory, ) } diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java index 869d854f7b23..9b71f8050c80 100644 --- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java +++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java @@ -30,6 +30,8 @@ import android.util.Log; import androidx.test.platform.app.InstrumentationRegistry; +import com.android.ravenwood.common.RavenwoodCommonUtils; + import org.junit.rules.TestRule; import org.junit.runner.Description; import org.junit.runner.Runner; @@ -229,7 +231,9 @@ public final class RavenwoodAwareTestRunner extends RavenwoodAwareTestRunnerBase s.evaluate(); onAfter(description, scope, order, null); } catch (Throwable t) { - if (onAfter(description, scope, order, t)) { + var shouldReportFailure = RavenwoodCommonUtils.runIgnoringException( + () -> onAfter(description, scope, order, t)); + if (shouldReportFailure == null || shouldReportFailure) { throw t; } } diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java index 678a97be60a2..1c1f15761329 100644 --- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java +++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java @@ -22,6 +22,8 @@ import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_INST_R import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_RESOURCE_APK; import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_VERBOSE_LOGGING; import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_VERSION_JAVA_SYSPROP; +import static com.android.ravenwood.common.RavenwoodCommonUtils.parseNullableInt; +import static com.android.ravenwood.common.RavenwoodCommonUtils.withDefault; import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; @@ -39,6 +41,7 @@ import android.content.pm.ApplicationInfo; import android.content.res.Resources; import android.os.Binder; import android.os.Build; +import android.os.Build.VERSION_CODES; import android.os.Bundle; import android.os.HandlerThread; import android.os.Looper; @@ -154,6 +157,13 @@ public class RavenwoodRuntimeEnvironmentController { private static RavenwoodAwareTestRunner sRunner; private static RavenwoodSystemProperties sProps; + private static final int DEFAULT_TARGET_SDK_LEVEL = VERSION_CODES.CUR_DEVELOPMENT; + private static final String DEFAULT_PACKAGE_NAME = "com.android.ravenwoodtests.defaultname"; + + private static int sTargetSdkLevel; + private static String sTestPackageName; + private static String sTargetPackageName; + /** * Initialize the global environment. */ @@ -235,9 +245,22 @@ public class RavenwoodRuntimeEnvironmentController { System.setProperty("android.junit.runner", "androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner"); + loadRavenwoodProperties(); + assertMockitoVersion(); } + private static void loadRavenwoodProperties() { + var props = RavenwoodSystemProperties.readProperties("ravenwood.properties"); + + sTargetSdkLevel = withDefault( + parseNullableInt(props.get("targetSdkVersionInt")), DEFAULT_TARGET_SDK_LEVEL); + sTargetPackageName = withDefault(props.get("packageName"), DEFAULT_PACKAGE_NAME); + sTestPackageName = withDefault(props.get("instPackageName"), sTargetPackageName); + + // TODO(b/377765941) Read them from the manifest too? + } + /** * Initialize the environment. */ @@ -256,7 +279,9 @@ public class RavenwoodRuntimeEnvironmentController { initInner(runner.mState.getConfig()); } catch (Exception th) { Log.e(TAG, "init() failed", th); - reset(); + + RavenwoodCommonUtils.runIgnoringException(()-> reset()); + SneakyThrow.sneakyThrow(th); } } @@ -267,6 +292,14 @@ public class RavenwoodRuntimeEnvironmentController { Thread.setDefaultUncaughtExceptionHandler(sUncaughtExceptionHandler); } + config.mTargetPackageName = sTargetPackageName; + config.mTestPackageName = sTestPackageName; + config.mTargetSdkLevel = sTargetSdkLevel; + + Log.i(TAG, "TargetPackageName=" + sTargetPackageName); + Log.i(TAG, "TestPackageName=" + sTestPackageName); + Log.i(TAG, "TargetSdkLevel=" + sTargetSdkLevel); + RavenwoodRuntimeState.sUid = config.mUid; RavenwoodRuntimeState.sPid = config.mPid; RavenwoodRuntimeState.sTargetSdkLevel = config.mTargetSdkLevel; @@ -349,8 +382,11 @@ public class RavenwoodRuntimeEnvironmentController { * Partially re-initialize after each test method invocation */ public static void reinit() { - var config = sRunner.mState.getConfig(); - Binder.restoreCallingIdentity(packBinderIdentityToken(false, config.mUid, config.mPid)); + // sRunner could be null, if there was a failure in the initialization. + if (sRunner != null) { + var config = sRunner.mState.getConfig(); + Binder.restoreCallingIdentity(packBinderIdentityToken(false, config.mUid, config.mPid)); + } } private static void initializeCompatIds(RavenwoodConfig config) { @@ -380,6 +416,9 @@ public class RavenwoodRuntimeEnvironmentController { /** * De-initialize. + * + * Note, we call this method when init() fails too, so this method should deal with + * any partially-initialized states. */ public static void reset() { if (RAVENWOOD_VERBOSE_LOGGING) { @@ -411,7 +450,9 @@ public class RavenwoodRuntimeEnvironmentController { config.mState.mSystemServerContext.cleanUp(); } - Looper.getMainLooper().quit(); + if (Looper.getMainLooper() != null) { + Looper.getMainLooper().quit(); + } Looper.clearMainLooperForTest(); ActivityManager.reset$ravenwood(); diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodConfig.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodConfig.java index 3ed8b0a748e1..7ca9239d2062 100644 --- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodConfig.java +++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodConfig.java @@ -22,7 +22,6 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.app.Instrumentation; import android.content.Context; -import android.os.Build; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; @@ -30,16 +29,12 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.ArrayList; import java.util.List; -import java.util.Objects; import java.util.concurrent.atomic.AtomicInteger; /** - * Represents how to configure the ravenwood environment for a test class. - * - * If a ravenwood test class has a public static field with the {@link Config} annotation, - * Ravenwood will extract the config from it and initializes the environment. The type of the - * field must be of {@link RavenwoodConfig}. + * @deprecated This class will be removed. Reach out to g/ravenwood if you need any features in it. */ +@Deprecated public final class RavenwoodConfig { /** * Use this to mark a field as the configuration. @@ -66,7 +61,7 @@ public final class RavenwoodConfig { String mTestPackageName; String mTargetPackageName; - int mTargetSdkLevel = Build.VERSION_CODES.CUR_DEVELOPMENT; + int mTargetSdkLevel; final RavenwoodSystemProperties mSystemProperties = new RavenwoodSystemProperties(); @@ -91,12 +86,6 @@ public final class RavenwoodConfig { return RavenwoodRule.isOnRavenwood(); } - private void setDefaults() { - if (mTargetPackageName == null) { - mTargetPackageName = mTestPackageName; - } - } - public static class Builder { private final RavenwoodConfig mConfig = new RavenwoodConfig(); @@ -120,28 +109,27 @@ public final class RavenwoodConfig { } /** - * Configure the package name of the test, which corresponds to - * {@link Instrumentation#getContext()}. + * @deprecated no longer used. Package name is set in the build file. (for now) */ + @Deprecated public Builder setPackageName(@NonNull String packageName) { - mConfig.mTestPackageName = Objects.requireNonNull(packageName); return this; } /** - * Configure the package name of the target app, which corresponds to - * {@link Instrumentation#getTargetContext()}. Defaults to {@link #setPackageName}. + * @deprecated no longer used. Package name is set in the build file. (for now) */ + @Deprecated public Builder setTargetPackageName(@NonNull String packageName) { - mConfig.mTargetPackageName = Objects.requireNonNull(packageName); return this; } + /** - * Configure the target SDK level of the test. + * @deprecated no longer used. Target SDK level is set in the build file. (for now) */ + @Deprecated public Builder setTargetSdkLevel(int sdkLevel) { - mConfig.mTargetSdkLevel = sdkLevel; return this; } @@ -154,34 +142,32 @@ public final class RavenwoodConfig { } /** - * 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 on non-Ravenwood environments. + * @deprecated Use {@link RavenwoodRule.Builder#setSystemPropertyImmutable(String, Object)} */ + @Deprecated public Builder setSystemPropertyImmutable(@NonNull String key, @Nullable Object value) { - mConfig.mSystemProperties.setValue(key, value); - mConfig.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 on non-Ravenwood environments. + * @deprecated Use {@link RavenwoodRule.Builder#setSystemPropertyMutable(String, Object)} */ + @Deprecated public Builder setSystemPropertyMutable(@NonNull String key, @Nullable Object value) { + return this; + } + + Builder setSystemPropertyImmutableReal(@NonNull String key, + @Nullable Object value) { + mConfig.mSystemProperties.setValue(key, value); + mConfig.mSystemProperties.setAccessReadOnly(key); + return this; + } + + Builder setSystemPropertyMutableReal(@NonNull String key, + @Nullable Object value) { mConfig.mSystemProperties.setValue(key, value); mConfig.mSystemProperties.setAccessReadWrite(key); return this; @@ -205,7 +191,6 @@ public final class RavenwoodConfig { } public RavenwoodConfig build() { - mConfig.setDefaults(); return mConfig; } } diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java index bfa3802ce583..5681a9040f63 100644 --- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java +++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java @@ -36,10 +36,8 @@ import java.util.Objects; import java.util.regex.Pattern; /** - * @deprecated Use {@link RavenwoodConfig} to configure the ravenwood environment instead. - * A {@link RavenwoodRule} is no longer needed for {@link DisabledOnRavenwood}. To get the - * {@link Context} and {@link Instrumentation}, use - * {@link androidx.test.platform.app.InstrumentationRegistry} instead. + * @deprecated This class is undergoing a major change. Reach out to g/ravenwood if you need + * any featues in it. */ @Deprecated public final class RavenwoodRule implements TestRule { @@ -128,11 +126,10 @@ public final class RavenwoodRule implements TestRule { } /** - * Configure the identity of this process to be the given package name for the duration - * of the test. Has no effect on non-Ravenwood environments. + * @deprecated no longer used. */ + @Deprecated public Builder setPackageName(@NonNull String packageName) { - mBuilder.setPackageName(packageName); return this; } @@ -155,7 +152,7 @@ public final class RavenwoodRule implements TestRule { * Has no effect on non-Ravenwood environments. */ public Builder setSystemPropertyImmutable(@NonNull String key, @Nullable Object value) { - mBuilder.setSystemPropertyImmutable(key, value); + mBuilder.setSystemPropertyImmutableReal(key, value); return this; } @@ -170,7 +167,7 @@ public final class RavenwoodRule implements TestRule { * Has no effect on non-Ravenwood environments. */ public Builder setSystemPropertyMutable(@NonNull String key, @Nullable Object value) { - mBuilder.setSystemPropertyMutable(key, value); + mBuilder.setSystemPropertyMutableReal(key, value); return this; } diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java index 3e4619f55c6d..9bd376a76f77 100644 --- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java +++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java @@ -52,7 +52,7 @@ public class RavenwoodSystemProperties { "vendor_dlkm", }; - private static Map<String, String> readProperties(String propFile) { + static Map<String, String> readProperties(String propFile) { // Use an ordered map just for cleaner dump log. final Map<String, String> ret = new LinkedHashMap<>(); try { @@ -60,7 +60,7 @@ public class RavenwoodSystemProperties { .map(String::trim) .filter(s -> !s.startsWith("#")) .map(s -> s.split("\\s*=\\s*", 2)) - .filter(a -> a.length == 2) + .filter(a -> a.length == 2 && a[1].length() > 0) .forEach(a -> ret.put(a[0], a[1])); } catch (IOException e) { throw new RuntimeException(e); diff --git a/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java b/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java index 520f050f0655..2a04d4469ef4 100644 --- a/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java +++ b/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java @@ -30,6 +30,7 @@ import java.lang.reflect.Member; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Arrays; +import java.util.function.Supplier; public class RavenwoodCommonUtils { private static final String TAG = "RavenwoodCommonUtils"; @@ -277,11 +278,55 @@ public class RavenwoodCommonUtils { (isStatic ? "static" : ""))); } + /** + * Run a supplier and swallow the exception, if any. + * + * It's a dangerous function. Only use it in an exception handler where we don't want to crash. + */ + @Nullable + public static <T> T runIgnoringException(@NonNull Supplier<T> s) { + try { + return s.get(); + } catch (Throwable th) { + log(TAG, "Warning: Exception detected! " + getStackTraceString(th)); + } + return null; + } + + /** + * Run a runnable and swallow the exception, if any. + * + * It's a dangerous function. Only use it in an exception handler where we don't want to crash. + */ + public static void runIgnoringException(@NonNull Runnable r) { + runIgnoringException(() -> { + r.run(); + return null; + }); + } + @NonNull - public static String getStackTraceString(@Nullable Throwable th) { + public static String getStackTraceString(@NonNull Throwable th) { StringWriter stringWriter = new StringWriter(); PrintWriter writer = new PrintWriter(stringWriter); th.printStackTrace(writer); return stringWriter.toString(); } + + /** Same as {@link Integer#parseInt(String)} but accepts null and returns null. */ + @Nullable + public static Integer parseNullableInt(@Nullable String value) { + if (value == null) { + return null; + } + return Integer.parseInt(value); + } + + /** + * @return {@code value} if it's non-null. Otherwise, returns {@code def}. + */ + @Nullable + public static <T> T withDefault(@Nullable T value, @Nullable T def) { + return value != null ? value : def; + } } diff --git a/ravenwood/tests/bivalentinst/Android.bp b/ravenwood/tests/bivalentinst/Android.bp index 41e45e5a6d95..31e3bcc3634f 100644 --- a/ravenwood/tests/bivalentinst/Android.bp +++ b/ravenwood/tests/bivalentinst/Android.bp @@ -27,6 +27,9 @@ android_ravenwood_test { "junit", "truth", ], + + package_name: "com.android.ravenwood.bivalentinsttest_self_inst", + resource_apk: "RavenwoodBivalentInstTest_self_inst_device", auto_gen_config: true, } @@ -53,6 +56,10 @@ android_ravenwood_test { "truth", ], resource_apk: "RavenwoodBivalentInstTestTarget", + + package_name: "com.android.ravenwood.bivalentinst_target_app", + inst_package_name: "com.android.ravenwood.bivalentinsttest_nonself_inst", + inst_resource_apk: "RavenwoodBivalentInstTest_nonself_inst_device", auto_gen_config: true, } diff --git a/ravenwood/tests/bivalentinst/test/com/android/ravenwoodtest/bivalentinst/RavenwoodInstrumentationTest_nonself.java b/ravenwood/tests/bivalentinst/test/com/android/ravenwoodtest/bivalentinst/RavenwoodInstrumentationTest_nonself.java index 92d43d714e14..db252d87c784 100644 --- a/ravenwood/tests/bivalentinst/test/com/android/ravenwoodtest/bivalentinst/RavenwoodInstrumentationTest_nonself.java +++ b/ravenwood/tests/bivalentinst/test/com/android/ravenwoodtest/bivalentinst/RavenwoodInstrumentationTest_nonself.java @@ -19,8 +19,6 @@ import static com.google.common.truth.Truth.assertThat; import android.app.Instrumentation; import android.content.Context; -import android.platform.test.ravenwood.RavenwoodConfig; -import android.platform.test.ravenwood.RavenwoodConfig.Config; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.platform.app.InstrumentationRegistry; @@ -39,11 +37,6 @@ public class RavenwoodInstrumentationTest_nonself { private static final String TEST_PACKAGE_NAME = "com.android.ravenwood.bivalentinsttest_nonself_inst"; - @Config - public static final RavenwoodConfig sConfig = new RavenwoodConfig.Builder() - .setPackageName(TEST_PACKAGE_NAME) - .setTargetPackageName(TARGET_PACKAGE_NAME) - .build(); private static Instrumentation sInstrumentation; private static Context sTestContext; diff --git a/ravenwood/tests/bivalentinst/test/com/android/ravenwoodtest/bivalentinst/RavenwoodInstrumentationTest_self.java b/ravenwood/tests/bivalentinst/test/com/android/ravenwoodtest/bivalentinst/RavenwoodInstrumentationTest_self.java index 2f35923dead2..94b18619efcd 100644 --- a/ravenwood/tests/bivalentinst/test/com/android/ravenwoodtest/bivalentinst/RavenwoodInstrumentationTest_self.java +++ b/ravenwood/tests/bivalentinst/test/com/android/ravenwoodtest/bivalentinst/RavenwoodInstrumentationTest_self.java @@ -19,8 +19,6 @@ import static com.google.common.truth.Truth.assertThat; import android.app.Instrumentation; import android.content.Context; -import android.platform.test.ravenwood.RavenwoodConfig; -import android.platform.test.ravenwood.RavenwoodConfig.Config; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.platform.app.InstrumentationRegistry; @@ -40,13 +38,6 @@ public class RavenwoodInstrumentationTest_self { private static final String TEST_PACKAGE_NAME = "com.android.ravenwood.bivalentinsttest_self_inst"; - @Config - public static final RavenwoodConfig sConfig = new RavenwoodConfig.Builder() - .setPackageName(TEST_PACKAGE_NAME) - .setTargetPackageName(TARGET_PACKAGE_NAME) - .build(); - - private static Instrumentation sInstrumentation; private static Context sTestContext; private static Context sTargetContext; diff --git a/ravenwood/tests/bivalenttest/Android.bp b/ravenwood/tests/bivalenttest/Android.bp index 40e6672a3c63..ac545dfb06cc 100644 --- a/ravenwood/tests/bivalenttest/Android.bp +++ b/ravenwood/tests/bivalenttest/Android.bp @@ -84,6 +84,8 @@ java_defaults { android_ravenwood_test { name: "RavenwoodBivalentTest", defaults: ["ravenwood-bivalent-defaults"], + target_sdk_version: "34", + package_name: "com.android.ravenwoodtest.bivalenttest", auto_gen_config: true, } diff --git a/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodConfigTest.java b/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodConfigTest.java index a5a16c14600b..306c2b39c70d 100644 --- a/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodConfigTest.java +++ b/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodConfigTest.java @@ -20,8 +20,6 @@ import static android.platform.test.ravenwood.RavenwoodConfig.isOnRavenwood; import static org.junit.Assert.assertEquals; import static org.junit.Assume.assumeTrue; -import android.platform.test.ravenwood.RavenwoodConfig; - import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.platform.app.InstrumentationRegistry; @@ -33,13 +31,7 @@ import org.junit.runner.RunWith; */ @RunWith(AndroidJUnit4.class) public class RavenwoodConfigTest { - private static final String PACKAGE_NAME = "com.test"; - - @RavenwoodConfig.Config - public static RavenwoodConfig sConfig = - new RavenwoodConfig.Builder() - .setPackageName(PACKAGE_NAME) - .build(); + private static final String PACKAGE_NAME = "com.android.ravenwoodtest.bivalenttest"; @Test public void testConfig() { diff --git a/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/compat/RavenwoodCompatFrameworkTest.kt b/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/compat/RavenwoodCompatFrameworkTest.kt index a95760db1a61..882c91c43ee9 100644 --- a/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/compat/RavenwoodCompatFrameworkTest.kt +++ b/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/compat/RavenwoodCompatFrameworkTest.kt @@ -16,8 +16,6 @@ package com.android.ravenwoodtest.bivalenttest.compat import android.app.compat.CompatChanges -import android.os.Build -import android.platform.test.ravenwood.RavenwoodConfig import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.internal.ravenwood.RavenwoodEnvironment.CompatIdsForTest import org.junit.Assert @@ -26,14 +24,6 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class RavenwoodCompatFrameworkTest { - companion object { - @JvmField // Expose as a raw field, not as a property. - @RavenwoodConfig.Config - val config = RavenwoodConfig.Builder() - .setTargetSdkLevel(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) - .build() - } - @Test fun testEnabled() { Assert.assertTrue(CompatChanges.isChangeEnabled(CompatIdsForTest.TEST_COMPAT_ID_1)) @@ -53,4 +43,4 @@ class RavenwoodCompatFrameworkTest { fun testEnabledAfterUForUApps() { Assert.assertFalse(CompatChanges.isChangeEnabled(CompatIdsForTest.TEST_COMPAT_ID_4)) } -}
\ No newline at end of file +} diff --git a/ravenwood/tests/coretest/test/com/android/ravenwoodtest/runnercallbacktests/RavenwoodRunnerConfigValidationTest.java b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/runnercallbacktests/RavenwoodRunnerConfigValidationTest.java index 02d10732245d..f94b98bc1fb8 100644 --- a/ravenwood/tests/coretest/test/com/android/ravenwoodtest/runnercallbacktests/RavenwoodRunnerConfigValidationTest.java +++ b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/runnercallbacktests/RavenwoodRunnerConfigValidationTest.java @@ -24,6 +24,7 @@ import android.platform.test.ravenwood.RavenwoodRule; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.platform.app.InstrumentationRegistry; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TestRule; @@ -32,6 +33,10 @@ import org.junit.runner.RunWith; /** * Test for @Config field extraction and validation. + * + * TODO(b/377765941) Most of the tests here will be obsolete and deleted with b/377765941, but + * some of the tests may need to be re-implemented one way or another. (e.g. the package name + * test.) Until that happens, we'll keep all tests here but add an {@code @Ignore} instead. */ @NoRavenizer // This class shouldn't be executed with RavenwoodAwareTestRunner. public class RavenwoodRunnerConfigValidationTest extends RavenwoodRunnerTestBase { @@ -59,6 +64,7 @@ public class RavenwoodRunnerConfigValidationTest extends RavenwoodRunnerTestBase testRunFinished: 1,0,0,0 """) // CHECKSTYLE:ON + @Ignore // Package name is no longer set via config. public static class ConfigInBaseClassTest extends ConfigInBaseClass { @Test public void test() { @@ -83,6 +89,7 @@ public class RavenwoodRunnerConfigValidationTest extends RavenwoodRunnerTestBase testRunFinished: 1,0,0,0 """) // CHECKSTYLE:ON + @Ignore // Package name is no longer set via config. public static class ConfigOverridingTest extends ConfigInBaseClass { static String PACKAGE_NAME_OVERRIDE = "com.ConfigOverridingTest"; @@ -376,6 +383,7 @@ public class RavenwoodRunnerConfigValidationTest extends RavenwoodRunnerTestBase testRunFinished: 1,0,0,0 """) // CHECKSTYLE:ON + @Ignore // Package name is no longer set via config. public static class RuleInBaseClassSuccessTest extends RuleInBaseClass { @Test @@ -437,6 +445,7 @@ public class RavenwoodRunnerConfigValidationTest extends RavenwoodRunnerTestBase testRunFinished: 1,1,0,0 """) // CHECKSTYLE:ON + @Ignore // Package name is no longer set via config. public static class RuleWithDifferentTypeInBaseClassSuccessTest extends RuleWithDifferentTypeInBaseClass { @Test diff --git a/ravenwood/tests/coretest/test/com/android/ravenwoodtest/runnercallbacktests/RavenwoodRunnerTestBase.java b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/runnercallbacktests/RavenwoodRunnerTestBase.java index f7a2198a9bc4..0e3d053e90b1 100644 --- a/ravenwood/tests/coretest/test/com/android/ravenwoodtest/runnercallbacktests/RavenwoodRunnerTestBase.java +++ b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/runnercallbacktests/RavenwoodRunnerTestBase.java @@ -25,6 +25,8 @@ import android.util.Log; import junitparams.JUnitParamsRunner; import junitparams.Parameters; + +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.Description; import org.junit.runner.JUnitCore; @@ -103,6 +105,7 @@ public abstract class RavenwoodRunnerTestBase { var thisClass = this.getClass(); var ret = Arrays.stream(thisClass.getNestMembers()) .filter((c) -> c.getAnnotation(Expected.class) != null) + .filter((c) -> c.getAnnotation(Ignore.class) == null) .toArray(Class[]::new); assertThat(ret.length).isGreaterThan(0); diff --git a/ravenwood/tests/runtime-test/Android.bp b/ravenwood/tests/runtime-test/Android.bp index 0c0df1f993aa..c3520031ea7d 100644 --- a/ravenwood/tests/runtime-test/Android.bp +++ b/ravenwood/tests/runtime-test/Android.bp @@ -9,7 +9,7 @@ package { android_ravenwood_test { name: "RavenwoodRuntimeTest", - + target_sdk_version: "34", libs: [ "ravenwood-helper-runtime", ], diff --git a/services/autofill/java/com/android/server/autofill/Helper.java b/services/autofill/java/com/android/server/autofill/Helper.java index e59bb42fd666..4f632c9e28a1 100644 --- a/services/autofill/java/com/android/server/autofill/Helper.java +++ b/services/autofill/java/com/android/server/autofill/Helper.java @@ -353,7 +353,10 @@ public final class Helper { private static void addAutofillableIds(@NonNull ViewNode node, @NonNull ArrayList<AutofillId> ids, boolean autofillableOnly) { if (!autofillableOnly || node.getAutofillType() != View.AUTOFILL_TYPE_NONE) { - ids.add(node.getAutofillId()); + AutofillId id = node.getAutofillId(); + if (id != null) { + ids.add(id); + } } final int size = node.getChildCount(); for (int i = 0; i < size; i++) { diff --git a/services/backup/java/com/android/server/backup/BackupAgentConnectionManager.java b/services/backup/java/com/android/server/backup/BackupAgentConnectionManager.java new file mode 100644 index 000000000000..5e4bab15952d --- /dev/null +++ b/services/backup/java/com/android/server/backup/BackupAgentConnectionManager.java @@ -0,0 +1,318 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.backup; + +import static com.android.server.backup.BackupManagerService.MORE_DEBUG; +import static com.android.server.backup.BackupManagerService.TAG; + +import android.annotation.Nullable; +import android.app.ActivityManager; +import android.app.ActivityManagerInternal; +import android.app.ApplicationThreadConstants; +import android.app.IActivityManager; +import android.app.IBackupAgent; +import android.app.backup.BackupAnnotations; +import android.app.compat.CompatChanges; +import android.compat.annotation.ChangeId; +import android.compat.annotation.EnabledSince; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.os.Binder; +import android.os.Build; +import android.os.IBinder; +import android.os.Process; +import android.os.RemoteException; +import android.os.UserHandle; +import android.util.ArraySet; +import android.util.Slog; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.LocalServices; +import com.android.server.backup.internal.LifecycleOperationStorage; + +import java.util.Set; + +/** + * Handles the lifecycle of {@link IBackupAgent}s that the {@link UserBackupManagerService} + * communicates with. + * + * <p>There can only be one agent that's connected to at a time. + * + * <p>There should be only one instance of this class per {@link UserBackupManagerService}. + */ +public class BackupAgentConnectionManager { + + /** + * Enables the OS making a decision on whether backup restricted mode should be used for apps + * that haven't explicitly opted in or out. See + * {@link android.content.pm.PackageManager#PROPERTY_USE_RESTRICTED_BACKUP_MODE} for details. + */ + @ChangeId + @EnabledSince(targetSdkVersion = Build.VERSION_CODES.BAKLAVA) + public static final long OS_DECIDES_BACKUP_RESTRICTED_MODE = 376661510; + + // The thread performing the sequence of queued backups binds to each app's agent + // in succession. Bind notifications are asynchronously delivered through the + // Activity Manager; use this lock object to signal when a requested binding has + // completed. + private final Object mAgentConnectLock = new Object(); + private IBackupAgent mConnectedAgent; + private volatile boolean mConnecting; + private final ArraySet<String> mRestoreNoRestrictedModePackages = new ArraySet<>(); + private final ArraySet<String> mBackupNoRestrictedModePackages = new ArraySet<>(); + + private final IActivityManager mActivityManager; + private final ActivityManagerInternal mActivityManagerInternal; + private final LifecycleOperationStorage mOperationStorage; + private final PackageManager mPackageManager; + private final UserBackupManagerService mUserBackupManagerService; + private final int mUserId; + private final String mUserIdMsg; + + BackupAgentConnectionManager(LifecycleOperationStorage operationStorage, + PackageManager packageManager, UserBackupManagerService userBackupManagerService, + int userId) { + mActivityManager = ActivityManager.getService(); + mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class); + mOperationStorage = operationStorage; + mPackageManager = packageManager; + mUserBackupManagerService = userBackupManagerService; + mUserId = userId; + mUserIdMsg = "[UserID:" + userId + "] "; + } + + /** + * Fires off a backup agent, blocking until it attaches (and ActivityManager will call + * {@link #agentConnected(String, IBinder)}) or until this operation times out. + * + * @param mode a {@code BACKUP_MODE} from {@link android.app.ApplicationThreadConstants}. + */ + @Nullable + public IBackupAgent bindToAgentSynchronous(ApplicationInfo app, int mode, + @BackupAnnotations.BackupDestination int backupDestination) { + IBackupAgent agent = null; + synchronized (mAgentConnectLock) { + mConnecting = true; + mConnectedAgent = null; + boolean useRestrictedMode = shouldUseRestrictedBackupModeForPackage(mode, + app.packageName); + try { + if (mActivityManager.bindBackupAgent(app.packageName, mode, mUserId, + backupDestination, useRestrictedMode)) { + Slog.d(TAG, mUserIdMsg + "awaiting agent for " + app); + + // success; wait for the agent to arrive + // only wait 10 seconds for the bind to happen + long timeoutMark = System.currentTimeMillis() + 10 * 1000; + while (mConnecting && mConnectedAgent == null && (System.currentTimeMillis() + < timeoutMark)) { + try { + mAgentConnectLock.wait(5000); + } catch (InterruptedException e) { + // just bail + Slog.w(TAG, mUserIdMsg + "Interrupted: " + e); + mConnecting = false; + mConnectedAgent = null; + } + } + + // if we timed out with no connect, abort and move on + if (mConnecting) { + Slog.w(TAG, mUserIdMsg + "Timeout waiting for agent " + app); + mConnectedAgent = null; + } + Slog.i(TAG, mUserIdMsg + "got agent " + mConnectedAgent); + agent = mConnectedAgent; + } + } catch (RemoteException e) { + // can't happen - ActivityManager is local + } + } + if (agent == null) { + mActivityManagerInternal.clearPendingBackup(mUserId); + } + return agent; + } + + /** + * Tell the ActivityManager that we are done with the {@link IBackupAgent} of this {@code app}. + * It will tell the app to destroy the agent. + */ + public void unbindAgent(ApplicationInfo app) { + try { + mActivityManager.unbindBackupAgent(app); + } catch (RemoteException e) { + // Can't happen - activity manager is local + } + } + + /** + * Callback: a requested backup agent has been instantiated. This should only be called from + * the + * {@link ActivityManager} when it's telling us that an agent is ready after a call to + * {@link #bindToAgentSynchronous(ApplicationInfo, int, int)}. + */ + public void agentConnected(String packageName, IBinder agentBinder) { + synchronized (mAgentConnectLock) { + if (getCallingUid() == android.os.Process.SYSTEM_UID) { + Slog.d(TAG, + mUserIdMsg + "agentConnected pkg=" + packageName + " agent=" + agentBinder); + mConnectedAgent = IBackupAgent.Stub.asInterface(agentBinder); + mConnecting = false; + } else { + Slog.w(TAG, mUserIdMsg + "Non-system process uid=" + getCallingUid() + + " claiming agent connected"); + } + mAgentConnectLock.notifyAll(); + } + } + + /** + * Callback: a backup agent has failed to come up, or has unexpectedly quit. If the agent failed + * to come up in the first place, the agentBinder argument will be {@code null}. This should + * only be called from the {@link ActivityManager}. + */ + public void agentDisconnected(String packageName) { + synchronized (mAgentConnectLock) { + if (getCallingUid() == Process.SYSTEM_UID) { + mConnectedAgent = null; + mConnecting = false; + } else { + Slog.w(TAG, mUserIdMsg + "Non-system process uid=" + getCallingUid() + + " claiming agent disconnected"); + } + Slog.w(TAG, mUserIdMsg + "agentDisconnected: the backup agent for " + packageName + + " died: cancel current operations"); + + // Offload operation cancellation off the main thread as the cancellation callbacks + // might call out to BackupTransport. Other operations started on the same package + // before the cancellation callback has executed will also be cancelled by the callback. + Runnable cancellationRunnable = () -> { + // handleCancel() causes the PerformFullTransportBackupTask to go on to + // tearDownAgentAndKill: that will unbindBackupAgent in the Activity Manager, so + // that the package being backed up doesn't get stuck in restricted mode until the + // backup time-out elapses. + for (int token : mOperationStorage.operationTokensForPackage(packageName)) { + if (MORE_DEBUG) { + Slog.d(TAG, + mUserIdMsg + "agentDisconnected: will handleCancel(all) for token:" + + Integer.toHexString(token)); + } + mUserBackupManagerService.handleCancel(token, true /* cancelAll */); + } + }; + getThreadForCancellation(cancellationRunnable).start(); + + mAgentConnectLock.notifyAll(); + } + } + + /** + * Marks the given set of packages as packages that should not be put into restricted mode if + * they are started for the given {@link BackupAnnotations.OperationType}. + */ + public void setNoRestrictedModePackages(Set<String> packageNames, + @BackupAnnotations.OperationType int opType) { + if (opType == BackupAnnotations.OperationType.BACKUP) { + mBackupNoRestrictedModePackages.clear(); + mBackupNoRestrictedModePackages.addAll(packageNames); + } else if (opType == BackupAnnotations.OperationType.RESTORE) { + mRestoreNoRestrictedModePackages.clear(); + mRestoreNoRestrictedModePackages.addAll(packageNames); + } else { + throw new IllegalArgumentException("opType must be BACKUP or RESTORE"); + } + } + + /** + * Clears the list of packages that should not be put into restricted mode for either backup or + * restore. + */ + public void clearNoRestrictedModePackages() { + mBackupNoRestrictedModePackages.clear(); + mRestoreNoRestrictedModePackages.clear(); + } + + /** + * If the app has specified {@link PackageManager#PROPERTY_USE_RESTRICTED_BACKUP_MODE}, then + * its value is returned. If it hasn't and it targets an SDK below + * {@link Build.VERSION_CODES#BAKLAVA} then returns true. If it targets a newer SDK, then + * returns the decision made by the {@link android.app.backup.BackupTransport}. + * + * <p>When this method is called, we should have already asked the transport and cached its + * response in {@link #mBackupNoRestrictedModePackages} or + * {@link #mRestoreNoRestrictedModePackages} so this method will immediately return without + * any IPC to the transport. + */ + private boolean shouldUseRestrictedBackupModeForPackage( + @BackupAnnotations.OperationType int mode, String packageName) { + if (!Flags.enableRestrictedModeChanges()) { + return true; + } + + // Key/Value apps are never put in restricted mode. + if (mode == ApplicationThreadConstants.BACKUP_MODE_INCREMENTAL + || mode == ApplicationThreadConstants.BACKUP_MODE_RESTORE) { + return false; + } + + try { + PackageManager.Property property = mPackageManager.getPropertyAsUser( + PackageManager.PROPERTY_USE_RESTRICTED_BACKUP_MODE, + packageName, /* className= */ null, mUserId); + if (property.isBoolean()) { + // If the package has explicitly specified, we won't ask the transport. + return property.getBoolean(); + } else { + Slog.w(TAG, + PackageManager.PROPERTY_USE_RESTRICTED_BACKUP_MODE + "must be a boolean."); + } + } catch (NameNotFoundException e) { + // This is expected when the package has not defined the property in its manifest. + } + + // The package has not specified the property. The behavior depends on the package's + // targetSdk. + // <36 gets the old behavior of always using restricted mode. + if (!CompatChanges.isChangeEnabled(OS_DECIDES_BACKUP_RESTRICTED_MODE, packageName, + UserHandle.of(mUserId))) { + return true; + } + + // Apps targeting >=36 get the behavior decided by the transport. + // By this point, we should have asked the transport and cached its decision. + if ((mode == ApplicationThreadConstants.BACKUP_MODE_FULL + && mBackupNoRestrictedModePackages.contains(packageName)) || ( + mode == ApplicationThreadConstants.BACKUP_MODE_RESTORE_FULL + && mRestoreNoRestrictedModePackages.contains(packageName))) { + Slog.d(TAG, "Transport requested no restricted mode for: " + packageName); + return false; + } + return true; + } + + @VisibleForTesting + Thread getThreadForCancellation(Runnable operation) { + return new Thread(operation, /* operationName */ "agent-disconnected"); + } + + @VisibleForTesting + int getCallingUid() { + return Binder.getCallingUid(); + } +} diff --git a/services/backup/java/com/android/server/backup/BackupManagerService.java b/services/backup/java/com/android/server/backup/BackupManagerService.java index 5f0071d47c89..3f6ede95eaf9 100644 --- a/services/backup/java/com/android/server/backup/BackupManagerService.java +++ b/services/backup/java/com/android/server/backup/BackupManagerService.java @@ -658,7 +658,8 @@ public class BackupManagerService extends IBackupManager.Stub { getServiceForUserIfCallerHasPermission(userId, "agentConnected()"); if (userBackupManagerService != null) { - userBackupManagerService.agentConnected(packageName, agentBinder); + userBackupManagerService.getBackupAgentConnectionManager().agentConnected(packageName, + agentBinder); } } @@ -683,7 +684,8 @@ public class BackupManagerService extends IBackupManager.Stub { getServiceForUserIfCallerHasPermission(userId, "agentDisconnected()"); if (userBackupManagerService != null) { - userBackupManagerService.agentDisconnected(packageName); + userBackupManagerService.getBackupAgentConnectionManager().agentDisconnected( + packageName); } } diff --git a/services/backup/java/com/android/server/backup/KeyValueAdbBackupEngine.java b/services/backup/java/com/android/server/backup/KeyValueAdbBackupEngine.java index f5d68362c70a..136bacdd6399 100644 --- a/services/backup/java/com/android/server/backup/KeyValueAdbBackupEngine.java +++ b/services/backup/java/com/android/server/backup/KeyValueAdbBackupEngine.java @@ -146,7 +146,8 @@ public class KeyValueAdbBackupEngine { private IBackupAgent bindToAgent(ApplicationInfo targetApp) { try { - return mBackupManagerService.bindToAgentSynchronous(targetApp, + return mBackupManagerService.getBackupAgentConnectionManager().bindToAgentSynchronous( + targetApp, ApplicationThreadConstants.BACKUP_MODE_INCREMENTAL, BackupAnnotations.BackupDestination.CLOUD); } catch (SecurityException e) { diff --git a/services/backup/java/com/android/server/backup/UserBackupManagerService.java b/services/backup/java/com/android/server/backup/UserBackupManagerService.java index 5de2fb30ac78..e085f6e63067 100644 --- a/services/backup/java/com/android/server/backup/UserBackupManagerService.java +++ b/services/backup/java/com/android/server/backup/UserBackupManagerService.java @@ -43,9 +43,7 @@ import android.app.ActivityManager; import android.app.ActivityManagerInternal; import android.app.AlarmManager; import android.app.AppGlobals; -import android.app.ApplicationThreadConstants; import android.app.IActivityManager; -import android.app.IBackupAgent; import android.app.PendingIntent; import android.app.backup.BackupAgent; import android.app.backup.BackupAnnotations; @@ -60,9 +58,6 @@ import android.app.backup.IBackupObserver; import android.app.backup.IFullBackupRestoreObserver; import android.app.backup.IRestoreSession; import android.app.backup.ISelectBackupTransportCallback; -import android.app.compat.CompatChanges; -import android.compat.annotation.ChangeId; -import android.compat.annotation.EnabledSince; import android.content.ActivityNotFoundException; import android.content.BroadcastReceiver; import android.content.ComponentName; @@ -83,7 +78,6 @@ import android.os.Build; import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; -import android.os.IBinder; import android.os.Message; import android.os.ParcelFileDescriptor; import android.os.PowerManager; @@ -302,21 +296,10 @@ public class UserBackupManagerService { private static final String BACKUP_FINISHED_ACTION = "android.intent.action.BACKUP_FINISHED"; private static final String BACKUP_FINISHED_PACKAGE_EXTRA = "packageName"; - /** - * Enables the OS making a decision on whether backup restricted mode should be used for apps - * that haven't explicitly opted in or out. See - * {@link PackageManager#PROPERTY_USE_RESTRICTED_BACKUP_MODE} for details. - */ - @ChangeId - @EnabledSince(targetSdkVersion = Build.VERSION_CODES.BAKLAVA) - public static final long OS_DECIDES_BACKUP_RESTRICTED_MODE = 376661510; - // Time delay for initialization operations that can be delayed so as not to consume too much // CPU on bring-up and increase time-to-UI. private static final long INITIALIZATION_DELAY_MILLIS = 3000; - // Timeout interval for deciding that a bind has taken too long. - private static final long BIND_TIMEOUT_INTERVAL = 10 * 1000; // Timeout interval for deciding that a clear-data has taken too long. private static final long CLEAR_DATA_TIMEOUT_INTERVAL = 30 * 1000; @@ -365,22 +348,9 @@ public class UserBackupManagerService { // Backups that we haven't started yet. Keys are package names. private final HashMap<String, BackupRequest> mPendingBackups = new HashMap<>(); - private final ArraySet<String> mRestoreNoRestrictedModePackages = new ArraySet<>(); - private final ArraySet<String> mBackupNoRestrictedModePackages = new ArraySet<>(); - // locking around the pending-backup management private final Object mQueueLock = new Object(); - private final UserBackupPreferences mBackupPreferences; - - // The thread performing the sequence of queued backups binds to each app's agent - // in succession. Bind notifications are asynchronously delivered through the - // Activity Manager; use this lock object to signal when a requested binding has - // completed. - private final Object mAgentConnectLock = new Object(); - private IBackupAgent mConnectedAgent; - private volatile boolean mConnecting; - private volatile boolean mBackupRunning; private volatile long mLastBackupPass; @@ -410,6 +380,7 @@ public class UserBackupManagerService { private ActiveRestoreSession mActiveRestoreSession; + private final BackupAgentConnectionManager mBackupAgentConnectionManager; private final LifecycleOperationStorage mOperationStorage; private final Random mTokenGenerator = new Random(); @@ -547,6 +518,8 @@ public class UserBackupManagerService { mRegisterTransportsRequestedTime = 0; mPackageManager = packageManager; mOperationStorage = operationStorage; + mBackupAgentConnectionManager = new BackupAgentConnectionManager(mOperationStorage, + mPackageManager, this, mUserId); mTransportManager = transportManager; mFullBackupQueue = new ArrayList<>(); mBackupHandler = backupHandler; @@ -599,6 +572,8 @@ public class UserBackupManagerService { mAgentTimeoutParameters.start(); mOperationStorage = new LifecycleOperationStorage(mUserId); + mBackupAgentConnectionManager = new BackupAgentConnectionManager(mOperationStorage, + mPackageManager, this, mUserId); Objects.requireNonNull(userBackupThread, "userBackupThread cannot be null"); mBackupHandler = new BackupHandler(this, mOperationStorage, userBackupThread); @@ -1660,67 +1635,6 @@ public class UserBackupManagerService { } } - /** Fires off a backup agent, blocking until it attaches or times out. */ - @Nullable - public IBackupAgent bindToAgentSynchronous(ApplicationInfo app, int mode, - @BackupDestination int backupDestination) { - IBackupAgent agent = null; - synchronized (mAgentConnectLock) { - mConnecting = true; - mConnectedAgent = null; - boolean useRestrictedMode = shouldUseRestrictedBackupModeForPackage(mode, - app.packageName); - try { - if (mActivityManager.bindBackupAgent(app.packageName, mode, mUserId, - backupDestination, useRestrictedMode)) { - Slog.d(TAG, addUserIdToLogMessage(mUserId, "awaiting agent for " + app)); - - // success; wait for the agent to arrive - // only wait 10 seconds for the bind to happen - long timeoutMark = System.currentTimeMillis() + BIND_TIMEOUT_INTERVAL; - while (mConnecting && mConnectedAgent == null - && (System.currentTimeMillis() < timeoutMark)) { - try { - mAgentConnectLock.wait(5000); - } catch (InterruptedException e) { - // just bail - Slog.w(TAG, addUserIdToLogMessage(mUserId, "Interrupted: " + e)); - mConnecting = false; - mConnectedAgent = null; - } - } - - // if we timed out with no connect, abort and move on - if (mConnecting) { - Slog.w( - TAG, - addUserIdToLogMessage(mUserId, "Timeout waiting for agent " + app)); - mConnectedAgent = null; - } - if (DEBUG) { - Slog.i(TAG, addUserIdToLogMessage(mUserId, "got agent " + mConnectedAgent)); - } - agent = mConnectedAgent; - } - } catch (RemoteException e) { - // can't happen - ActivityManager is local - } - } - if (agent == null) { - mActivityManagerInternal.clearPendingBackup(mUserId); - } - return agent; - } - - /** Unbind from a backup agent. */ - public void unbindAgent(ApplicationInfo app) { - try { - mActivityManager.unbindBackupAgent(app); - } catch (RemoteException e) { - // Can't happen - activity manager is local - } - } - /** * Clear an application's data after a failed restore, blocking until the operation completes or * times out. @@ -2493,10 +2407,6 @@ public class UserBackupManagerService { AppWidgetBackupBridge.restoreWidgetState(packageName, widgetData, mUserId); } - // ***************************** - // NEW UNIFIED RESTORE IMPLEMENTATION - // ***************************** - /** Schedule a backup pass for {@code packageName}. */ public void dataChangedImpl(String packageName) { HashSet<String> targets = dataChangedTargets(packageName); @@ -3122,91 +3032,6 @@ public class UserBackupManagerService { } } - /** - * Marks the given set of packages as packages that should not be put into restricted mode if - * they are started for the given {@link BackupAnnotations.OperationType}. - */ - public void setNoRestrictedModePackages(Set<String> packageNames, - @BackupAnnotations.OperationType int opType) { - if (opType == BackupAnnotations.OperationType.BACKUP) { - mBackupNoRestrictedModePackages.clear(); - mBackupNoRestrictedModePackages.addAll(packageNames); - } else if (opType == BackupAnnotations.OperationType.RESTORE) { - mRestoreNoRestrictedModePackages.clear(); - mRestoreNoRestrictedModePackages.addAll(packageNames); - } else { - throw new IllegalArgumentException("opType must be BACKUP or RESTORE"); - } - } - - /** - * Clears the list of packages that should not be put into restricted mode for either backup or - * restore. - */ - public void clearNoRestrictedModePackages() { - mBackupNoRestrictedModePackages.clear(); - mRestoreNoRestrictedModePackages.clear(); - } - - /** - * If the app has specified {@link PackageManager#PROPERTY_USE_RESTRICTED_BACKUP_MODE}, then - * its value is returned. If it hasn't and it targets an SDK below - * {@link Build.VERSION_CODES#BAKLAVA} then returns true. If it targets a newer SDK, then - * returns the decision made by the {@link android.app.backup.BackupTransport}. - * - * <p>When this method is called, we should have already asked the transport and cached its - * response in {@link #mBackupNoRestrictedModePackages} or - * {@link #mRestoreNoRestrictedModePackages} so this method will immediately return without - * any IPC to the transport. - */ - private boolean shouldUseRestrictedBackupModeForPackage( - @BackupAnnotations.OperationType int mode, String packageName) { - if (!Flags.enableRestrictedModeChanges()) { - return true; - } - - // Key/Value apps are never put in restricted mode. - if (mode == ApplicationThreadConstants.BACKUP_MODE_INCREMENTAL - || mode == ApplicationThreadConstants.BACKUP_MODE_RESTORE) { - return false; - } - - try { - PackageManager.Property property = mPackageManager.getPropertyAsUser( - PackageManager.PROPERTY_USE_RESTRICTED_BACKUP_MODE, - packageName, /* className= */ null, - mUserId); - if (property.isBoolean()) { - // If the package has explicitly specified, we won't ask the transport. - return property.getBoolean(); - } else { - Slog.w(TAG, PackageManager.PROPERTY_USE_RESTRICTED_BACKUP_MODE - + "must be a boolean."); - } - } catch (NameNotFoundException e) { - // This is expected when the package has not defined the property in its manifest. - } - - // The package has not specified the property. The behavior depends on the package's - // targetSdk. - // <36 gets the old behavior of always using restricted mode. - if (!CompatChanges.isChangeEnabled(OS_DECIDES_BACKUP_RESTRICTED_MODE, packageName, - UserHandle.of(mUserId))) { - return true; - } - - // Apps targeting >=36 get the behavior decided by the transport. - // By this point, we should have asked the transport and cached its decision. - if ((mode == ApplicationThreadConstants.BACKUP_MODE_FULL - && mBackupNoRestrictedModePackages.contains(packageName)) - || (mode == ApplicationThreadConstants.BACKUP_MODE_RESTORE_FULL - && mRestoreNoRestrictedModePackages.contains(packageName))) { - Slog.d(TAG, "Transport requested no restricted mode for: " + packageName); - return false; - } - return true; - } - private boolean startConfirmationUi(int token, String action) { try { Intent confIntent = new Intent(action); @@ -3898,83 +3723,6 @@ public class UserBackupManagerService { } /** - * Callback: a requested backup agent has been instantiated. This should only be called from the - * {@link ActivityManager}. - */ - public void agentConnected(String packageName, IBinder agentBinder) { - synchronized (mAgentConnectLock) { - if (Binder.getCallingUid() == Process.SYSTEM_UID) { - Slog.d( - TAG, - addUserIdToLogMessage( - mUserId, - "agentConnected pkg=" + packageName + " agent=" + agentBinder)); - mConnectedAgent = IBackupAgent.Stub.asInterface(agentBinder); - mConnecting = false; - } else { - Slog.w( - TAG, - addUserIdToLogMessage( - mUserId, - "Non-system process uid=" - + Binder.getCallingUid() - + " claiming agent connected")); - } - mAgentConnectLock.notifyAll(); - } - } - - /** - * Callback: a backup agent has failed to come up, or has unexpectedly quit. If the agent failed - * to come up in the first place, the agentBinder argument will be {@code null}. This should - * only be called from the {@link ActivityManager}. - */ - public void agentDisconnected(String packageName) { - synchronized (mAgentConnectLock) { - if (Binder.getCallingUid() == Process.SYSTEM_UID) { - mConnectedAgent = null; - mConnecting = false; - } else { - Slog.w( - TAG, - addUserIdToLogMessage( - mUserId, - "Non-system process uid=" - + Binder.getCallingUid() - + " claiming agent disconnected")); - } - Slog.w(TAG, "agentDisconnected: the backup agent for " + packageName - + " died: cancel current operations"); - - // Offload operation cancellation off the main thread as the cancellation callbacks - // might call out to BackupTransport. Other operations started on the same package - // before the cancellation callback has executed will also be cancelled by the callback. - Runnable cancellationRunnable = () -> { - // handleCancel() causes the PerformFullTransportBackupTask to go on to - // tearDownAgentAndKill: that will unbindBackupAgent in the Activity Manager, so - // that the package being backed up doesn't get stuck in restricted mode until the - // backup time-out elapses. - for (int token : mOperationStorage.operationTokensForPackage(packageName)) { - if (MORE_DEBUG) { - Slog.d(TAG, "agentDisconnected: will handleCancel(all) for token:" - + Integer.toHexString(token)); - } - handleCancel(token, true /* cancelAll */); - } - }; - getThreadForAsyncOperation(/* operationName */ "agent-disconnected", - cancellationRunnable).start(); - - mAgentConnectLock.notifyAll(); - } - } - - @VisibleForTesting - Thread getThreadForAsyncOperation(String operationName, Runnable operation) { - return new Thread(operation, operationName); - } - - /** * An application being installed will need a restore pass, then the {@link PackageManager} will * need to be told when the restore is finished. */ @@ -4521,4 +4269,8 @@ public class UserBackupManagerService { public IBackupManager getBackupManagerBinder() { return mBackupManagerBinder; } + + public BackupAgentConnectionManager getBackupAgentConnectionManager() { + return mBackupAgentConnectionManager; + } } diff --git a/services/backup/java/com/android/server/backup/fullbackup/FullBackupEngine.java b/services/backup/java/com/android/server/backup/fullbackup/FullBackupEngine.java index 12712063e344..b98cb1086680 100644 --- a/services/backup/java/com/android/server/backup/fullbackup/FullBackupEngine.java +++ b/services/backup/java/com/android/server/backup/fullbackup/FullBackupEngine.java @@ -314,7 +314,7 @@ public class FullBackupEngine { Slog.d(TAG, "Binding to full backup agent : " + mPkg.packageName); } mAgent = - backupManagerService.bindToAgentSynchronous( + backupManagerService.getBackupAgentConnectionManager().bindToAgentSynchronous( mPkg.applicationInfo, ApplicationThreadConstants.BACKUP_MODE_FULL, mBackupEligibilityRules.getBackupDestination()); } diff --git a/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java b/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java index be9cdc8692cb..65730c9591a8 100644 --- a/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java +++ b/services/backup/java/com/android/server/backup/fullbackup/PerformFullTransportBackupTask.java @@ -702,7 +702,8 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba } // Clear this to avoid using the memory until reboot. - mUserBackupManagerService.clearNoRestrictedModePackages(); + mUserBackupManagerService + .getBackupAgentConnectionManager().clearNoRestrictedModePackages(); Slog.i(TAG, "Full data backup pass finished."); mUserBackupManagerService.getWakelock().release(); @@ -741,7 +742,8 @@ public class PerformFullTransportBackupTask extends FullBackupTask implements Ba } packageNames = transport.getPackagesThatShouldNotUseRestrictedMode(packageNames, BACKUP); - mUserBackupManagerService.setNoRestrictedModePackages(packageNames, BACKUP); + mUserBackupManagerService.getBackupAgentConnectionManager().setNoRestrictedModePackages( + packageNames, BACKUP); } catch (RemoteException e) { Slog.i(TAG, "Failed to retrieve no restricted mode packages from transport"); } diff --git a/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java b/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java index 3a6e1cafa505..82232a653858 100644 --- a/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java +++ b/services/backup/java/com/android/server/backup/keyvalue/KeyValueBackupTask.java @@ -741,7 +741,7 @@ public class KeyValueBackupTask implements BackupRestoreTask, Runnable { final IBackupAgent agent; try { agent = - mBackupManagerService.bindToAgentSynchronous( + mBackupManagerService.getBackupAgentConnectionManager().bindToAgentSynchronous( packageInfo.applicationInfo, BACKUP_MODE_INCREMENTAL, mBackupEligibilityRules.getBackupDestination()); if (agent == null) { @@ -1302,7 +1302,8 @@ public class KeyValueBackupTask implements BackupRestoreTask, Runnable { // For PM metadata (for which applicationInfo is null) there is no agent-bound state. if (mCurrentPackage.applicationInfo != null) { - mBackupManagerService.unbindAgent(mCurrentPackage.applicationInfo); + mBackupManagerService.getBackupAgentConnectionManager().unbindAgent( + mCurrentPackage.applicationInfo); } mAgent = null; } diff --git a/services/backup/java/com/android/server/backup/restore/FullRestoreEngine.java b/services/backup/java/com/android/server/backup/restore/FullRestoreEngine.java index 2d99c96452da..b59e860f81fe 100644 --- a/services/backup/java/com/android/server/backup/restore/FullRestoreEngine.java +++ b/services/backup/java/com/android/server/backup/restore/FullRestoreEngine.java @@ -410,11 +410,7 @@ public class FullRestoreEngine extends RestoreEngine { // All set; now set up the IPC and launch the agent setUpPipes(); - mAgent = mBackupManagerService.bindToAgentSynchronous(mTargetApp, - FullBackup.KEY_VALUE_DATA_TOKEN.equals(info.domain) - ? ApplicationThreadConstants.BACKUP_MODE_RESTORE - : ApplicationThreadConstants.BACKUP_MODE_RESTORE_FULL, - mBackupEligibilityRules.getBackupDestination()); + mAgent = bindToAgent(info); mAgentPackage = pkg; } catch (IOException | NameNotFoundException e) { // fall through to error handling @@ -805,15 +801,12 @@ public class FullRestoreEngine extends RestoreEngine { return packages.contains(packageName); } - void sendOnRestorePackage(String name) { - if (mObserver != null) { - try { - // TODO: use a more user-friendly name string - mObserver.onRestorePackage(name); - } catch (RemoteException e) { - Slog.w(TAG, "full restore observer went away: restorePackage"); - mObserver = null; - } - } + private IBackupAgent bindToAgent(FileMetadata info) { + return mBackupManagerService.getBackupAgentConnectionManager().bindToAgentSynchronous( + mTargetApp, + FullBackup.KEY_VALUE_DATA_TOKEN.equals(info.domain) + ? ApplicationThreadConstants.BACKUP_MODE_RESTORE + : ApplicationThreadConstants.BACKUP_MODE_RESTORE_FULL, + mBackupEligibilityRules.getBackupDestination()); } } diff --git a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java index 5ee51a5aa189..e5c7e5cce757 100644 --- a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java +++ b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java @@ -814,7 +814,7 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { // Good to go! Set up and bind the agent... mAgent = - backupManagerService.bindToAgentSynchronous( + backupManagerService.getBackupAgentConnectionManager().bindToAgentSynchronous( mCurrentPackage.applicationInfo, ApplicationThreadConstants.BACKUP_MODE_RESTORE, mBackupEligibilityRules.getBackupDestination()); @@ -1364,7 +1364,7 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { backupManagerService.getBackupHandler().removeMessages(MSG_RESTORE_SESSION_TIMEOUT); // Clear this to avoid using the memory until reboot. - backupManagerService.clearNoRestrictedModePackages(); + backupManagerService.getBackupAgentConnectionManager().clearNoRestrictedModePackages(); // If we have a PM token, we must under all circumstances be sure to // handshake when we've finished. @@ -1838,7 +1838,8 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { } packageNames = transport.getPackagesThatShouldNotUseRestrictedMode(packageNames, RESTORE); - backupManagerService.setNoRestrictedModePackages(packageNames, RESTORE); + backupManagerService.getBackupAgentConnectionManager().setNoRestrictedModePackages( + packageNames, RESTORE); } catch (RemoteException e) { Slog.i(TAG, "Failed to retrieve restricted mode packages from transport"); } diff --git a/services/companion/java/com/android/server/companion/securechannel/SecureChannel.java b/services/companion/java/com/android/server/companion/securechannel/SecureChannel.java index dbeca82ade89..2d3782fb3181 100644 --- a/services/companion/java/com/android/server/companion/securechannel/SecureChannel.java +++ b/services/companion/java/com/android/server/companion/securechannel/SecureChannel.java @@ -16,6 +16,8 @@ package com.android.server.companion.securechannel; +import static android.security.attestationverification.AttestationVerificationManager.FLAG_FAILURE_UNKNOWN; + import android.annotation.NonNull; import android.content.Context; import android.os.Build; @@ -67,7 +69,7 @@ public class SecureChannel { private D2DConnectionContextV1 mConnectionContext; private String mAlias; - private int mVerificationResult; + private int mVerificationResult = FLAG_FAILURE_UNKNOWN; private boolean mPskVerified; diff --git a/services/core/java/com/android/server/accounts/AccountManagerService.java b/services/core/java/com/android/server/accounts/AccountManagerService.java index 679c7ac3ceac..3ce645158fe4 100644 --- a/services/core/java/com/android/server/accounts/AccountManagerService.java +++ b/services/core/java/com/android/server/accounts/AccountManagerService.java @@ -16,6 +16,10 @@ package com.android.server.accounts; +import static android.Manifest.permission.COPY_ACCOUNTS; +import static android.Manifest.permission.REMOVE_ACCOUNTS; +import static android.app.admin.flags.Flags.splitCreateManagedProfileEnabled; + import android.Manifest; import android.accounts.AbstractAccountAuthenticator; import android.accounts.Account; @@ -1739,9 +1743,11 @@ public class AccountManagerService public void copyAccountToUser(final IAccountManagerResponse response, final Account account, final int userFrom, int userTo) { int callingUid = Binder.getCallingUid(); - if (isCrossUser(callingUid, UserHandle.USER_ALL)) { + if (isCrossUser(callingUid, UserHandle.USER_ALL) + && !hasCopyAccountsPermission()) { throw new SecurityException("Calling copyAccountToUser requires " - + android.Manifest.permission.INTERACT_ACROSS_USERS_FULL); + + android.Manifest.permission.INTERACT_ACROSS_USERS_FULL + + " or " + COPY_ACCOUNTS); } final UserAccounts fromAccounts = getUserAccounts(userFrom); final UserAccounts toAccounts = getUserAccounts(userTo); @@ -1793,6 +1799,12 @@ public class AccountManagerService } } + private boolean hasCopyAccountsPermission() { + return splitCreateManagedProfileEnabled() + && mContext.checkCallingOrSelfPermission(COPY_ACCOUNTS) + == PackageManager.PERMISSION_GRANTED; + } + @Override public boolean accountAuthenticated(final Account account) { final int callingUid = Binder.getCallingUid(); @@ -2346,7 +2358,8 @@ public class AccountManagerService UserHandle user = UserHandle.of(userId); if (!isAccountManagedByCaller(account.type, callingUid, user.getIdentifier()) && !isSystemUid(callingUid) - && !isProfileOwner(callingUid)) { + && !isProfileOwner(callingUid) + && !hasRemoveAccountsPermission()) { String msg = String.format( "uid %s cannot remove accounts of type: %s", callingUid, @@ -2408,6 +2421,12 @@ public class AccountManagerService } } + private boolean hasRemoveAccountsPermission() { + return splitCreateManagedProfileEnabled() + && mContext.checkCallingOrSelfPermission(REMOVE_ACCOUNTS) + == PackageManager.PERMISSION_GRANTED; + } + @Override public boolean removeAccountExplicitly(Account account) { final int callingUid = Binder.getCallingUid(); diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index d880bce921aa..e166807083ff 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -19070,8 +19070,13 @@ public class ActivityManagerService extends IActivityManager.Stub */ @Override public boolean enableFgsNotificationRateLimit(boolean enable) { - enforceCallingPermission(permission.WRITE_DEVICE_CONFIG, - "enableFgsNotificationRateLimit"); + if (android.security.Flags.protectDeviceConfigFlags()) { + enforceCallingHasAtLeastOnePermission("enableFgsNotificationRateLimit", + permission.WRITE_DEVICE_CONFIG, permission.WRITE_ALLOWLISTED_DEVICE_CONFIG); + } else { + enforceCallingPermission(permission.WRITE_DEVICE_CONFIG, + "enableFgsNotificationRateLimit"); + } synchronized (this) { return mServices.enableFgsNotificationRateLimitLocked(enable); } diff --git a/services/core/java/com/android/server/am/AppStartInfoTracker.java b/services/core/java/com/android/server/am/AppStartInfoTracker.java index 3913d2f2ea92..961022b7231b 100644 --- a/services/core/java/com/android/server/am/AppStartInfoTracker.java +++ b/services/core/java/com/android/server/am/AppStartInfoTracker.java @@ -634,12 +634,20 @@ public final class AppStartInfoTracker { } final ApplicationStartInfo info = new ApplicationStartInfo(raw); + int uid = raw.getRealUid(); - AppStartInfoContainer container = mData.get(raw.getPackageName(), raw.getRealUid()); + // Isolated process starts won't be reasonably accessible if stored by their uid, don't + // store them. + if (com.android.server.am.Flags.appStartInfoIsolatedProcess() + && UserHandle.isIsolated(uid)) { + return null; + } + + AppStartInfoContainer container = mData.get(raw.getPackageName(), uid); if (container == null) { container = new AppStartInfoContainer(mAppStartInfoHistoryListSize); - container.mUid = info.getRealUid(); - mData.put(raw.getPackageName(), raw.getRealUid(), container); + container.mUid = uid; + mData.put(raw.getPackageName(), uid, container); } container.addStartInfoLocked(info); @@ -1010,6 +1018,17 @@ public final class AppStartInfoTracker { new AppStartInfoContainer(mAppStartInfoHistoryListSize); int uid = container.readFromProto(proto, AppsStartInfoProto.Package.USERS, pkgName); + + // If the isolated process flag is enabled and the uid is that of an isolated + // process, then break early so that the container will not be added to mData. + // This is expected only as a one time mitigation, records added after this flag + // is enabled should always return false for isIsolated and thereby always + // continue on. + if (com.android.server.am.Flags.appStartInfoIsolatedProcess() + && UserHandle.isIsolated(uid)) { + break; + } + synchronized (mLock) { mData.put(pkgName, uid, container); } diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java index ef5296eef492..78c4f74f3afa 100644 --- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java +++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java @@ -257,6 +257,7 @@ public class SettingsToPropertiesMapper { "wear_systems", "wear_sysui", "wear_system_managed_surfaces", + "wear_watchfaces", "window_surfaces", "windowing_frontend", "xr", diff --git a/services/core/java/com/android/server/am/flags.aconfig b/services/core/java/com/android/server/am/flags.aconfig index c59c40fc9cd8..6d247d227774 100644 --- a/services/core/java/com/android/server/am/flags.aconfig +++ b/services/core/java/com/android/server/am/flags.aconfig @@ -270,3 +270,13 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "app_start_info_isolated_process" + namespace: "system_performance" + description: "Adjust handling of isolated process records to be discarded." + bug: "374032823" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java b/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java index 82d5d4d8141d..e8786be4d8e6 100644 --- a/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java +++ b/services/core/java/com/android/server/biometrics/sensors/BiometricScheduler.java @@ -677,10 +677,11 @@ public class BiometricScheduler<T, U> { * Start the timeout for the watchdog. */ public void startWatchdog() { - if (mCurrentOperation == null) { + final BiometricSchedulerOperation operation = mCurrentOperation; + if (operation == null) { + Slog.e(TAG, "Current operation is null,no need to start watchdog"); return; } - final BiometricSchedulerOperation operation = mCurrentOperation; mHandler.postDelayed(() -> { if (operation == mCurrentOperation && !operation.isFinished()) { Counter.logIncrement("biometric.value_scheduler_watchdog_triggered_count"); diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index edad2473061c..e5b077d23bec 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -3031,6 +3031,11 @@ public class InputManagerService extends IInputManager.Stub return mKeyGestureController.getAppLaunchBookmarks(); } + @Override + public void resetLockedModifierState() { + mNative.resetLockedModifierState(); + } + private void handleCurrentUserChanged(@UserIdInt int userId) { mCurrentUserId = userId; mKeyGestureController.setCurrentUserId(userId); diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java index 8903c27b491a..728e44062e82 100644 --- a/services/core/java/com/android/server/input/NativeInputManagerService.java +++ b/services/core/java/com/android/server/input/NativeInputManagerService.java @@ -98,6 +98,8 @@ interface NativeInputManagerService { void toggleCapsLock(int deviceId); + void resetLockedModifierState(); + void displayRemoved(int displayId); void setInputDispatchMode(boolean enabled, boolean frozen); @@ -370,6 +372,9 @@ interface NativeInputManagerService { public native void toggleCapsLock(int deviceId); @Override + public native void resetLockedModifierState(); + + @Override public native void displayRemoved(int displayId); @Override diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index d8483f721306..b7af9a4b17bd 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -1310,7 +1310,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. // Do not reset the default (current) IME when it is a 3rd-party IME String selectedMethodId = bindingController.getSelectedMethodId(); final InputMethodSettings settings = InputMethodSettingsRepository.get(userId); - if (selectedMethodId != null + if (selectedMethodId != null && settings.getMethodMap().get(selectedMethodId) != null && !settings.getMethodMap().get(selectedMethodId).isSystem()) { return; } diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubClientManager.java b/services/core/java/com/android/server/location/contexthub/ContextHubClientManager.java index 0fdd0ae641df..5248a051404d 100644 --- a/services/core/java/com/android/server/location/contexthub/ContextHubClientManager.java +++ b/services/core/java/com/android/server/location/contexthub/ContextHubClientManager.java @@ -237,15 +237,16 @@ import java.util.function.Consumer; if (message.isBroadcastMessage()) { if (message.isReliable()) { - Log.e(TAG, "Received reliable broadcast message from " + message.getNanoAppId()); + Log.e(TAG, "Received reliable broadcast message from 0x" + + Long.toHexString(message.getNanoAppId())); return ErrorCode.PERMANENT_ERROR; } // Broadcast messages shouldn't be sent with any permissions tagged per CHRE API // requirements. if (!messagePermissions.isEmpty()) { - Log.e(TAG, "Received broadcast message with permissions from " - + message.getNanoAppId()); + Log.e(TAG, "Received broadcast message with permissions from 0x" + + Long.toHexString(message.getNanoAppId())); return ErrorCode.PERMANENT_ERROR; } diff --git a/services/core/java/com/android/server/location/contexthub/ContextHubService.java b/services/core/java/com/android/server/location/contexthub/ContextHubService.java index 946e89604553..7c9d9c57a8b0 100644 --- a/services/core/java/com/android/server/location/contexthub/ContextHubService.java +++ b/services/core/java/com/android/server/location/contexthub/ContextHubService.java @@ -326,8 +326,15 @@ public class ContextHubService extends IContextHubService.Stub { } if (Flags.offloadApi()) { - mHubInfoRegistry = new HubInfoRegistry(mContextHubWrapper); - Log.i(TAG, "Enabling generic offload API"); + HubInfoRegistry registry; + try { + registry = new HubInfoRegistry(mContextHubWrapper); + Log.i(TAG, "Enabling generic offload API"); + } catch (UnsupportedOperationException e) { + registry = null; + Log.w(TAG, "Generic offload API not supported, disabling"); + } + mHubInfoRegistry = registry; } else { mHubInfoRegistry = null; Log.i(TAG, "Disabling generic offload API"); diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java index fce008c23350..df5ecf872df4 100644 --- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java +++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java @@ -1006,9 +1006,9 @@ public final class MediaProjectionManagerService extends SystemService // Host app has 5 minutes to begin using the token before it is invalid. // Some apps show a dialog for the user to interact with (selecting recording resolution) // before starting capture, but after requesting consent. - final long mDefaultTimeoutMs = Duration.ofMinutes(5).toMillis(); + final long mDefaultTimeoutMillis = Duration.ofMinutes(5).toMillis(); // The creation timestamp in milliseconds, measured by {@link SystemClock#uptimeMillis}. - private final long mCreateTimeMs; + private final long mCreateTimeMillis; public final int uid; public final String packageName; public final UserHandle userHandle; @@ -1017,7 +1017,7 @@ public final class MediaProjectionManagerService extends SystemService private final int mType; // Values for tracking token validity. // Timeout value to compare creation time against. - private final long mTimeoutMs = mDefaultTimeoutMs; + private final long mTimeoutMillis = mDefaultTimeoutMillis; private final int mDisplayId; private IMediaProjectionCallback mCallback; @@ -1048,7 +1048,7 @@ public final class MediaProjectionManagerService extends SystemService userHandle = new UserHandle(UserHandle.getUserId(uid)); mTargetSdkVersion = targetSdkVersion; mIsPrivileged = isPrivileged; - mCreateTimeMs = mClock.uptimeMillis(); + mCreateTimeMillis = mClock.uptimeMillis(); mActivityManagerInternal.notifyMediaProjectionEvent(uid, asBinder(), MEDIA_PROJECTION_TOKEN_EVENT_CREATED); mDisplayId = displayId; @@ -1209,7 +1209,7 @@ public final class MediaProjectionManagerService extends SystemService } } Slog.d(TAG, "Content Recording: handling stopping this projection token" - + " createTime= " + mCreateTimeMs + + " createTime= " + mCreateTimeMillis + " countStarts= " + mCountStarts); stopProjectionLocked(this); mToken.unlinkToDeath(mDeathEater, 0); @@ -1273,7 +1273,7 @@ public final class MediaProjectionManagerService extends SystemService } long getCreateTimeMillis() { - return mCreateTimeMs; + return mCreateTimeMillis; } @android.annotation.EnforcePermission(android.Manifest.permission.MANAGE_MEDIA_PROJECTION) @@ -1281,8 +1281,8 @@ public final class MediaProjectionManagerService extends SystemService public boolean isValid() { isValid_enforcePermission(); synchronized (mLock) { - final long curMs = mClock.uptimeMillis(); - final boolean hasTimedOut = curMs - mCreateTimeMs > mTimeoutMs; + final long curMillis = mClock.uptimeMillis(); + final boolean hasTimedOut = curMillis - mCreateTimeMillis > mTimeoutMillis; final boolean virtualDisplayCreated = mVirtualDisplayId != INVALID_DISPLAY; final boolean isValid = !hasTimedOut && (mCountStarts <= 1) && !virtualDisplayCreated; diff --git a/services/core/java/com/android/server/media/quality/MediaQualityService.java b/services/core/java/com/android/server/media/quality/MediaQualityService.java index 00bab8af44f3..8495b6c403bf 100644 --- a/services/core/java/com/android/server/media/quality/MediaQualityService.java +++ b/services/core/java/com/android/server/media/quality/MediaQualityService.java @@ -28,6 +28,7 @@ import android.media.quality.ISoundProfileCallback; import android.media.quality.MediaQualityContract; import android.media.quality.ParamCapability; import android.media.quality.PictureProfile; +import android.media.quality.PictureProfileHandle; import android.media.quality.SoundProfile; import android.os.PersistableBundle; import android.util.Log; @@ -249,6 +250,11 @@ public class MediaQualityService extends SystemService { } @Override + public PictureProfileHandle getPictureProfileHandle(String id) { + return null; + } + + @Override public SoundProfile createSoundProfile(SoundProfile pp) { // TODO: implement return pp; diff --git a/services/core/java/com/android/server/security/authenticationpolicy/AuthenticationPolicyService.java b/services/core/java/com/android/server/security/authenticationpolicy/AuthenticationPolicyService.java index b8a4a9c26feb..6798a6146ae0 100644 --- a/services/core/java/com/android/server/security/authenticationpolicy/AuthenticationPolicyService.java +++ b/services/core/java/com/android/server/security/authenticationpolicy/AuthenticationPolicyService.java @@ -16,8 +16,11 @@ package com.android.server.security.authenticationpolicy; +import static android.Manifest.permission.MANAGE_SECURE_LOCK_DEVICE; + import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_ADAPTIVE_AUTH_REQUEST; +import android.annotation.EnforcePermission; import android.app.KeyguardManager; import android.content.Context; import android.content.pm.PackageManager; @@ -32,9 +35,14 @@ import android.hardware.biometrics.events.AuthenticationStoppedInfo; import android.hardware.biometrics.events.AuthenticationSucceededInfo; import android.os.Build; import android.os.Handler; +import android.os.IBinder; import android.os.Looper; import android.os.Message; import android.os.SystemClock; +import android.security.authenticationpolicy.AuthenticationPolicyManager; +import android.security.authenticationpolicy.DisableSecureLockDeviceParams; +import android.security.authenticationpolicy.EnableSecureLockDeviceParams; +import android.security.authenticationpolicy.IAuthenticationPolicyService; import android.util.Log; import android.util.Slog; import android.util.SparseIntArray; @@ -74,6 +82,7 @@ public class AuthenticationPolicyService extends SystemService { private final KeyguardManager mKeyguardManager; private final WindowManagerInternal mWindowManager; private final UserManagerInternal mUserManager; + private SecureLockDeviceServiceInternal mSecureLockDeviceService; @VisibleForTesting final SparseIntArray mFailedAttemptsForUser = new SparseIntArray(); private final SparseLongArray mLastLockedTimestamp = new SparseLongArray(); @@ -94,10 +103,16 @@ public class AuthenticationPolicyService extends SystemService { mWindowManager = Objects.requireNonNull( LocalServices.getService(WindowManagerInternal.class)); mUserManager = Objects.requireNonNull(LocalServices.getService(UserManagerInternal.class)); + if (android.security.Flags.secureLockdown()) { + mSecureLockDeviceService = Objects.requireNonNull( + LocalServices.getService(SecureLockDeviceServiceInternal.class)); + } } @Override - public void onStart() {} + public void onStart() { + publishBinderService(Context.AUTHENTICATION_POLICY_SERVICE, mService); + } @Override public void onBootPhase(int phase) { @@ -294,4 +309,36 @@ public class AuthenticationPolicyService extends SystemService { // next successful primary or biometric auth happens mLastLockedTimestamp.put(userId, SystemClock.elapsedRealtime()); } + + private final IBinder mService = new IAuthenticationPolicyService.Stub() { + /** + * @see AuthenticationPolicyManager#enableSecureLockDevice(EnableSecureLockDeviceParams) + * @param params EnableSecureLockDeviceParams for caller to supply params related + * to the secure lock device request + * @return @EnableSecureLockDeviceRequestStatus int indicating the result of the Secure + * Lock Device request + */ + @Override + @EnforcePermission(MANAGE_SECURE_LOCK_DEVICE) + @AuthenticationPolicyManager.EnableSecureLockDeviceRequestStatus + public int enableSecureLockDevice(EnableSecureLockDeviceParams params) { + enableSecureLockDevice_enforcePermission(); + return mSecureLockDeviceService.enableSecureLockDevice(params); + } + + /** + * @see AuthenticationPolicyManager#disableSecureLockDevice(DisableSecureLockDeviceParams) + * @param params @DisableSecureLockDeviceParams for caller to supply params related + * to the secure lock device request + * @return @DisableSecureLockDeviceRequestStatus int indicating the result of the Secure + * Lock Device request + */ + @Override + @EnforcePermission(MANAGE_SECURE_LOCK_DEVICE) + @AuthenticationPolicyManager.DisableSecureLockDeviceRequestStatus + public int disableSecureLockDevice(DisableSecureLockDeviceParams params) { + disableSecureLockDevice_enforcePermission(); + return mSecureLockDeviceService.disableSecureLockDevice(params); + } + }; } diff --git a/services/core/java/com/android/server/security/authenticationpolicy/SecureLockDeviceService.java b/services/core/java/com/android/server/security/authenticationpolicy/SecureLockDeviceService.java new file mode 100644 index 000000000000..7b89723deb6c --- /dev/null +++ b/services/core/java/com/android/server/security/authenticationpolicy/SecureLockDeviceService.java @@ -0,0 +1,120 @@ +/* + * 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.security.authenticationpolicy; + +import android.annotation.NonNull; +import android.content.Context; +import android.security.authenticationpolicy.AuthenticationPolicyManager; +import android.security.authenticationpolicy.AuthenticationPolicyManager.DisableSecureLockDeviceRequestStatus; +import android.security.authenticationpolicy.AuthenticationPolicyManager.EnableSecureLockDeviceRequestStatus; +import android.security.authenticationpolicy.DisableSecureLockDeviceParams; +import android.security.authenticationpolicy.EnableSecureLockDeviceParams; +import android.util.Slog; + +import com.android.server.LocalServices; +import com.android.server.SystemService; + +/** + * System service for remotely calling secure lock on the device. + * + * Callers will access this class via + * {@link com.android.server.security.authenticationpolicy.AuthenticationPolicyService}. + * + * @see AuthenticationPolicyService + * @see AuthenticationPolicyManager#enableSecureLockDevice + * @see AuthenticationPolicyManager#disableSecureLockDevice + * @hide + */ +public class SecureLockDeviceService extends SecureLockDeviceServiceInternal { + private static final String TAG = "SecureLockDeviceService"; + private final Context mContext; + + public SecureLockDeviceService(@NonNull Context context) { + mContext = context; + } + + private void start() { + // Expose private service for system components to use. + LocalServices.addService(SecureLockDeviceServiceInternal.class, this); + } + + /** + * @see AuthenticationPolicyManager#enableSecureLockDevice + * @param params EnableSecureLockDeviceParams for caller to supply params related + * to the secure lock device request + * @return @EnableSecureLockDeviceRequestStatus int indicating the result of the + * secure lock device request + * + * @hide + */ + @Override + @EnableSecureLockDeviceRequestStatus + public int enableSecureLockDevice(EnableSecureLockDeviceParams params) { + // (1) Call into system_server to lock device, configure allowed auth types + // for secure lock + // TODO: lock device, configure allowed authentication types for device entry + // (2) Call into framework to configure secure lock 2FA lockscreen + // update, UI & string updates + // TODO: implement 2FA lockscreen when SceneContainerFlag.isEnabled() + // TODO: implement 2FA lockscreen when !SceneContainerFlag.isEnabled() + // (3) Call into framework to configure keyguard security updates + // TODO: implement security updates + return AuthenticationPolicyManager.ERROR_UNSUPPORTED; + } + + /** + * @see AuthenticationPolicyManager#disableSecureLockDevice + * @param params @DisableSecureLockDeviceParams for caller to supply params related + * to the secure lock device request + * @return @DisableSecureLockDeviceRequestStatus int indicating the result of the + * secure lock device request + * + * @hide + */ + @Override + @DisableSecureLockDeviceRequestStatus + public int disableSecureLockDevice(DisableSecureLockDeviceParams params) { + // (1) Call into system_server to reset allowed auth types + // TODO: reset allowed authentication types for device entry; + // (2) Call into framework to disable secure lock 2FA lockscreen, reset UI + // & string updates + // TODO: implement reverting to normal lockscreen when SceneContainerFlag.isEnabled() + // TODO: implement reverting to normal lockscreen when !SceneContainerFlag.isEnabled() + // (3) Call into framework to revert keyguard security updates + // TODO: implement reverting security updates + return AuthenticationPolicyManager.ERROR_UNSUPPORTED; + } + + /** + * System service lifecycle. + */ + public static final class Lifecycle extends SystemService { + private final SecureLockDeviceService mService; + + public Lifecycle(@NonNull Context context) { + super(context); + mService = new SecureLockDeviceService(context); + } + + @Override + public void onStart() { + Slog.i(TAG, "Starting SecureLockDeviceService"); + mService.start(); + Slog.i(TAG, "Started SecureLockDeviceService"); + } + } +} diff --git a/services/core/java/com/android/server/security/authenticationpolicy/SecureLockDeviceServiceInternal.java b/services/core/java/com/android/server/security/authenticationpolicy/SecureLockDeviceServiceInternal.java new file mode 100644 index 000000000000..b90370956d8b --- /dev/null +++ b/services/core/java/com/android/server/security/authenticationpolicy/SecureLockDeviceServiceInternal.java @@ -0,0 +1,55 @@ +/* + * 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.security.authenticationpolicy; + +import android.security.authenticationpolicy.AuthenticationPolicyManager; +import android.security.authenticationpolicy.AuthenticationPolicyManager.DisableSecureLockDeviceRequestStatus; +import android.security.authenticationpolicy.AuthenticationPolicyManager.EnableSecureLockDeviceRequestStatus; +import android.security.authenticationpolicy.DisableSecureLockDeviceParams; +import android.security.authenticationpolicy.EnableSecureLockDeviceParams; + +/** + * Local system service interface for {@link SecureLockDeviceService}. + * + * <p>No permission / argument checks will be performed inside. + * Callers must check the calling app permission and the calling package name. + * + * @hide + */ +public abstract class SecureLockDeviceServiceInternal { + private static final String TAG = "SecureLockDeviceServiceInternal"; + + /** + * @see AuthenticationPolicyManager#enableSecureLockDevice(EnableSecureLockDeviceParams) + * @param params EnableSecureLockDeviceParams for caller to supply params related + * to the secure lock request + * @return @EnableSecureLockDeviceRequestStatus int indicating the result of the + * secure lock request + */ + @EnableSecureLockDeviceRequestStatus + public abstract int enableSecureLockDevice(EnableSecureLockDeviceParams params); + + /** + * @see AuthenticationPolicyManager#disableSecureLockDevice(DisableSecureLockDeviceParams) + * @param params @DisableSecureLockDeviceParams for caller to supply params related + * to the secure lock device request + * @return @DisableSecureLockDeviceRequestStatus int indicating the result of the + * secure lock device request + */ + @DisableSecureLockDeviceRequestStatus + public abstract int disableSecureLockDevice(DisableSecureLockDeviceParams params); +} diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index c6e6e761c0bc..f70dec175c06 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -630,8 +630,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // The locusId associated with this activity, if set. private LocusId mLocusId; - // Whether the activity is requesting to limit the system's educational dialogs - public boolean mShouldLimitSystemEducationDialogs; + // The timestamp of the last request to show the "Open in browser" education + public long mRequestOpenInBrowserEducationTimestamp; // Whether the activity was launched from a bubble. private boolean mLaunchedFromBubble; @@ -1623,6 +1623,11 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A newParent.setResumedActivity(this, "onParentChanged"); } mAppCompatController.getTransparentPolicy().start(); + if (mState == INITIALIZING && isRestrictedFixedOrientation(info.screenOrientation)) { + Slog.i(TAG, "Ignoring manifest-declared fixed orientation " + + ActivityInfo.screenOrientationToString(info.screenOrientation) + + " of " + this + " since target sdk 36"); + } } if (rootTask != null && rootTask.topRunningActivity() == this) { @@ -3192,6 +3197,17 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } /** + * Returns {@code true} if the orientation will be ignored for {@link #isUniversalResizeable()}. + */ + private boolean isRestrictedFixedOrientation( + @ActivityInfo.ScreenOrientation int orientation) { + // Exclude "locked" because it is not explicit portrait or landscape. + return orientation != ActivityInfo.SCREEN_ORIENTATION_LOCKED + && ActivityInfo.isFixedOrientation(orientation) + && isUniversalResizeable(); + } + + /** * Returns {@code true} if the fixed orientation, aspect ratio, resizability of this activity * will be ignored. */ @@ -7326,9 +7342,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A return mLocusId; } - void setLimitSystemEducationDialogs(boolean limitSystemEducationDialogs) { - if (mShouldLimitSystemEducationDialogs == limitSystemEducationDialogs) return; - mShouldLimitSystemEducationDialogs = limitSystemEducationDialogs; + void requestOpenInBrowserEducation() { + mRequestOpenInBrowserEducationTimestamp = System.currentTimeMillis(); final Task task = getTask(); if (task != null) { final boolean force = isVisibleRequested() && this == task.getTopNonFinishingActivity(); @@ -8123,7 +8138,13 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A ProtoLog.v(WM_DEBUG_ORIENTATION, "Setting requested orientation %s for %s", ActivityInfo.screenOrientationToString(requestedOrientation), this); - setOrientation(requestedOrientation, this); + final int resolvedOrientation = setOrientation(requestedOrientation, this); + if (resolvedOrientation != requestedOrientation + && isRestrictedFixedOrientation(requestedOrientation)) { + Slog.i(TAG, "Ignoring requested fixed orientation " + + ActivityInfo.screenOrientationToString(requestedOrientation) + + " of " + this + " since target sdk 36"); + } // Push the new configuration to the requested app in case where it's not pushed, e.g. when // the request is handled at task level with letterbox. @@ -8214,9 +8235,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A @ActivityInfo.ScreenOrientation protected int getOverrideOrientation() { int candidateOrientation = super.getOverrideOrientation(); - if (candidateOrientation != ActivityInfo.SCREEN_ORIENTATION_LOCKED - && ActivityInfo.isFixedOrientation(candidateOrientation) - && isUniversalResizeable()) { + if (isRestrictedFixedOrientation(candidateOrientation)) { candidateOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; } return mAppCompatController.getOrientationPolicy() diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index 198e14a16500..8ff08187c698 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -3915,12 +3915,11 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { } @Override - public void setLimitSystemEducationDialogs( - IBinder appToken, boolean limitSystemEducationDialogs) { + public void requestOpenInBrowserEducation(IBinder appToken) { synchronized (mGlobalLock) { final ActivityRecord r = ActivityRecord.isInRootTaskLocked(appToken); if (r != null) { - r.setLimitSystemEducationDialogs(limitSystemEducationDialogs); + r.requestOpenInBrowserEducation(); } } } diff --git a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java index 4857b02eaf7c..70a8f563275f 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java +++ b/services/core/java/com/android/server/wm/ActivityTaskSupervisor.java @@ -344,6 +344,11 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { private ActivityRecord mTopResumedActivity; /** + * Cached value of the topmost resumed activity that reported to the client. + */ + private ActivityRecord mLastReportedTopResumedActivity; + + /** * Flag indicating whether we're currently waiting for the previous top activity to handle the * loss of the state and report back before making new activity top resumed. */ @@ -2287,15 +2292,13 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { * sent to the new top resumed activity. */ ActivityRecord updateTopResumedActivityIfNeeded(String reason) { - if (!readyToResume()) { - return mTopResumedActivity; - } final ActivityRecord prevTopActivity = mTopResumedActivity; final Task topRootTask = mRootWindowContainer.getTopDisplayFocusedRootTask(); if (topRootTask == null || topRootTask.getTopResumedActivity() == prevTopActivity) { if (topRootTask == null) { // There's no focused task and there won't have any resumed activity either. scheduleTopResumedActivityStateLossIfNeeded(); + mTopResumedActivity = null; } if (mService.isSleepingLocked()) { // There won't be a next resumed activity. The top process should still be updated @@ -2339,25 +2342,27 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { /** Schedule current top resumed activity state loss */ private void scheduleTopResumedActivityStateLossIfNeeded() { - if (mTopResumedActivity == null) { + if (mLastReportedTopResumedActivity == null) { return; } // mTopResumedActivityWaitingForPrev == true at this point would mean that an activity // before the prevTopActivity one hasn't reported back yet. So server never sent the top // resumed state change message to prevTopActivity. - if (!mTopResumedActivityWaitingForPrev - && mTopResumedActivity.scheduleTopResumedActivityChanged(false /* onTop */)) { - scheduleTopResumedStateLossTimeout(mTopResumedActivity); + if (!mTopResumedActivityWaitingForPrev && readyToResume() + && mLastReportedTopResumedActivity.scheduleTopResumedActivityChanged( + false /* onTop */)) { + scheduleTopResumedStateLossTimeout(mLastReportedTopResumedActivity); mTopResumedActivityWaitingForPrev = true; + mLastReportedTopResumedActivity = null; } - mTopResumedActivity = null; } /** Schedule top resumed state change if previous top activity already reported back. */ private void scheduleTopResumedActivityStateIfNeeded() { - if (mTopResumedActivity != null && !mTopResumedActivityWaitingForPrev) { + if (mTopResumedActivity != null && !mTopResumedActivityWaitingForPrev && readyToResume()) { mTopResumedActivity.scheduleTopResumedActivityChanged(true /* onTop */); + mLastReportedTopResumedActivity = mTopResumedActivity; } } @@ -2611,6 +2616,10 @@ public class ActivityTaskSupervisor implements RecentTasks.Callbacks { */ void endDeferResume() { mDeferResumeCount--; + if (readyToResume() && mLastReportedTopResumedActivity != null + && mTopResumedActivity != mLastReportedTopResumedActivity) { + scheduleTopResumedActivityStateLossIfNeeded(); + } } /** @return True if resume can be called. */ diff --git a/services/core/java/com/android/server/wm/AppCompatCameraOverrides.java b/services/core/java/com/android/server/wm/AppCompatCameraOverrides.java index fbf9478b4fd9..9754595581a0 100644 --- a/services/core/java/com/android/server/wm/AppCompatCameraOverrides.java +++ b/services/core/java/com/android/server/wm/AppCompatCameraOverrides.java @@ -16,10 +16,9 @@ package com.android.server.wm; -import static android.app.CameraCompatTaskInfo.CAMERA_COMPAT_FREEFORM_NONE; import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION; -import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT; import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH; +import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT; import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE; import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA; import static android.content.pm.ActivityInfo.OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA; @@ -33,7 +32,6 @@ import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLAS import static com.android.server.wm.AppCompatUtils.isChangeEnabled; import android.annotation.NonNull; -import android.app.CameraCompatTaskInfo.FreeformCameraCompatMode; import com.android.server.wm.utils.OptPropFactory; import com.android.window.flags.Flags; @@ -165,13 +163,13 @@ class AppCompatCameraOverrides { * * <p>The treatment is enabled when the following conditions are met: * <ul> - * <li>Property gating the camera compatibility free-form treatment is enabled. - * <li>Activity isn't opted out by the device manufacturer with override. + * <li>Feature flag gating the camera compatibility free-form treatment is enabled. + * <li>Activity is opted in by the device manufacturer with override. * </ul> */ boolean shouldApplyFreeformTreatmentForCameraCompat() { - return Flags.enableCameraCompatForDesktopWindowing() && !isChangeEnabled(mActivityRecord, - OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT); + return Flags.enableCameraCompatForDesktopWindowing() && isChangeEnabled(mActivityRecord, + OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT); } boolean isOverrideOrientationOnlyForCameraEnabled() { @@ -202,22 +200,10 @@ class AppCompatCameraOverrides { && !mActivityRecord.shouldCreateAppCompatDisplayInsets(); } - @FreeformCameraCompatMode - int getFreeformCameraCompatMode() { - return mAppCompatCameraOverridesState.mFreeformCameraCompatMode; - } - - void setFreeformCameraCompatMode(@FreeformCameraCompatMode int freeformCameraCompatMode) { - mAppCompatCameraOverridesState.mFreeformCameraCompatMode = freeformCameraCompatMode; - } - static class AppCompatCameraOverridesState { // Whether activity "refresh" was requested but not finished in // ActivityRecord#activityResumedLocked following the camera compat force rotation in // DisplayRotationCompatPolicy. private boolean mIsRefreshRequested; - - @FreeformCameraCompatMode - private int mFreeformCameraCompatMode = CAMERA_COMPAT_FREEFORM_NONE; } } diff --git a/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java b/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java index 2a0252ab06fd..506477f67bfc 100644 --- a/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java +++ b/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java @@ -38,7 +38,6 @@ import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.CameraCompatTaskInfo; -import android.content.pm.ActivityInfo; import android.content.res.Configuration; import android.view.DisplayInfo; import android.view.Surface; @@ -46,7 +45,6 @@ import android.view.Surface; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.ProtoLog; import com.android.internal.protolog.WmProtoLogGroups; -import com.android.window.flags.Flags; /** * Policy for camera compatibility freeform treatment. @@ -124,26 +122,6 @@ final class CameraCompatFreeformPolicy implements CameraStateMonitor.CameraCompa return appBoundsChanged || displayRotationChanged; } - /** - * Whether activity is eligible for camera compatibility free-form treatment. - * - * <p>The treatment is applied to a fixed-orientation camera activity in free-form windowing - * mode. The treatment letterboxes or pillarboxes the activity to the expected orientation and - * provides changes to the camera and display orientation signals to match those expected on a - * portrait device in that orientation (for example, on a standard phone). - * - * <p>The treatment is enabled when the following conditions are met: - * <ul> - * <li>Property gating the camera compatibility free-form treatment is enabled. - * <li>Activity isn't opted out by the device manufacturer with override. - * </ul> - */ - @VisibleForTesting - boolean isCameraCompatForFreeformEnabledForActivity(@NonNull ActivityRecord activity) { - return Flags.enableCameraCompatForDesktopWindowing() && !activity.info.isChangeEnabled( - ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT); - } - @Override public void onCameraOpened(@NonNull ActivityRecord cameraActivity, @NonNull String cameraId) { @@ -159,16 +137,10 @@ final class CameraCompatFreeformPolicy implements CameraStateMonitor.CameraCompa } cameraActivity.recomputeConfiguration(); - updateCameraCompatMode(cameraActivity); cameraActivity.getTask().dispatchTaskInfoChangedIfNeeded(/* force= */ true); cameraActivity.ensureActivityConfiguration(/* ignoreVisibility= */ false); } - private void updateCameraCompatMode(@NonNull ActivityRecord cameraActivity) { - cameraActivity.mAppCompatController.getAppCompatCameraOverrides() - .setFreeformCameraCompatMode(getCameraCompatMode(cameraActivity)); - } - @Override public boolean onCameraClosed(@NonNull String cameraId) { // Top activity in the same task as the camera activity, or `null` if the task is @@ -277,7 +249,8 @@ final class CameraCompatFreeformPolicy implements CameraStateMonitor.CameraCompa boolean isTreatmentEnabledForActivity(@NonNull ActivityRecord activity, boolean checkOrientation) { int orientation = activity.getRequestedConfigurationOrientation(); - return isCameraCompatForFreeformEnabledForActivity(activity) + return activity.mAppCompatController.getAppCompatCameraOverrides() + .shouldApplyFreeformTreatmentForCameraCompat() && mCameraStateMonitor.isCameraRunningForActivity(activity) && (!checkOrientation || orientation != ORIENTATION_UNDEFINED) && activity.inFreeformWindowingMode() diff --git a/services/core/java/com/android/server/wm/DisplayArea.java b/services/core/java/com/android/server/wm/DisplayArea.java index 3b2479885f57..f40d636b522a 100644 --- a/services/core/java/com/android/server/wm/DisplayArea.java +++ b/services/core/java/com/android/server/wm/DisplayArea.java @@ -101,8 +101,6 @@ public class DisplayArea<T extends WindowContainer> extends WindowContainer<T> { DisplayArea(WindowManagerService wms, Type type, String name, int featureId) { super(wms); - // TODO(display-area): move this up to ConfigurationContainer - setOverrideOrientation(SCREEN_ORIENTATION_UNSET); mType = type; mName = name; mFeatureId = featureId; diff --git a/services/core/java/com/android/server/wm/RecentTasks.java b/services/core/java/com/android/server/wm/RecentTasks.java index 81a04af17703..f0e12fec3107 100644 --- a/services/core/java/com/android/server/wm/RecentTasks.java +++ b/services/core/java/com/android/server/wm/RecentTasks.java @@ -2025,22 +2025,9 @@ class RecentTasks { // Fill in some deprecated values. rti.id = rti.isRunning ? rti.taskId : INVALID_TASK_ID; rti.persistentId = rti.taskId; - rti.lastSnapshotData.set(tr.mLastTaskSnapshotData); if (!getTasksAllowed) { Task.trimIneffectiveInfo(tr, rti); } - - // Fill in organized child task info for the task created by organizer. - if (tr.mCreatedByOrganizer) { - for (int i = tr.getChildCount() - 1; i >= 0; i--) { - final Task childTask = tr.getChildAt(i).asTask(); - if (childTask != null && childTask.isOrganized()) { - final ActivityManager.RecentTaskInfo cti = new ActivityManager.RecentTaskInfo(); - childTask.fillTaskInfo(cti, true /* stripExtras */, tda); - rti.childrenTaskInfos.add(cti); - } - } - } return rti; } diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index dbc3b76c22a1..9504fc7d4d30 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -128,7 +128,6 @@ import android.annotation.Nullable; import android.annotation.UserIdInt; import android.app.Activity; import android.app.ActivityManager; -import android.app.ActivityManager.RecentTaskInfo.PersistedTaskSnapshotData; import android.app.ActivityManager.TaskDescription; import android.app.ActivityOptions; import android.app.ActivityTaskManager; @@ -256,9 +255,6 @@ class Task extends TaskFragment { private static final String ATTR_MIN_HEIGHT = "min_height"; private static final String ATTR_PERSIST_TASK_VERSION = "persist_task_version"; private static final String ATTR_WINDOW_LAYOUT_AFFINITY = "window_layout_affinity"; - private static final String ATTR_LAST_SNAPSHOT_TASK_SIZE = "last_snapshot_task_size"; - private static final String ATTR_LAST_SNAPSHOT_CONTENT_INSETS = "last_snapshot_content_insets"; - private static final String ATTR_LAST_SNAPSHOT_BUFFER_SIZE = "last_snapshot_buffer_size"; // How long to wait for all background Activities to redraw following a call to // convertToTranslucent(). @@ -472,10 +468,6 @@ class Task extends TaskFragment { // NOTE: This value needs to be persisted with each task private TaskDescription mTaskDescription; - // Information about the last snapshot that should be persisted with the task to allow SystemUI - // to layout without loading all the task snapshots - final PersistedTaskSnapshotData mLastTaskSnapshotData; - /** @see #setCanAffectSystemUiFlags */ private boolean mCanAffectSystemUiFlags = true; @@ -635,14 +627,13 @@ class Task extends TaskFragment { ComponentName _realActivity, ComponentName _origActivity, boolean _rootWasReset, boolean _autoRemoveRecents, int _userId, int _effectiveUid, String _lastDescription, long lastTimeMoved, boolean neverRelinquishIdentity, - TaskDescription _lastTaskDescription, PersistedTaskSnapshotData _lastSnapshotData, - int taskAffiliation, int prevTaskId, int nextTaskId, int callingUid, - String callingPackage, @Nullable String callingFeatureId, int resizeMode, - boolean supportsPictureInPicture, boolean _realActivitySuspended, - boolean userSetupComplete, int minWidth, int minHeight, ActivityInfo info, - IVoiceInteractionSession _voiceSession, IVoiceInteractor _voiceInteractor, - boolean _createdByOrganizer, IBinder _launchCookie, boolean _deferTaskAppear, - boolean _removeWithTaskOrganizer) { + TaskDescription _lastTaskDescription, int taskAffiliation, int prevTaskId, + int nextTaskId, int callingUid, String callingPackage, + @Nullable String callingFeatureId, int resizeMode, boolean supportsPictureInPicture, + boolean _realActivitySuspended, boolean userSetupComplete, int minWidth, int minHeight, + ActivityInfo info, IVoiceInteractionSession _voiceSession, + IVoiceInteractor _voiceInteractor, boolean _createdByOrganizer, IBinder _launchCookie, + boolean _deferTaskAppear, boolean _removeWithTaskOrganizer) { super(atmService, null /* fragmentToken */, _createdByOrganizer, false /* isEmbedded */); mTaskId = _taskId; @@ -652,9 +643,6 @@ class Task extends TaskFragment { mTaskDescription = _lastTaskDescription != null ? _lastTaskDescription : new TaskDescription(); - mLastTaskSnapshotData = _lastSnapshotData != null - ? _lastSnapshotData - : new PersistedTaskSnapshotData(); affinityIntent = _affinityIntent; affinity = _affinity; rootAffinity = _rootAffinity; @@ -3111,7 +3099,6 @@ class Task extends TaskFragment { } void onSnapshotChanged(TaskSnapshot snapshot) { - mLastTaskSnapshotData.set(snapshot); mAtmService.getTaskChangeNotificationController().notifyTaskSnapshotChanged( mTaskId, snapshot); } @@ -3423,8 +3410,8 @@ class Task extends TaskFragment { ? top.getLastParentBeforePip().mTaskId : INVALID_TASK_ID; info.shouldDockBigOverlays = top != null && top.shouldDockBigOverlays; info.mTopActivityLocusId = top != null ? top.getLocusId() : null; - info.isTopActivityLimitSystemEducationDialogs = top != null - ? top.mShouldLimitSystemEducationDialogs : false; + info.topActivityRequestOpenInBrowserEducationTimestamp = top != null + ? top.mRequestOpenInBrowserEducationTimestamp : 0; final Task parentTask = getParent() != null ? getParent().asTask() : null; info.parentTaskId = parentTask != null && parentTask.mCreatedByOrganizer ? parentTask.mTaskId @@ -3523,6 +3510,7 @@ class Task extends TaskFragment { info.capturedLink = null; info.capturedLinkTimestamp = 0; + info.topActivityRequestOpenInBrowserEducationTimestamp = 0; } @Nullable PictureInPictureParams getPictureInPictureParams() { @@ -3989,19 +3977,6 @@ class Task extends TaskFragment { out.attributeInt(null, ATTR_MIN_HEIGHT, mMinHeight); out.attributeInt(null, ATTR_PERSIST_TASK_VERSION, PERSIST_TASK_VERSION); - if (mLastTaskSnapshotData.taskSize != null) { - out.attribute(null, ATTR_LAST_SNAPSHOT_TASK_SIZE, - mLastTaskSnapshotData.taskSize.flattenToString()); - } - if (mLastTaskSnapshotData.contentInsets != null) { - out.attribute(null, ATTR_LAST_SNAPSHOT_CONTENT_INSETS, - mLastTaskSnapshotData.contentInsets.flattenToString()); - } - if (mLastTaskSnapshotData.bufferSize != null) { - out.attribute(null, ATTR_LAST_SNAPSHOT_BUFFER_SIZE, - mLastTaskSnapshotData.bufferSize.flattenToString()); - } - if (affinityIntent != null) { out.startTag(null, TAG_AFFINITYINTENT); affinityIntent.saveToXml(out); @@ -4068,7 +4043,6 @@ class Task extends TaskFragment { int taskId = INVALID_TASK_ID; final int outerDepth = in.getDepth(); TaskDescription taskDescription = new TaskDescription(); - PersistedTaskSnapshotData lastSnapshotData = new PersistedTaskSnapshotData(); int taskAffiliation = INVALID_TASK_ID; int prevTaskId = INVALID_TASK_ID; int nextTaskId = INVALID_TASK_ID; @@ -4175,15 +4149,6 @@ class Task extends TaskFragment { case ATTR_PERSIST_TASK_VERSION: persistTaskVersion = Integer.parseInt(attrValue); break; - case ATTR_LAST_SNAPSHOT_TASK_SIZE: - lastSnapshotData.taskSize = Point.unflattenFromString(attrValue); - break; - case ATTR_LAST_SNAPSHOT_CONTENT_INSETS: - lastSnapshotData.contentInsets = Rect.unflattenFromString(attrValue); - break; - case ATTR_LAST_SNAPSHOT_BUFFER_SIZE: - lastSnapshotData.bufferSize = Point.unflattenFromString(attrValue); - break; default: if (!attrName.startsWith(TaskDescription.ATTR_TASKDESCRIPTION_PREFIX)) { Slog.w(TAG, "Task: Unknown attribute=" + attrName); @@ -4277,7 +4242,6 @@ class Task extends TaskFragment { .setLastTimeMoved(lastTimeOnTop) .setNeverRelinquishIdentity(neverRelinquishIdentity) .setLastTaskDescription(taskDescription) - .setLastSnapshotData(lastSnapshotData) .setTaskAffiliation(taskAffiliation) .setPrevAffiliateTaskId(prevTaskId) .setNextAffiliateTaskId(nextTaskId) @@ -6443,7 +6407,6 @@ class Task extends TaskFragment { private long mLastTimeMoved; private boolean mNeverRelinquishIdentity; private TaskDescription mLastTaskDescription; - private PersistedTaskSnapshotData mLastSnapshotData; private int mTaskAffiliation; private int mPrevAffiliateTaskId = INVALID_TASK_ID; private int mNextAffiliateTaskId = INVALID_TASK_ID; @@ -6671,11 +6634,6 @@ class Task extends TaskFragment { return this; } - private Builder setLastSnapshotData(PersistedTaskSnapshotData lastSnapshotData) { - mLastSnapshotData = lastSnapshotData; - return this; - } - private Builder setOrigActivity(ComponentName origActivity) { mOrigActivity = origActivity; return this; @@ -6824,7 +6782,7 @@ class Task extends TaskFragment { return new Task(mAtmService, mTaskId, mIntent, mAffinityIntent, mAffinity, mRootAffinity, mRealActivity, mOrigActivity, mRootWasReset, mAutoRemoveRecents, mUserId, mEffectiveUid, mLastDescription, mLastTimeMoved, - mNeverRelinquishIdentity, mLastTaskDescription, mLastSnapshotData, + mNeverRelinquishIdentity, mLastTaskDescription, mTaskAffiliation, mPrevAffiliateTaskId, mNextAffiliateTaskId, mCallingUid, mCallingPackage, mCallingFeatureId, mResizeMode, mSupportsPictureInPicture, mRealActivitySuspended, mUserSetupComplete, mMinWidth, mMinHeight, diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index 5f92bb626154..2397e032fcc8 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -1676,30 +1676,36 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< * @param orientation the specified orientation. Needs to be one of {@link ScreenOrientation}. * @param requestingContainer the container which orientation request has changed. Mostly used * to ensure it gets correct configuration. + * @return the resolved override orientation of this window container. */ - void setOrientation(@ScreenOrientation int orientation, + @ScreenOrientation + int setOrientation(@ScreenOrientation int orientation, @Nullable WindowContainer requestingContainer) { if (getOverrideOrientation() == orientation) { - return; + return orientation; } - setOverrideOrientation(orientation); final WindowContainer parent = getParent(); - if (parent != null) { - if (getConfiguration().orientation != getRequestedConfigurationOrientation() - // Update configuration directly only if the change won't be dispatched from - // ancestor. This prevents from computing intermediate configuration when the - // parent also needs to be updated from the ancestor. E.g. the app requests - // portrait but the task is still in landscape. While updating from display, - // the task can be updated to portrait first so the configuration can be - // computed in a consistent environment. - && (inMultiWindowMode() + if (parent == null) { + return orientation; + } + // The derived class can return a result that is different from the given orientation. + final int resolvedOrientation = getOverrideOrientation(); + if (getConfiguration().orientation != getRequestedConfigurationOrientation( + false /* forDisplay */, resolvedOrientation) + // Update configuration directly only if the change won't be dispatched from + // ancestor. This prevents from computing intermediate configuration when the + // parent also needs to be updated from the ancestor. E.g. the app requests + // portrait but the task is still in landscape. While updating from display, + // the task can be updated to portrait first so the configuration can be + // computed in a consistent environment. + && (inMultiWindowMode() || !handlesOrientationChangeFromDescendant(orientation))) { - // Resolve the requested orientation. - onConfigurationChanged(parent.getConfiguration()); - } - onDescendantOrientationChanged(requestingContainer); + // Resolve the requested orientation. + onConfigurationChanged(parent.getConfiguration()); } + onDescendantOrientationChanged(requestingContainer); + return resolvedOrientation; } @ScreenOrientation diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index 091896590b6b..c42aa37d847b 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -788,9 +788,7 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub deferResume = false; // Already calls ensureActivityConfig mService.mRootWindowContainer.ensureActivitiesVisible(); - if (!mService.mRootWindowContainer.resumeFocusedTasksTopActivities()) { - mService.mTaskSupervisor.updateTopResumedActivityIfNeeded("endWCT-effects"); - } + mService.mRootWindowContainer.resumeFocusedTasksTopActivities(); } else if ((effects & TRANSACT_EFFECTS_CLIENT_CONFIG) != 0) { for (int i = haveConfigChanges.size() - 1; i >= 0; --i) { haveConfigChanges.valueAt(i).forAllActivities(r -> { @@ -816,10 +814,6 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub mService.mTaskSupervisor.setDeferRootVisibilityUpdate(false /* deferUpdate */); if (deferResume) { mService.mTaskSupervisor.endDeferResume(); - // Transient launching the Recents via HIERARCHY_OP_TYPE_PENDING_INTENT directly - // resume the Recents activity with no TRANSACT_EFFECTS_LIFECYCLE. Explicitly - // checks if the top resumed activity should be updated after defer-resume ended. - mService.mTaskSupervisor.updateTopResumedActivityIfNeeded("endWCT"); } mService.continueWindowLayout(); } diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 81af78e5ce25..6009848f9308 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -182,7 +182,6 @@ import static com.android.server.wm.WindowStateProto.UNRESTRICTED_KEEP_CLEAR_ARE import static com.android.server.wm.WindowStateProto.VIEW_VISIBILITY; import static com.android.server.wm.WindowStateProto.WINDOW_CONTAINER; import static com.android.server.wm.WindowStateProto.WINDOW_FRAMES; -import static com.android.window.flags.Flags.secureWindowState; import static com.android.window.flags.Flags.surfaceTrustedOverlay; import android.annotation.CallSuper; @@ -1187,9 +1186,7 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP if (surfaceTrustedOverlay() && isWindowTrustedOverlay()) { getPendingTransaction().setTrustedOverlay(mSurfaceControl, true); } - if (secureWindowState()) { - getPendingTransaction().setSecure(mSurfaceControl, isSecureLocked()); - } + getPendingTransaction().setSecure(mSurfaceControl, isSecureLocked()); // All apps should be considered as occluding when computing TrustedPresentation Thresholds. final boolean canOccludePresentation = !mSession.mCanAddInternalSystemWindow; getPendingTransaction().setCanOccludePresentation(mSurfaceControl, canOccludePresentation); @@ -6174,18 +6171,10 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP void setSecureLocked(boolean isSecure) { ProtoLog.i(WM_SHOW_TRANSACTIONS, "SURFACE isSecure=%b: %s", isSecure, getName()); - if (secureWindowState()) { - if (mSurfaceControl == null) { - return; - } - getPendingTransaction().setSecure(mSurfaceControl, isSecure); - } else { - if (mWinAnimator.mSurfaceControl == null) { - return; - } - getPendingTransaction().setSecure(mWinAnimator.mSurfaceControl, - isSecure); + if (mSurfaceControl == null) { + return; } + getPendingTransaction().setSecure(mSurfaceControl, isSecure); if (mDisplayContent != null) { mDisplayContent.refreshImeSecureFlag(getSyncTransaction()); } diff --git a/services/core/java/com/android/server/wm/WindowStateAnimator.java b/services/core/java/com/android/server/wm/WindowStateAnimator.java index a934eea690f7..0154d95d888d 100644 --- a/services/core/java/com/android/server/wm/WindowStateAnimator.java +++ b/services/core/java/com/android/server/wm/WindowStateAnimator.java @@ -49,7 +49,6 @@ import static com.android.server.wm.WindowStateAnimatorProto.DRAW_STATE; import static com.android.server.wm.WindowStateAnimatorProto.SURFACE; import static com.android.server.wm.WindowStateAnimatorProto.SYSTEM_DECOR_RECT; import static com.android.server.wm.WindowSurfaceControllerProto.SHOWN; -import static com.android.window.flags.Flags.secureWindowState; import static com.android.window.flags.Flags.setScPropertiesInClient; import android.content.Context; @@ -310,12 +309,6 @@ class WindowStateAnimator { int flags = SurfaceControl.HIDDEN; final WindowManager.LayoutParams attrs = w.mAttrs; - if (!secureWindowState()) { - if (w.isSecureLocked()) { - flags |= SurfaceControl.SECURE; - } - } - if ((mWin.mAttrs.privateFlags & PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY) != 0) { flags |= SurfaceControl.SKIP_SCREENSHOT; } diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp index e38337540ad9..dece612c9424 100644 --- a/services/core/jni/com_android_server_input_InputManagerService.cpp +++ b/services/core/jni/com_android_server_input_InputManagerService.cpp @@ -2330,6 +2330,12 @@ static void nativeToggleCapsLock(JNIEnv* env, jobject nativeImplObj, jint device im->getInputManager()->getReader().toggleCapsLockState(deviceId); } +static void resetLockedModifierState(JNIEnv* env, jobject nativeImplObj) { + NativeInputManager* im = getNativeInputManager(env, nativeImplObj); + + im->getInputManager()->getReader().resetLockedModifierState(); +} + static void nativeDisplayRemoved(JNIEnv* env, jobject nativeImplObj, jint displayId) { NativeInputManager* im = getNativeInputManager(env, nativeImplObj); @@ -3134,6 +3140,7 @@ static const JNINativeMethod gInputManagerMethods[] = { {"verifyInputEvent", "(Landroid/view/InputEvent;)Landroid/view/VerifiedInputEvent;", (void*)nativeVerifyInputEvent}, {"toggleCapsLock", "(I)V", (void*)nativeToggleCapsLock}, + {"resetLockedModifierState", "()V", (void*)resetLockedModifierState}, {"displayRemoved", "(I)V", (void*)nativeDisplayRemoved}, {"setFocusedApplication", "(ILandroid/view/InputApplicationHandle;)V", (void*)nativeSetFocusedApplication}, diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index da478f38498b..dc10dd9f47a2 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -251,6 +251,7 @@ import com.android.server.security.KeyAttestationApplicationIdProviderService; import com.android.server.security.KeyChainSystemService; import com.android.server.security.advancedprotection.AdvancedProtectionService; import com.android.server.security.authenticationpolicy.AuthenticationPolicyService; +import com.android.server.security.authenticationpolicy.SecureLockDeviceService; import com.android.server.security.forensic.ForensicService; import com.android.server.security.rkp.RemoteProvisioningService; import com.android.server.selinux.SelinuxAuditLogsService; @@ -2659,6 +2660,12 @@ public final class SystemServer implements Dumpable { mSystemServiceManager.startService(AuthService.class); t.traceEnd(); + if (android.security.Flags.secureLockdown()) { + t.traceBegin("StartSecureLockDeviceService.Lifecycle"); + mSystemServiceManager.startService(SecureLockDeviceService.Lifecycle.class); + t.traceEnd(); + } + if (android.adaptiveauth.Flags.enableAdaptiveAuth()) { t.traceBegin("StartAuthenticationPolicyService"); mSystemServiceManager.startService(AuthenticationPolicyService.class); @@ -3062,7 +3069,10 @@ public final class SystemServer implements Dumpable { if (com.android.ranging.flags.Flags.rangingStackEnabled()) { if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_UWB) || context.getPackageManager().hasSystemFeature( - PackageManager.FEATURE_WIFI_RTT)) { + PackageManager.FEATURE_WIFI_RTT) + || (com.android.ranging.flags.Flags.rangingCsEnabled() + && context.getPackageManager().hasSystemFeature( + PackageManager.FEATURE_BLUETOOTH_LE_CHANNEL_SOUNDING))) { t.traceBegin("RangingService"); // TODO: b/375264320 - Remove after RELEASE_RANGING_STACK is ramped to next. try { diff --git a/services/robotests/backup/src/com/android/server/backup/BackupManagerServiceRoboTest.java b/services/robotests/backup/src/com/android/server/backup/BackupManagerServiceRoboTest.java index a547d0f94ea3..4e9fff230bac 100644 --- a/services/robotests/backup/src/com/android/server/backup/BackupManagerServiceRoboTest.java +++ b/services/robotests/backup/src/com/android/server/backup/BackupManagerServiceRoboTest.java @@ -31,6 +31,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import static org.robolectric.Shadows.shadowOf; import static org.testng.Assert.expectThrows; @@ -96,6 +97,7 @@ public class BackupManagerServiceRoboTest { @UserIdInt private int mUserTwoId; @Mock private UserBackupManagerService mUserSystemService; @Mock private UserBackupManagerService mUserOneService; + @Mock private BackupAgentConnectionManager mUserOneBackupAgentConnectionManager; @Mock private UserBackupManagerService mUserTwoService; /** Setup */ @@ -116,6 +118,9 @@ public class BackupManagerServiceRoboTest { mShadowContext.grantPermissions(BACKUP); mShadowContext.grantPermissions(INTERACT_ACROSS_USERS_FULL); + when(mUserOneService.getBackupAgentConnectionManager()).thenReturn( + mUserOneBackupAgentConnectionManager); + ShadowBinder.setCallingUid(Process.SYSTEM_UID); } @@ -226,7 +231,7 @@ public class BackupManagerServiceRoboTest { backupManagerService.agentConnected(mUserOneId, TEST_PACKAGE, agentBinder); - verify(mUserOneService).agentConnected(TEST_PACKAGE, agentBinder); + verify(mUserOneBackupAgentConnectionManager).agentConnected(TEST_PACKAGE, agentBinder); } /** Test that the backup service does not route methods for non-registered users. */ @@ -239,7 +244,8 @@ public class BackupManagerServiceRoboTest { backupManagerService.agentConnected(mUserTwoId, TEST_PACKAGE, agentBinder); - verify(mUserOneService, never()).agentConnected(TEST_PACKAGE, agentBinder); + verify(mUserOneBackupAgentConnectionManager, never()).agentConnected(TEST_PACKAGE, + agentBinder); } /** Test that the backup service routes methods correctly to the user that requests it. */ diff --git a/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java b/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java index 7349c14ef62b..aeb1ba93f049 100644 --- a/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java +++ b/services/robotests/backup/src/com/android/server/backup/keyvalue/KeyValueBackupTaskTest.java @@ -107,6 +107,7 @@ import com.android.internal.backup.IBackupTransport; import com.android.internal.infra.AndroidFuture; import com.android.server.EventLogTags; import com.android.server.LocalServices; +import com.android.server.backup.BackupAgentConnectionManager; import com.android.server.backup.BackupRestoreTask; import com.android.server.backup.DataChangedJournal; import com.android.server.backup.KeyValueBackupJob; @@ -167,7 +168,6 @@ import java.util.List; import java.util.concurrent.TimeoutException; import java.util.stream.Stream; -// TODO: Test agents timing out @RunWith(RobolectricTestRunner.class) @Config( shadows = { @@ -195,6 +195,7 @@ public class KeyValueBackupTaskTest { @Mock private IBackupManagerMonitor mMonitor; @Mock private OnTaskFinishedListener mListener; @Mock private PackageManagerInternal mPackageManagerInternal; + @Mock private BackupAgentConnectionManager mBackupAgentConnectionManager; private UserBackupManagerService mBackupManagerService; private TransportData mTransport; @@ -257,6 +258,8 @@ public class KeyValueBackupTaskTest { when(mBackupManagerService.getBaseStateDir()).thenReturn(mBaseStateDir); when(mBackupManagerService.getDataDir()).thenReturn(mDataDir); when(mBackupManagerService.getBackupManagerBinder()).thenReturn(mBackupManager); + when(mBackupManagerService.getBackupAgentConnectionManager()).thenReturn( + mBackupAgentConnectionManager); mBackupHandler = mBackupManagerService.getBackupHandler(); mShadowBackupLooper = shadowOf(mBackupHandler.getLooper()); @@ -749,7 +752,8 @@ public class KeyValueBackupTaskTest { /** * Agent unavailable means {@link - * UserBackupManagerService#bindToAgentSynchronous(ApplicationInfo, int)} returns {@code null}. + * BackupAgentConnectionManager#bindToAgentSynchronous(ApplicationInfo, int, int)} returns + * {@code null}. * * @see #setUpAgent(PackageData) */ @@ -805,7 +809,7 @@ public class KeyValueBackupTaskTest { TransportMock transportMock = setUpInitializedTransport(mTransport); setUpAgent(PACKAGE_1); doThrow(SecurityException.class) - .when(mBackupManagerService) + .when(mBackupAgentConnectionManager) .bindToAgentSynchronous(argThat(applicationInfo(PACKAGE_1)), anyInt(), anyInt()); KeyValueBackupTask task = createKeyValueBackupTask(transportMock, PACKAGE_1); @@ -823,7 +827,7 @@ public class KeyValueBackupTaskTest { TransportMock transportMock = setUpInitializedTransport(mTransport); setUpAgent(PACKAGE_1); doThrow(SecurityException.class) - .when(mBackupManagerService) + .when(mBackupAgentConnectionManager) .bindToAgentSynchronous(argThat(applicationInfo(PACKAGE_1)), anyInt(), anyInt()); KeyValueBackupTask task = createKeyValueBackupTask(transportMock, true, PACKAGE_1); @@ -861,7 +865,7 @@ public class KeyValueBackupTaskTest { runTask(task); verify(mBackupManagerService).setWorkSource(null); - verify(mBackupManagerService).unbindAgent(argThat(applicationInfo(PACKAGE_1))); + verify(mBackupAgentConnectionManager).unbindAgent(argThat(applicationInfo(PACKAGE_1))); } @Test @@ -1097,7 +1101,7 @@ public class KeyValueBackupTaskTest { runTask(task); verify(agentMock.agentBinder).fail(any()); - verify(mBackupManagerService).unbindAgent(argThat(applicationInfo(PACKAGE_1))); + verify(mBackupAgentConnectionManager).unbindAgent(argThat(applicationInfo(PACKAGE_1))); } @Test @@ -1418,7 +1422,8 @@ public class KeyValueBackupTaskTest { .isEqualTo("newState".getBytes()); assertCleansUpFiles(mTransport, PM_PACKAGE); // We don't unbind PM - verify(mBackupManagerService, never()).unbindAgent(argThat(applicationInfo(PM_PACKAGE))); + verify(mBackupAgentConnectionManager, never()).unbindAgent( + argThat(applicationInfo(PM_PACKAGE))); } @Test @@ -1439,7 +1444,8 @@ public class KeyValueBackupTaskTest { runTask(task); - verify(mBackupManagerService, never()).unbindAgent(argThat(applicationInfo(PM_PACKAGE))); + verify(mBackupAgentConnectionManager, never()).unbindAgent( + argThat(applicationInfo(PM_PACKAGE))); } @Test @@ -1642,9 +1648,10 @@ public class KeyValueBackupTaskTest { runTask(task); - InOrder inOrder = inOrder(agentMock.agent, mBackupManagerService); + InOrder inOrder = inOrder(agentMock.agent, mBackupAgentConnectionManager); inOrder.verify(agentMock.agent).onQuotaExceeded(anyLong(), eq(1234L)); - inOrder.verify(mBackupManagerService).unbindAgent(argThat(applicationInfo(PACKAGE_1))); + inOrder.verify(mBackupAgentConnectionManager).unbindAgent( + argThat(applicationInfo(PACKAGE_1))); } @Test @@ -2634,12 +2641,12 @@ public class KeyValueBackupTaskTest { doNothing().when(backupAgentBinder).fail(any()); if (packageData.available) { doReturn(backupAgentBinder) - .when(mBackupManagerService) + .when(mBackupAgentConnectionManager) .bindToAgentSynchronous(argThat(applicationInfo(packageData)), anyInt(), anyInt()); } else { doReturn(null) - .when(mBackupManagerService) + .when(mBackupAgentConnectionManager) .bindToAgentSynchronous(argThat(applicationInfo(packageData)), anyInt(), anyInt()); } @@ -2976,7 +2983,7 @@ public class KeyValueBackupTaskTest { private void assertCleansUpFilesAndAgent(TransportData transport, PackageData packageData) { assertCleansUpFiles(transport, packageData); - verify(mBackupManagerService).unbindAgent(argThat(applicationInfo(packageData))); + verify(mBackupAgentConnectionManager).unbindAgent(argThat(applicationInfo(packageData))); } private void assertCleansUpFiles(TransportData transport, PackageData packageData) { diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java index 5bb6b19cd63e..d08715586580 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/inputmethodservice/InputMethodServiceTest.java @@ -194,7 +194,13 @@ public class InputMethodServiceTest { () -> assertThat(mActivity.hideImeWithWindowInsetsController()).isTrue(), true /* expected */, false /* inputViewStarted */); - assertThat(mInputMethodService.isInputViewShown()).isFalse(); + if (mFlagsValueProvider.getBoolean(Flags.FLAG_REFACTOR_INSETS_CONTROLLER)) { + // The IME visibility is only sent at the end of the animation. Therefore, we have to + // wait until the visibility was sent to the server and the IME window hidden. + eventually(() -> assertThat(mInputMethodService.isInputViewShown()).isFalse()); + } else { + assertThat(mInputMethodService.isInputViewShown()).isFalse(); + } } /** diff --git a/services/tests/mockingservicestests/src/com/android/server/backup/BackupAgentConnectionManagerTest.java b/services/tests/mockingservicestests/src/com/android/server/backup/BackupAgentConnectionManagerTest.java new file mode 100644 index 000000000000..19e43b6fa851 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/backup/BackupAgentConnectionManagerTest.java @@ -0,0 +1,381 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.backup; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; + +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.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.ActivityManager; +import android.app.ActivityManagerInternal; +import android.app.ApplicationThreadConstants; +import android.app.IActivityManager; +import android.app.IBackupAgent; +import android.app.backup.BackupAnnotations.BackupDestination; +import android.app.backup.BackupAnnotations.OperationType; +import android.compat.testing.PlatformCompatChangeRule; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.os.Process; +import android.os.UserHandle; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.annotations.Presubmit; +import android.platform.test.flag.junit.SetFlagsRule; + +import androidx.test.runner.AndroidJUnit4; + +import com.android.server.LocalServices; +import com.android.server.backup.internal.LifecycleOperationStorage; + +import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges; +import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges; + +import com.google.common.collect.ImmutableSet; + +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestRule; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.mockito.MockitoSession; +import org.mockito.quality.Strictness; + +import java.util.Set; + +@Presubmit +@RunWith(AndroidJUnit4.class) +public class BackupAgentConnectionManagerTest { + private static final String TEST_PACKAGE = "com.test.package"; + + @Rule + public TestRule compatChangeRule = new PlatformCompatChangeRule(); + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + @Mock + IActivityManager mActivityManager; + @Mock + ActivityManagerInternal mActivityManagerInternal; + @Mock + LifecycleOperationStorage mOperationStorage; + @Mock + UserBackupManagerService mUserBackupManagerService; + @Mock + IBackupAgent.Stub mBackupAgentStub; + @Mock + PackageManager mPackageManager; + + private BackupAgentConnectionManager mConnectionManager; + private MockitoSession mSession; + private ApplicationInfo mTestApplicationInfo; + private IBackupAgent mBackupAgentResult; + private Thread mTestThread; + + @Before + public void setUp() throws Exception { + mSession = mockitoSession().initMocks(this).mockStatic(ActivityManager.class).mockStatic( + LocalServices.class).strictness(Strictness.LENIENT).startMocking(); + MockitoAnnotations.initMocks(this); + + doReturn(mActivityManager).when(ActivityManager::getService); + doReturn(mActivityManagerInternal).when( + () -> LocalServices.getService(ActivityManagerInternal.class)); + // Real package manager throws if a property is not defined. + when(mPackageManager.getPropertyAsUser(any(), any(), any(), anyInt())).thenThrow( + new PackageManager.NameNotFoundException()); + + mConnectionManager = spy( + new BackupAgentConnectionManager(mOperationStorage, mPackageManager, + mUserBackupManagerService, UserHandle.USER_SYSTEM)); + + mTestApplicationInfo = new ApplicationInfo(); + mTestApplicationInfo.packageName = TEST_PACKAGE; + + mBackupAgentResult = null; + mTestThread = null; + } + + @After + public void tearDown() { + if (mSession != null) { + mSession.finishMocking(); + } + } + + @Test + public void bindToAgentSynchronous_amReturnsFailure_returnsNullAndClearsPendingBackups() + throws Exception { + when(mActivityManager.bindBackupAgent(eq(TEST_PACKAGE), anyInt(), anyInt(), + anyInt(), anyBoolean())).thenReturn(false); + + IBackupAgent result = mConnectionManager.bindToAgentSynchronous(mTestApplicationInfo, + ApplicationThreadConstants.BACKUP_MODE_FULL, BackupDestination.CLOUD); + + assertThat(result).isNull(); + verify(mActivityManagerInternal).clearPendingBackup(UserHandle.USER_SYSTEM); + } + + @Test + public void bindToAgentSynchronous_agentDisconnectedCalled_returnsNullAndClearsPendingBackups() + throws Exception { + when(mActivityManager.bindBackupAgent(eq(TEST_PACKAGE), anyInt(), anyInt(), + anyInt(), anyBoolean())).thenReturn(true); + // This is so that IBackupAgent.Stub.asInterface() works. + when(mBackupAgentStub.queryLocalInterface(any())).thenReturn(mBackupAgentStub); + when(mConnectionManager.getCallingUid()).thenReturn(Process.SYSTEM_UID); + + // This is going to block until it receives the callback so we need to run it on a + // separate thread. + Thread testThread = new Thread(() -> setBackupAgentResult( + mConnectionManager.bindToAgentSynchronous(mTestApplicationInfo, + ApplicationThreadConstants.BACKUP_MODE_FULL, BackupDestination.CLOUD)), + "backup-agent-connection-manager-test"); + testThread.start(); + // Give the testThread a head start, otherwise agentConnected() might run before + // bindToAgentSynchronous() is called. + Thread.sleep(500); + mConnectionManager.agentDisconnected(TEST_PACKAGE); + testThread.join(); + + assertThat(mBackupAgentResult).isNull(); + verify(mActivityManagerInternal).clearPendingBackup(UserHandle.USER_SYSTEM); + } + + @Test + public void bindToAgentSynchronous_agentConnectedCalled_returnsBackupAgent() throws Exception { + when(mActivityManager.bindBackupAgent(eq(TEST_PACKAGE), anyInt(), anyInt(), + anyInt(), anyBoolean())).thenReturn(true); + // This is so that IBackupAgent.Stub.asInterface() works. + when(mBackupAgentStub.queryLocalInterface(any())).thenReturn(mBackupAgentStub); + when(mConnectionManager.getCallingUid()).thenReturn(Process.SYSTEM_UID); + + // This is going to block until it receives the callback so we need to run it on a + // separate thread. + Thread testThread = new Thread(() -> setBackupAgentResult( + mConnectionManager.bindToAgentSynchronous(mTestApplicationInfo, + ApplicationThreadConstants.BACKUP_MODE_FULL, BackupDestination.CLOUD)), + "backup-agent-connection-manager-test"); + testThread.start(); + // Give the testThread a head start, otherwise agentConnected() might run before + // bindToAgentSynchronous() is called. + Thread.sleep(500); + mConnectionManager.agentConnected(TEST_PACKAGE, mBackupAgentStub); + testThread.join(); + + assertThat(mBackupAgentResult).isEqualTo(mBackupAgentStub); + verify(mActivityManagerInternal, never()).clearPendingBackup(anyInt()); + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_RESTRICTED_MODE_CHANGES) + public void bindToAgentSynchronous_restrictedModeChangesFlagOff_shouldUseRestrictedMode() + throws Exception { + mConnectionManager.bindToAgentSynchronous(mTestApplicationInfo, + ApplicationThreadConstants.BACKUP_MODE_FULL, BackupDestination.CLOUD); + + verify(mActivityManager).bindBackupAgent(eq(TEST_PACKAGE), anyInt(), anyInt(), anyInt(), + /* useRestrictedMode= */ eq(true)); + // Make sure we never hit the code that checks the property. + verify(mPackageManager, never()).getPropertyAsUser(any(), any(), any(), anyInt()); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_RESTRICTED_MODE_CHANGES) + public void bindToAgentSynchronous_keyValueBackup_shouldNotUseRestrictedMode() + throws Exception { + mConnectionManager.bindToAgentSynchronous(mTestApplicationInfo, + ApplicationThreadConstants.BACKUP_MODE_INCREMENTAL, BackupDestination.CLOUD); + + verify(mActivityManager).bindBackupAgent(eq(TEST_PACKAGE), anyInt(), anyInt(), anyInt(), + /* useRestrictedMode= */ eq(false)); + // Make sure we never hit the code that checks the property. + verify(mPackageManager, never()).getPropertyAsUser(any(), any(), any(), anyInt()); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_RESTRICTED_MODE_CHANGES) + public void bindToAgentSynchronous_keyValueRestore_shouldNotUseRestrictedMode() + throws Exception { + mConnectionManager.bindToAgentSynchronous(mTestApplicationInfo, + ApplicationThreadConstants.BACKUP_MODE_RESTORE, BackupDestination.CLOUD); + + verify(mActivityManager).bindBackupAgent(eq(TEST_PACKAGE), anyInt(), anyInt(), anyInt(), + /* useRestrictedMode= */ eq(false)); + // Make sure we never hit the code that checks the property. + verify(mPackageManager, never()).getPropertyAsUser(any(), any(), any(), anyInt()); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_RESTRICTED_MODE_CHANGES) + public void bindToAgentSynchronous_packageOptedIn_shouldUseRestrictedMode() throws Exception { + reset(mPackageManager); + when(mPackageManager.getPropertyAsUser( + eq(PackageManager.PROPERTY_USE_RESTRICTED_BACKUP_MODE), eq(TEST_PACKAGE), any(), + anyInt())).thenReturn(new PackageManager.Property( + PackageManager.PROPERTY_USE_RESTRICTED_BACKUP_MODE, /* value= */ true, + TEST_PACKAGE, /* className= */ null)); + + mConnectionManager.bindToAgentSynchronous(mTestApplicationInfo, + ApplicationThreadConstants.BACKUP_MODE_FULL, BackupDestination.CLOUD); + + verify(mActivityManager).bindBackupAgent(eq(TEST_PACKAGE), anyInt(), anyInt(), anyInt(), + /* useRestrictedMode= */ eq(true)); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_RESTRICTED_MODE_CHANGES) + public void bindToAgentSynchronous_packageOptedOut_shouldNotUseRestrictedMode() + throws Exception { + reset(mPackageManager); + when(mPackageManager.getPropertyAsUser( + eq(PackageManager.PROPERTY_USE_RESTRICTED_BACKUP_MODE), eq(TEST_PACKAGE), any(), + anyInt())).thenReturn(new PackageManager.Property( + PackageManager.PROPERTY_USE_RESTRICTED_BACKUP_MODE, /* value= */ false, + TEST_PACKAGE, /* className= */ null)); + + mConnectionManager.bindToAgentSynchronous(mTestApplicationInfo, + ApplicationThreadConstants.BACKUP_MODE_FULL, BackupDestination.CLOUD); + + verify(mActivityManager).bindBackupAgent(eq(TEST_PACKAGE), anyInt(), anyInt(), anyInt(), + /* useRestrictedMode= */ eq(false)); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_RESTRICTED_MODE_CHANGES) + @DisableCompatChanges({BackupAgentConnectionManager.OS_DECIDES_BACKUP_RESTRICTED_MODE}) + public void bindToAgentSynchronous_targetSdkBelowB_shouldUseRestrictedMode() throws Exception { + reset(mPackageManager); + // Mock that the app has not explicitly set the property. + when(mPackageManager.getPropertyAsUser( + eq(PackageManager.PROPERTY_USE_RESTRICTED_BACKUP_MODE), eq(TEST_PACKAGE), any(), + anyInt())).thenThrow(new PackageManager.NameNotFoundException()); + + mConnectionManager.bindToAgentSynchronous(mTestApplicationInfo, + ApplicationThreadConstants.BACKUP_MODE_FULL, BackupDestination.CLOUD); + + verify(mActivityManager).bindBackupAgent(eq(TEST_PACKAGE), anyInt(), anyInt(), anyInt(), + /* useRestrictedMode= */ eq(true)); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_RESTRICTED_MODE_CHANGES) + @EnableCompatChanges({BackupAgentConnectionManager.OS_DECIDES_BACKUP_RESTRICTED_MODE}) + public void bindToAgentSynchronous_targetSdkB_notInList_shouldUseRestrictedMode() + throws Exception { + reset(mPackageManager); + // Mock that the app has not explicitly set the property. + when(mPackageManager.getPropertyAsUser( + eq(PackageManager.PROPERTY_USE_RESTRICTED_BACKUP_MODE), eq(TEST_PACKAGE), any(), + anyInt())).thenThrow(new PackageManager.NameNotFoundException()); + mConnectionManager.clearNoRestrictedModePackages(); + + mConnectionManager.bindToAgentSynchronous(mTestApplicationInfo, + ApplicationThreadConstants.BACKUP_MODE_FULL, BackupDestination.CLOUD); + + verify(mActivityManager).bindBackupAgent(eq(TEST_PACKAGE), anyInt(), anyInt(), anyInt(), + /* useRestrictedMode= */ eq(true)); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_RESTRICTED_MODE_CHANGES) + @EnableCompatChanges({BackupAgentConnectionManager.OS_DECIDES_BACKUP_RESTRICTED_MODE}) + public void bindToAgentSynchronous_forRestore_targetSdkB_inList_shouldNotUseRestrictedMode() + throws Exception { + reset(mPackageManager); + // Mock that the app has not explicitly set the property. + when(mPackageManager.getPropertyAsUser( + eq(PackageManager.PROPERTY_USE_RESTRICTED_BACKUP_MODE), eq(TEST_PACKAGE), any(), + anyInt())).thenThrow(new PackageManager.NameNotFoundException()); + mConnectionManager.setNoRestrictedModePackages(Set.of(TEST_PACKAGE), OperationType.RESTORE); + + mConnectionManager.bindToAgentSynchronous(mTestApplicationInfo, + ApplicationThreadConstants.BACKUP_MODE_RESTORE_FULL, BackupDestination.CLOUD); + + verify(mActivityManager).bindBackupAgent(eq(TEST_PACKAGE), anyInt(), anyInt(), anyInt(), + /* useRestrictedMode= */ eq(false)); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_RESTRICTED_MODE_CHANGES) + @EnableCompatChanges({BackupAgentConnectionManager.OS_DECIDES_BACKUP_RESTRICTED_MODE}) + public void bindToAgentSynchronous_forBackup_targetSdkB_inList_shouldNotUseRestrictedMode() + throws Exception { + reset(mPackageManager); + // Mock that the app has not explicitly set the property. + when(mPackageManager.getPropertyAsUser( + eq(PackageManager.PROPERTY_USE_RESTRICTED_BACKUP_MODE), eq(TEST_PACKAGE), any(), + anyInt())).thenThrow(new PackageManager.NameNotFoundException()); + mConnectionManager.setNoRestrictedModePackages(Set.of(TEST_PACKAGE), OperationType.BACKUP); + + mConnectionManager.bindToAgentSynchronous(mTestApplicationInfo, + ApplicationThreadConstants.BACKUP_MODE_FULL, BackupDestination.CLOUD); + + verify(mActivityManager).bindBackupAgent(eq(TEST_PACKAGE), anyInt(), anyInt(), anyInt(), + /* useRestrictedMode= */ eq(false)); + } + + @Test + public void agentDisconnected_cancelsCurrentOperations() throws Exception { + when(mOperationStorage.operationTokensForPackage(eq(TEST_PACKAGE))).thenReturn( + ImmutableSet.of(123, 456, 789)); + when(mConnectionManager.getThreadForCancellation(any())).thenAnswer(invocation -> { + Thread testThread = new Thread((Runnable) invocation.getArgument(0), + "agent-disconnected-test"); + setTestThread(testThread); + return testThread; + }); + + mConnectionManager.agentDisconnected(TEST_PACKAGE); + + mTestThread.join(); + verify(mUserBackupManagerService).handleCancel(eq(123), eq(true)); + verify(mUserBackupManagerService).handleCancel(eq(456), eq(true)); + verify(mUserBackupManagerService).handleCancel(eq(789), eq(true)); + } + + @Test + public void unbindAgent_callsAmUnbindBackupAgent() throws Exception { + mConnectionManager.unbindAgent(mTestApplicationInfo); + + verify(mActivityManager).unbindBackupAgent(eq(mTestApplicationInfo)); + } + + // Needed because variables can't be assigned directly inside lambdas in Java. + private void setBackupAgentResult(IBackupAgent result) { + mBackupAgentResult = result; + } + + // Needed because variables can't be assigned directly inside lambdas in Java. + private void setTestThread(Thread thread) { + mTestThread = thread; + } +} diff --git a/services/tests/mockingservicestests/src/com/android/server/backup/UserBackupManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/backup/UserBackupManagerServiceTest.java index 07f2188d30eb..7a9e96f1bc4c 100644 --- a/services/tests/mockingservicestests/src/com/android/server/backup/UserBackupManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/backup/UserBackupManagerServiceTest.java @@ -18,20 +18,18 @@ package com.android.server.backup; 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.junit.Assert.fail; + 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.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.when; import android.annotation.UserIdInt; import android.app.ActivityManagerInternal; -import android.app.ApplicationThreadConstants; import android.app.IActivityManager; import android.app.backup.BackupAgent; import android.app.backup.BackupAnnotations.BackupDestination; @@ -47,8 +45,6 @@ import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.os.Handler; -import android.platform.test.annotations.DisableFlags; -import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import android.platform.test.flag.junit.SetFlagsRule; import android.provider.Settings; @@ -57,7 +53,6 @@ import android.util.FeatureFlagUtils; import android.util.KeyValueListParser; import androidx.test.core.app.ApplicationProvider; -import androidx.test.filters.FlakyTest; import androidx.test.runner.AndroidJUnit4; import com.android.server.backup.internal.BackupHandler; @@ -69,8 +64,6 @@ import com.android.server.backup.transport.TransportConnection; import com.android.server.backup.utils.BackupEligibilityRules; import com.android.server.backup.utils.BackupManagerMonitorEventSender; -import com.google.common.collect.ImmutableSet; - import org.junit.After; import org.junit.Before; import org.junit.Rule; @@ -84,11 +77,6 @@ import org.mockito.quality.Strictness; import java.util.Arrays; import java.util.List; -import java.util.Set; -import java.util.function.IntConsumer; - -import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges; -import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges; @Presubmit @RunWith(AndroidJUnit4.class) @@ -96,7 +84,6 @@ public class UserBackupManagerServiceTest { private static final String TEST_PACKAGE = "package1"; private static final String[] TEST_PACKAGES = new String[] { TEST_PACKAGE }; private static final String TEST_TRANSPORT = "transport"; - private static final int WORKER_THREAD_TIMEOUT_MILLISECONDS = 100; @UserIdInt private static final int USER_ID = 0; @Rule @@ -278,33 +265,6 @@ public class UserBackupManagerServiceTest { } @Test - @FlakyTest - public void testAgentDisconnected_cancelsCurrentOperations() throws Exception { - when(mOperationStorage.operationTokensForPackage(eq("com.android.foo"))).thenReturn( - ImmutableSet.of(123, 456, 789) - ); - - mService.agentDisconnected("com.android.foo"); - - mService.waitForAsyncOperation(); - verify(mOperationStorage).cancelOperation(eq(123), eq(true), any(IntConsumer.class)); - verify(mOperationStorage).cancelOperation(eq(456), eq(true), any()); - verify(mOperationStorage).cancelOperation(eq(789), eq(true), any()); - } - - @Test - public void testAgentDisconnected_unknownPackageName_cancelsNothing() throws Exception { - when(mOperationStorage.operationTokensForPackage(eq("com.android.foo"))).thenReturn( - ImmutableSet.of() - ); - - mService.agentDisconnected("com.android.foo"); - - verify(mOperationStorage, never()) - .cancelOperation(anyInt(), anyBoolean(), any(IntConsumer.class)); - } - - @Test public void testReportDelayedRestoreResult_sendsLogsToMonitor() throws Exception { PackageInfo packageInfo = getPackageInfo(TEST_PACKAGE); when(mPackageManager.getPackageInfoAsUser(anyString(), @@ -324,158 +284,6 @@ public class UserBackupManagerServiceTest { eq(packageInfo), eq(results), eq(OperationType.RESTORE)); } - @Test - @DisableFlags(Flags.FLAG_ENABLE_RESTRICTED_MODE_CHANGES) - public void bindToAgentSynchronous_restrictedModeChangesFlagOff_shouldUseRestrictedMode() - throws Exception { - mService.bindToAgentSynchronous(mTestPackageApplicationInfo, - ApplicationThreadConstants.BACKUP_MODE_FULL, BackupDestination.CLOUD); - - verify(mActivityManager).bindBackupAgent(eq(TEST_PACKAGE), anyInt(), anyInt(), anyInt(), - /* useRestrictedMode= */ eq(true)); - // Make sure we never hit the code that checks the property. - verify(mPackageManager, never()).getPropertyAsUser(any(), any(), any(), anyInt()); - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_RESTRICTED_MODE_CHANGES) - public void bindToAgentSynchronous_keyValueBackup_shouldNotUseRestrictedMode() - throws Exception { - mService.bindToAgentSynchronous(mTestPackageApplicationInfo, - ApplicationThreadConstants.BACKUP_MODE_INCREMENTAL, BackupDestination.CLOUD); - - verify(mActivityManager).bindBackupAgent(eq(TEST_PACKAGE), anyInt(), anyInt(), anyInt(), - /* useRestrictedMode= */ eq(false)); - // Make sure we never hit the code that checks the property. - verify(mPackageManager, never()).getPropertyAsUser(any(), any(), any(), anyInt()); - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_RESTRICTED_MODE_CHANGES) - public void bindToAgentSynchronous_keyValueRestore_shouldNotUseRestrictedMode() - throws Exception { - mService.bindToAgentSynchronous(mTestPackageApplicationInfo, - ApplicationThreadConstants.BACKUP_MODE_RESTORE, BackupDestination.CLOUD); - - verify(mActivityManager).bindBackupAgent(eq(TEST_PACKAGE), anyInt(), anyInt(), anyInt(), - /* useRestrictedMode= */ eq(false)); - // Make sure we never hit the code that checks the property. - verify(mPackageManager, never()).getPropertyAsUser(any(), any(), any(), anyInt()); - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_RESTRICTED_MODE_CHANGES) - public void bindToAgentSynchronous_packageOptedIn_shouldUseRestrictedMode() - throws Exception { - when(mPackageManager.getPropertyAsUser( - eq(PackageManager.PROPERTY_USE_RESTRICTED_BACKUP_MODE), - eq(TEST_PACKAGE), any(), anyInt())).thenReturn(new PackageManager.Property( - PackageManager.PROPERTY_USE_RESTRICTED_BACKUP_MODE, /* value= */ true, - TEST_PACKAGE, /* className= */ null)); - - mService.bindToAgentSynchronous(mTestPackageApplicationInfo, - ApplicationThreadConstants.BACKUP_MODE_FULL, BackupDestination.CLOUD); - - verify(mActivityManager).bindBackupAgent(eq(TEST_PACKAGE), anyInt(), anyInt(), anyInt(), - /* useRestrictedMode= */ eq(true)); - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_RESTRICTED_MODE_CHANGES) - public void bindToAgentSynchronous_packageOptedOut_shouldNotUseRestrictedMode() - throws Exception { - when(mPackageManager.getPropertyAsUser( - eq(PackageManager.PROPERTY_USE_RESTRICTED_BACKUP_MODE), - eq(TEST_PACKAGE), any(), anyInt())).thenReturn(new PackageManager.Property( - PackageManager.PROPERTY_USE_RESTRICTED_BACKUP_MODE, /* value= */ false, - TEST_PACKAGE, /* className= */ null)); - - mService.bindToAgentSynchronous(mTestPackageApplicationInfo, - ApplicationThreadConstants.BACKUP_MODE_FULL, BackupDestination.CLOUD); - - verify(mActivityManager).bindBackupAgent(eq(TEST_PACKAGE), anyInt(), anyInt(), anyInt(), - /* useRestrictedMode= */ eq(false)); - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_RESTRICTED_MODE_CHANGES) - @DisableCompatChanges({UserBackupManagerService.OS_DECIDES_BACKUP_RESTRICTED_MODE}) - public void bindToAgentSynchronous_targetSdkBelowB_shouldUseRestrictedMode() - throws Exception { - // Mock that the app has not explicitly set the property. - when(mPackageManager.getPropertyAsUser( - eq(PackageManager.PROPERTY_USE_RESTRICTED_BACKUP_MODE), - eq(TEST_PACKAGE), any(), anyInt())).thenThrow( - new PackageManager.NameNotFoundException() - ); - - mService.bindToAgentSynchronous(mTestPackageApplicationInfo, - ApplicationThreadConstants.BACKUP_MODE_FULL, BackupDestination.CLOUD); - - verify(mActivityManager).bindBackupAgent(eq(TEST_PACKAGE), anyInt(), anyInt(), anyInt(), - /* useRestrictedMode= */ eq(true)); - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_RESTRICTED_MODE_CHANGES) - @EnableCompatChanges({UserBackupManagerService.OS_DECIDES_BACKUP_RESTRICTED_MODE}) - public void bindToAgentSynchronous_targetSdkB_notInList_shouldUseRestrictedMode() - throws Exception { - // Mock that the app has not explicitly set the property. - when(mPackageManager.getPropertyAsUser( - eq(PackageManager.PROPERTY_USE_RESTRICTED_BACKUP_MODE), - eq(TEST_PACKAGE), any(), anyInt())).thenThrow( - new PackageManager.NameNotFoundException() - ); - mService.clearNoRestrictedModePackages(); - - mService.bindToAgentSynchronous(mTestPackageApplicationInfo, - ApplicationThreadConstants.BACKUP_MODE_FULL, BackupDestination.CLOUD); - - verify(mActivityManager).bindBackupAgent(eq(TEST_PACKAGE), anyInt(), anyInt(), anyInt(), - /* useRestrictedMode= */ eq(true)); - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_RESTRICTED_MODE_CHANGES) - @EnableCompatChanges({UserBackupManagerService.OS_DECIDES_BACKUP_RESTRICTED_MODE}) - public void bindToAgentSynchronous_forRestore_targetSdkB_inList_shouldNotUseRestrictedMode() - throws Exception { - // Mock that the app has not explicitly set the property. - when(mPackageManager.getPropertyAsUser( - eq(PackageManager.PROPERTY_USE_RESTRICTED_BACKUP_MODE), - eq(TEST_PACKAGE), any(), anyInt())).thenThrow( - new PackageManager.NameNotFoundException() - ); - mService.setNoRestrictedModePackages(Set.of(TEST_PACKAGE), OperationType.RESTORE); - - mService.bindToAgentSynchronous(mTestPackageApplicationInfo, - ApplicationThreadConstants.BACKUP_MODE_RESTORE_FULL, BackupDestination.CLOUD); - - verify(mActivityManager).bindBackupAgent(eq(TEST_PACKAGE), anyInt(), anyInt(), anyInt(), - /* useRestrictedMode= */ eq(false)); - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_RESTRICTED_MODE_CHANGES) - @EnableCompatChanges({UserBackupManagerService.OS_DECIDES_BACKUP_RESTRICTED_MODE}) - public void bindToAgentSynchronous_forBackup_targetSdkB_inList_shouldNotUseRestrictedMode() - throws Exception { - // Mock that the app has not explicitly set the property. - when(mPackageManager.getPropertyAsUser( - eq(PackageManager.PROPERTY_USE_RESTRICTED_BACKUP_MODE), - eq(TEST_PACKAGE), any(), anyInt())).thenThrow( - new PackageManager.NameNotFoundException() - ); - mService.setNoRestrictedModePackages(Set.of(TEST_PACKAGE), OperationType.BACKUP); - - mService.bindToAgentSynchronous(mTestPackageApplicationInfo, - ApplicationThreadConstants.BACKUP_MODE_FULL, BackupDestination.CLOUD); - - verify(mActivityManager).bindBackupAgent(eq(TEST_PACKAGE), anyInt(), anyInt(), anyInt(), - /* useRestrictedMode= */ eq(false)); - } - private static PackageInfo getPackageInfo(String packageName) { PackageInfo packageInfo = new PackageInfo(); packageInfo.applicationInfo = new ApplicationInfo(); @@ -487,8 +295,6 @@ public class UserBackupManagerServiceTest { boolean isEnabledStatePersisted = false; boolean shouldUseNewBackupEligibilityRules = false; - private volatile Thread mWorkerThread = null; - TestBackupService() { super(mContext, mPackageManager, mOperationStorage, mTransportManager, mBackupHandler, createConstants(mContext), mActivityManager, mActivityManagerInternal); @@ -523,26 +329,8 @@ public class UserBackupManagerServiceTest { } @Override - Thread getThreadForAsyncOperation(String operationName, Runnable operation) { - mWorkerThread = super.getThreadForAsyncOperation(operationName, operation); - return mWorkerThread; - } - - @Override BackupManagerMonitorEventSender getBMMEventSender(IBackupManagerMonitor monitor) { return mBackupManagerMonitorEventSender; } - - private void waitForAsyncOperation() { - if (mWorkerThread == null) { - return; - } - - try { - mWorkerThread.join(/* millis */ WORKER_THREAD_TIMEOUT_MILLISECONDS); - } catch (InterruptedException e) { - fail("Failed waiting for worker thread to complete: " + e.getMessage()); - } - } } } diff --git a/services/tests/mockingservicestests/src/com/android/server/backup/fullbackup/PerformFullTransportBackupTaskTest.java b/services/tests/mockingservicestests/src/com/android/server/backup/fullbackup/PerformFullTransportBackupTaskTest.java index 331057398949..e618433862f2 100644 --- a/services/tests/mockingservicestests/src/com/android/server/backup/fullbackup/PerformFullTransportBackupTaskTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/backup/fullbackup/PerformFullTransportBackupTaskTest.java @@ -33,6 +33,7 @@ import android.platform.test.annotations.Presubmit; import androidx.test.runner.AndroidJUnit4; +import com.android.server.backup.BackupAgentConnectionManager; import com.android.server.backup.BackupAgentTimeoutParameters; import com.android.server.backup.OperationStorage; import com.android.server.backup.TransportManager; @@ -66,6 +67,8 @@ public class PerformFullTransportBackupTaskTest { @Mock UserBackupManagerService mBackupManagerService; @Mock + BackupAgentConnectionManager mBackupAgentConnectionManager; + @Mock BackupTransportClient mBackupTransportClient; @Mock CountDownLatch mLatch; @@ -95,6 +98,8 @@ public class PerformFullTransportBackupTaskTest { when(mBackupManagerService.isSetupComplete()).thenReturn(true); when(mBackupManagerService.getAgentTimeoutParameters()).thenReturn( mBackupAgentTimeoutParameters); + when(mBackupManagerService.getBackupAgentConnectionManager()).thenReturn( + mBackupAgentConnectionManager); when(mBackupManagerService.getTransportManager()).thenReturn(mTransportManager); when(mTransportManager.getCurrentTransportClient(any())).thenReturn(mTransportConnection); when(mTransportConnection.connectOrThrow(any())).thenReturn(mBackupTransportClient); @@ -142,11 +147,11 @@ public class PerformFullTransportBackupTaskTest { mTask.run(); - InOrder inOrder = inOrder(mBackupManagerService); - inOrder.verify(mBackupManagerService).setNoRestrictedModePackages( + InOrder inOrder = inOrder(mBackupAgentConnectionManager); + inOrder.verify(mBackupAgentConnectionManager).setNoRestrictedModePackages( eq(Set.of("package1")), eq(BackupAnnotations.OperationType.BACKUP)); - inOrder.verify(mBackupManagerService).clearNoRestrictedModePackages(); + inOrder.verify(mBackupAgentConnectionManager).clearNoRestrictedModePackages(); } private void createTask(String[] packageNames) { diff --git a/services/tests/mockingservicestests/src/com/android/server/backup/restore/PerformUnifiedRestoreTaskTest.java b/services/tests/mockingservicestests/src/com/android/server/backup/restore/PerformUnifiedRestoreTaskTest.java index 055adf68ee0f..351aac357c44 100644 --- a/services/tests/mockingservicestests/src/com/android/server/backup/restore/PerformUnifiedRestoreTaskTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/backup/restore/PerformUnifiedRestoreTaskTest.java @@ -44,6 +44,7 @@ import androidx.test.InstrumentationRegistry; import androidx.test.runner.AndroidJUnit4; import com.android.modules.utils.testing.TestableDeviceConfig; +import com.android.server.backup.BackupAgentConnectionManager; import com.android.server.backup.Flags; import com.android.server.backup.UserBackupManagerService; import com.android.server.backup.internal.BackupHandler; @@ -95,6 +96,8 @@ public class PerformUnifiedRestoreTaskTest { private TransportConnection mTransportConnection; @Mock private BackupTransportClient mBackupTransportClient; + @Mock + private BackupAgentConnectionManager mBackupAgentConnectionManager; private Set<String> mExcludedkeys = new HashSet<>(); private Map<String, String> mBackupData = new HashMap<>(); @@ -122,6 +125,9 @@ public class PerformUnifiedRestoreTaskTest { mContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + when(mBackupManagerService.getBackupAgentConnectionManager()).thenReturn( + mBackupAgentConnectionManager); + mBackupDataSource = new ArrayDeque<>(mBackupData.keySet()); when(mBackupDataInput.readNextHeader()) .then((Answer<Boolean>) invocation -> !mBackupDataSource.isEmpty()); @@ -166,7 +172,7 @@ public class PerformUnifiedRestoreTaskTest { mRestoreTask.setNoRestrictedModePackages(mBackupTransportClient, new PackageInfo[]{packageInfo1, packageInfo2}); - verify(mBackupManagerService).setNoRestrictedModePackages( + verify(mBackupAgentConnectionManager).setNoRestrictedModePackages( eq(Set.of("package1")), eq(BackupAnnotations.OperationType.RESTORE)); } diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java index d66bb00ae879..c6870adb8464 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java @@ -65,6 +65,7 @@ import android.app.job.JobInfo; import android.app.usage.UsageEvents; import android.app.usage.UsageStatsManager; import android.app.usage.UsageStatsManagerInternal; +import android.compat.testing.PlatformCompatChangeRule; import android.content.ComponentName; import android.content.Context; import android.content.pm.ApplicationInfo; @@ -103,10 +104,13 @@ import com.android.server.job.controllers.QuotaController.TimedEvent; import com.android.server.job.controllers.QuotaController.TimingSession; import com.android.server.usage.AppStandbyInternal; +import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges; + import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.junit.rules.TestRule; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatchers; @@ -135,6 +139,9 @@ public class QuotaControllerTest { private static final int SOURCE_USER_ID = 0; @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + + @Rule + public TestRule compatChangeRule = new PlatformCompatChangeRule(); private QuotaController mQuotaController; private QuotaController.QcConstants mQcConstants; private JobSchedulerService.Constants mConstants = new JobSchedulerService.Constants(); @@ -303,7 +310,7 @@ public class QuotaControllerTest { private int getProcessStateQuotaFreeThreshold() { synchronized (mQuotaController.mLock) { - return mQuotaController.getProcessStateQuotaFreeThreshold(); + return mQuotaController.getProcessStateQuotaFreeThreshold(mSourceUid); } } @@ -5197,6 +5204,101 @@ public class QuotaControllerTest { assertFalse(jobBg2.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)); } + @Test + @EnableCompatChanges({QuotaController.OVERRIDE_QUOTA_ENFORCEMENT_TO_TOP_STARTED_JOBS, + QuotaController.OVERRIDE_QUOTA_ENFORCEMENT_TO_FGS_JOBS}) + @RequiresFlagsEnabled({Flags.FLAG_ENFORCE_QUOTA_POLICY_TO_TOP_STARTED_JOBS, + Flags.FLAG_ENFORCE_QUOTA_POLICY_TO_FGS_JOBS}) + public void testTracking_OutOfQuota_ForegroundAndBackground_CompactChangeOverrides() { + setDischarging(); + + JobStatus jobBg = createJobStatus("testTracking_OutOfQuota_ForegroundAndBackground", 1); + JobStatus jobTop = createJobStatus("testTracking_OutOfQuota_ForegroundAndBackground", 2); + trackJobs(jobBg, jobTop); + setStandbyBucket(WORKING_INDEX, jobTop, jobBg); // 2 hour window + // Now the package only has 20 seconds to run. + final long remainingTimeMs = 20 * SECOND_IN_MILLIS; + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, + createTimingSession( + JobSchedulerService.sElapsedRealtimeClock.millis() - HOUR_IN_MILLIS, + 10 * MINUTE_IN_MILLIS - remainingTimeMs, 1), false); + + InOrder inOrder = inOrder(mJobSchedulerService); + + // UID starts out inactive. + setProcessState(ActivityManager.PROCESS_STATE_SERVICE); + // Start the job. + synchronized (mQuotaController.mLock) { + mQuotaController.prepareForExecutionLocked(jobBg); + } + advanceElapsedClock(remainingTimeMs / 2); + // New job starts after UID is in the foreground. Since the app is now in the foreground, it + // should continue to have remainingTimeMs / 2 time remaining. + setProcessState(ActivityManager.PROCESS_STATE_TOP); + synchronized (mQuotaController.mLock) { + mQuotaController.prepareForExecutionLocked(jobTop); + } + advanceElapsedClock(remainingTimeMs); + + // Wait for some extra time to allow for job processing. + inOrder.verify(mJobSchedulerService, + timeout(remainingTimeMs + 2 * SECOND_IN_MILLIS).times(0)) + .onControllerStateChanged(argThat(jobs -> jobs.size() > 0)); + synchronized (mQuotaController.mLock) { + assertEquals(remainingTimeMs / 2, + mQuotaController.getRemainingExecutionTimeLocked(jobBg)); + assertEquals(remainingTimeMs / 2, + mQuotaController.getRemainingExecutionTimeLocked(jobTop)); + } + // Go to a background state. + setProcessState(ActivityManager.PROCESS_STATE_TOP_SLEEPING); + advanceElapsedClock(remainingTimeMs / 2 + 1); + // Only Bg job will be changed from in-quota to out-of-quota. + inOrder.verify(mJobSchedulerService, + timeout(remainingTimeMs / 2 + 2 * SECOND_IN_MILLIS).times(1)) + .onControllerStateChanged(argThat(jobs -> jobs.size() == 1)); + // Top job should still be allowed to run. + assertFalse(jobBg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)); + assertTrue(jobTop.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)); + + // New jobs to run. + JobStatus jobBg2 = createJobStatus("testTracking_OutOfQuota_ForegroundAndBackground", 3); + JobStatus jobTop2 = createJobStatus("testTracking_OutOfQuota_ForegroundAndBackground", 4); + JobStatus jobFg = createJobStatus("testTracking_OutOfQuota_ForegroundAndBackground", 5); + setStandbyBucket(WORKING_INDEX, jobBg2, jobTop2, jobFg); + + advanceElapsedClock(20 * SECOND_IN_MILLIS); + setProcessState(ActivityManager.PROCESS_STATE_TOP); + inOrder.verify(mJobSchedulerService, timeout(SECOND_IN_MILLIS).times(1)) + .onControllerStateChanged(argThat(jobs -> jobs.size() == 1)); + trackJobs(jobFg, jobTop); + synchronized (mQuotaController.mLock) { + mQuotaController.prepareForExecutionLocked(jobTop); + } + assertTrue(jobTop.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)); + assertTrue(jobFg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)); + assertTrue(jobBg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)); + + // App still in foreground so everything should be in quota. + advanceElapsedClock(20 * SECOND_IN_MILLIS); + setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE); + assertTrue(jobTop.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)); + assertTrue(jobFg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)); + assertTrue(jobBg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)); + + advanceElapsedClock(20 * SECOND_IN_MILLIS); + setProcessState(ActivityManager.PROCESS_STATE_SERVICE); + inOrder.verify(mJobSchedulerService, timeout(SECOND_IN_MILLIS).times(1)) + .onControllerStateChanged(argThat(jobs -> jobs.size() == 2)); + // App is now in background and out of quota. Fg should now change to out of quota since it + // wasn't started. Top should remain in quota since it started when the app was in TOP. + assertTrue(jobTop.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)); + assertFalse(jobFg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)); + assertFalse(jobBg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)); + trackJobs(jobBg2); + assertFalse(jobBg2.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA)); + } + /** * Tests that TOP jobs are stopped when an app runs out of quota. */ diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/WakelockPowerStatsCollectorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/WakelockPowerStatsCollectorTest.java index 0d5d277b00ef..ed927c6ab699 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/WakelockPowerStatsCollectorTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/WakelockPowerStatsCollectorTest.java @@ -26,8 +26,7 @@ import android.content.Context; import android.os.Process; import android.platform.test.annotations.DisableFlags; import android.platform.test.flag.junit.SetFlagsRule; -import android.platform.test.ravenwood.RavenwoodConfig; -import android.platform.test.ravenwood.RavenwoodConfig.Config; +import android.platform.test.ravenwood.RavenwoodRule; import com.android.internal.os.PowerStats; import com.android.server.power.feature.flags.Flags; @@ -39,10 +38,9 @@ import org.junit.Test; public class WakelockPowerStatsCollectorTest { - @Config - public static final RavenwoodConfig sConfig = - new RavenwoodConfig.Builder() - .setProvideMainThread(true) + @Rule + public final RavenwoodRule mRule = + new RavenwoodRule.Builder() .setSystemPropertyImmutable( "persist.sys.com.android.server.power.feature.flags." + "framework_wakelock_info-override", diff --git a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java index 510c2bcabad0..b2a7d20fb948 100644 --- a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java @@ -664,7 +664,7 @@ public class MediaProjectionManagerServiceTest { mClockInjector); MediaProjectionManagerService.MediaProjection projection = createProjectionPreconditions( service); - mClock.fastForward(projection.mDefaultTimeoutMs + 10); + mClock.fastForward(projection.mDefaultTimeoutMillis + 10); // Immediate timeout - so no longer valid. assertThat(projection.isValid()).isFalse(); diff --git a/services/tests/servicestests/src/com/android/server/security/authenticationpolicy/AuthenticationPolicyServiceTest.java b/services/tests/servicestests/src/com/android/server/security/authenticationpolicy/AuthenticationPolicyServiceTest.java index 2238a1be97a1..ee8eb9b35088 100644 --- a/services/tests/servicestests/src/com/android/server/security/authenticationpolicy/AuthenticationPolicyServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/security/authenticationpolicy/AuthenticationPolicyServiceTest.java @@ -19,12 +19,14 @@ package com.android.server.security.authenticationpolicy; import static android.adaptiveauth.Flags.FLAG_ENABLE_ADAPTIVE_AUTH; import static android.adaptiveauth.Flags.FLAG_REPORT_BIOMETRIC_AUTH_ATTEMPTS; import static android.security.Flags.FLAG_REPORT_PRIMARY_AUTH_ATTEMPTS; +import static android.security.authenticationpolicy.AuthenticationPolicyManager.ERROR_UNSUPPORTED; import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_ADAPTIVE_AUTH_REQUEST; import static com.android.server.security.authenticationpolicy.AuthenticationPolicyService.MAX_ALLOWED_FAILED_AUTH_ATTEMPTS; import static org.junit.Assert.assertEquals; import static org.junit.Assume.assumeTrue; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; @@ -95,6 +97,8 @@ public class AuthenticationPolicyServiceTest { private WindowManagerInternal mWindowManager; @Mock private UserManagerInternal mUserManager; + @Mock + private SecureLockDeviceServiceInternal mSecureLockDeviceService; @Captor ArgumentCaptor<LockSettingsStateListener> mLockSettingsStateListenerCaptor; @@ -123,6 +127,11 @@ public class AuthenticationPolicyServiceTest { LocalServices.addService(WindowManagerInternal.class, mWindowManager); LocalServices.removeServiceForTest(UserManagerInternal.class); LocalServices.addService(UserManagerInternal.class, mUserManager); + if (android.security.Flags.secureLockdown()) { + LocalServices.removeServiceForTest(SecureLockDeviceServiceInternal.class); + LocalServices.addService(SecureLockDeviceServiceInternal.class, + mSecureLockDeviceService); + } mAuthenticationPolicyService = new AuthenticationPolicyService( mContext, mLockPatternUtils); @@ -136,6 +145,12 @@ public class AuthenticationPolicyServiceTest { // Set PRIMARY_USER_ID as the parent of MANAGED_PROFILE_USER_ID when(mUserManager.getProfileParentId(eq(MANAGED_PROFILE_USER_ID))) .thenReturn(PRIMARY_USER_ID); + if (android.security.Flags.secureLockdown()) { + when(mSecureLockDeviceService.enableSecureLockDevice(any())) + .thenReturn(ERROR_UNSUPPORTED); + when(mSecureLockDeviceService.disableSecureLockDevice(any())) + .thenReturn(ERROR_UNSUPPORTED); + } } @After @@ -143,6 +158,9 @@ public class AuthenticationPolicyServiceTest { LocalServices.removeServiceForTest(LockSettingsInternal.class); LocalServices.removeServiceForTest(WindowManagerInternal.class); LocalServices.removeServiceForTest(UserManagerInternal.class); + if (android.security.Flags.secureLockdown()) { + LocalServices.removeServiceForTest(SecureLockDeviceServiceInternal.class); + } } @Test diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index 704c1b858b8d..e6b4bc98ea4c 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -17178,8 +17178,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test @EnableFlags(android.app.Flags.FLAG_API_RICH_ONGOING) public void testSetCanBePromoted_granted() throws Exception { - mContext.getTestablePermissions().setPermission( - android.Manifest.permission.USE_COLORIZED_NOTIFICATIONS, PERMISSION_GRANTED); // qualifying posted notification Notification n = new Notification.Builder(mContext, mTestNotificationChannel.getId()) .setSmallIcon(android.R.drawable.sym_def_app_icon) @@ -17254,8 +17252,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test @EnableFlags(android.app.Flags.FLAG_API_RICH_ONGOING) public void testSetCanBePromoted_granted_onlyNotifiesOnce() throws Exception { - mContext.getTestablePermissions().setPermission( - android.Manifest.permission.USE_COLORIZED_NOTIFICATIONS, PERMISSION_GRANTED); // qualifying posted notification Notification n = new Notification.Builder(mContext, mTestNotificationChannel.getId()) .setSmallIcon(android.R.drawable.sym_def_app_icon) @@ -17285,8 +17281,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test @EnableFlags(android.app.Flags.FLAG_API_RICH_ONGOING) public void testSetCanBePromoted_revoked() throws Exception { - mContext.getTestablePermissions().setPermission( - android.Manifest.permission.USE_COLORIZED_NOTIFICATIONS, PERMISSION_GRANTED); // start from true state mBinderService.setCanBePromoted(mPkg, mUid, true, true); @@ -17350,8 +17344,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test @EnableFlags(android.app.Flags.FLAG_API_RICH_ONGOING) public void testSetCanBePromoted_revoked_onlyNotifiesOnce() throws Exception { - mContext.getTestablePermissions().setPermission( - android.Manifest.permission.USE_COLORIZED_NOTIFICATIONS, PERMISSION_GRANTED); // start from true state mBinderService.setCanBePromoted(mPkg, mUid, true, true); @@ -17387,8 +17379,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { public void testPostPromotableNotification() throws Exception { mBinderService.setCanBePromoted(mPkg, mUid, true, true); assertThat(mBinderService.appCanBePromoted(mPkg, mUid)).isTrue(); - mContext.getTestablePermissions().setPermission( - android.Manifest.permission.USE_COLORIZED_NOTIFICATIONS, PERMISSION_GRANTED); Notification n = new Notification.Builder(mContext, mTestNotificationChannel.getId()) .setSmallIcon(android.R.drawable.sym_def_app_icon) @@ -17415,8 +17405,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test @EnableFlags(android.app.Flags.FLAG_API_RICH_ONGOING) public void testPostPromotableNotification_noPermission() throws Exception { - mContext.getTestablePermissions().setPermission( - android.Manifest.permission.USE_COLORIZED_NOTIFICATIONS, PERMISSION_GRANTED); Notification n = new Notification.Builder(mContext, mTestNotificationChannel.getId()) .setSmallIcon(android.R.drawable.sym_def_app_icon) .setStyle(new Notification.BigTextStyle().setBigContentTitle("BIG")) @@ -17444,8 +17432,6 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @EnableFlags(android.app.Flags.FLAG_API_RICH_ONGOING) public void testPostPromotableNotification_unimportantNotification() throws Exception { mBinderService.setCanBePromoted(mPkg, mUid, true, true); - mContext.getTestablePermissions().setPermission( - android.Manifest.permission.USE_COLORIZED_NOTIFICATIONS, PERMISSION_GRANTED); Notification n = new Notification.Builder(mContext, mMinChannel.getId()) .setSmallIcon(android.R.drawable.sym_def_app_icon) .setStyle(new Notification.BigTextStyle().setBigContentTitle("BIG")) diff --git a/services/tests/wmtests/AndroidManifest.xml b/services/tests/wmtests/AndroidManifest.xml index 6e6b70d319ab..5f2f3ed67432 100644 --- a/services/tests/wmtests/AndroidManifest.xml +++ b/services/tests/wmtests/AndroidManifest.xml @@ -17,10 +17,10 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.frameworks.wmtests"> - <!-- Uses API introduced in P (28) --> + <!-- Uses API introduced in S (31). Using SDK 31+ avoids Google Play Protect popups. --> <uses-sdk android:minSdkVersion="1" - android:targetSdkVersion="28" /> + android:targetSdkVersion="31" /> <uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" /> <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" /> diff --git a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java index 7f260f85a755..70f57eb40385 100644 --- a/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/ActivityTaskSupervisorTests.java @@ -357,6 +357,25 @@ public class ActivityTaskSupervisorTests extends WindowTestsBase { assertEquals(activity1.app, mAtm.mTopApp); } + @Test + public void testTopResumedActivity_deferResume() { + final ActivityRecord activity1 = new ActivityBuilder(mAtm).setCreateTask(true).build(); + final ActivityRecord activity2 = new ActivityBuilder(mAtm).setCreateTask(true).build(); + activity2.setState(ActivityRecord.State.RESUMED, "test"); + assertEquals(activity2.app, mAtm.mTopApp); + reset(activity2); + + // Verify that no top-resumed activity changes to the client while defer-resume enabled. + mSupervisor.beginDeferResume(); + activity1.getTask().moveToFront("test"); + activity1.setState(ActivityRecord.State.RESUMED, "test"); + verify(activity2, never()).scheduleTopResumedActivityChanged(eq(false)); + + // Verify that the change is scheduled to the client after defer-resumed disabled + mSupervisor.endDeferResume(); + verify(activity2).scheduleTopResumedActivityChanged(eq(false)); + } + /** * We need to launch home again after user unlocked for those displays that do not have * encryption aware home app. diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraOverridesTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraOverridesTest.java index b91a5b7afe26..d5ed048032e7 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraOverridesTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatCameraOverridesTest.java @@ -17,8 +17,8 @@ package com.android.server.wm; import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FORCE_ROTATION; -import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT; import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_REFRESH; +import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT; import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_ENABLE_REFRESH_VIA_PAUSE; import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA; import static android.content.pm.ActivityInfo.OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA; @@ -228,9 +228,8 @@ public class AppCompatCameraOverridesTest extends WindowTestsBase { } @Test - @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT}) @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) - public void testShouldApplyCameraCompatFreeformTreatment_overrideEnabled_returnsFalse() { + public void testShouldApplyCameraCompatFreeformTreatment_notEnabledByOverride_returnsFalse() { runTestScenario((robot) -> { robot.activity().createActivityWithComponentInNewTask(); @@ -239,19 +238,9 @@ public class AppCompatCameraOverridesTest extends WindowTestsBase { } @Test - @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT}) + @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) - public void testShouldApplyCameraCompatFreeformTreatment_disabledByOverride_returnsFalse() { - runTestScenario((robot) -> { - robot.activity().createActivityWithComponentInNewTask(); - - robot.checkShouldApplyFreeformTreatmentForCameraCompat(false); - }); - } - - @Test - @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) - public void testShouldApplyCameraCompatFreeformTreatment_notDisabledByOverride_returnsTrue() { + public void testShouldApplyCameraCompatFreeformTreatment_overrideAndFlagEnabled_returnsTrue() { runTestScenario((robot) -> { robot.activity().createActivityWithComponentInNewTask(); @@ -261,8 +250,9 @@ public class AppCompatCameraOverridesTest extends WindowTestsBase { @Test @EnableCompatChanges({OVERRIDE_ORIENTATION_ONLY_FOR_CAMERA, - OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA}) - public void testShouldRecomputeConfigurationForCameraCompat() { + OVERRIDE_MIN_ASPECT_RATIO_ONLY_FOR_CAMERA, + OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) + public void testShouldRecomputeConfigurationForFreeformTreatment() { runTestScenario((robot) -> { robot.conf().enableCameraCompatSplitScreenAspectRatio(true); robot.applyOnActivity((a) -> { diff --git a/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java index e447565a55bd..c427583d3001 100644 --- a/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/CameraCompatFreeformPolicyTests.java @@ -25,7 +25,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.servertransaction.ActivityLifecycleItem.ON_PAUSE; import static android.app.servertransaction.ActivityLifecycleItem.ON_STOP; -import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT; +import static android.content.pm.ActivityInfo.OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_FULL_USER; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; @@ -96,61 +96,55 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { private static final String TEST_PACKAGE_1 = "com.android.frameworks.wmtests"; private static final String TEST_PACKAGE_2 = "com.test.package.two"; private static final String CAMERA_ID_1 = "camera-1"; - private static final String CAMERA_ID_2 = "camera-2"; - private CameraManager mMockCameraManager; - private Handler mMockHandler; private AppCompatConfiguration mAppCompatConfiguration; private CameraManager.AvailabilityCallback mCameraAvailabilityCallback; private CameraCompatFreeformPolicy mCameraCompatFreeformPolicy; private ActivityRecord mActivity; - private Task mTask; private ActivityRefresher mActivityRefresher; @Before public void setUp() throws Exception { mAppCompatConfiguration = mDisplayContent.mWmService.mAppCompatConfiguration; spyOn(mAppCompatConfiguration); - when(mAppCompatConfiguration.isCameraCompatTreatmentEnabled()) - .thenReturn(true); - when(mAppCompatConfiguration.isCameraCompatRefreshEnabled()) - .thenReturn(true); + when(mAppCompatConfiguration.isCameraCompatTreatmentEnabled()).thenReturn(true); + when(mAppCompatConfiguration.isCameraCompatRefreshEnabled()).thenReturn(true); when(mAppCompatConfiguration.isCameraCompatRefreshCycleThroughStopEnabled()) .thenReturn(true); - mMockCameraManager = mock(CameraManager.class); + final CameraManager mockCameraManager = mock(CameraManager.class); doAnswer(invocation -> { mCameraAvailabilityCallback = invocation.getArgument(1); return null; - }).when(mMockCameraManager).registerAvailabilityCallback( + }).when(mockCameraManager).registerAvailabilityCallback( any(Executor.class), any(CameraManager.AvailabilityCallback.class)); - when(mContext.getSystemService(CameraManager.class)).thenReturn(mMockCameraManager); + when(mContext.getSystemService(CameraManager.class)).thenReturn(mockCameraManager); mDisplayContent.setIgnoreOrientationRequest(true); - mMockHandler = mock(Handler.class); + final Handler mockHandler = mock(Handler.class); - when(mMockHandler.postDelayed(any(Runnable.class), anyLong())).thenAnswer( + when(mockHandler.postDelayed(any(Runnable.class), anyLong())).thenAnswer( invocation -> { ((Runnable) invocation.getArgument(0)).run(); return null; }); - mActivityRefresher = new ActivityRefresher(mDisplayContent.mWmService, mMockHandler); - CameraStateMonitor cameraStateMonitor = - new CameraStateMonitor(mDisplayContent, mMockHandler); - mCameraCompatFreeformPolicy = - new CameraCompatFreeformPolicy(mDisplayContent, cameraStateMonitor, - mActivityRefresher); + mActivityRefresher = new ActivityRefresher(mDisplayContent.mWmService, mockHandler); + final CameraStateMonitor cameraStateMonitor = new CameraStateMonitor(mDisplayContent, + mockHandler); + mCameraCompatFreeformPolicy = new CameraCompatFreeformPolicy(mDisplayContent, + cameraStateMonitor, mActivityRefresher); setDisplayRotation(Surface.ROTATION_90); mCameraCompatFreeformPolicy.start(); cameraStateMonitor.startListeningToCameraState(); } - @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @Test + @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) + @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) public void testFullscreen_doesNotActivateCameraCompatMode() { configureActivity(SCREEN_ORIENTATION_PORTRAIT, WINDOWING_MODE_FULLSCREEN); doReturn(false).when(mActivity).inFreeformWindowingMode(); @@ -160,23 +154,26 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { assertNotInCameraCompatMode(); } - @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @Test + @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) + @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) public void testOrientationUnspecified_doesNotActivateCameraCompatMode() { configureActivity(SCREEN_ORIENTATION_UNSPECIFIED); assertNotInCameraCompatMode(); } - @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @Test + @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) + @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) public void testNoCameraConnection_doesNotActivateCameraCompatMode() { configureActivity(SCREEN_ORIENTATION_PORTRAIT); assertNotInCameraCompatMode(); } - @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @Test + @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) + @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) public void testCameraConnected_deviceInPortrait_portraitCameraCompatMode() throws Exception { configureActivity(SCREEN_ORIENTATION_PORTRAIT); setDisplayRotation(Surface.ROTATION_0); @@ -187,6 +184,8 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { } @Test + @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) + @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) public void testCameraConnected_deviceInLandscape_portraitCameraCompatMode() throws Exception { configureActivity(SCREEN_ORIENTATION_PORTRAIT); setDisplayRotation(Surface.ROTATION_270); @@ -197,6 +196,8 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { } @Test + @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) + @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) public void testCameraConnected_deviceInPortrait_landscapeCameraCompatMode() throws Exception { configureActivity(SCREEN_ORIENTATION_LANDSCAPE); setDisplayRotation(Surface.ROTATION_0); @@ -207,6 +208,8 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { } @Test + @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) + @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) public void testCameraConnected_deviceInLandscape_landscapeCameraCompatMode() throws Exception { configureActivity(SCREEN_ORIENTATION_LANDSCAPE); setDisplayRotation(Surface.ROTATION_270); @@ -216,8 +219,9 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { assertActivityRefreshRequested(/* refreshRequested */ false); } - @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) @Test + @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) + @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) public void testCameraReconnected_cameraCompatModeAndRefresh() throws Exception { configureActivity(SCREEN_ORIENTATION_PORTRAIT); setDisplayRotation(Surface.ROTATION_270); @@ -236,6 +240,8 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { } @Test + @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) + @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) public void testCameraOpenedForDifferentPackage_notInCameraCompatMode() { configureActivity(SCREEN_ORIENTATION_PORTRAIT); @@ -246,27 +252,32 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) - @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT}) - public void testShouldApplyCameraCompatFreeformTreatment_overrideEnabled_returnsFalse() { + public void testShouldApplyCameraCompatFreeformTreatment_overrideNotEnabled_returnsFalse() { configureActivity(SCREEN_ORIENTATION_PORTRAIT); - assertTrue(mActivity.info - .isChangeEnabled(OVERRIDE_CAMERA_COMPAT_DISABLE_FREEFORM_WINDOWING_TREATMENT)); - assertFalse(mCameraCompatFreeformPolicy.isCameraCompatForFreeformEnabledForActivity( - mActivity)); + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + + assertFalse(mCameraCompatFreeformPolicy.isTreatmentEnabledForActivity(mActivity, + /* checkOrientation */ true)); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) - public void testShouldApplyCameraCompatFreeformTreatment_notDisabledByOverride_returnsTrue() { + @EnableCompatChanges(OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT) + public void testShouldApplyCameraCompatFreeformTreatment_enabledByOverride_returnsTrue() { configureActivity(SCREEN_ORIENTATION_PORTRAIT); - assertTrue(mCameraCompatFreeformPolicy.isCameraCompatForFreeformEnabledForActivity( - mActivity)); + mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1); + + assertTrue(mActivity.info + .isChangeEnabled(OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT)); + assertTrue(mCameraCompatFreeformPolicy.isTreatmentEnabledForActivity(mActivity, + /* checkOrientation */ true)); } @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) + @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) public void testShouldRefreshActivity_appBoundsChanged_returnsTrue() { configureActivity(SCREEN_ORIENTATION_PORTRAIT); Configuration oldConfiguration = createConfiguration(/* letterbox= */ false); @@ -279,6 +290,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) + @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) public void testShouldRefreshActivity_displayRotationChanged_returnsTrue() { configureActivity(SCREEN_ORIENTATION_PORTRAIT); Configuration oldConfiguration = createConfiguration(/* letterbox= */ true); @@ -294,6 +306,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) + @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) public void testShouldRefreshActivity_appBoundsNorDisplayChanged_returnsFalse() { configureActivity(SCREEN_ORIENTATION_PORTRAIT); Configuration oldConfiguration = createConfiguration(/* letterbox= */ true); @@ -309,6 +322,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) + @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) public void testOnActivityConfigurationChanging_refreshDisabledViaFlag_noRefresh() throws Exception { configureActivity(SCREEN_ORIENTATION_PORTRAIT); @@ -324,6 +338,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) + @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) public void testOnActivityConfigurationChanging_cycleThroughStopDisabled() throws Exception { when(mAppCompatConfiguration.isCameraCompatRefreshCycleThroughStopEnabled()) .thenReturn(false); @@ -338,6 +353,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) + @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) public void testOnActivityConfigurationChanging_cycleThroughStopDisabledForApp() throws Exception { configureActivity(SCREEN_ORIENTATION_PORTRAIT); @@ -352,6 +368,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) + @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) public void testGetCameraCompatAspectRatio_activityNotInCameraCompat_returnsDefaultAspRatio() { configureActivity(SCREEN_ORIENTATION_FULL_USER); @@ -365,6 +382,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) + @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) public void testGetCameraCompatAspectRatio_activityInCameraCompat_returnsConfigAspectRatio() { configureActivity(SCREEN_ORIENTATION_PORTRAIT); final float configAspectRatio = 1.5f; @@ -380,6 +398,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { @Test @EnableFlags(FLAG_ENABLE_CAMERA_COMPAT_FOR_DESKTOP_WINDOWING) + @EnableCompatChanges({OVERRIDE_CAMERA_COMPAT_ENABLE_FREEFORM_WINDOWING_TREATMENT}) public void testGetCameraCompatAspectRatio_inCameraCompatPerAppOverride_returnDefAspectRatio() { configureActivity(SCREEN_ORIENTATION_PORTRAIT); final float configAspectRatio = 1.5f; @@ -406,7 +425,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { private void configureActivityAndDisplay(@ScreenOrientation int activityOrientation, @Orientation int naturalOrientation, @WindowingMode int windowingMode) { - mTask = new TaskBuilder(mSupervisor) + final Task task = new TaskBuilder(mSupervisor) .setDisplay(mDisplayContent) .setWindowingMode(windowingMode) .build(); @@ -416,7 +435,7 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { .setComponent(ComponentName.createRelative(mContext, com.android.server.wm.CameraCompatFreeformPolicyTests.class.getName())) .setScreenOrientation(activityOrientation) - .setTask(mTask) + .setTask(task) .build(); spyOn(mActivity.mAppCompatController.getAppCompatCameraOverrides()); @@ -429,13 +448,11 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { } private void assertInCameraCompatMode(@CameraCompatTaskInfo.FreeformCameraCompatMode int mode) { - assertEquals(mode, mActivity.mAppCompatController.getAppCompatCameraOverrides() - .getFreeformCameraCompatMode()); + assertEquals(mode, mCameraCompatFreeformPolicy.getCameraCompatMode(mActivity)); } private void assertNotInCameraCompatMode() { - assertEquals(CAMERA_COMPAT_FREEFORM_NONE, mActivity.mAppCompatController - .getAppCompatCameraOverrides().getFreeformCameraCompatMode()); + assertInCameraCompatMode(CAMERA_COMPAT_FREEFORM_NONE); } private void assertActivityRefreshRequested(boolean refreshRequested) throws Exception { @@ -471,7 +488,8 @@ public class CameraCompatFreeformPolicyTests extends WindowTestsBase { private Configuration createConfiguration(boolean letterbox) { final Configuration configuration = new Configuration(); - Rect bounds = letterbox ? new Rect(300, 0, 700, 600) : new Rect(0, 0, 1000, 600); + Rect bounds = letterbox ? new Rect(/*left*/ 300, /*top*/ 0, /*right*/ 700, /*bottom*/ 600) + : new Rect(/*left*/ 0, /*top*/ 0, /*right*/ 1000, /*bottom*/ 600); configuration.windowConfiguration.setAppBounds(bounds); return configuration; } diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java index df17cd1d24b7..7ed8283efffd 100644 --- a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java @@ -496,24 +496,6 @@ public class RecentTasksTest extends WindowTestsBase { } @Test - public void testAppendOrganizedChildTaskInfo() { - final Task root = createTaskBuilder(".CreatedByOrganizerRoot").build(); - root.mCreatedByOrganizer = true; - // Add organized and non-organized child. - final Task child1 = createTaskBuilder(".Task1").setParentTask(root).build(); - final Task child2 = createTaskBuilder(".Task2").setParentTask(root).build(); - doReturn(true).when(child1).isOrganized(); - doReturn(false).when(child2).isOrganized(); - mRecentTasks.add(root); - - // Make sure only organized child will be appended. - final List<RecentTaskInfo> infos = getRecentTasks(0 /* flags */); - final List<RecentTaskInfo> childrenTaskInfos = infos.get(0).childrenTaskInfos; - assertEquals(childrenTaskInfos.size(), 1); - assertEquals(childrenTaskInfos.get(0).taskId, child1.mTaskId); - } - - @Test public void testAddTasksHomeClearUntrackedTasks_expectFinish() { // There may be multiple tasks with the same base intent by flags (FLAG_ACTIVITY_NEW_TASK | // FLAG_ACTIVITY_MULTIPLE_TASK). If the previous task is still active, it should be removed @@ -1420,46 +1402,6 @@ public class RecentTasksTest extends WindowTestsBase { } @Test - public void testLastSnapshotData_snapshotSaved() { - final TaskSnapshot snapshot = createSnapshot(new Point(100, 100), new Point(80, 80)); - final Task task1 = createTaskBuilder(".Task").build(); - task1.onSnapshotChanged(snapshot); - - mRecentTasks.add(task1); - final List<RecentTaskInfo> infos = getRecentTasks(0 /* flags */); - final RecentTaskInfo.PersistedTaskSnapshotData lastSnapshotData = - infos.get(0).lastSnapshotData; - assertTrue(lastSnapshotData.taskSize.equals(100, 100)); - assertTrue(lastSnapshotData.bufferSize.equals(80, 80)); - } - - @Test - public void testLastSnapshotData_noBuffer() { - final Task task1 = createTaskBuilder(".Task").build(); - final TaskSnapshot snapshot = createSnapshot(new Point(100, 100), null); - task1.onSnapshotChanged(snapshot); - - mRecentTasks.add(task1); - final List<RecentTaskInfo> infos = getRecentTasks(0 /* flags */); - final RecentTaskInfo.PersistedTaskSnapshotData lastSnapshotData = - infos.get(0).lastSnapshotData; - assertTrue(lastSnapshotData.taskSize.equals(100, 100)); - assertNull(lastSnapshotData.bufferSize); - } - - @Test - public void testLastSnapshotData_notSet() { - final Task task1 = createTaskBuilder(".Task").build(); - - mRecentTasks.add(task1); - final List<RecentTaskInfo> infos = getRecentTasks(0 /* flags */); - final RecentTaskInfo.PersistedTaskSnapshotData lastSnapshotData = - infos.get(0).lastSnapshotData; - assertNull(lastSnapshotData.taskSize); - assertNull(lastSnapshotData.bufferSize); - } - - @Test public void testCreateRecentTaskInfo_detachedTask() { final Task task = createTaskBuilder(".Task").build(); final ComponentName componentName = getUniqueComponentName(); diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java index 6490cbe3e31a..7cfdec664a92 100644 --- a/telephony/java/android/telephony/CarrierConfigManager.java +++ b/telephony/java/android/telephony/CarrierConfigManager.java @@ -9734,6 +9734,35 @@ public class CarrierConfigManager { "carrier_supported_satellite_services_per_provider_bundle"; /** + * A PersistableBundle that contains a list of key-value pairs, where the values are integer + * arrays. + * <p> + * Keys are the IDs of regional satellite configs as strings and values are + * integer arrays of earfcns in the corresponding regions. + * + * An example config for two regions "1" and "2": + * <pre>{@code + * <carrier_config> + * <pbundle_as_map name="regional_satellite_earfcn_bundle"> + * <int-array name = "1" num = "2"> + * <item value = "100"/> + * <item value = "200"/> + * </int-array> + * <int-array name = "2" num = "1"> + * <item value = "200"/> + * </int-array> + * </pbundle_as_map> + * </carrier_config> + * }</pre> + * <p> + * This config is empty by default. + * @hide + */ + @FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN) + public static final String KEY_REGIONAL_SATELLITE_EARFCN_BUNDLE = + "regional_satellite_earfcn_bundle"; + + /** * This config enables modem to scan satellite PLMNs specified as per * {@link #KEY_CARRIER_SUPPORTED_SATELLITE_SERVICES_PER_PROVIDER_BUNDLE} and attach to same * in case cellular networks are not enabled. This will need specific agreement between @@ -11264,6 +11293,9 @@ public class CarrierConfigManager { sDefaults.putPersistableBundle( KEY_CARRIER_SUPPORTED_SATELLITE_SERVICES_PER_PROVIDER_BUNDLE, PersistableBundle.EMPTY); + sDefaults.putPersistableBundle( + KEY_REGIONAL_SATELLITE_EARFCN_BUNDLE, + PersistableBundle.EMPTY); sDefaults.putBoolean(KEY_SATELLITE_ATTACH_SUPPORTED_BOOL, false); sDefaults.putInt(KEY_SATELLITE_CONNECTION_HYSTERESIS_SEC_INT, 180); sDefaults.putIntArray(KEY_NTN_LTE_RSRP_THRESHOLDS_INT_ARRAY, diff --git a/test-mock/src/android/test/mock/MockContentResolver.java b/test-mock/src/android/test/mock/MockContentResolver.java index 8f4bcccb0cba..af6ee3d0f71b 100644 --- a/test-mock/src/android/test/mock/MockContentResolver.java +++ b/test-mock/src/android/test/mock/MockContentResolver.java @@ -24,6 +24,7 @@ import android.content.Context; import android.content.IContentProvider; import android.database.ContentObserver; import android.net.Uri; +import android.util.Log; import java.util.Collection; import java.util.HashMap; @@ -53,6 +54,7 @@ import java.util.Map; * </div> */ public class MockContentResolver extends ContentResolver { + private static final String TAG = "MockContentResolver"; Map<String, ContentProvider> mProviders; /** @@ -105,6 +107,7 @@ public class MockContentResolver extends ContentResolver { if (provider != null) { return provider.getIContentProvider(); } else { + Log.w(TAG, "Provider does not exist: " + name); return null; } } diff --git a/tests/AppJankTest/src/android/app/jank/tests/JankDataProcessorTest.java b/tests/AppJankTest/src/android/app/jank/tests/JankDataProcessorTest.java index 2cd625eec032..4d495adf727b 100644 --- a/tests/AppJankTest/src/android/app/jank/tests/JankDataProcessorTest.java +++ b/tests/AppJankTest/src/android/app/jank/tests/JankDataProcessorTest.java @@ -18,7 +18,9 @@ package android.app.jank.tests; import static org.junit.Assert.assertEquals; +import android.app.jank.AppJankStats; import android.app.jank.Flags; +import android.app.jank.FrameOverrunHistogram; import android.app.jank.JankDataProcessor; import android.app.jank.StateTracker; import android.platform.test.annotations.RequiresFlagsEnabled; @@ -39,6 +41,7 @@ import org.junit.Test; import org.junit.runner.RunWith; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; @RunWith(AndroidJUnit4.class) @@ -154,6 +157,73 @@ public class JankDataProcessorTest { assertEquals(totalFrames, histogramFrames); } + @Test + @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) + public void mergeAppJankStats_confirmStatAddedToPendingStats() { + HashMap<String, JankDataProcessor.PendingJankStat> pendingStats = + mJankDataProcessor.getPendingJankStats(); + + assertEquals(pendingStats.size(), 0); + + AppJankStats jankStats = getAppJankStats(); + mJankDataProcessor.mergeJankStats(jankStats, sActivityName); + + pendingStats = mJankDataProcessor.getPendingJankStats(); + + assertEquals(pendingStats.size(), 1); + } + + /** + * This test confirms matching states are combined into one pending stat. When JankStats are + * merged from outside the platform they will contain widget category, widget id and widget + * state. If an incoming JankStats matches a pending stat on all those fields the incoming + * JankStat will be merged into the existing stat. + */ + @Test + @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) + public void mergeAppJankStats_confirmStatsWithMatchingStatesAreCombinedIntoOnePendingStat() { + AppJankStats jankStats = getAppJankStats(); + mJankDataProcessor.mergeJankStats(jankStats, sActivityName); + + HashMap<String, JankDataProcessor.PendingJankStat> pendingStats = + mJankDataProcessor.getPendingJankStats(); + assertEquals(pendingStats.size(), 1); + + AppJankStats secondJankStat = getAppJankStats(); + mJankDataProcessor.mergeJankStats(secondJankStat, sActivityName); + + pendingStats = mJankDataProcessor.getPendingJankStats(); + + assertEquals(pendingStats.size(), 1); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) + public void mergeAppJankStats_whenStatsWithMatchingStatesMerge_confirmFrameCountsAdded() { + AppJankStats jankStats = getAppJankStats(); + mJankDataProcessor.mergeJankStats(jankStats, sActivityName); + mJankDataProcessor.mergeJankStats(jankStats, sActivityName); + + HashMap<String, JankDataProcessor.PendingJankStat> pendingStats = + mJankDataProcessor.getPendingJankStats(); + + String statKey = pendingStats.keySet().iterator().next(); + JankDataProcessor.PendingJankStat pendingStat = pendingStats.get(statKey); + + assertEquals(pendingStats.size(), 1); + // The same jankStats objects are merged twice, this should result in the frame counts being + // doubled. + assertEquals(jankStats.getJankyFrameCount() * 2, pendingStat.getJankyFrames()); + assertEquals(jankStats.getTotalFrameCount() * 2, pendingStat.getTotalFrames()); + + int[] originalHistogramBuckets = jankStats.getFrameOverrunHistogram().getBucketCounters(); + int[] frameOverrunBuckets = pendingStat.getFrameOverrunBuckets(); + + for (int i = 0; i < frameOverrunBuckets.length; i++) { + assertEquals(originalHistogramBuckets[i] * 2, frameOverrunBuckets[i]); + } + } + // TODO b/375005277 add tests that cover logging and releasing resources back to pool. private long getTotalFramesCounted() { @@ -276,4 +346,26 @@ public class JankDataProcessorTest { return mockData; } + private AppJankStats getAppJankStats() { + AppJankStats jankStats = new AppJankStats( + /*App Uid*/APP_ID, + /*Widget Id*/"test widget id", + /*Widget Category*/AppJankStats.SCROLL, + /*Widget State*/AppJankStats.SCROLLING, + /*Total Frames*/100, + /*Janky Frames*/25, + getOverrunHistogram() + ); + return jankStats; + } + + private FrameOverrunHistogram getOverrunHistogram() { + FrameOverrunHistogram overrunHistogram = new FrameOverrunHistogram(); + overrunHistogram.addFrameOverrunMillis(-2); + overrunHistogram.addFrameOverrunMillis(1); + overrunHistogram.addFrameOverrunMillis(5); + overrunHistogram.addFrameOverrunMillis(25); + return overrunHistogram; + } + } diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt index 2c998c4217ac..64328275085d 100644 --- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt +++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/DesktopModeAppHelper.kt @@ -92,7 +92,7 @@ open class DesktopModeAppHelper(private val innerHelper: IStandardAppHelper) : } /** Move an app to Desktop by dragging the app handle at the top. */ - fun enterDesktopModeWithDrag( + private fun enterDesktopModeWithDrag( wmHelper: WindowManagerStateHelper, device: UiDevice, motionEventHelper: MotionEventHelper = MotionEventHelper(getInstrumentation(), TOUCH) @@ -155,14 +155,16 @@ open class DesktopModeAppHelper(private val innerHelper: IStandardAppHelper) : ?: error("Unable to find resource $MINIMIZE_BUTTON_VIEW\n") } - fun minimizeDesktopApp(wmHelper: WindowManagerStateHelper, device: UiDevice) { + fun minimizeDesktopApp(wmHelper: WindowManagerStateHelper, device: UiDevice, isPip: Boolean = false) { val caption = getCaptionForTheApp(wmHelper, device) val minimizeButton = getMinimizeButtonForTheApp(caption) minimizeButton.click() wmHelper .StateSyncBuilder() .withAppTransitionIdle() - .withWindowSurfaceDisappeared(innerHelper) + .apply { + if (isPip) withPipShown() else withWindowSurfaceDisappeared(innerHelper) + } .waitForAndVerify() } diff --git a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/StartMediaProjectionAppHelper.kt b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/StartMediaProjectionAppHelper.kt index 69fde0168b14..9e488486e16a 100644 --- a/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/StartMediaProjectionAppHelper.kt +++ b/tests/FlickerTests/test-apps/app-helpers/src/com/android/server/wm/flicker/helpers/StartMediaProjectionAppHelper.kt @@ -65,10 +65,45 @@ constructor( .waitForAndVerify() } + fun startSingleAppMediaProjectionWithExtraIntent( + wmHelper: WindowManagerStateHelper, + targetApp: StandardAppHelper + ) { + clickStartMediaProjectionWithExtraIntentButton() + chooseSingleAppOption() + startScreenSharing() + selectTargetApp(targetApp.appName) + wmHelper + .StateSyncBuilder() + .withAppTransitionIdle() + .withHomeActivityVisible() + .waitForAndVerify() + } + + fun startSingleAppMediaProjectionFromRecents( + wmHelper: WindowManagerStateHelper, + targetApp: StandardAppHelper, + recentTasksIndex: Int = 0, + ) { + clickStartMediaProjectionButton() + chooseSingleAppOption() + startScreenSharing() + selectTargetAppRecent(recentTasksIndex) + wmHelper + .StateSyncBuilder() + .withAppTransitionIdle() + .withWindowSurfaceAppeared(targetApp) + .waitForAndVerify() + } + private fun clickStartMediaProjectionButton() { findObject(By.res(packageName, START_MEDIA_PROJECTION_BUTTON_ID)).also { it.click() } } + private fun clickStartMediaProjectionWithExtraIntentButton() { + findObject(By.res(packageName, START_MEDIA_PROJECTION_NEW_INTENT_BUTTON_ID)).also { it.click() } + } + private fun chooseEntireScreenOption() { findObject(By.res(SCREEN_SHARE_OPTIONS_PATTERN)).also { it.click() } @@ -92,6 +127,13 @@ constructor( findObject(By.text(targetAppName)).also { it.click() } } + private fun selectTargetAppRecent(recentTasksIndex: Int) { + // Scroll to to find target app to launch then click app icon it to start capture + val recentsTasksRecycler = + findObject(By.res(SYSTEMUI_PACKAGE, MEDIA_PROJECTION_RECENT_TASKS)) + recentsTasksRecycler.children[recentTasksIndex].also{ it.click() } + } + private fun chooseSingleAppOption() { findObject(By.res(SCREEN_SHARE_OPTIONS_PATTERN)).also { it.click() } @@ -116,8 +158,10 @@ constructor( const val TIMEOUT: Long = 5000L const val ACCEPT_RESOURCE_ID: String = "android:id/button1" const val START_MEDIA_PROJECTION_BUTTON_ID: String = "button_start_mp" + const val START_MEDIA_PROJECTION_NEW_INTENT_BUTTON_ID: String = "button_start_mp_new_intent" val SCREEN_SHARE_OPTIONS_PATTERN: Pattern = Pattern.compile("$SYSTEMUI_PACKAGE:id/screen_share_mode_(options|spinner)") + const val MEDIA_PROJECTION_RECENT_TASKS: String = "media_projection_recent_tasks_recycler" const val ENTIRE_SCREEN_STRING_RES_NAME: String = "screen_share_permission_dialog_option_entire_screen" const val SINGLE_APP_STRING_RES_NAME: String = diff --git a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_start_media_projection.xml b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_start_media_projection.xml index 46f01e6c9752..c34d2003ef42 100644 --- a/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_start_media_projection.xml +++ b/tests/FlickerTests/test-apps/flickerapp/res/layout/activity_start_media_projection.xml @@ -16,17 +16,27 @@ --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - android:layout_width="wrap_content" - android:layout_height="wrap_content" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:gravity="center" android:orientation="vertical" android:background="@android:color/holo_orange_light"> <Button android:id="@+id/button_start_mp" - android:layout_width="500dp" - android:layout_height="500dp" + android:layout_margin="16dp" + android:layout_width="wrap_content" + android:layout_height="wrap_content" android:gravity="center_vertical|center_horizontal" android:text="Start Media Projection" android:textAppearance="?android:attr/textAppearanceLarge"/> + <Button + android:id="@+id/button_start_mp_new_intent" + android:layout_margin="16dp" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:gravity="center_vertical|center_horizontal" + android:text="Start Media Projection with extra intent" + android:textAppearance="?android:attr/textAppearanceLarge"/> </LinearLayout>
\ No newline at end of file diff --git a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/StartMediaProjectionActivity.java b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/StartMediaProjectionActivity.java index a24a48269d7c..b29b87450197 100644 --- a/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/StartMediaProjectionActivity.java +++ b/tests/FlickerTests/test-apps/flickerapp/src/com/android/server/wm/flicker/testapp/StartMediaProjectionActivity.java @@ -19,7 +19,8 @@ package com.android.server.wm.flicker.testapp; import static com.android.wm.shell.flicker.utils.MediaProjectionUtils.EXTRA_MESSENGER; import static com.android.wm.shell.flicker.utils.MediaProjectionUtils.MSG_SERVICE_DESTROYED; import static com.android.wm.shell.flicker.utils.MediaProjectionUtils.MSG_START_FOREGROUND_DONE; -import static com.android.wm.shell.flicker.utils.MediaProjectionUtils.REQUEST_CODE; +import static com.android.wm.shell.flicker.utils.MediaProjectionUtils.REQUEST_CODE_NORMAL; +import static com.android.wm.shell.flicker.utils.MediaProjectionUtils.REQUEST_CODE_EXTRA_INTENT; import android.app.Activity; import android.content.ComponentName; @@ -71,13 +72,17 @@ public class StartMediaProjectionActivity extends Activity { setContentView(R.layout.activity_start_media_projection); Button startMediaProjectionButton = findViewById(R.id.button_start_mp); + Button startMediaProjectionButton2 = findViewById(R.id.button_start_mp_new_intent); startMediaProjectionButton.setOnClickListener(v -> - startActivityForResult(mService.createScreenCaptureIntent(), REQUEST_CODE)); + startActivityForResult(mService.createScreenCaptureIntent(), REQUEST_CODE_NORMAL)); + startMediaProjectionButton2.setOnClickListener(v -> + startActivityForResult(mService.createScreenCaptureIntent(), + REQUEST_CODE_EXTRA_INTENT)); } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { - if (requestCode != REQUEST_CODE) { + if (requestCode != REQUEST_CODE_NORMAL && requestCode != REQUEST_CODE_EXTRA_INTENT) { throw new IllegalStateException("Unknown request code: " + requestCode); } if (resultCode != RESULT_OK) { @@ -85,6 +90,11 @@ public class StartMediaProjectionActivity extends Activity { } Log.d(TAG, "onActivityResult"); startMediaProjectionService(resultCode, data); + if (requestCode == REQUEST_CODE_EXTRA_INTENT) { + Intent startMain = new Intent(Intent.ACTION_MAIN); + startMain.addCategory(Intent.CATEGORY_HOME); + startActivity(startMain); + } } private void startMediaProjectionService(int resultCode, Intent resultData) { @@ -122,7 +132,7 @@ public class StartMediaProjectionActivity extends Activity { displayBounds.width(), displayBounds.height(), PixelFormat.RGBA_8888, 1); mVirtualDisplay = mMediaProjection.createVirtualDisplay( - "DanielDisplay", + "TestDisplay", displayBounds.width(), displayBounds.height(), DisplayMetrics.DENSITY_HIGH, |