diff options
1026 files changed, 33606 insertions, 8643 deletions
@@ -47,3 +47,4 @@ per-file MULTIUSER_OWNERS = file:/MULTIUSER_OWNERS per-file BROADCASTS_OWNERS = file:/BROADCASTS_OWNERS per-file ADPF_OWNERS = file:/ADPF_OWNERS per-file GAME_MANAGER_OWNERS = file:/GAME_MANAGER_OWNERS +per-file SDK_OWNERS = file:/SDK_OWNERS diff --git a/SDK_OWNERS b/SDK_OWNERS new file mode 100644 index 000000000000..c9ca47aa4703 --- /dev/null +++ b/SDK_OWNERS @@ -0,0 +1,6 @@ +amhk@google.com +kimalexander@google.com +lus@google.com +michaelwr@google.com +nanaasiedu@google.com +paulduffin@google.com diff --git a/apex/jobscheduler/framework/aconfig/job.aconfig b/apex/jobscheduler/framework/aconfig/job.aconfig index debd85096488..79aef1e6a19a 100644 --- a/apex/jobscheduler/framework/aconfig/job.aconfig +++ b/apex/jobscheduler/framework/aconfig/job.aconfig @@ -37,3 +37,11 @@ flag { description: "Ignore the important_while_foreground flag and change the related APIs to be not effective" bug: "374175032" } + +flag { + name: "get_pending_job_reasons_api" + is_exported: true + namespace: "backstage_power" + description: "Introduce a new getPendingJobReasons() API which returns reasons why a job may not have executed. Also deprecate the existing getPendingJobReason() API." + bug: "372031023" +} diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java index 790fc6bf3718..909a9b30ada4 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobServiceContext.java @@ -618,6 +618,26 @@ public final class JobServiceContext implements ServiceConnection { return mRunningJob; } + @VisibleForTesting + void setRunningJobLockedForTest(JobStatus job) { + mRunningJob = job; + } + + @VisibleForTesting + void setJobParamsLockedForTest(JobParameters params) { + mParams = params; + } + + @VisibleForTesting + void setRunningCallbackLockedForTest(JobCallback callback) { + mRunningCallback = callback; + } + + @VisibleForTesting + void setPendingStopReasonLockedForTest(int stopReason) { + mPendingStopReason = stopReason; + } + @JobConcurrencyManager.WorkType int getRunningJobWorkType() { return mRunningJobWorkType; @@ -786,6 +806,7 @@ public final class JobServiceContext implements ServiceConnection { executing = getRunningJobLocked(); } if (executing != null && jobId == executing.getJobId()) { + executing.setAbandoned(true); final StringBuilder stateSuffix = new StringBuilder(); stateSuffix.append(TRACE_ABANDONED_JOB); stateSuffix.append(executing.getBatteryName()); @@ -1364,8 +1385,9 @@ public final class JobServiceContext implements ServiceConnection { } /** Process MSG_TIMEOUT here. */ + @VisibleForTesting @GuardedBy("mLock") - private void handleOpTimeoutLocked() { + void handleOpTimeoutLocked() { switch (mVerb) { case VERB_BINDING: // The system may have been too busy. Don't drop the job or trigger an ANR. @@ -1427,9 +1449,25 @@ public final class JobServiceContext implements ServiceConnection { // Not an error - client ran out of time. Slog.i(TAG, "Client timed out while executing (no jobFinished received)." + " Sending onStop: " + getRunningJobNameLocked()); - mParams.setStopReason(JobParameters.STOP_REASON_TIMEOUT, - JobParameters.INTERNAL_STOP_REASON_TIMEOUT, "client timed out"); - sendStopMessageLocked("timeout while executing"); + + final JobStatus executing = getRunningJobLocked(); + int stopReason = JobParameters.STOP_REASON_TIMEOUT; + int internalStopReason = JobParameters.INTERNAL_STOP_REASON_TIMEOUT; + final StringBuilder stopMessage = new StringBuilder("timeout while executing"); + final StringBuilder debugStopReason = new StringBuilder("client timed out"); + + if (android.app.job.Flags.handleAbandonedJobs() + && executing != null && executing.isAbandoned()) { + final String abandonedMessage = " and maybe abandoned"; + stopReason = JobParameters.STOP_REASON_TIMEOUT_ABANDONED; + internalStopReason = JobParameters.INTERNAL_STOP_REASON_TIMEOUT_ABANDONED; + stopMessage.append(abandonedMessage); + debugStopReason.append(abandonedMessage); + } + + mParams.setStopReason(stopReason, + internalStopReason, debugStopReason.toString()); + sendStopMessageLocked(stopMessage.toString()); } else if (nowElapsed >= earliestStopTimeElapsed) { // We've given the app the minimum execution time. See if we should stop it or // let it continue running @@ -1479,8 +1517,9 @@ public final class JobServiceContext implements ServiceConnection { * Already running, need to stop. Will switch {@link #mVerb} from VERB_EXECUTING -> * VERB_STOPPING. */ + @VisibleForTesting @GuardedBy("mLock") - private void sendStopMessageLocked(@Nullable String reason) { + void sendStopMessageLocked(@Nullable String reason) { removeOpTimeOutLocked(); if (mVerb != VERB_EXECUTING) { Slog.e(TAG, "Sending onStopJob for a job that isn't started. " + mRunningJob); diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java index e3af1d894762..1dc5a714337c 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java @@ -576,6 +576,13 @@ public final class JobStatus { private String mSystemTraceTag; /** + * Job maybe abandoned by not calling + * {@link android.app.job.JobService#jobFinished(JobParameters, boolean)} while + * the strong reference to {@link android.app.job.JobParameters} is lost + */ + private boolean mIsAbandoned; + + /** * Core constructor for JobStatus instances. All other ctors funnel down to this one. * * @param job The actual requested parameters for the job @@ -725,6 +732,8 @@ public final class JobStatus { updateNetworkBytesLocked(); updateMediaBackupExemptionStatus(); + + mIsAbandoned = false; } /** Copy constructor: used specifically when cloning JobStatus objects for persistence, @@ -1061,6 +1070,16 @@ public final class JobStatus { return job.getTraceTag(); } + /** Returns if the job maybe abandoned */ + public boolean isAbandoned() { + return mIsAbandoned; + } + + /** Set the job maybe abandoned state*/ + public void setAbandoned(boolean abandoned) { + mIsAbandoned = abandoned; + } + /** Returns a trace tag using debug information provided by job scheduler service. */ @NonNull public String computeSystemTraceTag() { 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 ff4af69fd77c..8bd3ef4f4d1a 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 @@ -121,6 +121,9 @@ public final class QuotaController extends StateController { private static final String ALARM_TAG_CLEANUP = "*job.cleanup*"; private static final String ALARM_TAG_QUOTA_CHECK = "*job.quota_check*"; + private static final String TRACE_QUOTA_STATE_CHANGED_TAG = "QuotaStateChanged:"; + private static final String TRACE_QUOTA_STATE_CHANGED_DELIMITER = "#"; + private static final int SYSTEM_APP_CHECK_FLAGS = PackageManager.MATCH_DIRECT_BOOT_AWARE | PackageManager.MATCH_DIRECT_BOOT_UNAWARE | PackageManager.GET_PERMISSIONS | PackageManager.MATCH_KNOWN_PACKAGES; @@ -2657,11 +2660,12 @@ public final class QuotaController extends StateController { if (timeRemainingMs <= 50) { // Less than 50 milliseconds left. Start process of shutting down jobs. if (DEBUG) Slog.d(TAG, pkg + " has reached its quota."); - if (Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) { - Trace.instantForTrack(Trace.TRACE_TAG_SYSTEM_SERVER, - JobSchedulerService.TRACE_TRACK_NAME, - pkg + "#" + MSG_REACHED_TIME_QUOTA); - } + final StringBuilder traceMsg = new StringBuilder(); + traceMsg.append(TRACE_QUOTA_STATE_CHANGED_TAG) + .append(pkg) + .append(TRACE_QUOTA_STATE_CHANGED_DELIMITER) + .append(MSG_REACHED_TIME_QUOTA); + Trace.instant(Trace.TRACE_TAG_POWER, traceMsg.toString()); mStateChangedListener.onControllerStateChanged( maybeUpdateConstraintForPkgLocked( sElapsedRealtimeClock.millis(), @@ -2690,11 +2694,12 @@ public final class QuotaController extends StateController { pkg.userId, pkg.packageName); if (timeRemainingMs <= 0) { if (DEBUG) Slog.d(TAG, pkg + " has reached its EJ quota."); - if (Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) { - Trace.instantForTrack(Trace.TRACE_TAG_SYSTEM_SERVER, - JobSchedulerService.TRACE_TRACK_NAME, - pkg + "#" + MSG_REACHED_EJ_TIME_QUOTA); - } + final StringBuilder traceMsg = new StringBuilder(); + traceMsg.append(TRACE_QUOTA_STATE_CHANGED_TAG) + .append(pkg) + .append(TRACE_QUOTA_STATE_CHANGED_DELIMITER) + .append(MSG_REACHED_EJ_TIME_QUOTA); + Trace.instant(Trace.TRACE_TAG_POWER, traceMsg.toString()); mStateChangedListener.onControllerStateChanged( maybeUpdateConstraintForPkgLocked( sElapsedRealtimeClock.millis(), @@ -2719,11 +2724,12 @@ public final class QuotaController extends StateController { Slog.d(TAG, pkg + " has reached its count quota."); } - if (Trace.isTagEnabled(Trace.TRACE_TAG_SYSTEM_SERVER)) { - Trace.instantForTrack(Trace.TRACE_TAG_SYSTEM_SERVER, - JobSchedulerService.TRACE_TRACK_NAME, - pkg + "#" + MSG_REACHED_COUNT_QUOTA); - } + final StringBuilder traceMsg = new StringBuilder(); + traceMsg.append(TRACE_QUOTA_STATE_CHANGED_TAG) + .append(pkg) + .append(TRACE_QUOTA_STATE_CHANGED_DELIMITER) + .append(MSG_REACHED_COUNT_QUOTA); + Trace.instant(Trace.TRACE_TAG_POWER, traceMsg.toString()); mStateChangedListener.onControllerStateChanged( maybeUpdateConstraintForPkgLocked( diff --git a/boot/boot-image-profile-extra.txt b/boot/boot-image-profile-extra.txt index 11ca1dcc181e..e31eb3a993f0 100644 --- a/boot/boot-image-profile-extra.txt +++ b/boot/boot-image-profile-extra.txt @@ -23,3 +23,4 @@ HSPLandroid/graphics/Color;->luminance()F # For now, compile all methods in MessageQueue to avoid performance cliffs for # flagged/evolving hot code paths. See: b/338098106 HSPLandroid/os/MessageQueue;->* +HSPLandroid/os/MessageQueue$*;->* diff --git a/core/api/current.txt b/core/api/current.txt index 8bd4367dece8..015321352145 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -294,7 +294,7 @@ package android { field public static final String SET_ALARM = "com.android.alarm.permission.SET_ALARM"; field public static final String SET_ALWAYS_FINISH = "android.permission.SET_ALWAYS_FINISH"; field public static final String SET_ANIMATION_SCALE = "android.permission.SET_ANIMATION_SCALE"; - field @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public static final String SET_BIOMETRIC_DIALOG_ADVANCED = "android.permission.SET_BIOMETRIC_DIALOG_ADVANCED"; + field public static final String SET_BIOMETRIC_DIALOG_ADVANCED = "android.permission.SET_BIOMETRIC_DIALOG_ADVANCED"; field public static final String SET_DEBUG_APP = "android.permission.SET_DEBUG_APP"; field @Deprecated public static final String SET_PREFERRED_APPLICATIONS = "android.permission.SET_PREFERRED_APPLICATIONS"; field public static final String SET_PROCESS_LIMIT = "android.permission.SET_PROCESS_LIMIT"; @@ -1006,6 +1006,7 @@ package android { field public static final int insetRight = 16843192; // 0x10101b8 field public static final int insetTop = 16843193; // 0x10101b9 field public static final int installLocation = 16843447; // 0x10102b7 + field @FlaggedApi("android.security.enable_intent_matching_flags") public static final int intentMatchingFlags; field public static final int interactiveUiTimeout = 16844181; // 0x1010595 field public static final int interpolator = 16843073; // 0x1010141 field public static final int intro = 16844395; // 0x101066b @@ -1609,6 +1610,7 @@ package android { field public static final int summaryColumn = 16843426; // 0x10102a2 field public static final int summaryOff = 16843248; // 0x10101f0 field public static final int summaryOn = 16843247; // 0x10101ef + field @FlaggedApi("android.view.accessibility.supplemental_description") public static final int supplementalDescription; field public static final int supportedTypes = 16844369; // 0x1010651 field public static final int supportsAssist = 16844016; // 0x10104f0 field public static final int supportsBatteryGameMode = 16844374; // 0x1010656 @@ -4616,6 +4618,7 @@ 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); @@ -8783,7 +8786,8 @@ package android.app.appfunctions { @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public final class AppFunctionManager { method @RequiresPermission(anyOf={"android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED", "android.permission.EXECUTE_APP_FUNCTIONS"}, conditional=true) public void executeAppFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>); - method public void isAppFunctionEnabled(@NonNull String, @NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,java.lang.Exception>); + method @RequiresPermission(anyOf={"android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED", "android.permission.EXECUTE_APP_FUNCTIONS"}, conditional=true) public void isAppFunctionEnabled(@NonNull String, @NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,java.lang.Exception>); + method public void isAppFunctionEnabled(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,java.lang.Exception>); method public void setAppFunctionEnabled(@NonNull String, int, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,java.lang.Exception>); field public static final int APP_FUNCTION_STATE_DEFAULT = 0; // 0x0 field public static final int APP_FUNCTION_STATE_DISABLED = 2; // 0x2 @@ -8816,6 +8820,7 @@ package android.app.appfunctions { @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public final class ExecuteAppFunctionResponse implements android.os.Parcelable { method public int describeContents(); + method public int getErrorCategory(); method @Nullable public String getErrorMessage(); method @NonNull public android.os.Bundle getExtras(); method public int getResultCode(); @@ -8825,14 +8830,19 @@ package android.app.appfunctions { method @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") @NonNull public static android.app.appfunctions.ExecuteAppFunctionResponse newSuccess(@NonNull android.app.appsearch.GenericDocument, @Nullable android.os.Bundle); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.app.appfunctions.ExecuteAppFunctionResponse> CREATOR; + field public static final int ERROR_CATEGORY_APP = 3; // 0x3 + field public static final int ERROR_CATEGORY_REQUEST_ERROR = 1; // 0x1 + field public static final int ERROR_CATEGORY_SYSTEM = 2; // 0x2 + field public static final int ERROR_CATEGORY_UNKNOWN = 0; // 0x0 field public static final String PROPERTY_RETURN_VALUE = "returnValue"; - field public static final int RESULT_APP_UNKNOWN_ERROR = 2; // 0x2 - field public static final int RESULT_CANCELLED = 6; // 0x6 - field public static final int RESULT_DENIED = 1; // 0x1 - field public static final int RESULT_DISABLED = 5; // 0x5 - field public static final int RESULT_INTERNAL_ERROR = 3; // 0x3 - field public static final int RESULT_INVALID_ARGUMENT = 4; // 0x4 + field public static final int RESULT_APP_UNKNOWN_ERROR = 3000; // 0xbb8 + field public static final int RESULT_CANCELLED = 2001; // 0x7d1 + field public static final int RESULT_DENIED = 1000; // 0x3e8 + field public static final int RESULT_DISABLED = 1002; // 0x3ea + field public static final int RESULT_FUNCTION_NOT_FOUND = 1003; // 0x3eb + field public static final int RESULT_INVALID_ARGUMENT = 1001; // 0x3e9 field public static final int RESULT_OK = 0; // 0x0 + field public static final int RESULT_SYSTEM_ERROR = 2000; // 0x7d0 } } @@ -9677,9 +9687,11 @@ package android.app.wallpaper { method @Nullable public String getId(); method @Nullable public android.net.Uri getThumbnail(); method @Nullable public CharSequence getTitle(); + method @NonNull public static android.app.wallpaper.WallpaperDescription readFromStream(@NonNull java.io.InputStream) throws java.io.IOException; method @NonNull public android.app.wallpaper.WallpaperDescription.Builder toBuilder(); method public void writeToParcel(@NonNull android.os.Parcel, int); - field @Nullable public static final android.os.Parcelable.Creator<android.app.wallpaper.WallpaperDescription> CREATOR; + method public void writeToStream(@NonNull java.io.OutputStream) throws java.io.IOException; + field @NonNull public static final android.os.Parcelable.Creator<android.app.wallpaper.WallpaperDescription> CREATOR; } public static final class WallpaperDescription.Builder { @@ -9832,6 +9844,7 @@ package android.appwidget { field public static final int RESIZE_VERTICAL = 2; // 0x2 field public static final int WIDGET_CATEGORY_HOME_SCREEN = 1; // 0x1 field public static final int WIDGET_CATEGORY_KEYGUARD = 2; // 0x2 + field @FlaggedApi("android.appwidget.flags.not_keyguard_category") public static final int WIDGET_CATEGORY_NOT_KEYGUARD = 8; // 0x8 field public static final int WIDGET_CATEGORY_SEARCHBOX = 4; // 0x4 field public static final int WIDGET_FEATURE_CONFIGURATION_OPTIONAL = 4; // 0x4 field public static final int WIDGET_FEATURE_HIDE_FROM_PICKER = 2; // 0x2 @@ -10941,6 +10954,7 @@ package android.content { field public static final int RECEIVER_VISIBLE_TO_INSTANT_APPS = 1; // 0x1 field public static final String RESTRICTIONS_SERVICE = "restrictions"; field public static final String ROLE_SERVICE = "role"; + field @FlaggedApi("com.android.internal.telephony.flags.satellite_state_change_listener") public static final String SATELLITE_SERVICE = "satellite"; field public static final String SEARCH_SERVICE = "search"; field @FlaggedApi("android.os.security_state_service") public static final String SECURITY_STATE_SERVICE = "security_state"; field public static final String SENSOR_SERVICE = "sensor"; @@ -16747,7 +16761,7 @@ package android.graphics { method public boolean hasGlyph(String); method public final boolean isAntiAlias(); method public final boolean isDither(); - method public boolean isElegantTextHeight(); + method @Deprecated @FlaggedApi("com.android.text.flags.deprecate_elegant_text_height_api") public boolean isElegantTextHeight(); method public final boolean isFakeBoldText(); method public final boolean isFilterBitmap(); method public final boolean isLinearText(); @@ -16768,7 +16782,7 @@ package android.graphics { method public void setColor(@ColorLong long); method public android.graphics.ColorFilter setColorFilter(android.graphics.ColorFilter); method public void setDither(boolean); - method public void setElegantTextHeight(boolean); + method @Deprecated @FlaggedApi("com.android.text.flags.deprecate_elegant_text_height_api") public void setElegantTextHeight(boolean); method public void setEndHyphenEdit(int); method public void setFakeBoldText(boolean); method public void setFilterBitmap(boolean); @@ -17361,6 +17375,25 @@ package android.graphics { method public boolean setUseCompositingLayer(boolean, @Nullable android.graphics.Paint); } + @FlaggedApi("com.android.graphics.hwui.flags.runtime_color_filters_blenders") public class RuntimeColorFilter extends android.graphics.ColorFilter { + ctor public RuntimeColorFilter(@NonNull String); + method public void setColorUniform(@NonNull String, @ColorInt int); + method public void setColorUniform(@NonNull String, @ColorLong long); + method public void setColorUniform(@NonNull String, @NonNull android.graphics.Color); + method public void setFloatUniform(@NonNull String, float); + method public void setFloatUniform(@NonNull String, float, float); + method public void setFloatUniform(@NonNull String, float, float, float); + method public void setFloatUniform(@NonNull String, float, float, float, float); + method public void setFloatUniform(@NonNull String, @NonNull float[]); + method public void setInputColorFilter(@NonNull String, @NonNull android.graphics.ColorFilter); + method public void setInputShader(@NonNull String, @NonNull android.graphics.Shader); + method public void setIntUniform(@NonNull String, int); + method public void setIntUniform(@NonNull String, int, int); + method public void setIntUniform(@NonNull String, int, int, int); + method public void setIntUniform(@NonNull String, int, int, int, int); + method public void setIntUniform(@NonNull String, @NonNull int[]); + } + public class RuntimeShader extends android.graphics.Shader { ctor public RuntimeShader(@NonNull String); method public void setColorUniform(@NonNull String, @ColorInt int); @@ -19103,11 +19136,11 @@ package android.hardware.biometrics { method @RequiresPermission(android.Manifest.permission.USE_BIOMETRIC) public void authenticate(@NonNull android.hardware.biometrics.BiometricPrompt.CryptoObject, @NonNull android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull android.hardware.biometrics.BiometricPrompt.AuthenticationCallback); method @RequiresPermission(android.Manifest.permission.USE_BIOMETRIC) public void authenticate(@NonNull android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull android.hardware.biometrics.BiometricPrompt.AuthenticationCallback); method @Nullable public int getAllowedAuthenticators(); - method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @Nullable public android.hardware.biometrics.PromptContentView getContentView(); + method @Nullable public android.hardware.biometrics.PromptContentView getContentView(); method @Nullable public CharSequence getDescription(); - method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @Nullable @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public android.graphics.Bitmap getLogoBitmap(); - method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @Nullable @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public String getLogoDescription(); - method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @DrawableRes @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public int getLogoRes(); + method @Nullable @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public android.graphics.Bitmap getLogoBitmap(); + method @Nullable @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public String getLogoDescription(); + method @DrawableRes @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public int getLogoRes(); method @Nullable public CharSequence getNegativeButtonText(); method @Nullable public CharSequence getSubtitle(); method @NonNull public CharSequence getTitle(); @@ -19156,12 +19189,12 @@ package android.hardware.biometrics { method @NonNull public android.hardware.biometrics.BiometricPrompt build(); method @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setAllowedAuthenticators(int); method @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setConfirmationRequired(boolean); - method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setContentView(@NonNull android.hardware.biometrics.PromptContentView); + method @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setContentView(@NonNull android.hardware.biometrics.PromptContentView); method @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setDescription(@NonNull CharSequence); method @Deprecated @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setDeviceCredentialAllowed(boolean); - method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @NonNull @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public android.hardware.biometrics.BiometricPrompt.Builder setLogoBitmap(@NonNull android.graphics.Bitmap); - method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @NonNull @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public android.hardware.biometrics.BiometricPrompt.Builder setLogoDescription(@NonNull String); - method @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") @NonNull @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public android.hardware.biometrics.BiometricPrompt.Builder setLogoRes(@DrawableRes int); + method @NonNull @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public android.hardware.biometrics.BiometricPrompt.Builder setLogoBitmap(@NonNull android.graphics.Bitmap); + method @NonNull @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public android.hardware.biometrics.BiometricPrompt.Builder setLogoDescription(@NonNull String); + method @NonNull @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public android.hardware.biometrics.BiometricPrompt.Builder setLogoRes(@DrawableRes int); method @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setNegativeButton(@NonNull CharSequence, @NonNull java.util.concurrent.Executor, @NonNull android.content.DialogInterface.OnClickListener); method @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setSubtitle(@NonNull CharSequence); method @NonNull public android.hardware.biometrics.BiometricPrompt.Builder setTitle(@NonNull CharSequence); @@ -19184,27 +19217,27 @@ package android.hardware.biometrics { method @Nullable public java.security.Signature getSignature(); } - @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public interface PromptContentItem { + public interface PromptContentItem { } - @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public final class PromptContentItemBulletedText implements android.os.Parcelable android.hardware.biometrics.PromptContentItem { + public final class PromptContentItemBulletedText implements android.os.Parcelable android.hardware.biometrics.PromptContentItem { ctor public PromptContentItemBulletedText(@NonNull String); method public int describeContents(); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.hardware.biometrics.PromptContentItemBulletedText> CREATOR; } - @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public final class PromptContentItemPlainText implements android.os.Parcelable android.hardware.biometrics.PromptContentItem { + public final class PromptContentItemPlainText implements android.os.Parcelable android.hardware.biometrics.PromptContentItem { ctor public PromptContentItemPlainText(@NonNull String); method public int describeContents(); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.hardware.biometrics.PromptContentItemPlainText> CREATOR; } - @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public interface PromptContentView { + public interface PromptContentView { } - @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public final class PromptContentViewWithMoreOptionsButton implements android.os.Parcelable android.hardware.biometrics.PromptContentView { + public final class PromptContentViewWithMoreOptionsButton implements android.os.Parcelable android.hardware.biometrics.PromptContentView { method public int describeContents(); method @Nullable @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public String getDescription(); method @NonNull @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public android.content.DialogInterface.OnClickListener getMoreOptionsButtonListener(); @@ -19219,7 +19252,7 @@ package android.hardware.biometrics { method @NonNull @RequiresPermission(android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED) public android.hardware.biometrics.PromptContentViewWithMoreOptionsButton.Builder setMoreOptionsButtonListener(@NonNull java.util.concurrent.Executor, @NonNull android.content.DialogInterface.OnClickListener); } - @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") public final class PromptVerticalListContentView implements android.os.Parcelable android.hardware.biometrics.PromptContentView { + public final class PromptVerticalListContentView implements android.os.Parcelable android.hardware.biometrics.PromptContentView { method public int describeContents(); method @Nullable public String getDescription(); method @NonNull public java.util.List<android.hardware.biometrics.PromptContentItem> getListItems(); @@ -19314,6 +19347,8 @@ package android.hardware.camera2 { field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<int[]> AUTOMOTIVE_LENS_FACING; field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<java.lang.Integer> AUTOMOTIVE_LOCATION; field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<int[]> COLOR_CORRECTION_AVAILABLE_ABERRATION_MODES; + field @FlaggedApi("com.android.internal.camera.flags.color_temperature") @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<int[]> COLOR_CORRECTION_AVAILABLE_MODES; + field @FlaggedApi("com.android.internal.camera.flags.color_temperature") @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<android.util.Range<java.lang.Integer>> COLOR_CORRECTION_COLOR_TEMPERATURE_RANGE; field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<int[]> CONTROL_AE_AVAILABLE_ANTIBANDING_MODES; field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<int[]> CONTROL_AE_AVAILABLE_MODES; field @NonNull public static final android.hardware.camera2.CameraCharacteristics.Key<android.util.Range<java.lang.Integer>[]> CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES; @@ -19615,6 +19650,7 @@ package android.hardware.camera2 { field public static final int COLOR_CORRECTION_ABERRATION_MODE_FAST = 1; // 0x1 field public static final int COLOR_CORRECTION_ABERRATION_MODE_HIGH_QUALITY = 2; // 0x2 field public static final int COLOR_CORRECTION_ABERRATION_MODE_OFF = 0; // 0x0 + field @FlaggedApi("com.android.internal.camera.flags.color_temperature") public static final int COLOR_CORRECTION_MODE_CCT = 3; // 0x3 field public static final int COLOR_CORRECTION_MODE_FAST = 1; // 0x1 field public static final int COLOR_CORRECTION_MODE_HIGH_QUALITY = 2; // 0x2 field public static final int COLOR_CORRECTION_MODE_TRANSFORM_MATRIX = 0; // 0x0 @@ -19901,6 +19937,8 @@ package android.hardware.camera2 { method public void writeToParcel(android.os.Parcel, int); field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Boolean> BLACK_LEVEL_LOCK; field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> COLOR_CORRECTION_ABERRATION_MODE; + field @FlaggedApi("com.android.internal.camera.flags.color_temperature") @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> COLOR_CORRECTION_COLOR_TEMPERATURE; + field @FlaggedApi("com.android.internal.camera.flags.color_temperature") @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> COLOR_CORRECTION_COLOR_TINT; field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<android.hardware.camera2.params.RggbChannelVector> COLOR_CORRECTION_GAINS; field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<java.lang.Integer> COLOR_CORRECTION_MODE; field @NonNull public static final android.hardware.camera2.CaptureRequest.Key<android.hardware.camera2.params.ColorSpaceTransform> COLOR_CORRECTION_TRANSFORM; @@ -19991,6 +20029,8 @@ package android.hardware.camera2 { method public int getSequenceId(); field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Boolean> BLACK_LEVEL_LOCK; field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> COLOR_CORRECTION_ABERRATION_MODE; + field @FlaggedApi("com.android.internal.camera.flags.color_temperature") @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> COLOR_CORRECTION_COLOR_TEMPERATURE; + field @FlaggedApi("com.android.internal.camera.flags.color_temperature") @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> COLOR_CORRECTION_COLOR_TINT; field @NonNull public static final android.hardware.camera2.CaptureResult.Key<android.hardware.camera2.params.RggbChannelVector> COLOR_CORRECTION_GAINS; field @NonNull public static final android.hardware.camera2.CaptureResult.Key<java.lang.Integer> COLOR_CORRECTION_MODE; field @NonNull public static final android.hardware.camera2.CaptureResult.Key<android.hardware.camera2.params.ColorSpaceTransform> COLOR_CORRECTION_TRANSFORM; @@ -20109,6 +20149,7 @@ package android.hardware.camera2 { public class MultiResolutionImageReader implements java.lang.AutoCloseable { ctor public MultiResolutionImageReader(@NonNull java.util.Collection<android.hardware.camera2.params.MultiResolutionStreamInfo>, int, @IntRange(from=1) int); + ctor @FlaggedApi("com.android.internal.camera.flags.multiresolution_imagereader_usage_public") public MultiResolutionImageReader(@NonNull java.util.Collection<android.hardware.camera2.params.MultiResolutionStreamInfo>, int, @IntRange(from=1) int, long); method public void close(); method protected void finalize(); method public void flush(); @@ -21371,6 +21412,7 @@ package android.media { field public static final int TYPE_IP = 20; // 0x14 field public static final int TYPE_LINE_ANALOG = 5; // 0x5 field public static final int TYPE_LINE_DIGITAL = 6; // 0x6 + field @FlaggedApi("android.media.audio.enable_multichannel_group_device") public static final int TYPE_MULTICHANNEL_GROUP = 32; // 0x20 field public static final int TYPE_REMOTE_SUBMIX = 25; // 0x19 field public static final int TYPE_TELEPHONY = 18; // 0x12 field public static final int TYPE_TV_TUNER = 17; // 0x11 @@ -22668,7 +22710,6 @@ package android.media { method public void sendEvent(int, int, @Nullable byte[]) throws android.media.MediaCasException; method public void setEventListener(@Nullable android.media.MediaCas.EventListener, @Nullable android.os.Handler); method public void setPrivateData(@NonNull byte[]) throws android.media.MediaCasException; - method @FlaggedApi("com.android.media.flags.update_client_profile_priority") public boolean updateResourcePriority(int, int); field public static final int PLUGIN_STATUS_PHYSICAL_MODULE_CHANGED = 0; // 0x0 field public static final int PLUGIN_STATUS_SESSION_NUMBER_CHANGED = 1; // 0x1 field public static final int SCRAMBLING_MODE_AES128 = 9; // 0x9 @@ -23089,6 +23130,65 @@ package android.media { field public static final int AC4Profile11 = 514; // 0x202 field public static final int AC4Profile21 = 1026; // 0x402 field public static final int AC4Profile22 = 1028; // 0x404 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel11Band0 = 513; // 0x201 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel11Band1 = 514; // 0x202 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel11Band2 = 516; // 0x204 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel11Band3 = 520; // 0x208 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel1Band0 = 257; // 0x101 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel1Band1 = 258; // 0x102 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel1Band2 = 260; // 0x104 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel1Band3 = 264; // 0x108 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel21Band0 = 2049; // 0x801 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel21Band1 = 2050; // 0x802 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel21Band2 = 2052; // 0x804 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel21Band3 = 2056; // 0x808 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel2Band0 = 1025; // 0x401 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel2Band1 = 1026; // 0x402 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel2Band2 = 1028; // 0x404 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel2Band3 = 1032; // 0x408 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel31Band0 = 8193; // 0x2001 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel31Band1 = 8194; // 0x2002 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel31Band2 = 8196; // 0x2004 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel31Band3 = 8200; // 0x2008 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel3Band0 = 4097; // 0x1001 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel3Band1 = 4098; // 0x1002 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel3Band2 = 4100; // 0x1004 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel3Band3 = 4104; // 0x1008 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel41Band0 = 32769; // 0x8001 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel41Band1 = 32770; // 0x8002 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel41Band2 = 32772; // 0x8004 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel41Band3 = 32776; // 0x8008 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel4Band0 = 16385; // 0x4001 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel4Band1 = 16386; // 0x4002 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel4Band2 = 16388; // 0x4004 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel4Band3 = 16392; // 0x4008 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel51Band0 = 131073; // 0x20001 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel51Band1 = 131074; // 0x20002 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel51Band2 = 131076; // 0x20004 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel51Band3 = 131080; // 0x20008 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel5Band0 = 65537; // 0x10001 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel5Band1 = 65538; // 0x10002 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel5Band2 = 65540; // 0x10004 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel5Band3 = 65544; // 0x10008 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel61Band0 = 524289; // 0x80001 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel61Band1 = 524290; // 0x80002 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel61Band2 = 524292; // 0x80004 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel61Band3 = 524296; // 0x80008 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel6Band0 = 262145; // 0x40001 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel6Band1 = 262146; // 0x40002 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel6Band2 = 262148; // 0x40004 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel6Band3 = 262152; // 0x40008 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel71Band0 = 2097153; // 0x200001 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel71Band1 = 2097154; // 0x200002 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel71Band2 = 2097156; // 0x200004 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel71Band3 = 2097160; // 0x200008 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel7Band0 = 1048577; // 0x100001 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel7Band1 = 1048578; // 0x100002 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel7Band2 = 1048580; // 0x100004 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVLevel7Band3 = 1048584; // 0x100008 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVProfile422_10 = 1; // 0x1 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVProfile422_10HDR10 = 4096; // 0x1000 + field @FlaggedApi("android.media.codec.apv_support") public static final int APVProfile422_10HDR10Plus = 8192; // 0x2000 field public static final int AV1Level2 = 1; // 0x1 field public static final int AV1Level21 = 2; // 0x2 field public static final int AV1Level22 = 4; // 0x4 @@ -23939,6 +24039,7 @@ package android.media { field public static final String MIMETYPE_TEXT_CEA_708 = "text/cea-708"; field public static final String MIMETYPE_TEXT_SUBRIP = "application/x-subrip"; field public static final String MIMETYPE_TEXT_VTT = "text/vtt"; + field @FlaggedApi("android.media.codec.apv_support") public static final String MIMETYPE_VIDEO_APV = "video/apv"; field public static final String MIMETYPE_VIDEO_AV1 = "video/av01"; field public static final String MIMETYPE_VIDEO_AVC = "video/avc"; field public static final String MIMETYPE_VIDEO_DOLBY_VISION = "video/dolby-vision"; @@ -24568,6 +24669,7 @@ package android.media { field @FlaggedApi("com.android.media.flags.enable_audio_policies_device_and_bluetooth_controller") public static final int TYPE_HDMI_ARC = 10; // 0xa field @FlaggedApi("com.android.media.flags.enable_audio_policies_device_and_bluetooth_controller") public static final int TYPE_HDMI_EARC = 29; // 0x1d field public static final int TYPE_HEARING_AID = 23; // 0x17 + field @FlaggedApi("android.media.audio.enable_multichannel_group_device") public static final int TYPE_MULTICHANNEL_SPEAKER_GROUP = 32; // 0x20 field public static final int TYPE_REMOTE_AUDIO_VIDEO_RECEIVER = 1003; // 0x3eb field @FlaggedApi("com.android.media.flags.enable_new_media_route_2_info_types") public static final int TYPE_REMOTE_CAR = 1008; // 0x3f0 field @FlaggedApi("com.android.media.flags.enable_new_media_route_2_info_types") public static final int TYPE_REMOTE_COMPUTER = 1006; // 0x3ee @@ -34179,7 +34281,7 @@ package android.os { method public android.os.StrictMode.VmPolicy build(); method @NonNull public android.os.StrictMode.VmPolicy.Builder detectActivityLeaks(); method @NonNull public android.os.StrictMode.VmPolicy.Builder detectAll(); - method @FlaggedApi("com.android.window.flags.bal_strict_mode") @NonNull public android.os.StrictMode.VmPolicy.Builder detectBlockedBackgroundActivityLaunch(); + method @FlaggedApi("com.android.window.flags.bal_strict_mode_ro") @NonNull public android.os.StrictMode.VmPolicy.Builder detectBlockedBackgroundActivityLaunch(); method @NonNull public android.os.StrictMode.VmPolicy.Builder detectCleartextNetwork(); method @NonNull public android.os.StrictMode.VmPolicy.Builder detectContentUriWithoutPermission(); method @NonNull public android.os.StrictMode.VmPolicy.Builder detectCredentialProtectedWhileLocked(); @@ -34192,7 +34294,7 @@ package android.os { method @NonNull public android.os.StrictMode.VmPolicy.Builder detectNonSdkApiUsage(); method @NonNull public android.os.StrictMode.VmPolicy.Builder detectUnsafeIntentLaunch(); method @NonNull public android.os.StrictMode.VmPolicy.Builder detectUntaggedSockets(); - method @FlaggedApi("com.android.window.flags.bal_strict_mode") @NonNull public android.os.StrictMode.VmPolicy.Builder ignoreBlockedBackgroundActivityLaunch(); + method @FlaggedApi("com.android.window.flags.bal_strict_mode_ro") @NonNull public android.os.StrictMode.VmPolicy.Builder ignoreBlockedBackgroundActivityLaunch(); method @NonNull public android.os.StrictMode.VmPolicy.Builder penaltyDeath(); method @NonNull public android.os.StrictMode.VmPolicy.Builder penaltyDeathOnCleartextNetwork(); method @NonNull public android.os.StrictMode.VmPolicy.Builder penaltyDeathOnFileUriExposure(); @@ -34435,6 +34537,7 @@ package android.os { method public int describeContents(); method @NonNull public static android.os.VibrationEffect.Composition startComposition(); method @FlaggedApi("android.os.vibrator.normalized_pwle_effects") @NonNull public static android.os.VibrationEffect.WaveformEnvelopeBuilder startWaveformEnvelope(); + method @FlaggedApi("android.os.vibrator.normalized_pwle_effects") @NonNull public static android.os.VibrationEffect.WaveformEnvelopeBuilder startWaveformEnvelope(@FloatRange(from=0) float); field @NonNull public static final android.os.Parcelable.Creator<android.os.VibrationEffect> CREATOR; field public static final int DEFAULT_AMPLITUDE = -1; // 0xffffffff field public static final int EFFECT_CLICK = 0; // 0x0 @@ -37199,7 +37302,7 @@ package android.provider { } public static final class ContactsContract.Settings implements android.provider.ContactsContract.SettingsColumns { - method @Nullable public static android.accounts.Account getDefaultAccount(@NonNull android.content.ContentResolver); + method @Deprecated @FlaggedApi("android.provider.new_default_account_api_enabled") @Nullable public static android.accounts.Account getDefaultAccount(@NonNull android.content.ContentResolver); field public static final String ACTION_SET_DEFAULT_ACCOUNT = "android.provider.action.SET_DEFAULT_ACCOUNT"; field public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/setting"; field public static final String CONTENT_TYPE = "vnd.android.cursor.dir/setting"; @@ -39815,7 +39918,7 @@ package android.security { package android.security.advancedprotection { - @FlaggedApi("android.security.aapm_api") public class AdvancedProtectionManager { + @FlaggedApi("android.security.aapm_api") public final class AdvancedProtectionManager { method @RequiresPermission(android.Manifest.permission.QUERY_ADVANCED_PROTECTION_MODE) public boolean isAdvancedProtectionEnabled(); method @RequiresPermission(android.Manifest.permission.QUERY_ADVANCED_PROTECTION_MODE) public void registerAdvancedProtectionCallback(@NonNull java.util.concurrent.Executor, @NonNull android.security.advancedprotection.AdvancedProtectionManager.Callback); method @RequiresPermission(android.Manifest.permission.QUERY_ADVANCED_PROTECTION_MODE) public void unregisterAdvancedProtectionCallback(@NonNull android.security.advancedprotection.AdvancedProtectionManager.Callback); @@ -41481,6 +41584,7 @@ package android.service.notification { method public final void cancelNotification(String); method public final void cancelNotifications(String[]); method public final void clearRequestedListenerHints(); + method @FlaggedApi("android.service.notification.notification_conversation_channel_management") @Nullable public final android.app.NotificationChannel createConversationNotificationChannelForPackage(@NonNull String, @NonNull android.os.UserHandle, @NonNull String, @NonNull String); method public android.service.notification.StatusBarNotification[] getActiveNotifications(); method public android.service.notification.StatusBarNotification[] getActiveNotifications(String[]); method public final int getCurrentInterruptionFilter(); @@ -42085,6 +42189,7 @@ package android.service.wallpaper { ctor public WallpaperService(); method public final android.os.IBinder onBind(android.content.Intent); method @MainThread public abstract android.service.wallpaper.WallpaperService.Engine onCreateEngine(); + method @FlaggedApi("android.app.live_wallpaper_content_handling") @MainThread @Nullable public android.service.wallpaper.WallpaperService.Engine onCreateEngine(@NonNull android.app.wallpaper.WallpaperDescription); field public static final String SERVICE_INTERFACE = "android.service.wallpaper.WallpaperService"; field public static final String SERVICE_META_DATA = "android.service.wallpaper"; } @@ -42100,6 +42205,7 @@ package android.service.wallpaper { method public boolean isPreview(); method public boolean isVisible(); method public void notifyColorsChanged(); + method @FlaggedApi("android.app.live_wallpaper_content_handling") @Nullable public android.app.wallpaper.WallpaperDescription onApplyWallpaper(int); method @MainThread public void onApplyWindowInsets(android.view.WindowInsets); method @MainThread public android.os.Bundle onCommand(String, int, int, int, android.os.Bundle, boolean); method @MainThread @Nullable public android.app.WallpaperColors onComputeColors(); @@ -47770,6 +47876,19 @@ package android.telephony.mbms { } +package android.telephony.satellite { + + @FlaggedApi("com.android.internal.telephony.flags.satellite_state_change_listener") public final class SatelliteManager { + method @FlaggedApi("com.android.internal.telephony.flags.satellite_state_change_listener") @RequiresPermission(anyOf={android.Manifest.permission.READ_BASIC_PHONE_STATE, "android.permission.READ_PRIVILEGED_PHONE_STATE", android.Manifest.permission.READ_PHONE_STATE, "carrier privileges"}) public void registerStateChangeListener(@NonNull java.util.concurrent.Executor, @NonNull android.telephony.satellite.SatelliteStateChangeListener); + method @FlaggedApi("com.android.internal.telephony.flags.satellite_state_change_listener") @RequiresPermission(anyOf={android.Manifest.permission.READ_BASIC_PHONE_STATE, "android.permission.READ_PRIVILEGED_PHONE_STATE", android.Manifest.permission.READ_PHONE_STATE, "carrier privileges"}) public void unregisterStateChangeListener(@NonNull android.telephony.satellite.SatelliteStateChangeListener); + } + + @FlaggedApi("com.android.internal.telephony.flags.satellite_state_change_listener") public interface SatelliteStateChangeListener { + method public void onEnabledStateChanged(boolean); + } + +} + package android.text { @Deprecated public class AlteredCharSequence implements java.lang.CharSequence android.text.GetChars { @@ -51080,6 +51199,7 @@ package android.view { method @NonNull public android.view.DisplayShape getShape(); method @Deprecated public void getSize(android.graphics.Point); method public int getState(); + method @FlaggedApi("com.android.server.display.feature.flags.enable_get_suggested_frame_rate") public float getSuggestedFrameRate(int); method public android.view.Display.Mode[] getSupportedModes(); method @Deprecated public float[] getSupportedRefreshRates(); method @Deprecated public int getWidth(); @@ -51097,6 +51217,8 @@ package android.view { field public static final int FLAG_ROUND = 16; // 0x10 field public static final int FLAG_SECURE = 2; // 0x2 field public static final int FLAG_SUPPORTS_PROTECTED_BUFFERS = 1; // 0x1 + field @FlaggedApi("com.android.server.display.feature.flags.enable_get_suggested_frame_rate") public static final int FRAME_RATE_CATEGORY_HIGH = 1; // 0x1 + field @FlaggedApi("com.android.server.display.feature.flags.enable_get_suggested_frame_rate") public static final int FRAME_RATE_CATEGORY_NORMAL = 0; // 0x0 field public static final int INVALID_DISPLAY = -1; // 0xffffffff field public static final int STATE_DOZE = 3; // 0x3 field public static final int STATE_DOZE_SUSPEND = 4; // 0x4 @@ -53125,6 +53247,7 @@ package android.view { method public android.animation.StateListAnimator getStateListAnimator(); method protected int getSuggestedMinimumHeight(); method protected int getSuggestedMinimumWidth(); + method @FlaggedApi("android.view.accessibility.supplemental_description") @Nullable public CharSequence getSupplementalDescription(); method @NonNull public java.util.List<android.graphics.Rect> getSystemGestureExclusionRects(); method @Deprecated public int getSystemUiVisibility(); method public Object getTag(); @@ -53505,6 +53628,7 @@ package android.view { method public void setSoundEffectsEnabled(boolean); method public void setStateDescription(@Nullable CharSequence); method public void setStateListAnimator(android.animation.StateListAnimator); + method @FlaggedApi("android.view.accessibility.supplemental_description") public void setSupplementalDescription(@Nullable CharSequence); method public void setSystemGestureExclusionRects(@NonNull java.util.List<android.graphics.Rect>); method @Deprecated public void setSystemUiVisibility(int); method public void setTag(Object); @@ -54059,6 +54183,7 @@ package android.view { method public void onStopNestedScroll(android.view.View); method public void onViewAdded(android.view.View); method public void onViewRemoved(android.view.View); + method @FlaggedApi("android.view.flags.toolkit_viewgroup_set_requested_frame_rate_api") public void propagateRequestedFrameRate(float, boolean); method public void recomputeViewAttributes(android.view.View); method public void removeAllViews(); method public void removeAllViewsInLayout(); @@ -55111,6 +55236,7 @@ package android.view.accessibility { field public static final int CONTENT_CHANGE_TYPE_PANE_TITLE = 8; // 0x8 field public static final int CONTENT_CHANGE_TYPE_STATE_DESCRIPTION = 64; // 0x40 field public static final int CONTENT_CHANGE_TYPE_SUBTREE = 1; // 0x1 + field @FlaggedApi("android.view.accessibility.supplemental_description") public static final int CONTENT_CHANGE_TYPE_SUPPLEMENTAL_DESCRIPTION = 32768; // 0x8000 field public static final int CONTENT_CHANGE_TYPE_TEXT = 2; // 0x2 field public static final int CONTENT_CHANGE_TYPE_UNDEFINED = 0; // 0x0 field @NonNull public static final android.os.Parcelable.Creator<android.view.accessibility.AccessibilityEvent> CREATOR; @@ -55273,6 +55399,7 @@ package android.view.accessibility { method @Nullable public android.view.accessibility.AccessibilityNodeInfo getParent(int); method public android.view.accessibility.AccessibilityNodeInfo.RangeInfo getRangeInfo(); method @Nullable public CharSequence getStateDescription(); + method @FlaggedApi("android.view.accessibility.supplemental_description") @Nullable public CharSequence getSupplementalDescription(); method public CharSequence getText(); method public int getTextSelectionEnd(); method public int getTextSelectionStart(); @@ -55295,6 +55422,7 @@ package android.view.accessibility { method public boolean isDismissable(); method public boolean isEditable(); method public boolean isEnabled(); + method @FlaggedApi("android.view.accessibility.a11y_is_required_api") public boolean isFieldRequired(); method public boolean isFocusable(); method public boolean isFocused(); method @FlaggedApi("android.view.accessibility.granular_scrolling") public boolean isGranularScrollingSupported(); @@ -55349,6 +55477,7 @@ package android.view.accessibility { method public void setEnabled(boolean); method public void setError(CharSequence); method @FlaggedApi("android.view.accessibility.a11y_expansion_state_api") public void setExpandedState(int); + method @FlaggedApi("android.view.accessibility.a11y_is_required_api") public void setFieldRequired(boolean); method public void setFocusable(boolean); method public void setFocused(boolean); method @FlaggedApi("android.view.accessibility.granular_scrolling") public void setGranularScrollingSupported(boolean); @@ -55381,6 +55510,7 @@ package android.view.accessibility { method public void setSource(android.view.View); method public void setSource(android.view.View, int); method public void setStateDescription(@Nullable CharSequence); + method @FlaggedApi("android.view.accessibility.supplemental_description") public void setSupplementalDescription(@Nullable CharSequence); method public void setText(CharSequence); method public void setTextEntryKey(boolean); method public void setTextSelectable(boolean); @@ -55583,6 +55713,7 @@ package android.view.accessibility { method public int getType(); method @Deprecated public static android.view.accessibility.AccessibilityNodeInfo.RangeInfo obtain(int, float, float, float); field public static final int RANGE_TYPE_FLOAT = 1; // 0x1 + field @FlaggedApi("android.view.accessibility.indeterminate_range_info") public static final int RANGE_TYPE_INDETERMINATE = 3; // 0x3 field public static final int RANGE_TYPE_INT = 0; // 0x0 field public static final int RANGE_TYPE_PERCENT = 2; // 0x2 } @@ -61917,6 +62048,11 @@ package android.window { method public void markSyncReady(); } + @FlaggedApi("com.android.window.flags.predictive_back_system_override_callback") public final class SystemOnBackInvokedCallbacks { + method @FlaggedApi("com.android.window.flags.predictive_back_system_override_callback") @NonNull public static android.window.OnBackInvokedCallback finishAndRemoveTaskCallback(@NonNull android.app.Activity); + method @FlaggedApi("com.android.window.flags.predictive_back_system_override_callback") @NonNull public static android.window.OnBackInvokedCallback moveTaskToBackCallback(@NonNull android.app.Activity); + } + @FlaggedApi("com.android.window.flags.trusted_presentation_listener_for_window") public final class TrustedPresentationThresholds implements android.os.Parcelable { ctor @FlaggedApi("com.android.window.flags.trusted_presentation_listener_for_window") public TrustedPresentationThresholds(@FloatRange(from=0.0f, fromInclusive=false, to=1.0f) float, @FloatRange(from=0.0f, fromInclusive=false, to=1.0f) float, @IntRange(from=1) int); method @FlaggedApi("com.android.window.flags.trusted_presentation_listener_for_window") public int describeContents(); diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt index 4d1a42314d97..bc73220cb4c1 100644 --- a/core/api/module-lib-current.txt +++ b/core/api/module-lib-current.txt @@ -102,6 +102,7 @@ package android.content { method @NonNull public android.os.UserHandle getUser(); field public static final String PAC_PROXY_SERVICE = "pac_proxy"; field public static final String TEST_NETWORK_SERVICE = "test_network"; + field @FlaggedApi("android.os.mainline_vcn_platform_api") public static final String VCN_MANAGEMENT_SERVICE = "vcn_management"; field @FlaggedApi("android.webkit.update_service_ipc_wrapper") public static final String WEBVIEW_UPDATE_SERVICE = "webviewupdate"; } @@ -144,21 +145,12 @@ package android.hardware.usb { public class UsbManager { method @RequiresPermission(android.Manifest.permission.MANAGE_USB) public int getGadgetHalVersion(); - method @RequiresPermission(android.Manifest.permission.MANAGE_USB) public int getUsbBandwidthMbps(); method @RequiresPermission(android.Manifest.permission.MANAGE_USB) public int getUsbHalVersion(); field public static final int GADGET_HAL_NOT_SUPPORTED = -1; // 0xffffffff field public static final int GADGET_HAL_V1_0 = 10; // 0xa field public static final int GADGET_HAL_V1_1 = 11; // 0xb field public static final int GADGET_HAL_V1_2 = 12; // 0xc field public static final int GADGET_HAL_V2_0 = 20; // 0x14 - field public static final int USB_DATA_TRANSFER_RATE_10G = 10240; // 0x2800 - field public static final int USB_DATA_TRANSFER_RATE_20G = 20480; // 0x5000 - field public static final int USB_DATA_TRANSFER_RATE_40G = 40960; // 0xa000 - field public static final int USB_DATA_TRANSFER_RATE_5G = 5120; // 0x1400 - field public static final int USB_DATA_TRANSFER_RATE_FULL_SPEED = 12; // 0xc - field public static final int USB_DATA_TRANSFER_RATE_HIGH_SPEED = 480; // 0x1e0 - field public static final int USB_DATA_TRANSFER_RATE_LOW_SPEED = 2; // 0x2 - field public static final int USB_DATA_TRANSFER_RATE_UNKNOWN = -1; // 0xffffffff field public static final int USB_HAL_NOT_SUPPORTED = -1; // 0xffffffff field public static final int USB_HAL_RETRY = -2; // 0xfffffffe field public static final int USB_HAL_V1_0 = 10; // 0xa diff --git a/core/api/system-current.txt b/core/api/system-current.txt index f9cd31651623..02cd00d0f86a 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -382,6 +382,7 @@ package android { field public static final String SHOW_KEYGUARD_MESSAGE = "android.permission.SHOW_KEYGUARD_MESSAGE"; field public static final String SHUTDOWN = "android.permission.SHUTDOWN"; field public static final String SIGNAL_REBOOT_READINESS = "android.permission.SIGNAL_REBOOT_READINESS"; + field @FlaggedApi("android.media.tv.flags.kids_mode_tvdb_sharing") public static final String SINGLE_USER_TIS_ACCESS = "android.permission.SINGLE_USER_TIS_ACCESS"; field public static final String SOUND_TRIGGER_RUN_IN_BATTERY_SAVER = "android.permission.SOUND_TRIGGER_RUN_IN_BATTERY_SAVER"; field public static final String STAGE_HEALTH_CONNECT_REMOTE_DATA = "android.permission.STAGE_HEALTH_CONNECT_REMOTE_DATA"; field public static final String START_ACTIVITIES_FROM_BACKGROUND = "android.permission.START_ACTIVITIES_FROM_BACKGROUND"; @@ -703,6 +704,7 @@ package android.app { field public static final String OPSTR_READ_MEDIA_IMAGES = "android:read_media_images"; field public static final String OPSTR_READ_MEDIA_VIDEO = "android:read_media_video"; field public static final String OPSTR_READ_MEDIA_VISUAL_USER_SELECTED = "android:read_media_visual_user_selected"; + field @FlaggedApi("android.permission.flags.platform_skin_temperature_enabled") public static final String OPSTR_READ_SKIN_TEMPERATURE = "android:read_skin_temperature"; field public static final String OPSTR_READ_WRITE_HEALTH_DATA = "android:read_write_health_data"; field public static final String OPSTR_RECEIVE_AMBIENT_TRIGGER_AUDIO = "android:receive_ambient_trigger_audio"; field public static final String OPSTR_RECEIVE_EMERGENCY_BROADCAST = "android:receive_emergency_broadcast"; @@ -1047,6 +1049,7 @@ package android.app { method public int getUserLockedFields(); method public boolean isDeleted(); method public void populateFromXml(org.xmlpull.v1.XmlPullParser); + method @FlaggedApi("android.service.notification.notification_conversation_channel_management") public void setImportantConversation(boolean); method public org.json.JSONObject toJson() throws org.json.JSONException; method public void writeXml(org.xmlpull.v1.XmlSerializer) throws java.io.IOException; field public static final int USER_LOCKED_SOUND = 32; // 0x20 @@ -1264,8 +1267,10 @@ package android.app { public class WallpaperManager { method @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL) public void clearWallpaper(int, int); method @FloatRange(from=0.0f, to=1.0f) @RequiresPermission(android.Manifest.permission.SET_WALLPAPER_DIM_AMOUNT) public float getWallpaperDimAmount(); + method @FlaggedApi("android.app.live_wallpaper_content_handling") @Nullable @RequiresPermission(android.Manifest.permission.READ_WALLPAPER_INTERNAL) public android.app.wallpaper.WallpaperInstance getWallpaperInstance(int); method public void setDisplayOffset(android.os.IBinder, int, int); method @RequiresPermission(android.Manifest.permission.SET_WALLPAPER_COMPONENT) public boolean setWallpaperComponent(android.content.ComponentName); + method @FlaggedApi("android.app.live_wallpaper_content_handling") @RequiresPermission(allOf={android.Manifest.permission.SET_WALLPAPER_COMPONENT, android.Manifest.permission.INTERACT_ACROSS_USERS_FULL}, conditional=true) public boolean setWallpaperComponentWithDescription(@NonNull android.app.wallpaper.WallpaperDescription, int); method @RequiresPermission(android.Manifest.permission.SET_WALLPAPER_COMPONENT) public boolean setWallpaperComponentWithFlags(@NonNull android.content.ComponentName, int); method @RequiresPermission(android.Manifest.permission.SET_WALLPAPER_DIM_AMOUNT) public void setWallpaperDimAmount(@FloatRange(from=0.0f, to=1.0f) float); } @@ -5286,11 +5291,13 @@ package android.hardware.display { public final class VirtualDisplayConfig implements android.os.Parcelable { method @FlaggedApi("android.companion.virtualdevice.flags.virtual_display_insets") @Nullable public android.view.DisplayCutout getDisplayCutout(); method @FlaggedApi("android.companion.virtual.flags.vdm_custom_home") public boolean isHomeSupported(); + method @FlaggedApi("com.android.window.flags.vdm_force_app_universal_resizable_api") public boolean isIgnoreActivitySizeRestrictions(); } public static final class VirtualDisplayConfig.Builder { method @FlaggedApi("android.companion.virtualdevice.flags.virtual_display_insets") @NonNull public android.hardware.display.VirtualDisplayConfig.Builder setDisplayCutout(@Nullable android.view.DisplayCutout); method @FlaggedApi("android.companion.virtual.flags.vdm_custom_home") @NonNull public android.hardware.display.VirtualDisplayConfig.Builder setHomeSupported(boolean); + method @FlaggedApi("com.android.window.flags.vdm_force_app_universal_resizable_api") @NonNull public android.hardware.display.VirtualDisplayConfig.Builder setIgnoreActivitySizeRestrictions(boolean); } } @@ -7066,6 +7073,7 @@ package android.hardware.usb { public class UsbManager { method @RequiresPermission(android.Manifest.permission.MANAGE_USB) public long getCurrentFunctions(); method @NonNull @RequiresPermission(android.Manifest.permission.MANAGE_USB) public java.util.List<android.hardware.usb.UsbPort> getPorts(); + method @FlaggedApi("android.hardware.usb.flags.expose_usb_speed_system_api") @RequiresPermission(android.Manifest.permission.MANAGE_USB) public int getUsbBandwidthMbps(); method @RequiresPermission(android.Manifest.permission.MANAGE_USB) public void grantPermission(android.hardware.usb.UsbDevice, String); method public static boolean isUvcSupportEnabled(); method @RequiresPermission(android.Manifest.permission.MANAGE_USB) public void registerDisplayPortAltModeInfoListener(@NonNull java.util.concurrent.Executor, @NonNull android.hardware.usb.UsbManager.DisplayPortAltModeInfoListener); @@ -7092,6 +7100,14 @@ package android.hardware.usb { field public static final long FUNCTION_UVC = 128L; // 0x80L field public static final String USB_CONFIGURED = "configured"; field public static final String USB_CONNECTED = "connected"; + field @FlaggedApi("android.hardware.usb.flags.expose_usb_speed_system_api") public static final int USB_DATA_TRANSFER_RATE_10G = 10240; // 0x2800 + field @FlaggedApi("android.hardware.usb.flags.expose_usb_speed_system_api") public static final int USB_DATA_TRANSFER_RATE_20G = 20480; // 0x5000 + field @FlaggedApi("android.hardware.usb.flags.expose_usb_speed_system_api") public static final int USB_DATA_TRANSFER_RATE_40G = 40960; // 0xa000 + field @FlaggedApi("android.hardware.usb.flags.expose_usb_speed_system_api") public static final int USB_DATA_TRANSFER_RATE_5G = 5120; // 0x1400 + field @FlaggedApi("android.hardware.usb.flags.expose_usb_speed_system_api") public static final int USB_DATA_TRANSFER_RATE_FULL_SPEED = 12; // 0xc + field @FlaggedApi("android.hardware.usb.flags.expose_usb_speed_system_api") public static final int USB_DATA_TRANSFER_RATE_HIGH_SPEED = 480; // 0x1e0 + field @FlaggedApi("android.hardware.usb.flags.expose_usb_speed_system_api") public static final int USB_DATA_TRANSFER_RATE_LOW_SPEED = 2; // 0x2 + field @FlaggedApi("android.hardware.usb.flags.expose_usb_speed_system_api") public static final int USB_DATA_TRANSFER_RATE_UNKNOWN = -1; // 0xffffffff field public static final String USB_FUNCTION_NCM = "ncm"; field public static final String USB_FUNCTION_RNDIS = "rndis"; } @@ -7218,6 +7234,7 @@ package android.media { field @RequiresPermission(allOf={android.Manifest.permission.MODIFY_PHONE_STATE, android.Manifest.permission.MODIFY_AUDIO_ROUTING}) public static final int USAGE_CALL_ASSISTANT = 17; // 0x11 field @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public static final int USAGE_EMERGENCY = 1000; // 0x3e8 field @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public static final int USAGE_SAFETY = 1001; // 0x3e9 + field @FlaggedApi("android.media.audio.speaker_cleanup_usage") @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public static final int USAGE_SPEAKER_CLEANUP = 1004; // 0x3ec field @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public static final int USAGE_VEHICLE_STATUS = 1002; // 0x3ea } @@ -7375,7 +7392,7 @@ package android.media { field public static final String EXTRA_VOLUME_STREAM_VALUE = "android.media.EXTRA_VOLUME_STREAM_VALUE"; field public static final int FLAG_BLUETOOTH_ABS_VOLUME = 64; // 0x40 field @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) public static final int STREAM_ASSISTANT = 11; // 0xb - field public static final int STREAM_BLUETOOTH_SCO = 6; // 0x6 + field @Deprecated @FlaggedApi("android.media.audio.deprecate_stream_bt_sco") public static final int STREAM_BLUETOOTH_SCO = 6; // 0x6 field public static final int SUCCESS = 0; // 0x0 } @@ -7572,6 +7589,11 @@ package android.media { method @NonNull public android.media.HwAudioSource.Builder setAudioDeviceInfo(@NonNull android.media.AudioDeviceInfo); } + public final class MediaCas implements java.lang.AutoCloseable { + method @FlaggedApi("android.media.tv.flags.set_resource_holder_retain") @RequiresPermission("android.permission.TUNER_RESOURCE_ACCESS") public void setResourceHolderRetain(boolean); + method @FlaggedApi("com.android.media.flags.update_client_profile_priority") @RequiresPermission("android.permission.TUNER_RESOURCE_ACCESS") public boolean updateResourcePriority(int, int); + } + public final class MediaCodec { method @NonNull @RequiresPermission(android.Manifest.permission.MEDIA_RESOURCE_OVERRIDE_PID) public static android.media.MediaCodec createByCodecNameForClient(@NonNull String, int, int) throws java.io.IOException; } @@ -7587,7 +7609,7 @@ package android.media { public final class MediaRecorder.AudioSource { field @RequiresPermission(android.Manifest.permission.CAPTURE_AUDIO_OUTPUT) public static final int ECHO_REFERENCE = 1997; // 0x7cd field @RequiresPermission(android.Manifest.permission.CAPTURE_AUDIO_HOTWORD) public static final int HOTWORD = 1999; // 0x7cf - field @RequiresPermission(android.Manifest.permission.CAPTURE_AUDIO_OUTPUT) public static final int RADIO_TUNER = 1998; // 0x7ce + field @RequiresPermission(android.Manifest.permission.CAPTURE_TUNER_AUDIO_INPUT) public static final int RADIO_TUNER = 1998; // 0x7ce field @RequiresPermission(android.Manifest.permission.ACCESS_ULTRASOUND) public static final int ULTRASOUND = 2000; // 0x7d0 } @@ -8144,6 +8166,7 @@ package android.media.tv { method @RequiresPermission("android.permission.TUNER_RESOURCE_ACCESS") public int getClientPid(@NonNull String); method @RequiresPermission("android.permission.TUNER_RESOURCE_ACCESS") public int getClientPriority(int, @NonNull String); method @RequiresPermission("android.permission.TUNER_RESOURCE_ACCESS") public int getClientPriority(int); + method @FlaggedApi("android.media.tv.flags.kids_mode_tvdb_sharing") @RequiresPermission(android.Manifest.permission.SINGLE_USER_TIS_ACCESS) public int getClientUserId(@NonNull String); method @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_TUNED_INFO) public java.util.List<android.media.tv.TunedInfo> getCurrentTunedInfos(); method @NonNull @RequiresPermission("android.permission.DVB_DEVICE") public java.util.List<android.media.tv.DvbDeviceInfo> getDvbDeviceList(); method @Nullable @RequiresPermission(android.Manifest.permission.TIS_EXTENSION_INTERFACE) public android.os.IBinder getExtensionInterface(@NonNull String, @NonNull String); @@ -8344,6 +8367,7 @@ package android.media.tv.tuner { method public int setLnaEnabled(boolean); method public int setMaxNumberOfFrontends(int, @IntRange(from=0) int); method public void setOnTuneEventListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.tv.tuner.frontend.OnTuneEventListener); + method @FlaggedApi("android.media.tv.flags.set_resource_holder_retain") @RequiresPermission("android.permission.TUNER_RESOURCE_ACCESS") public void setResourceHolderRetain(boolean); method public void setResourceLostListener(@NonNull java.util.concurrent.Executor, @NonNull android.media.tv.tuner.Tuner.OnResourceLostListener); method public void shareFrontendFromTuner(@NonNull android.media.tv.tuner.Tuner); method public int transferOwner(@NonNull android.media.tv.tuner.Tuner); @@ -10991,7 +11015,7 @@ package android.os { public class Environment { method @NonNull public static java.io.File getDataCePackageDirectoryForUser(@NonNull java.util.UUID, @NonNull android.os.UserHandle, @NonNull String); method @NonNull public static java.io.File getDataDePackageDirectoryForUser(@NonNull java.util.UUID, @NonNull android.os.UserHandle, @NonNull String); - method @FlaggedApi("android.crashrecovery.flags.enable_crashrecovery") @NonNull public static java.io.File getDataSystemDeDirectory(); + method @FlaggedApi("android.crashrecovery.flags.enable_crashrecovery") @NonNull public static java.io.File getDataSystemDeviceProtectedDirectory(); method @NonNull public static java.util.Collection<java.io.File> getInternalMediaDirectories(); method @NonNull public static java.io.File getOdmDirectory(); method @NonNull public static java.io.File getOemDirectory(); @@ -12091,7 +12115,7 @@ package android.provider { } public static final class ContactsContract.Settings implements android.provider.ContactsContract.SettingsColumns { - method @RequiresPermission(android.Manifest.permission.SET_DEFAULT_ACCOUNT_FOR_CONTACTS) public static void setDefaultAccount(@NonNull android.content.ContentResolver, @Nullable android.accounts.Account); + method @Deprecated @FlaggedApi("android.provider.new_default_account_api_enabled") @RequiresPermission(android.Manifest.permission.SET_DEFAULT_ACCOUNT_FOR_CONTACTS) public static void setDefaultAccount(@NonNull android.content.ContentResolver, @Nullable android.accounts.Account); } public static final class ContactsContract.SimContacts { @@ -12448,7 +12472,16 @@ package android.security { package android.security.advancedprotection { - @FlaggedApi("android.security.aapm_api") public class AdvancedProtectionManager { + @FlaggedApi("android.security.aapm_api") public final class AdvancedProtectionFeature implements android.os.Parcelable { + ctor public AdvancedProtectionFeature(@NonNull String); + method public int describeContents(); + method @NonNull public String getId(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.security.advancedprotection.AdvancedProtectionFeature> CREATOR; + } + + @FlaggedApi("android.security.aapm_api") public final class AdvancedProtectionManager { + method @NonNull @RequiresPermission(android.Manifest.permission.SET_ADVANCED_PROTECTION_MODE) public java.util.List<android.security.advancedprotection.AdvancedProtectionFeature> getAdvancedProtectionFeatures(); method @RequiresPermission(android.Manifest.permission.SET_ADVANCED_PROTECTION_MODE) public void setAdvancedProtectionEnabled(boolean); } @@ -15575,9 +15608,9 @@ package android.telephony { } @FlaggedApi("com.android.internal.telephony.flags.emergency_callback_mode_notification") public static interface TelephonyCallback.EmergencyCallbackModeListener { - method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public void onCallbackModeRestarted(int, @NonNull java.time.Duration, int); - method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public void onCallbackModeStarted(int, @NonNull java.time.Duration, int); - method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public void onCallbackModeStopped(int, int, int); + method public void onCallbackModeRestarted(int, @NonNull java.time.Duration, int); + method public void onCallbackModeStarted(int, @NonNull java.time.Duration, int); + method public void onCallbackModeStopped(int, int, int); } public static interface TelephonyCallback.LinkCapacityEstimateChangedListener { @@ -15686,6 +15719,9 @@ package android.telephony { method @FlaggedApi("android.permission.flags.get_emergency_role_holder_api_enabled") @Nullable @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public String getEmergencyAssistancePackageName(); method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public boolean getEmergencyCallbackMode(); method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public int getEmergencyNumberDbVersion(); + method @FlaggedApi("com.android.internal.telephony.flags.support_isim_record") @NonNull @RequiresPermission(value=android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE, conditional=true) public java.util.List<java.lang.String> getImsPcscfAddresses(); + method @FlaggedApi("com.android.internal.telephony.flags.support_isim_record") @Nullable @RequiresPermission(android.Manifest.permission.USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER) public String getImsPrivateUserIdentity(); + method @FlaggedApi("com.android.internal.telephony.flags.support_isim_record") @NonNull @RequiresPermission(value=android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE, conditional=true) public java.util.List<android.net.Uri> getImsPublicUserIdentities(); method @Nullable @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public String getIsimDomain(); method @Nullable @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public String getIsimIst(); method @FlaggedApi("com.android.server.telecom.flags.get_last_known_cell_identity") @Nullable @RequiresPermission(allOf={android.Manifest.permission.ACCESS_FINE_LOCATION, android.Manifest.permission.ACCESS_LAST_KNOWN_CELL_ID}) public android.telephony.CellIdentity getLastKnownCellIdentity(); @@ -15704,6 +15740,7 @@ package android.telephony { method @Deprecated @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public int getSimCardState(int); method @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public int getSimCardState(int, int); method @Nullable @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public java.util.Locale getSimLocale(); + method @FlaggedApi("com.android.internal.telephony.flags.support_isim_record") @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public void getSimServiceTable(int, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<byte[],java.lang.Exception>); method @NonNull @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public java.util.Collection<android.telephony.UiccSlotMapping> getSimSlotMapping(); method @NonNull @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public java.util.List<android.telephony.RadioAccessSpecifier> getSystemSelectionChannels(); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public java.util.List<android.telephony.TelephonyHistogram> getTelephonyHistograms(); @@ -16828,8 +16865,11 @@ package android.telephony.ims { method public void callSessionRttMessageReceived(String); method public void callSessionRttModifyRequestReceived(android.telephony.ims.ImsCallProfile); method public void callSessionRttModifyResponseReceived(int); + method @FlaggedApi("com.android.internal.telephony.flags.support_ims_mmtel_interface") public final void callSessionSendAnbrQuery(int, int, @IntRange(from=0) int); method public void callSessionSuppServiceReceived(android.telephony.ims.ImsSuppServiceNotification); method public void callSessionTerminated(android.telephony.ims.ImsReasonInfo); + method @FlaggedApi("com.android.internal.telephony.flags.support_ims_mmtel_interface") public void callSessionTransferFailed(@NonNull android.telephony.ims.ImsReasonInfo); + method @FlaggedApi("com.android.internal.telephony.flags.support_ims_mmtel_interface") public void callSessionTransferred(); method public void callSessionTtyModeReceived(int); method public void callSessionUpdateFailed(android.telephony.ims.ImsReasonInfo); method public void callSessionUpdateReceived(android.telephony.ims.ImsCallProfile); @@ -17634,6 +17674,26 @@ package android.telephony.ims.feature { method public int getRadioTech(); } + @FlaggedApi("com.android.internal.telephony.flags.support_ims_mmtel_interface") public final class ConnectionFailureInfo implements android.os.Parcelable { + method public int describeContents(); + method public int getCauseCode(); + method public int getReason(); + method public int getWaitTimeMillis(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.telephony.ims.feature.ConnectionFailureInfo> CREATOR; + field public static final int REASON_ACCESS_DENIED = 1; // 0x1 + field public static final int REASON_NAS_FAILURE = 2; // 0x2 + field public static final int REASON_NONE = 0; // 0x0 + field public static final int REASON_NO_SERVICE = 7; // 0x7 + field public static final int REASON_PDN_NOT_AVAILABLE = 8; // 0x8 + field public static final int REASON_RACH_FAILURE = 3; // 0x3 + field public static final int REASON_RF_BUSY = 9; // 0x9 + field public static final int REASON_RLC_FAILURE = 4; // 0x4 + field public static final int REASON_RRC_REJECT = 5; // 0x5 + field public static final int REASON_RRC_TIMEOUT = 6; // 0x6 + field public static final int REASON_UNSPECIFIED = 65535; // 0xffff + } + public abstract class ImsFeature { ctor public ImsFeature(); method public abstract void changeEnabledCapabilities(android.telephony.ims.feature.CapabilityChangeRequest, android.telephony.ims.feature.ImsFeature.CapabilityCallbackProxy); @@ -17660,6 +17720,11 @@ package android.telephony.ims.feature { method public void onChangeCapabilityConfigurationError(int, int, int); } + @FlaggedApi("com.android.internal.telephony.flags.support_ims_mmtel_interface") public interface ImsTrafficSessionCallback { + method public void onError(@NonNull android.telephony.ims.feature.ConnectionFailureInfo); + method public void onReady(); + } + public class MmTelFeature extends android.telephony.ims.feature.ImsFeature { ctor public MmTelFeature(); ctor public MmTelFeature(@NonNull java.util.concurrent.Executor); @@ -17672,6 +17737,7 @@ package android.telephony.ims.feature { method @NonNull public android.telephony.ims.stub.ImsMultiEndpointImplBase getMultiEndpoint(); method @NonNull public android.telephony.ims.stub.ImsSmsImplBase getSmsImplementation(); method @NonNull public android.telephony.ims.stub.ImsUtImplBase getUt(); + method @FlaggedApi("com.android.internal.telephony.flags.support_ims_mmtel_interface") public final void modifyImsTrafficSession(int, @NonNull android.telephony.ims.feature.ImsTrafficSessionCallback); method public final void notifyCapabilitiesStatusChanged(@NonNull android.telephony.ims.feature.MmTelFeature.MmTelCapabilities); method @Deprecated public final void notifyIncomingCall(@NonNull android.telephony.ims.stub.ImsCallSessionImplBase, @NonNull android.os.Bundle); method @Nullable public final android.telephony.ims.ImsCallSessionListener notifyIncomingCall(@NonNull android.telephony.ims.stub.ImsCallSessionImplBase, @NonNull String, @NonNull android.os.Bundle); @@ -17692,10 +17758,24 @@ package android.telephony.ims.feature { method public void setTerminalBasedCallWaitingStatus(boolean); method public void setUiTtyMode(int, @Nullable android.os.Message); method public int shouldProcessCall(@NonNull String[]); + method @FlaggedApi("com.android.internal.telephony.flags.support_ims_mmtel_interface") public final void startImsTrafficSession(int, int, int, @NonNull java.util.concurrent.Executor, @NonNull android.telephony.ims.feature.ImsTrafficSessionCallback); + method @FlaggedApi("com.android.internal.telephony.flags.support_ims_mmtel_interface") public final void stopImsTrafficSession(@NonNull android.telephony.ims.feature.ImsTrafficSessionCallback); + method @FlaggedApi("com.android.internal.telephony.flags.support_ims_mmtel_interface") public final void triggerEpsFallback(int); field public static final int AUDIO_HANDLER_ANDROID = 0; // 0x0 field public static final int AUDIO_HANDLER_BASEBAND = 1; // 0x1 + field @FlaggedApi("com.android.internal.telephony.flags.support_ims_mmtel_interface") public static final int EPS_FALLBACK_REASON_NO_NETWORK_RESPONSE = 2; // 0x2 + field @FlaggedApi("com.android.internal.telephony.flags.support_ims_mmtel_interface") public static final int EPS_FALLBACK_REASON_NO_NETWORK_TRIGGER = 1; // 0x1 field public static final String EXTRA_IS_UNKNOWN_CALL = "android.telephony.ims.feature.extra.IS_UNKNOWN_CALL"; field public static final String EXTRA_IS_USSD = "android.telephony.ims.feature.extra.IS_USSD"; + field @FlaggedApi("com.android.internal.telephony.flags.support_ims_mmtel_interface") public static final int IMS_TRAFFIC_DIRECTION_INCOMING = 0; // 0x0 + field @FlaggedApi("com.android.internal.telephony.flags.support_ims_mmtel_interface") public static final int IMS_TRAFFIC_DIRECTION_OUTGOING = 1; // 0x1 + field @FlaggedApi("com.android.internal.telephony.flags.support_ims_mmtel_interface") public static final int IMS_TRAFFIC_TYPE_EMERGENCY = 0; // 0x0 + field @FlaggedApi("com.android.internal.telephony.flags.support_ims_mmtel_interface") public static final int IMS_TRAFFIC_TYPE_EMERGENCY_SMS = 1; // 0x1 + field @FlaggedApi("com.android.internal.telephony.flags.support_ims_mmtel_interface") public static final int IMS_TRAFFIC_TYPE_REGISTRATION = 5; // 0x5 + field @FlaggedApi("com.android.internal.telephony.flags.support_ims_mmtel_interface") public static final int IMS_TRAFFIC_TYPE_SMS = 4; // 0x4 + field @FlaggedApi("com.android.internal.telephony.flags.support_ims_mmtel_interface") public static final int IMS_TRAFFIC_TYPE_UT_XCAP = 6; // 0x6 + field @FlaggedApi("com.android.internal.telephony.flags.support_ims_mmtel_interface") public static final int IMS_TRAFFIC_TYPE_VIDEO = 3; // 0x3 + field @FlaggedApi("com.android.internal.telephony.flags.support_ims_mmtel_interface") public static final int IMS_TRAFFIC_TYPE_VOICE = 2; // 0x2 field public static final int PROCESS_CALL_CSFB = 1; // 0x1 field public static final int PROCESS_CALL_IMS = 0; // 0x0 } @@ -18179,7 +18259,7 @@ package android.telephony.satellite { method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public void onSatelliteDatagramReceived(long, @NonNull android.telephony.satellite.SatelliteDatagram, int, @NonNull java.util.function.Consumer<java.lang.Void>); } - @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") public final class SatelliteManager { + @FlaggedApi("com.android.internal.telephony.flags.satellite_state_change_listener") public final class SatelliteManager { method @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void addAttachRestrictionForCarrier(int, int, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>); method @FlaggedApi("com.android.internal.telephony.flags.oem_enabled_satellite_flag") @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public void deprovisionService(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.lang.Integer>); method @FlaggedApi("com.android.internal.telephony.flags.carrier_enabled_satellite_flag") @NonNull @RequiresPermission(android.Manifest.permission.SATELLITE_COMMUNICATION) public java.util.Set<java.lang.Integer> getAttachRestrictionReasonsForCarrier(int); @@ -19001,10 +19081,10 @@ package android.webkit { field public final android.content.pm.Signature[] signatures; } - public final class WebViewUpdateService { - method public static android.webkit.WebViewProviderInfo[] getAllWebViewPackages(); - method public static String getCurrentWebViewPackageName(); - method @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS) public static android.webkit.WebViewProviderInfo[] getValidWebViewPackages(); + @Deprecated @FlaggedApi("android.webkit.update_service_ipc_wrapper") public final class WebViewUpdateService { + method @Deprecated public static android.webkit.WebViewProviderInfo[] getAllWebViewPackages(); + method @Deprecated public static String getCurrentWebViewPackageName(); + method @Deprecated @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS) public static android.webkit.WebViewProviderInfo[] getValidWebViewPackages(); } } diff --git a/core/api/test-current.txt b/core/api/test-current.txt index a0c4fdb90bf1..98d6f58e6bcf 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -369,6 +369,7 @@ package android.app { } public final class NotificationChannel implements android.os.Parcelable { + method @FlaggedApi("android.service.notification.notification_conversation_channel_management") @NonNull public android.app.NotificationChannel copy(); method public int getOriginalImportance(); method public boolean isImportanceLockedByCriticalDeviceFunction(); method public void lockFields(int); @@ -376,7 +377,7 @@ package android.app { method public void setDeletedTimeMs(long); method public void setDemoted(boolean); method public void setImportanceLockedByCriticalDeviceFunction(boolean); - method public void setImportantConversation(boolean); + method @FlaggedApi("android.service.notification.notification_conversation_channel_management") public void setImportantConversation(boolean); method public void setOriginalImportance(int); method public void setUserVisibleTaskShown(boolean); field @FlaggedApi("android.service.notification.notification_classification") public static final String NEWS_ID = "android.app.news"; @@ -1029,6 +1030,7 @@ package android.content { public abstract class Context { method @NonNull public java.io.File getCrateDir(@NonNull String); method public abstract int getDisplayId(); + method @NonNull public java.util.List<android.content.IntentFilter> getRegisteredIntentFilters(@NonNull android.content.BroadcastReceiver); method @NonNull public android.os.UserHandle getUser(); method public int getUserId(); method public void setAutofillOptions(@Nullable android.content.AutofillOptions); diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index 4fb35c3d5f5c..7a1c759a3ec4 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -1262,6 +1262,33 @@ 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> + */ + @FlaggedApi(com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB_EDUCATION) + public final void setLimitSystemEducationDialogs(boolean limitSystemEducationDialogs) { + try { + ActivityTaskManager + .getService().setLimitSystemEducationDialogs(mToken, limitSystemEducationDialogs); + } catch (RemoteException e) { + // Empty + } + } + /** Return the application that owns this activity. */ public final Application getApplication() { return mApplication; diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 50cd2672e3fe..ca98da76b78f 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -1747,7 +1747,7 @@ public final class ActivityThread extends ClientTransactionHandler printRow(pw, TWO_COUNT_COLUMNS, "Death Recipients:", binderDeathObjectCount, "WebViews:", webviewInstanceCount); - if (com.android.libcore.Flags.nativeMetrics()) { + if (com.android.libcore.readonly.Flags.nativeMetrics()) { dumpMemInfoNativeAllocations(pw); } diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index 56538d92baa0..2b0e86c3205c 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -1618,9 +1618,12 @@ public class AppOpsManager { /** @hide Access to read heart rate sensor. */ public static final int OP_READ_HEART_RATE = AppProtoEnums.APP_OP_READ_HEART_RATE; + /** @hide Access to read skin temperature. */ + public static final int OP_READ_SKIN_TEMPERATURE = AppProtoEnums.APP_OP_READ_SKIN_TEMPERATURE; + /** @hide */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) - public static final int _NUM_OP = 150; + public static final int _NUM_OP = 151; /** * All app ops represented as strings. @@ -1774,6 +1777,7 @@ public class AppOpsManager { OPSTR_EMERGENCY_LOCATION, OPSTR_RECEIVE_SENSITIVE_NOTIFICATIONS, OPSTR_READ_HEART_RATE, + OPSTR_READ_SKIN_TEMPERATURE, }) public @interface AppOpString {} @@ -2516,6 +2520,11 @@ public class AppOpsManager { @FlaggedApi(Flags.FLAG_REPLACE_BODY_SENSOR_PERMISSION_ENABLED) public static final String OPSTR_READ_HEART_RATE = "android:read_heart_rate"; + /** @hide Access to read skin temperature. */ + @SystemApi + @FlaggedApi(Flags.FLAG_PLATFORM_SKIN_TEMPERATURE_ENABLED) + public static final String OPSTR_READ_SKIN_TEMPERATURE = "android:read_skin_temperature"; + /** {@link #sAppOpsToNote} not initialized yet for this op */ private static final byte SHOULD_COLLECT_NOTE_OP_NOT_INITIALIZED = 0; /** Should not collect noting of this app-op in {@link #sAppOpsToNote} */ @@ -2591,6 +2600,7 @@ public class AppOpsManager { OP_POST_NOTIFICATION, // Health Flags.replaceBodySensorPermissionEnabled() ? OP_READ_HEART_RATE : OP_NONE, + Flags.platformSkinTemperatureEnabled() ? OP_READ_SKIN_TEMPERATURE : OP_NONE, }; /** @@ -3103,6 +3113,11 @@ public class AppOpsManager { .setPermission(Flags.replaceBodySensorPermissionEnabled() ? HealthPermissions.READ_HEART_RATE : null) .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(), + new AppOpInfo.Builder(OP_READ_SKIN_TEMPERATURE, OPSTR_READ_SKIN_TEMPERATURE, + "READ_SKIN_TEMPERATURE").setPermission( + Flags.platformSkinTemperatureEnabled() + ? HealthPermissions.READ_SKIN_TEMPERATURE : null) + .setDefaultMode(AppOpsManager.MODE_ALLOWED).build(), }; // The number of longs needed to form a full bitmask of app ops diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index ecef0db46bb2..2cf718ef364f 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -1962,6 +1962,24 @@ class ContextImpl extends Context { } } + @Override + @NonNull + public List<IntentFilter> getRegisteredIntentFilters(@NonNull BroadcastReceiver receiver) { + if (mPackageInfo != null) { + IIntentReceiver rd = mPackageInfo.findRegisteredReceiverDispatcher( + receiver, getOuterContext()); + try { + final List<IntentFilter> filters = ActivityManager.getService() + .getRegisteredIntentFilters(rd); + return filters == null ? new ArrayList<>() : filters; + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } else { + throw new RuntimeException("Not supported in system context"); + } + } + private void validateServiceIntent(Intent service) { if (service.getComponent() == null && service.getPackage() == null) { if (getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.LOLLIPOP) { diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl index f880901429f7..34a3ad19b270 100644 --- a/core/java/android/app/IActivityManager.aidl +++ b/core/java/android/app/IActivityManager.aidl @@ -181,6 +181,7 @@ interface IActivityManager { in IntentFilter filter, in String requiredPermission, int userId, int flags); @UnsupportedAppUsage void unregisterReceiver(in IIntentReceiver receiver); + List<IntentFilter> getRegisteredIntentFilters(in IIntentReceiver receiver); /** @deprecated Use {@link #broadcastIntentWithFeature} instead */ @UnsupportedAppUsage(maxTargetSdk=29, publicAlternatives="Use {@link android.content.Context#sendBroadcast(android.content.Intent)} instead") int broadcastIntent(in IApplicationThread caller, in Intent intent, diff --git a/core/java/android/app/IActivityTaskManager.aidl b/core/java/android/app/IActivityTaskManager.aidl index 003104ab2b0b..ec7b72ec677e 100644 --- a/core/java/android/app/IActivityTaskManager.aidl +++ b/core/java/android/app/IActivityTaskManager.aidl @@ -242,6 +242,9 @@ interface IActivityTaskManager { boolean supportsLocalVoiceInteraction(); + // Sets whether system educational dialogs should be limited + void setLimitSystemEducationDialogs(IBinder appToken, boolean limitSystemEducationDialogs); + // Get device configuration ConfigurationInfo getDeviceConfigurationInfo(); diff --git a/core/java/android/app/INotificationManager.aidl b/core/java/android/app/INotificationManager.aidl index a7597b4d566d..a97fa18a3599 100644 --- a/core/java/android/app/INotificationManager.aidl +++ b/core/java/android/app/INotificationManager.aidl @@ -175,6 +175,7 @@ interface INotificationManager void setOnNotificationPostedTrimFromListener(in INotificationListener token, int trim); void setInterruptionFilter(String pkg, int interruptionFilter, boolean fromUser); + NotificationChannel createConversationNotificationChannelForPackageFromPrivilegedListener(in INotificationListener token, String pkg, in UserHandle user, String parentChannelId, String conversationId); void updateNotificationChannelGroupFromPrivilegedListener(in INotificationListener token, String pkg, in UserHandle user, in NotificationChannelGroup group); void updateNotificationChannelFromPrivilegedListener(in INotificationListener token, String pkg, in UserHandle user, in NotificationChannel channel); ParceledListSlice getNotificationChannelsFromPrivilegedListener(in INotificationListener token, String pkg, in UserHandle user); diff --git a/core/java/android/app/IWallpaperManager.aidl b/core/java/android/app/IWallpaperManager.aidl index 5acb9b5cf9ae..f693e9ba11ec 100644 --- a/core/java/android/app/IWallpaperManager.aidl +++ b/core/java/android/app/IWallpaperManager.aidl @@ -24,6 +24,8 @@ import android.os.ParcelFileDescriptor; import android.app.IWallpaperManagerCallback; import android.app.ILocalWallpaperColorConsumer; import android.app.WallpaperInfo; +import android.app.wallpaper.WallpaperDescription; +import android.app.wallpaper.WallpaperInstance; import android.content.ComponentName; import android.app.WallpaperColors; @@ -61,8 +63,8 @@ interface IWallpaperManager { /** * Set the live wallpaper. */ - void setWallpaperComponentChecked(in ComponentName name, in String callingPackage, int which, - int userId); + void setWallpaperComponentChecked(in WallpaperDescription description, in String callingPackage, + int which, int userId); /** * Set the live wallpaper. This only affects the system wallpaper. @@ -129,6 +131,13 @@ interface IWallpaperManager { */ WallpaperInfo getWallpaperInfoWithFlags(int which, int userId); + /** + * Return the instance information about the wallpaper described by `which`, or null if lock + * screen is requested and it is the same as home. + */ + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.READ_WALLPAPER_INTERNAL)") + WallpaperInstance getWallpaperInstance(int which, int userId); + /** * Return a file descriptor for the file that contains metadata about the given user's * wallpaper. diff --git a/core/java/android/app/LoadedApk.java b/core/java/android/app/LoadedApk.java index 1e45d6fd1674..b8233bc498b8 100644 --- a/core/java/android/app/LoadedApk.java +++ b/core/java/android/app/LoadedApk.java @@ -1627,6 +1627,18 @@ public final class LoadedApk { } } + IIntentReceiver findRegisteredReceiverDispatcher(BroadcastReceiver r, Context context) { + synchronized (mReceivers) { + final ArrayMap<BroadcastReceiver, LoadedApk.ReceiverDispatcher> map = + mReceivers.get(context); + if (map != null) { + final LoadedApk.ReceiverDispatcher rd = map.get(r); + return rd == null ? null : rd.getIIntentReceiver(); + } + return null; + } + } + public IIntentReceiver forgetReceiverDispatcher(Context context, BroadcastReceiver r) { synchronized (mReceivers) { diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index ca1662e6bfd0..c6c0395d1b93 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -11831,7 +11831,7 @@ public class Notification implements Parcelable if (length <= 0) continue; try { - totalLength += Math.addExact(totalLength, length); + totalLength = Math.addExact(totalLength, length); segments.add(sanitizeSegment(segment, backgroundColor, defaultProgressColor)); } catch (ArithmeticException e) { @@ -11853,7 +11853,6 @@ public class Notification implements Parcelable for (Point point : mProgressPoints) { final int position = point.getPosition(); if (position < 0 || position > totalLength) continue; - points.add(sanitizePoint(point, backgroundColor, defaultProgressColor)); } diff --git a/core/java/android/app/NotificationChannel.java b/core/java/android/app/NotificationChannel.java index ebe7b3a4fc51..73d26b834497 100644 --- a/core/java/android/app/NotificationChannel.java +++ b/core/java/android/app/NotificationChannel.java @@ -15,6 +15,8 @@ */ package android.app; +import static android.service.notification.Flags.FLAG_NOTIFICATION_CONVERSATION_CHANNEL_MANAGEMENT; + import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; @@ -475,9 +477,10 @@ public final class NotificationChannel implements Parcelable { dest.writeBoolean(mImportanceLockedDefaultApp); } - /** - * @hide - */ + /** @hide */ + @TestApi + @NonNull + @FlaggedApi(FLAG_NOTIFICATION_CONVERSATION_CHANNEL_MANAGEMENT) public NotificationChannel copy() { NotificationChannel copy = new NotificationChannel(mId, mName, mImportance); copy.setDescription(mDesc); @@ -548,10 +551,10 @@ public final class NotificationChannel implements Parcelable { mDeletedTime = time; } - /** - * @hide - */ + /** @hide */ @TestApi + @SystemApi + @FlaggedApi(FLAG_NOTIFICATION_CONVERSATION_CHANNEL_MANAGEMENT) public void setImportantConversation(boolean importantConvo) { mImportantConvo = importantConvo; } diff --git a/core/java/android/app/PropertyInvalidatedCache.java b/core/java/android/app/PropertyInvalidatedCache.java index 26bfec348a6f..038dcdb7efc9 100644 --- a/core/java/android/app/PropertyInvalidatedCache.java +++ b/core/java/android/app/PropertyInvalidatedCache.java @@ -17,6 +17,7 @@ package android.app; import static android.text.TextUtils.formatSimple; +import static com.android.internal.util.Preconditions.checkArgumentPositive; import android.annotation.NonNull; import android.annotation.Nullable; @@ -40,6 +41,7 @@ import com.android.internal.os.BackgroundThread; import dalvik.annotation.optimization.CriticalNative; import dalvik.annotation.optimization.FastNative; +import dalvik.annotation.optimization.NeverCompile; import java.io.ByteArrayOutputStream; import java.io.FileOutputStream; @@ -201,6 +203,23 @@ public class PropertyInvalidatedCache<Query, Result> { } /** + * The list of known and legal modules. The list is not sorted. + */ + private static final String[] sValidModule = { + MODULE_SYSTEM, MODULE_BLUETOOTH, MODULE_TELEPHONY, MODULE_TEST, + }; + + /** + * Verify that the module string is in the legal list. Throw if it is not. + */ + private static void throwIfInvalidModule(@NonNull String name) { + for (int i = 0; i < sValidModule.length; i++) { + if (sValidModule[i].equals(name)) return; + } + throw new IllegalArgumentException("invalid module: " + name); + } + + /** * All legal keys start with one of the following strings. */ private static final String[] sValidKeyPrefix = { @@ -254,8 +273,11 @@ public class PropertyInvalidatedCache<Query, Result> { // written to global store. private static final int NONCE_BYPASS = 3; + // The largest reserved nonce value. Update this whenever a reserved nonce is added. + private static final int MAX_RESERVED_NONCE = NONCE_BYPASS; + private static boolean isReservedNonce(long n) { - return n >= NONCE_UNSET && n <= NONCE_BYPASS; + return n >= NONCE_UNSET && n <= MAX_RESERVED_NONCE; } /** @@ -293,7 +315,7 @@ public class PropertyInvalidatedCache<Query, Result> { private long mMisses = 0; @GuardedBy("mLock") - private long[] mSkips = new long[]{ 0, 0, 0, 0 }; + private long[] mSkips = new long[MAX_RESERVED_NONCE + 1]; @GuardedBy("mLock") private long mMissOverflow = 0; @@ -811,10 +833,20 @@ public class PropertyInvalidatedCache<Query, Result> { return false; // Always disable shared memory on Ravenwood. (for now) } + /** + * Keys that cannot be put in shared memory yet. + */ + private static boolean inSharedMemoryDenyList(@NonNull String name) { + final String pkginfo = PREFIX_SYSTEM + "package_info"; + return name.equals(pkginfo); + }; + // Return true if this cache can use shared memory for its nonce. Shared memory may be used // if the module is the system. private static boolean sharedMemoryOkay(@NonNull String name) { - return sSharedMemoryAvailable && name.startsWith(PREFIX_SYSTEM); + return sSharedMemoryAvailable + && name.startsWith(PREFIX_SYSTEM) + && !inSharedMemoryDenyList(name); } /** @@ -844,6 +876,73 @@ public class PropertyInvalidatedCache<Query, Result> { } /** + * A public argument builder to configure cache behavior. The root instance requires a + * module; this is immutable. New instances are created with member methods. It is important + * to note that the member methods create new instances: they do not modify 'this'. The api + * is allowed to be null in the record constructor to facility reuse of Args instances. + * @hide + */ + public static record Args(@NonNull String mModule, @Nullable String mApi, int mMaxEntries) { + + // Validation: the module must be one of the known module strings and the maxEntries must + // be positive. + public Args { + throwIfInvalidModule(mModule); + checkArgumentPositive(mMaxEntries, "max cache size must be positive"); + } + + // The base constructor must include the module. Modules do not change in a source file, + // so even if the Args is reused, the module will not/should not change. The api is null, + // which is not legal, but there is no reasonable default. Clients must call the api + // method to set the field properly. + public Args(@NonNull String module) { + this(module, /* api */ null, /* maxEntries */ 32); + } + + public Args api(@NonNull String api) { + return new Args(mModule, api, mMaxEntries); + } + + public Args maxEntries(int val) { + return new Args(mModule, mApi, val); + } + } + + /** + * Make a new property invalidated cache. The key is computed from the module and api + * parameters. + * + * @param args The cache configuration. + * @param cacheName Name of this cache in debug and dumpsys + * @param computer The code to compute values that are not in the cache. + * @hide + */ + public PropertyInvalidatedCache(@NonNull Args args, @NonNull String cacheName, + @Nullable QueryHandler<Query, Result> computer) { + mPropertyName = createPropertyName(args.mModule, args.mApi); + mCacheName = cacheName; + mNonce = getNonceHandler(mPropertyName); + mMaxEntries = args.mMaxEntries; + mCache = createMap(); + mComputer = (computer != null) ? computer : new DefaultComputer<>(this); + registerCache(); + } + + /** + * Burst a property name into module and api. Throw if the key is invalid. This method is + * used in to transition legacy cache constructors to the args constructor. + */ + private static Args parseProperty(@NonNull String name) { + throwIfInvalidCacheKey(name); + // Strip off the leading well-known prefix. + String base = name.substring(CACHE_KEY_PREFIX.length() + 1); + int dot = base.indexOf("."); + String module = base.substring(0, dot); + String api = base.substring(dot + 1); + return new Args(module).api(api); + } + + /** * Make a new property invalidated cache. This constructor names the cache after the * property name. New clients should prefer the constructor that takes an explicit * cache name. @@ -857,7 +956,7 @@ public class PropertyInvalidatedCache<Query, Result> { * @hide */ public PropertyInvalidatedCache(int maxEntries, @NonNull String propertyName) { - this(maxEntries, propertyName, propertyName); + this(parseProperty(propertyName).maxEntries(maxEntries), propertyName, null); } /** @@ -873,13 +972,7 @@ public class PropertyInvalidatedCache<Query, Result> { */ public PropertyInvalidatedCache(int maxEntries, @NonNull String propertyName, @NonNull String cacheName) { - mPropertyName = propertyName; - mCacheName = cacheName; - mNonce = getNonceHandler(mPropertyName); - mMaxEntries = maxEntries; - mComputer = new DefaultComputer<>(this); - mCache = createMap(); - registerCache(); + this(parseProperty(propertyName).maxEntries(maxEntries), cacheName, null); } /** @@ -897,13 +990,7 @@ public class PropertyInvalidatedCache<Query, Result> { @TestApi public PropertyInvalidatedCache(int maxEntries, @NonNull String module, @NonNull String api, @NonNull String cacheName, @NonNull QueryHandler<Query, Result> computer) { - mPropertyName = createPropertyName(module, api); - mCacheName = cacheName; - mNonce = getNonceHandler(mPropertyName); - mMaxEntries = maxEntries; - mComputer = computer; - mCache = createMap(); - registerCache(); + this(new Args(module).maxEntries(maxEntries).api(api), cacheName, computer); } // Create a map. This should be called only from the constructor. @@ -1171,7 +1258,8 @@ public class PropertyInvalidatedCache<Query, Result> { public @Nullable Result query(@NonNull Query query) { // Let access to mDisabled race: it's atomic anyway. long currentNonce = (!isDisabled()) ? getCurrentNonce() : NONCE_DISABLED; - if (bypass(query)) { + if (!isReservedNonce(currentNonce) + && bypass(query)) { currentNonce = NONCE_BYPASS; } for (;;) { @@ -1639,54 +1727,62 @@ public class PropertyInvalidatedCache<Query, Result> { return false; } - /** - * helper method to check if dump should be skipped due to zero values - * @param args takes command arguments to check if -brief is present - * @return True if dump should be skipped - */ - private boolean skipDump(String[] args) { - for (String a : args) { - if (a.equals(BRIEF)) { - return (mSkips[NONCE_CORKED] + mSkips[NONCE_UNSET] + mSkips[NONCE_DISABLED] - + mSkips[NONCE_BYPASS] + mHits + mMisses) == 0; - } + @GuardedBy("mLock") + private long getSkipsLocked() { + int sum = 0; + for (int i = 0; i < mSkips.length; i++) { + sum += mSkips[i]; + } + return sum; + } + + // Return true if this cache has had any activity. If the hits, misses, and skips are all + // zero then the client never tried to use the cache. + private boolean isActive() { + synchronized (mLock) { + return mHits + mMisses + getSkipsLocked() > 0; } - return false; } + @NeverCompile private void dumpContents(PrintWriter pw, boolean detailed, String[] args) { // If the user has requested specific caches and this is not one of them, return // immediately. if (detailed && !showDetailed(args)) { return; } + // Does the user want brief output? + boolean brief = false; + for (String a : args) brief |= a.equals(BRIEF); NonceHandler.Stats stats = mNonce.getStats(); synchronized (mLock) { - if (!skipDump(args)) { - pw.println(formatSimple(" Cache Name: %s", cacheName())); - pw.println(formatSimple(" Property: %s", mPropertyName)); - final long skips = - mSkips[NONCE_CORKED] + mSkips[NONCE_UNSET] + mSkips[NONCE_DISABLED] - + mSkips[NONCE_BYPASS]; - pw.println(formatSimple( - " Hits: %d, Misses: %d, Skips: %d, Clears: %d", - mHits, mMisses, skips, mClears)); - pw.println(formatSimple( - " Skip-corked: %d, Skip-unset: %d, Skip-bypass: %d, Skip-other: %d", - mSkips[NONCE_CORKED], mSkips[NONCE_UNSET], - mSkips[NONCE_BYPASS], mSkips[NONCE_DISABLED])); - pw.println(formatSimple( - " Nonce: 0x%016x, Invalidates: %d, CorkedInvalidates: %d", - mLastSeenNonce, stats.invalidated, stats.corkedInvalidates)); - pw.println(formatSimple( - " Current Size: %d, Max Size: %d, HW Mark: %d, Overflows: %d", - mCache.size(), mMaxEntries, mHighWaterMark, mMissOverflow)); - pw.println(formatSimple(" Enabled: %s", mDisabled ? "false" : "true")); - pw.println(""); + if (brief && !isActive()) { + return; } + pw.println(formatSimple(" Cache Name: %s", cacheName())); + pw.println(formatSimple(" Property: %s", mPropertyName)); + pw.println(formatSimple( + " Hits: %d, Misses: %d, Skips: %d, Clears: %d", + mHits, mMisses, getSkipsLocked(), mClears)); + + // Print all the skip reasons. + pw.format(" Skip-%s: %d", sNonceName[0], mSkips[0]); + for (int i = 1; i < mSkips.length; i++) { + pw.format(", Skip-%s: %d", sNonceName[i], mSkips[i]); + } + pw.println(); + + pw.println(formatSimple( + " Nonce: 0x%016x, Invalidates: %d, CorkedInvalidates: %d", + mLastSeenNonce, stats.invalidated, stats.corkedInvalidates)); + pw.println(formatSimple( + " Current Size: %d, Max Size: %d, HW Mark: %d, Overflows: %d", + mCache.size(), mMaxEntries, mHighWaterMark, mMissOverflow)); + pw.println(formatSimple(" Enabled: %s", mDisabled ? "false" : "true")); + // No specific cache was requested. This is the default, and no details // should be dumped. if (!detailed) { @@ -1713,6 +1809,7 @@ public class PropertyInvalidatedCache<Query, Result> { * specific caches (selection is by cache name or property name); if these switches * are used then the output includes both cache statistics and cache entries. */ + @NeverCompile private static void dumpCacheInfo(@NonNull PrintWriter pw, @NonNull String[] args) { if (!sEnabled) { pw.println(" Caching is disabled in this process."); @@ -1745,6 +1842,7 @@ public class PropertyInvalidatedCache<Query, Result> { * are used then the output includes both cache statistics and cache entries. * @hide */ + @NeverCompile public static void dumpCacheInfo(@NonNull ParcelFileDescriptor pfd, @NonNull String[] args) { // Create a PrintWriter that uses a byte array. The code can safely write to // this array without fear of blocking. The completed byte array will be sent diff --git a/core/java/android/app/TaskInfo.java b/core/java/android/app/TaskInfo.java index 081ce31e0886..0a4d8f213501 100644 --- a/core/java/android/app/TaskInfo.java +++ b/core/java/android/app/TaskInfo.java @@ -340,6 +340,12 @@ public class TaskInfo { public int requestedVisibleTypes; /** + * Whether the top activity has requested to limit educational dialogs shown by the system. + * @hide + */ + public boolean isTopActivityLimitSystemEducationDialogs; + + /** * Encapsulate specific App Compat information. * @hide */ @@ -486,6 +492,8 @@ public class TaskInfo { && Objects.equals(capturedLink, that.capturedLink) && capturedLinkTimestamp == that.capturedLinkTimestamp && requestedVisibleTypes == that.requestedVisibleTypes + && isTopActivityLimitSystemEducationDialogs + == that.isTopActivityLimitSystemEducationDialogs && appCompatTaskInfo.equalsForTaskOrganizer(that.appCompatTaskInfo) && Objects.equals(topActivityMainWindowFrame, that.topActivityMainWindowFrame); } @@ -562,6 +570,7 @@ public class TaskInfo { capturedLink = source.readTypedObject(Uri.CREATOR); capturedLinkTimestamp = source.readLong(); requestedVisibleTypes = source.readInt(); + isTopActivityLimitSystemEducationDialogs = source.readBoolean(); appCompatTaskInfo = source.readTypedObject(AppCompatTaskInfo.CREATOR); topActivityMainWindowFrame = source.readTypedObject(Rect.CREATOR); } @@ -616,6 +625,7 @@ public class TaskInfo { dest.writeTypedObject(capturedLink, flags); dest.writeLong(capturedLinkTimestamp); dest.writeInt(requestedVisibleTypes); + dest.writeBoolean(isTopActivityLimitSystemEducationDialogs); dest.writeTypedObject(appCompatTaskInfo, flags); dest.writeTypedObject(topActivityMainWindowFrame, flags); } @@ -660,6 +670,8 @@ public class TaskInfo { + " capturedLink=" + capturedLink + " capturedLinkTimestamp=" + capturedLinkTimestamp + " requestedVisibleTypes=" + requestedVisibleTypes + + " isTopActivityLimitSystemEducationDialogs=" + + isTopActivityLimitSystemEducationDialogs + " appCompatTaskInfo=" + appCompatTaskInfo + " topActivityMainWindowFrame=" + topActivityMainWindowFrame + "}"; diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java index 014e4660f944..479f3df9affb 100644 --- a/core/java/android/app/WallpaperManager.java +++ b/core/java/android/app/WallpaperManager.java @@ -21,10 +21,12 @@ import static android.Manifest.permission.READ_WALLPAPER_INTERNAL; import static android.Manifest.permission.SET_WALLPAPER_DIM_AMOUNT; import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.os.ParcelFileDescriptor.MODE_READ_ONLY; +import static android.app.Flags.FLAG_LIVE_WALLPAPER_CONTENT_HANDLING; import static com.android.window.flags.Flags.FLAG_MULTI_CROP; import static com.android.window.flags.Flags.multiCrop; +import android.Manifest; import android.annotation.FlaggedApi; import android.annotation.FloatRange; import android.annotation.IntDef; @@ -39,6 +41,8 @@ import android.annotation.SystemService; import android.annotation.TestApi; import android.annotation.UiContext; import android.app.compat.CompatChanges; +import android.app.wallpaper.WallpaperDescription; +import android.app.wallpaper.WallpaperInstance; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledSince; import android.compat.annotation.UnsupportedAppUsage; @@ -319,12 +323,12 @@ public class WallpaperManager { * This is only used internally by the framework and the WallpaperBackupAgent. * @hide */ - @IntDef(value = { + @IntDef(prefix = { "ORIENTATION_" }, value = { ORIENTATION_UNKNOWN, - PORTRAIT, - LANDSCAPE, - SQUARE_PORTRAIT, - SQUARE_LANDSCAPE, + ORIENTATION_PORTRAIT, + ORIENTATION_LANDSCAPE, + ORIENTATION_SQUARE_PORTRAIT, + ORIENTATION_SQUARE_LANDSCAPE, }) @Retention(RetentionPolicy.SOURCE) public @interface ScreenOrientation {} @@ -338,25 +342,25 @@ public class WallpaperManager { * Portrait orientation of most screens * @hide */ - public static final int PORTRAIT = 0; + public static final int ORIENTATION_PORTRAIT = 0; /** * Landscape orientation of most screens * @hide */ - public static final int LANDSCAPE = 1; + public static final int ORIENTATION_LANDSCAPE = 1; /** * Portrait orientation with similar width and height (e.g. the inner screen of a foldable) * @hide */ - public static final int SQUARE_PORTRAIT = 2; + public static final int ORIENTATION_SQUARE_PORTRAIT = 2; /** * Landscape orientation with similar width and height (e.g. the inner screen of a foldable) * @hide */ - public static final int SQUARE_LANDSCAPE = 3; + public static final int ORIENTATION_SQUARE_LANDSCAPE = 3; /** * Converts a (width, height) screen size to a {@link ScreenOrientation}. @@ -367,10 +371,10 @@ public class WallpaperManager { public static @ScreenOrientation int getOrientation(Point screenSize) { float ratio = ((float) screenSize.x) / screenSize.y; // ratios between 3/4 and 4/3 are considered square - return ratio >= 4 / 3f ? LANDSCAPE - : ratio > 1f ? SQUARE_LANDSCAPE - : ratio > 3 / 4f ? SQUARE_PORTRAIT - : PORTRAIT; + return ratio >= 4 / 3f ? ORIENTATION_LANDSCAPE + : ratio > 1f ? ORIENTATION_SQUARE_LANDSCAPE + : ratio > 3 / 4f ? ORIENTATION_SQUARE_PORTRAIT + : ORIENTATION_PORTRAIT; } /** @@ -379,10 +383,10 @@ public class WallpaperManager { */ public static @ScreenOrientation int getRotatedOrientation(@ScreenOrientation int orientation) { switch (orientation) { - case PORTRAIT: return LANDSCAPE; - case LANDSCAPE: return PORTRAIT; - case SQUARE_PORTRAIT: return SQUARE_LANDSCAPE; - case SQUARE_LANDSCAPE: return SQUARE_PORTRAIT; + case ORIENTATION_PORTRAIT: return ORIENTATION_LANDSCAPE; + case ORIENTATION_LANDSCAPE: return ORIENTATION_PORTRAIT; + case ORIENTATION_SQUARE_PORTRAIT: return ORIENTATION_SQUARE_LANDSCAPE; + case ORIENTATION_SQUARE_LANDSCAPE: return ORIENTATION_SQUARE_PORTRAIT; default: return ORIENTATION_UNKNOWN; } } @@ -2023,6 +2027,34 @@ public class WallpaperManager { } /** + * Returns the description of the designated wallpaper. Returns null if the lock screen + * wallpaper is requested lock screen wallpaper is not set. + + * @param which Specifies wallpaper to request (home or lock). + * @throws IllegalArgumentException if {@code which} is not exactly one of + * {{@link #FLAG_SYSTEM},{@link #FLAG_LOCK}}. + * + * @hide + */ + @Nullable + @FlaggedApi(FLAG_LIVE_WALLPAPER_CONTENT_HANDLING) + @RequiresPermission(READ_WALLPAPER_INTERNAL) + @SystemApi + public WallpaperInstance getWallpaperInstance(@SetWallpaperFlags int which) { + checkExactlyOneWallpaperFlagSet(which); + try { + if (sGlobals.mService == null) { + Log.w(TAG, "WallpaperService not running"); + throw new RuntimeException(new DeadSystemException()); + } else { + return sGlobals.mService.getWallpaperInstance(which, mContext.getUserId()); + } + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * Get an open, readable file descriptor for the file that contains metadata about the * context user's wallpaper. * @@ -2955,15 +2987,66 @@ public class WallpaperManager { * * @hide */ - @RequiresPermission(android.Manifest.permission.SET_WALLPAPER_COMPONENT) + @RequiresPermission(allOf = {android.Manifest.permission.SET_WALLPAPER_COMPONENT, + Manifest.permission.INTERACT_ACROSS_USERS_FULL}, conditional = true) public boolean setWallpaperComponentWithFlags(@NonNull ComponentName name, @SetWallpaperFlags int which, int userId) { + WallpaperDescription description = new WallpaperDescription.Builder().setComponent( + name).build(); + return setWallpaperComponentWithDescription(description, which, userId); + } + + /** + * Set the implementation of {@link android.service.wallpaper.WallpaperService} used to render + * wallpaper, along with associated metadata. + * + * <p>This can only be called by packages with android.permission.SET_WALLPAPER_COMPONENT + * permission. The caller must hold the INTERACT_ACROSS_USERS_FULL permission to change + * another user's wallpaper. + * </p> + * + * @param description wallpaper component and metadata to set + * @param which Specifies wallpaper destination (home and/or lock). + * @return true on success, otherwise false + * + * @hide + */ + @FlaggedApi(FLAG_LIVE_WALLPAPER_CONTENT_HANDLING) + @SystemApi + @RequiresPermission(allOf = {android.Manifest.permission.SET_WALLPAPER_COMPONENT, + Manifest.permission.INTERACT_ACROSS_USERS_FULL}, conditional = true) + public boolean setWallpaperComponentWithDescription(@NonNull WallpaperDescription description, + @SetWallpaperFlags int which) { + return setWallpaperComponentWithDescription(description, which, mContext.getUserId()); + } + + /** + * Set the implementation of {@link android.service.wallpaper.WallpaperService} used to render + * wallpaper, along with associated metadata. + * + * <p>This can only be called by packages with android.permission.SET_WALLPAPER_COMPONENT + * permission. The caller must hold the INTERACT_ACROSS_USERS_FULL permission to change + * another user's wallpaper. + * </p> + * + * @param description wallpaper component and metadata to set + * @param which Specifies wallpaper destination (home and/or lock). + * @param userId User for whom the component should be set. + * @return true on success, otherwise false + * + * @hide + */ + @FlaggedApi(FLAG_LIVE_WALLPAPER_CONTENT_HANDLING) + @RequiresPermission(allOf = {android.Manifest.permission.SET_WALLPAPER_COMPONENT, + Manifest.permission.INTERACT_ACROSS_USERS_FULL}, conditional = true) + public boolean setWallpaperComponentWithDescription(@NonNull WallpaperDescription description, + @SetWallpaperFlags int which, int userId) { if (sGlobals.mService == null) { Log.w(TAG, "WallpaperManagerService not running"); throw new RuntimeException(new DeadSystemException()); } try { - sGlobals.mService.setWallpaperComponentChecked(name, mContext.getOpPackageName(), + sGlobals.mService.setWallpaperComponentChecked(description, mContext.getOpPackageName(), which, userId); return true; } catch (RemoteException e) { diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig index fd583773d4e9..3aaca25eca15 100644 --- a/core/java/android/app/admin/flags/flags.aconfig +++ b/core/java/android/app/admin/flags/flags.aconfig @@ -341,3 +341,11 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "set_mte_policy_coexistence" + is_exported: true + namespace: "enterprise" + description: "Enables coexistence support for Setting MTE policy." + bug: "376213673" +} diff --git a/core/java/android/app/appfunctions/AppFunctionManager.java b/core/java/android/app/appfunctions/AppFunctionManager.java index 8944bb9c9c4c..5ddb590add4c 100644 --- a/core/java/android/app/appfunctions/AppFunctionManager.java +++ b/core/java/android/app/appfunctions/AppFunctionManager.java @@ -50,29 +50,39 @@ import java.util.function.Consumer; * <p>**Building App Functions:** * * <p>Most developers should build app functions through the AppFunctions SDK. This SDK library - * offers a more convenient and type-safe way to represent the inputs and outputs of an app - * function, using custom data classes called "AppFunction Schemas". + * offers a more convenient and type-safe way to build app functions. The SDK provides predefined + * function schemas for common use cases and associated data classes for function parameters and + * return values. Apps only have to implement the provided interfaces. Internally, the SDK converts + * these data classes into {@link ExecuteAppFunctionRequest#getParameters()} and {@link + * ExecuteAppFunctionResponse#getResultDocument()}. * - * <p>The suggested way to build an app function is to use the AppFunctions SDK. The SDK provides - * custom data classes (AppFunctions Schemas) and handles the conversion to the underlying {@link - * android.app.appsearch.GenericDocument}/{@link android.os.Bundle} format used in {@link - * ExecuteAppFunctionRequest} and {@link ExecuteAppFunctionResponse}. - * - * <p>**Discovering (Listing) App Functions:** + * <p>**Discovering App Functions:** * * <p>When there is a package change or the device starts up, the metadata of available functions is - * indexed on-device by {@link AppSearchManager}. AppSearch stores the indexed information as a - * {@code AppFunctionStaticMetadata} document. This allows other apps and the app itself to discover - * these functions using the AppSearch search APIs. Visibility to this metadata document is based on - * the packages that have visibility to the app providing the app functions. + * indexed on-device by {@link AppSearchManager}. AppSearch stores the indexed information as an + * {@code AppFunctionStaticMetadata} document. This document contains the {@code functionIdentifier} + * and the schema information that the app function implements. This allows other apps and the app + * itself to discover these functions using the AppSearch search APIs. Visibility to this metadata + * document is based on the packages that have visibility to the app providing the app functions. + * AppFunction SDK provides a convenient way to achieve this and is the preferred method. * * <p>**Executing App Functions:** * - * <p>Requests to execute a function are built using the {@link ExecuteAppFunctionRequest} class. - * Callers need the {@code android.permission.EXECUTE_APP_FUNCTIONS} or {@code + * <p>To execute an app function, the caller app can retrieve the {@code functionIdentifier} from + * the {@code AppFunctionStaticMetadata} document and use it to build an {@link + * ExecuteAppFunctionRequest}. Then, invoke {@link #executeAppFunction} with the request to execute + * the app function. Callers need the {@code android.permission.EXECUTE_APP_FUNCTIONS} or {@code * android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED} permission to execute app functions from other - * apps. An app has automatic visibility to its own functions and doesn't need these permissions to - * call its own functions via {@code AppFunctionManager}. + * apps. An app can always execute its own app functions and doesn't need these permissions. + * AppFunction SDK provides a convenient way to achieve this and is the preferred method. + * + * <p>**Example:** + * + * <p>An assistant app is trying to fulfill the user request "Save XYZ into my note". The assistant + * app should first list all available app functions as {@code AppFunctionStaticMetadata} documents + * from AppSearch. Then, it should identify an app function that implements the {@code CreateNote} + * schema. Finally, the assistant app can invoke {@link #executeAppFunction} with the {@code + * functionIdentifier} of the chosen function. */ @FlaggedApi(FLAG_ENABLE_APP_FUNCTION_MANAGER) @SystemService(Context.APP_FUNCTION_SERVICE) @@ -202,12 +212,13 @@ public final class AppFunctionManager { /** * Returns a boolean through a callback, indicating whether the app function is enabled. * - * <p>* This method can only check app functions owned by the caller, or those where the caller + * <p>This method can only check app functions owned by the caller, or those where the caller * has visibility to the owner package and holds either the {@link * Manifest.permission#EXECUTE_APP_FUNCTIONS} or {@link * Manifest.permission#EXECUTE_APP_FUNCTIONS_TRUSTED} permission. * - * <p>If operation fails, the callback's {@link OutcomeReceiver#onError} is called with errors: + * <p>If the operation fails, the callback's {@link OutcomeReceiver#onError} is called with + * errors: * * <ul> * <li>{@link IllegalArgumentException}, if the function is not found or the caller does not @@ -221,23 +232,47 @@ public final class AppFunctionManager { * @param executor the executor to run the request * @param callback the callback to receive the function enabled check result */ + @RequiresPermission( + anyOf = { + Manifest.permission.EXECUTE_APP_FUNCTIONS_TRUSTED, + Manifest.permission.EXECUTE_APP_FUNCTIONS + }, + conditional = true) public void isAppFunctionEnabled( @NonNull String functionIdentifier, @NonNull String targetPackage, @NonNull Executor executor, @NonNull OutcomeReceiver<Boolean, Exception> callback) { - Objects.requireNonNull(functionIdentifier); - Objects.requireNonNull(targetPackage); - Objects.requireNonNull(executor); - Objects.requireNonNull(callback); - AppSearchManager appSearchManager = mContext.getSystemService(AppSearchManager.class); - if (appSearchManager == null) { - callback.onError(new IllegalStateException("Failed to get AppSearchManager.")); - return; - } + isAppFunctionEnabledInternal(functionIdentifier, targetPackage, executor, callback); + } - AppFunctionManagerHelper.isAppFunctionEnabled( - functionIdentifier, targetPackage, appSearchManager, executor, callback); + /** + * Returns a boolean through a callback, indicating whether the app function is enabled. + * + * <p>This method can only check app functions owned by the caller, unlike {@link + * #isAppFunctionEnabled(String, String, Executor, OutcomeReceiver)}, which allows specifying a + * different target package. + * + * <p>If the operation fails, the callback's {@link OutcomeReceiver#onError} is called with + * errors: + * + * <ul> + * <li>{@link IllegalArgumentException}, if the function is not found or the caller does not + * have access to it. + * </ul> + * + * @param functionIdentifier the identifier of the app function to check (unique within the + * target package) and in most cases, these are automatically generated by the AppFunctions + * SDK + * @param executor the executor to run the request + * @param callback the callback to receive the function enabled check result + */ + public void isAppFunctionEnabled( + @NonNull String functionIdentifier, + @NonNull Executor executor, + @NonNull OutcomeReceiver<Boolean, Exception> callback) { + isAppFunctionEnabledInternal( + functionIdentifier, mContext.getPackageName(), executor, callback); } /** @@ -280,6 +315,25 @@ public final class AppFunctionManager { } } + private void isAppFunctionEnabledInternal( + @NonNull String functionIdentifier, + @NonNull String targetPackage, + @NonNull Executor executor, + @NonNull OutcomeReceiver<Boolean, Exception> callback) { + Objects.requireNonNull(functionIdentifier); + Objects.requireNonNull(targetPackage); + Objects.requireNonNull(executor); + Objects.requireNonNull(callback); + AppSearchManager appSearchManager = mContext.getSystemService(AppSearchManager.class); + if (appSearchManager == null) { + callback.onError(new IllegalStateException("Failed to get AppSearchManager.")); + return; + } + + AppFunctionManagerHelper.isAppFunctionEnabled( + functionIdentifier, targetPackage, appSearchManager, executor, callback); + } + private static class CallbackWrapper extends IAppFunctionEnabledCallback.Stub { private final OutcomeReceiver<Void, Exception> mCallback; diff --git a/core/java/android/app/appfunctions/ExecuteAppFunctionRequest.java b/core/java/android/app/appfunctions/ExecuteAppFunctionRequest.java index 4c5e8c130a73..41bb62270e9f 100644 --- a/core/java/android/app/appfunctions/ExecuteAppFunctionRequest.java +++ b/core/java/android/app/appfunctions/ExecuteAppFunctionRequest.java @@ -114,18 +114,14 @@ public final class ExecuteAppFunctionRequest implements Parcelable { * <p>The bundle may have missing parameters. Developers are advised to implement defensive * handling measures. * - * <p>Similar to {@link #getFunctionIdentifier()} the parameters required by a function can be - * obtained by querying AppSearch for the corresponding {@code AppFunctionStaticMetadata}. This - * metadata will contain enough information for the caller to resolve the required parameters - * either using information from the metadata itself or using the AppFunction SDK for function - * callers. + * @see AppFunctionManager on how to determine the expected parameters. */ @NonNull public GenericDocument getParameters() { return mParameters.getValue(); } - /** Returns the additional data relevant to this function execution. */ + /** Returns the additional metadata for this function execution request. */ @NonNull public Bundle getExtras() { return mExtras; @@ -153,19 +149,31 @@ public final class ExecuteAppFunctionRequest implements Parcelable { @NonNull private GenericDocument mParameters = new GenericDocument.Builder<>("", "", "").build(); + /** + * Creates a new instance of this builder class. + * + * @param targetPackageName The package name of the target app providing the app function to + * invoke. + * @param functionIdentifier The identifier used by the {@link AppFunctionService} from the + * target app to uniquely identify the function to be invoked. + */ public Builder(@NonNull String targetPackageName, @NonNull String functionIdentifier) { mTargetPackageName = Objects.requireNonNull(targetPackageName); mFunctionIdentifier = Objects.requireNonNull(functionIdentifier); } - /** Sets the additional data relevant to this function execution. */ + /** Sets the additional metadata for this function execution request. */ @NonNull public Builder setExtras(@NonNull Bundle extras) { mExtras = Objects.requireNonNull(extras); return this; } - /** Sets the function parameters. */ + /** + * Sets the function parameters. + * + * @see #ExecuteAppFunctionRequest#getParameters() + */ @NonNull public Builder setParameters(@NonNull GenericDocument parameters) { Objects.requireNonNull(parameters); diff --git a/core/java/android/app/appfunctions/ExecuteAppFunctionResponse.java b/core/java/android/app/appfunctions/ExecuteAppFunctionResponse.java index c907ef114286..cdf02e6f5a09 100644 --- a/core/java/android/app/appfunctions/ExecuteAppFunctionResponse.java +++ b/core/java/android/app/appfunctions/ExecuteAppFunctionResponse.java @@ -73,38 +73,102 @@ public final class ExecuteAppFunctionResponse implements Parcelable { */ public static final String PROPERTY_RETURN_VALUE = "returnValue"; - /** The call was successful. */ + /** + * The call was successful. + * + * <p>This result code does not belong in an error category. + */ public static final int RESULT_OK = 0; - /** The caller does not have the permission to execute an app function. */ - public static final int RESULT_DENIED = 1; + /** + * The caller does not have the permission to execute an app function. + * + * <p>This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category. + */ + public static final int RESULT_DENIED = 1000; + + /** + * The caller supplied invalid arguments to the execution request. + * + * <p>This error may be considered similar to {@link IllegalArgumentException}. + * + * <p>This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category. + */ + public static final int RESULT_INVALID_ARGUMENT = 1001; + + /** + * The caller tried to execute a disabled app function. + * + * <p>This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category. + */ + public static final int RESULT_DISABLED = 1002; + + /** + * The caller tried to execute a function that does not exist. + * + * <p>This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category. + */ + public static final int RESULT_FUNCTION_NOT_FOUND = 1003; + + /** + * An internal unexpected error coming from the system. + * + * <p>This error is in the {@link #ERROR_CATEGORY_SYSTEM} category. + */ + public static final int RESULT_SYSTEM_ERROR = 2000; + + /** + * The operation was cancelled. Use this error code to report that a cancellation is done after + * receiving a cancellation signal. + * + * <p>This error is in the {@link #ERROR_CATEGORY_SYSTEM} category. + */ + public static final int RESULT_CANCELLED = 2001; /** * An unknown error occurred while processing the call in the AppFunctionService. * * <p>This error is thrown when the service is connected in the remote application but an * unexpected error is thrown from the bound application. + * + * <p>This error is in the {@link #ERROR_CATEGORY_APP} category. */ - public static final int RESULT_APP_UNKNOWN_ERROR = 2; + public static final int RESULT_APP_UNKNOWN_ERROR = 3000; - /** An internal unexpected error coming from the system. */ - public static final int RESULT_INTERNAL_ERROR = 3; + /** + * The error category is unknown. + * + * <p>This is the default value for {@link #getErrorCategory}. + */ + public static final int ERROR_CATEGORY_UNKNOWN = 0; /** - * The caller supplied invalid arguments to the call. + * The error is caused by the app requesting a function execution. * - * <p>This error may be considered similar to {@link IllegalArgumentException}. + * <p>For example, the caller provided invalid parameters in the execution request e.g. an + * invalid function ID. + * + * <p>Errors in the category fall in the range 1000-1999 inclusive. */ - public static final int RESULT_INVALID_ARGUMENT = 4; + public static final int ERROR_CATEGORY_REQUEST_ERROR = 1; - /** The caller tried to execute a disabled app function. */ - public static final int RESULT_DISABLED = 5; + /** + * The error is caused by an issue in the system. + * + * <p>For example, the AppFunctionService implementation is not found by the system. + * + * <p>Errors in the category fall in the range 2000-2999 inclusive. + */ + public static final int ERROR_CATEGORY_SYSTEM = 2; /** - * The operation was cancelled. Use this error code to report that a cancellation is done after - * receiving a cancellation signal. + * The error is caused by the app providing the function. + * + * <p>For example, the app crashed when the system is executing the request. + * + * <p>Errors in the category fall in the range 3000-3999 inclusive. */ - public static final int RESULT_CANCELLED = 6; + public static final int ERROR_CATEGORY_APP = 3; /** The result code of the app function execution. */ @ResultCode private final int mResultCode; @@ -156,7 +220,7 @@ public final class ExecuteAppFunctionResponse implements Parcelable { * Returns a successful response. * * @param resultDocument The return value of the executed function. - * @param extras The additional metadata data relevant to this function execution response. + * @param extras The additional metadata for this function execution response. */ @NonNull @FlaggedApi(FLAG_ENABLE_APP_FUNCTION_MANAGER) @@ -174,7 +238,7 @@ public final class ExecuteAppFunctionResponse implements Parcelable { * Returns a failure response. * * @param resultCode The result code of the app function execution. - * @param extras The additional metadata data relevant to this function execution response. + * @param extras The additional metadata for this function execution response. * @param errorMessage The error message associated with the result, if any. */ @NonNull @@ -198,6 +262,36 @@ public final class ExecuteAppFunctionResponse implements Parcelable { } /** + * Returns the error category of the {@link ExecuteAppFunctionResponse}. + * + * <p>This method categorizes errors based on their underlying cause, allowing developers to + * implement targeted error handling and provide more informative error messages to users. It + * maps ranges of result codes to specific error categories. + * + * <p>When constructing a {@link #newFailure} response, use the appropriate result code value to + * ensure correct categorization of the failed response. + * + * <p>This method returns {@code ERROR_CATEGORY_UNKNOWN} if the result code does not belong to + * any error category, for example, in the case of a successful result with {@link #RESULT_OK}. + * + * <p>See {@link ErrorCategory} for a complete list of error categories and their corresponding + * result code ranges. + */ + @ErrorCategory + public int getErrorCategory() { + if (mResultCode >= 1000 && mResultCode < 2000) { + return ERROR_CATEGORY_REQUEST_ERROR; + } + if (mResultCode >= 2000 && mResultCode < 3000) { + return ERROR_CATEGORY_SYSTEM; + } + if (mResultCode >= 3000 && mResultCode < 4000) { + return ERROR_CATEGORY_APP; + } + return ERROR_CATEGORY_UNKNOWN; + } + + /** * Returns a generic document containing the return value of the executed function. * * <p>The {@link #PROPERTY_RETURN_VALUE} key can be used to obtain the return value. @@ -216,13 +310,15 @@ public final class ExecuteAppFunctionResponse implements Parcelable { * // Do something with the returnValue * } * </pre> + * + * @see AppFunctionManager on how to determine the expected function return. */ @NonNull public GenericDocument getResultDocument() { return mResultDocumentWrapper.getValue(); } - /** Returns the extras of the app function execution. */ + /** Returns the additional metadata for this function execution response. */ @NonNull public Bundle getExtras() { return mExtras; @@ -278,11 +374,28 @@ public final class ExecuteAppFunctionResponse implements Parcelable { RESULT_OK, RESULT_DENIED, RESULT_APP_UNKNOWN_ERROR, - RESULT_INTERNAL_ERROR, + RESULT_FUNCTION_NOT_FOUND, + RESULT_SYSTEM_ERROR, RESULT_INVALID_ARGUMENT, RESULT_DISABLED, RESULT_CANCELLED }) @Retention(RetentionPolicy.SOURCE) public @interface ResultCode {} + + /** + * Error categories. + * + * @hide + */ + @IntDef( + prefix = {"ERROR_CATEGORY_"}, + value = { + ERROR_CATEGORY_UNKNOWN, + ERROR_CATEGORY_REQUEST_ERROR, + ERROR_CATEGORY_APP, + ERROR_CATEGORY_SYSTEM + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ErrorCategory {} } diff --git a/core/java/android/app/backup/NotificationLoggingConstants.java b/core/java/android/app/backup/NotificationLoggingConstants.java new file mode 100644 index 000000000000..3601e862c02f --- /dev/null +++ b/core/java/android/app/backup/NotificationLoggingConstants.java @@ -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 android.app.backup; + +/** + * @hide + */ +public class NotificationLoggingConstants { + + // Key under which the payload blob is stored + public static final String KEY_NOTIFICATIONS = "notifications"; + + /** + * Events for android.service.notification.ZenModeConfig - the configuration for all modes + * settings for a single user + */ + @BackupRestoreEventLogger.BackupRestoreDataType + public static final String DATA_TYPE_ZEN_CONFIG = KEY_NOTIFICATIONS + ":zen_config"; + /** + * Events for android.service.notification.ZenModeConfig.ZenRule - a single mode within a + * ZenModeConfig + */ + @BackupRestoreEventLogger.BackupRestoreDataType + public static final String DATA_TYPE_ZEN_RULES = KEY_NOTIFICATIONS + ":zen_rules"; + /** + * Events for globally stored notifications data that aren't stored in settings, like whether + * to hide silent notification in the status bar + */ + @BackupRestoreEventLogger.BackupRestoreDataType + public static final String DATA_TYPE_NOTIF_GLOBAL = KEY_NOTIFICATIONS + ":global"; + /** + * Events for package specific notification settings, including app and + * android.app.NotificationChannel level settings. + */ + @BackupRestoreEventLogger.BackupRestoreDataType + public static final String DATA_TYPE_NOTIF_PACKAGES = KEY_NOTIFICATIONS + ":packages"; + /** + * Events for approved ManagedServices (NotificationListenerServices, + * NotificationAssistantService, ConditionProviderService). + */ + @BackupRestoreEventLogger.BackupRestoreDataType + public static final String DATA_TYPE_MANAGED_SERVICE_PRIMARY_APPROVED = KEY_NOTIFICATIONS + + ":managed_service_primary_approved"; + /** + * Events for what types of notifications NotificationListenerServices cannot see + */ + @BackupRestoreEventLogger.BackupRestoreDataType + public static final String DATA_TYPE_NLS_RESTRICTED = KEY_NOTIFICATIONS + + ":nls_restricted"; + /** + * Events for ManagedServices that are approved because they have a different primary + * ManagedService (ConditionProviderService). + */ + @BackupRestoreEventLogger.BackupRestoreDataType + public static final String DATA_TYPE_MANAGED_SERVICE_SECONDARY_APPROVED = KEY_NOTIFICATIONS + + ":managed_service_secondary_approved"; + /** + * Events for individual snoozed notifications. + */ + @BackupRestoreEventLogger.BackupRestoreDataType + public static final String DATA_TYPE_SNOOZED = KEY_NOTIFICATIONS + ":snoozed"; + + + @BackupRestoreEventLogger.BackupRestoreError + public static final String ERROR_XML_PARSING = KEY_NOTIFICATIONS + ":invalid_xml_parsing"; +} diff --git a/core/java/android/app/contextualsearch/flags.aconfig b/core/java/android/app/contextualsearch/flags.aconfig index 3b0c86795738..5e09517d1edc 100644 --- a/core/java/android/app/contextualsearch/flags.aconfig +++ b/core/java/android/app/contextualsearch/flags.aconfig @@ -12,4 +12,18 @@ flag { namespace: "machine_learning" description: "Flag to refresh the token to the callback" bug: "309689654" +} + +flag { + name: "multi_window_screen_context" + namespace: "machine_learning" + description: "Report screen context and positions for all windows." + bug: "371065456" +} + +flag { + name: "contextual_search_window_layer" + namespace: "machine_learning" + description: "Identify live contextual search UI to exclude from contextual search screenshot." + bug: "372510690" }
\ No newline at end of file diff --git a/core/java/android/app/jank/JankDataProcessor.java b/core/java/android/app/jank/JankDataProcessor.java new file mode 100644 index 000000000000..981a9167c2da --- /dev/null +++ b/core/java/android/app/jank/JankDataProcessor.java @@ -0,0 +1,351 @@ +/* + * 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.app.jank; + +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.app.jank.StateTracker.StateData; +import android.util.Log; +import android.util.Pools.SimplePool; +import android.view.SurfaceControl.JankData; + +import androidx.annotation.VisibleForTesting; + +import com.android.internal.util.FrameworkStatsLog; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +/** + * This class is responsible for associating frames received from SurfaceFlinger to active widget + * states and logging those states back to the platform. + * @hide + */ +@FlaggedApi(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) +public class JankDataProcessor { + + private static final int MAX_IN_MEMORY_STATS = 25; + private static final int LOG_BATCH_FREQUENCY = 50; + private int mCurrentBatchCount = 0; + private StateTracker mStateTracker = null; + private ArrayList<StateData> mPendingStates = new ArrayList<>(); + private SimplePool<PendingJankStat> mPendingJankStatsPool = + new SimplePool<>(MAX_IN_MEMORY_STATS); + private HashMap<String, PendingJankStat> mPendingJankStats = new HashMap<>(); + + public JankDataProcessor(@NonNull StateTracker stateTracker) { + mStateTracker = stateTracker; + } + + /** + * Called once per batch of JankData. + * @param jankData data received from SurfaceFlinger to be processed + * @param activityName name of the activity that is tracking jank metrics. + * @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); + + // TODO b/376332122 Look to see if this logic can be optimized. + for (int i = 0; i < jankData.size(); i++) { + JankData frame = jankData.get(i); + // for each frame we need to check if the state was active during that time. + for (int j = 0; j < mPendingStates.size(); j++) { + StateData pendingState = mPendingStates.get(j); + // This state was active during the frame + if (frame.frameVsyncId >= pendingState.mVsyncIdStart + && frame.frameVsyncId <= pendingState.mVsyncIdEnd) { + recordFrameCount(frame, pendingState, activityName, appUid); + + pendingState.mProcessed = true; + } + } + } + // At this point we have attributed all frames to a state. + if (mCurrentBatchCount >= LOG_BATCH_FREQUENCY) { + logMetricCounts(); + } + // return the StatData object back to the pool to be reused. + jankDataProcessingComplete(); + } + + /** + * Returns the aggregate map of different pending jank stats. + */ + @VisibleForTesting + public HashMap<String, PendingJankStat> getPendingJankStats() { + return mPendingJankStats; + } + + private void jankDataProcessingComplete() { + mStateTracker.stateProcessingComplete(); + mPendingStates.clear(); + } + + /** + * Determine if frame is Janky and add to existing memory counter or create a new one. + */ + private void recordFrameCount(JankData frameData, StateData stateData, String activityName, + int appUid) { + // Check if we have an existing Jank state + PendingJankStat jankStats = mPendingJankStats.get(stateData.mStateDataKey); + + if (jankStats == null) { + // Check if we have space for another pending stat + if (mPendingJankStats.size() > MAX_IN_MEMORY_STATS) { + return; + } + + jankStats = mPendingJankStatsPool.acquire(); + if (jankStats == null) { + jankStats = new PendingJankStat(); + } + jankStats.clearStats(); + jankStats.mActivityName = activityName; + jankStats.mUid = appUid; + mPendingJankStats.put(stateData.mStateDataKey, jankStats); + } + // This state has already been accounted for + if (jankStats.processedVsyncId == frameData.frameVsyncId) return; + + jankStats.mTotalFrames += 1; + if (frameData.jankType == JankData.JANK_APPLICATION) { + jankStats.mJankyFrames += 1; + } + jankStats.recordFrameOverrun(frameData.actualAppFrameTimeNs); + jankStats.processedVsyncId = frameData.frameVsyncId; + + } + + /** + * When called will log pending Jank stats currently stored in memory to the platform. Will not + * clear any pending widget states. + */ + public void logMetricCounts() { + //TODO b/374607503 when api changes are in add enum mapping for category and state. + + try { + mPendingJankStats.values().forEach(stat -> { + FrameworkStatsLog.write(FrameworkStatsLog.JANK_FRAME_COUNT_BY_WIDGET, + /*app uid*/ stat.getUid(), + /*activity name*/ stat.getActivityName(), + /*widget id*/ stat.getWidgetId(), + /*refresh rate*/ stat.getRefreshRate(), + /*widget category*/ 0, + /*widget state*/ 0, + /*total frames*/ stat.getTotalFrames(), + /*janky frames*/ stat.getJankyFrames(), + /*histogram*/ stat.mFrameOverrunBuckets); + Log.d(stat.mActivityName, stat.toString()); + // return the pending stat to the pool it will be reset the next time its + // used. + mPendingJankStatsPool.release(stat); + } + ); + // All stats have been recorded and added back to the pool for reuse, clear the pending + // stats. + mPendingJankStats.clear(); + mCurrentBatchCount = 0; + } catch (Exception exception) { + // TODO b/374608358 handle logging exceptions. + } + } + + public static final class PendingJankStat { + private static final int NANOS_PER_MS = 1000000; + public long processedVsyncId = -1; + + // UID of the app + private int mUid; + + // The name of the activity that is currently collecting frame metrics. + private String mActivityName; + + // The id that has been set for the widget. + private String mWidgetId; + + // A general category that the widget applies to. + private String mWidgetCategory; + + // The states that the UI elements can report + private String mWidgetState; + + // The number of frames reported during this state. + private long mTotalFrames; + + // Total number of frames determined to be janky during the reported state. + private long mJankyFrames; + + private int mRefreshRate; + + private static final int[] sFrameOverrunHistogramBounds = { + Integer.MIN_VALUE, -200, -150, -100, -90, -80, -70, -60, -50, -40, -30, -25, -20, + -18, -16, -14, -12, -10, -8, -6, -4, -2, 0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 25, + 30, 40, 50, 60, 70, 80, 90, 100, 150, 200, 300, 400, 500, 600, 700, 800, 900, 1000 + }; + private final int[] mFrameOverrunBuckets = new int[sFrameOverrunHistogramBounds.length]; + + // Histogram of frame duration overruns encoded in predetermined buckets. + public PendingJankStat() { + } + public long getProcessedVsyncId() { + return processedVsyncId; + } + + public void setProcessedVsyncId(long processedVsyncId) { + this.processedVsyncId = processedVsyncId; + } + + public int getUid() { + return mUid; + } + + public void setUid(int uid) { + mUid = uid; + } + + public String getActivityName() { + return mActivityName; + } + + public void setActivityName(String activityName) { + mActivityName = activityName; + } + + public String getWidgetId() { + return mWidgetId; + } + + public void setWidgetId(String widgetId) { + mWidgetId = widgetId; + } + + public String getWidgetCategory() { + return mWidgetCategory; + } + + public void setWidgetCategory(String widgetCategory) { + mWidgetCategory = widgetCategory; + } + + public String getWidgetState() { + return mWidgetState; + } + + public void setWidgetState(String widgetState) { + mWidgetState = widgetState; + } + + public long getTotalFrames() { + return mTotalFrames; + } + + public void setTotalFrames(long totalFrames) { + mTotalFrames = totalFrames; + } + + public long getJankyFrames() { + return mJankyFrames; + } + + public void setJankyFrames(long jankyFrames) { + mJankyFrames = jankyFrames; + } + + public int[] getFrameOverrunBuckets() { + return mFrameOverrunBuckets; + } + + public int getRefreshRate() { + return mRefreshRate; + } + + public void setRefreshRate(int refreshRate) { + mRefreshRate = refreshRate; + } + + /** + * Will convert the frame time from ns to ms and record how long the frame took to render. + */ + public void recordFrameOverrun(long frameTimeNano) { + try { + // TODO b/375650163 calculate frame overrun from refresh rate. + int frameTimeMillis = (int) frameTimeNano / NANOS_PER_MS; + mFrameOverrunBuckets[indexForFrameOverrun(frameTimeMillis)]++; + } catch (IndexOutOfBoundsException exception) { + // TODO b/375650163 figure out how to handle this if it happens. + } + } + + /** + * resets all fields in the object back to defaults. + */ + public void clearStats() { + this.mUid = -1; + this.mActivityName = ""; + this.processedVsyncId = -1; + this.mJankyFrames = 0; + this.mTotalFrames = 0; + this.mWidgetCategory = ""; + this.mWidgetState = ""; + this.mRefreshRate = 0; + clearHistogram(); + } + + private void clearHistogram() { + for (int i = 0; i < mFrameOverrunBuckets.length; i++) { + mFrameOverrunBuckets[i] = 0; + } + } + + // This takes the overrun time and returns what bucket it belongs to in the histogram. + private int indexForFrameOverrun(int overrunTime) { + if (overrunTime < 20) { + if (overrunTime >= -20) { + return (overrunTime + 20) / 2 + 12; + } + if (overrunTime >= -30) { + return (overrunTime + 30) / 5 + 10; + } + if (overrunTime >= -100) { + return (overrunTime + 100) / 10 + 3; + } + if (overrunTime >= -200) { + return (overrunTime + 200) / 50 + 1; + } + return 0; + } + if (overrunTime < 30) { + return (overrunTime - 20) / 5 + 32; + } + if (overrunTime < 100) { + return (overrunTime - 30) / 10 + 34; + } + if (overrunTime < 200) { + return (overrunTime - 50) / 100 + 41; + } + if (overrunTime < 1000) { + return (overrunTime - 200) / 100 + 43; + } + return sFrameOverrunHistogramBounds.length - 1; + } + + } +} diff --git a/core/java/android/app/jank/StateTracker.java b/core/java/android/app/jank/StateTracker.java index cb457ff64430..c86d5a5cff20 100644 --- a/core/java/android/app/jank/StateTracker.java +++ b/core/java/android/app/jank/StateTracker.java @@ -36,7 +36,6 @@ import java.util.concurrent.ConcurrentHashMap; * @hide */ @FlaggedApi(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) -@VisibleForTesting public class StateTracker { // Used to synchronize access to mPreviousStates. @@ -188,7 +187,6 @@ public class StateTracker { /** * @hide */ - @VisibleForTesting public static class StateData { // Concatenated string of widget category, widget state and widget id. diff --git a/core/java/android/app/notification.aconfig b/core/java/android/app/notification.aconfig index 1d4c18f6a62c..0fc4291d15ab 100644 --- a/core/java/android/app/notification.aconfig +++ b/core/java/android/app/notification.aconfig @@ -66,6 +66,16 @@ flag { } flag { + name: "modes_multiuser" + namespace: "systemui" + description: "Fixes for modes (and DND/Zen in general) when callers are not the current user" + bug: "323163267" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "api_tvextender" is_exported: true namespace: "systemui" diff --git a/core/java/android/app/performance.aconfig b/core/java/android/app/performance.aconfig index 7484a8558841..f51f748bc71f 100644 --- a/core/java/android/app/performance.aconfig +++ b/core/java/android/app/performance.aconfig @@ -7,7 +7,7 @@ flag { is_exported: true is_fixed_read_only: true description: "PropertyInvalidatedCache uses shared memory for nonces." - bug: "366552454" + bug: "360897450" } flag { @@ -16,5 +16,5 @@ flag { is_exported: true is_fixed_read_only: true description: "Enforce PropertyInvalidatedCache.setTestMode() protocol" - bug: "366552454" + bug: "360897450" } diff --git a/core/java/android/app/trust/ITrustManager.aidl b/core/java/android/app/trust/ITrustManager.aidl index 740f5932f902..730bb73da3bb 100644 --- a/core/java/android/app/trust/ITrustManager.aidl +++ b/core/java/android/app/trust/ITrustManager.aidl @@ -41,5 +41,6 @@ interface ITrustManager { void unlockedByBiometricForUser(int userId, in BiometricSourceType source); void clearAllBiometricRecognized(in BiometricSourceType target, int unlockedUser); boolean isActiveUnlockRunning(int userId); + @EnforcePermission("ACCESS_FINE_LOCATION") boolean isInSignificantPlace(); } diff --git a/core/java/android/app/trust/TrustManager.java b/core/java/android/app/trust/TrustManager.java index 88d4d691cd97..1ef83cdf3f85 100644 --- a/core/java/android/app/trust/TrustManager.java +++ b/core/java/android/app/trust/TrustManager.java @@ -305,6 +305,7 @@ public class TrustManager { * * @hide */ + @RequiresPermission(Manifest.permission.ACCESS_FINE_LOCATION) public boolean isInSignificantPlace() { try { return mService.isInSignificantPlace(); diff --git a/core/java/android/app/wallpaper/WallpaperDescription.java b/core/java/android/app/wallpaper/WallpaperDescription.java index dedcb48f3ad7..c3d6340be41f 100644 --- a/core/java/android/app/wallpaper/WallpaperDescription.java +++ b/core/java/android/app/wallpaper/WallpaperDescription.java @@ -18,6 +18,8 @@ package android.app.wallpaper; import static android.app.Flags.FLAG_LIVE_WALLPAPER_CONTENT_HANDLING; +import static java.nio.charset.StandardCharsets.UTF_8; + import android.annotation.FlaggedApi; import android.app.WallpaperInfo; import android.content.ComponentName; @@ -29,6 +31,7 @@ import android.text.Html; import android.text.Spanned; import android.text.SpannedString; import android.util.Log; +import android.util.Xml; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -40,6 +43,8 @@ import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -149,6 +154,46 @@ public final class WallpaperDescription implements Parcelable { return Objects.hash(mComponent, mId); } + ////// Stream read/write + + /** + * Writes the content of the {@link WallpaperDescription} to a {@link OutputStream}. + * + * <p>The content can be read by {@link #readFromStream}. This method is intended for use by + * trusted apps only, and the format is not guaranteed to be stable.</p> + */ + public void writeToStream(@NonNull OutputStream outputStream) throws IOException { + TypedXmlSerializer serializer = Xml.newFastSerializer(); + serializer.setOutput(outputStream, UTF_8.name()); + serializer.startTag(null, "description"); + try { + saveToXml(serializer); + } catch (XmlPullParserException e) { + throw new IOException(e); + } + serializer.endTag(null, "description"); + serializer.flush(); + } + + /** + * Reads a {@link PersistableBundle} from an {@link InputStream}. + * + * <p>The stream must be generated by {@link #writeToStream}. This method is intended for use by + * trusted apps only, and the format is not guaranteed to be stable.</p> + */ + @NonNull + public static WallpaperDescription readFromStream(@NonNull InputStream inputStream) + throws IOException { + try { + TypedXmlPullParser parser = Xml.newFastPullParser(); + parser.setInput(inputStream, UTF_8.name()); + parser.next(); + return WallpaperDescription.restoreFromXml(parser); + } catch (XmlPullParserException e) { + throw new IOException(e); + } + } + ////// XML storage /** @hide */ @@ -255,7 +300,7 @@ public final class WallpaperDescription implements Parcelable { mContent = PersistableBundle.CREATOR.createFromParcel(in); } - @Nullable + @NonNull public static final Creator<WallpaperDescription> CREATOR = new Creator<>() { @Override public WallpaperDescription createFromParcel(Parcel source) { diff --git a/core/java/android/appwidget/AppWidgetHost.java b/core/java/android/appwidget/AppWidgetHost.java index 72f992ab3917..79eeaa914771 100644 --- a/core/java/android/appwidget/AppWidgetHost.java +++ b/core/java/android/appwidget/AppWidgetHost.java @@ -306,6 +306,38 @@ public class AppWidgetHost { } /** + * Returns an {@link IntentSender} for starting the configuration activity for the widget. + * + * @return The {@link IntentSender} or null if service is currently unavailable + * + * @throws android.content.ActivityNotFoundException If configuration activity is not found. + * + * @see #startAppWidgetConfigureActivityForResult + * + * @hide + */ + @Nullable + public final IntentSender getIntentSenderForConfigureActivity(int appWidgetId, + int intentFlags) { + if (sService == null) { + return null; + } + + IntentSender intentSender; + try { + intentSender = sService.createAppWidgetConfigIntentSender(mContextOpPackageName, + appWidgetId, intentFlags); + } catch (RemoteException e) { + throw new RuntimeException("system server dead?", e); + } + + if (intentSender == null) { + throw new ActivityNotFoundException(); + } + return intentSender; + } + + /** * Starts an app widget provider configure activity for result on behalf of the caller. * Use this method if the provider is in another profile as you are not allowed to start * an activity in another profile. You can optionally provide a request code that is @@ -330,18 +362,11 @@ public class AppWidgetHost { return; } try { - IntentSender intentSender = sService.createAppWidgetConfigIntentSender( - mContextOpPackageName, appWidgetId, intentFlags); - if (intentSender != null) { - activity.startIntentSenderForResult(intentSender, requestCode, null, 0, 0, 0, - options); - } else { - throw new ActivityNotFoundException(); - } + IntentSender intentSender = getIntentSenderForConfigureActivity(appWidgetId, + intentFlags); + activity.startIntentSenderForResult(intentSender, requestCode, null, 0, 0, 0, options); } catch (IntentSender.SendIntentException e) { throw new ActivityNotFoundException(); - } catch (RemoteException e) { - throw new RuntimeException("system server dead?", e); } } diff --git a/core/java/android/appwidget/AppWidgetManager.java b/core/java/android/appwidget/AppWidgetManager.java index d8142fd9687c..8f20ea034cf6 100644 --- a/core/java/android/appwidget/AppWidgetManager.java +++ b/core/java/android/appwidget/AppWidgetManager.java @@ -246,7 +246,8 @@ public class AppWidgetManager { * this widget. Can have the value {@link * AppWidgetProviderInfo#WIDGET_CATEGORY_HOME_SCREEN} or {@link * AppWidgetProviderInfo#WIDGET_CATEGORY_KEYGUARD} or {@link - * AppWidgetProviderInfo#WIDGET_CATEGORY_SEARCHBOX}. + * AppWidgetProviderInfo#WIDGET_CATEGORY_SEARCHBOX} or {@link + * AppWidgetProviderInfo#WIDGET_CATEGORY_NOT_KEYGUARD}. */ public static final String OPTION_APPWIDGET_HOST_CATEGORY = "appWidgetCategory"; diff --git a/core/java/android/appwidget/AppWidgetProviderInfo.java b/core/java/android/appwidget/AppWidgetProviderInfo.java index 1a80cac2f06a..75c76d16ac5c 100644 --- a/core/java/android/appwidget/AppWidgetProviderInfo.java +++ b/core/java/android/appwidget/AppWidgetProviderInfo.java @@ -93,11 +93,26 @@ public class AppWidgetProviderInfo implements Parcelable { */ public static final int WIDGET_CATEGORY_SEARCHBOX = 4; + /** + * Indicates that the widget should never be shown on the keyguard. + * + * <p>Some keyguard style features may decide that {@link #WIDGET_CATEGORY_KEYGUARD} isn't + * required to be added by an app to show on the feature when chosen by a user. + * This category allows for a stronger statement about placement of the widget that, even in the + * above case, this widget should not be offered on the keyguard. + * + * <p>Setting this category doesn't change the behavior of AppWidgetManager queries, it is the + * responsibility of the widget surface to respect this value. + */ + @FlaggedApi(android.appwidget.flags.Flags.FLAG_NOT_KEYGUARD_CATEGORY) + public static final int WIDGET_CATEGORY_NOT_KEYGUARD = 8; + /** @hide */ @IntDef(flag = true, prefix = { "FLAG_" }, value = { WIDGET_CATEGORY_HOME_SCREEN, WIDGET_CATEGORY_KEYGUARD, WIDGET_CATEGORY_SEARCHBOX, + WIDGET_CATEGORY_NOT_KEYGUARD, }) @Retention(RetentionPolicy.SOURCE) public @interface CategoryFlags {} diff --git a/core/java/android/appwidget/flags.aconfig b/core/java/android/appwidget/flags.aconfig index 3839b5fa2599..940a4d208d99 100644 --- a/core/java/android/appwidget/flags.aconfig +++ b/core/java/android/appwidget/flags.aconfig @@ -83,3 +83,12 @@ flag { is_exported: true is_fixed_read_only: true } + +flag { + name: "not_keyguard_category" + namespace: "app_widgets" + description: "Adds the NOT_KEYGUARD category to AppWidgetInfo categories" + bug: "365169792" + is_exported: true + is_fixed_read_only: true +} diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig index 9eb6d5624f75..9af20164a66d 100644 --- a/core/java/android/companion/virtual/flags/flags.aconfig +++ b/core/java/android/companion/virtual/flags/flags.aconfig @@ -145,3 +145,11 @@ flag { bug: "370657575" is_exported: true } + +flag { + namespace: "virtual_devices" + name: "default_device_camera_access_policy" + description: "API for default device camera access policy" + bug: "371173368" + is_exported: true +} diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 5893da371ed1..186f7b3e111c 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -3658,6 +3658,27 @@ public abstract class Context { public abstract void unregisterReceiver(BroadcastReceiver receiver); /** + * Returns the list of {@link IntentFilter} objects that have been registered for the given + * {@link BroadcastReceiver}. + * + * @param receiver The {@link BroadcastReceiver} whose registered intent filters + * should be retrieved. + * + * @return A list of registered intent filters, or an empty list if the given receiver is not + * registered. + * + * @throws NullPointerException if the {@code receiver} is {@code null}. + * + * @hide + */ + @SuppressLint("UnflaggedApi") // TestApi + @TestApi + @NonNull + public List<IntentFilter> getRegisteredIntentFilters(@NonNull BroadcastReceiver receiver) { + throw new RuntimeException("Not implemented. Must override in a subclass."); + } + + /** * Request that a given application service be started. The Intent * should either contain the complete class name of a specific service * implementation to start, or a specific package name to target. If the @@ -4873,6 +4894,8 @@ public abstract class Context { * @see android.net.vcn.VcnManager * @hide */ + @FlaggedApi(android.os.Flags.FLAG_MAINLINE_VCN_PLATFORM_API) + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) public static final String VCN_MANAGEMENT_SERVICE = "vcn_management"; /** @@ -6654,8 +6677,8 @@ public abstract class Context { * * @see #getSystemService(String) * @see android.telephony.satellite.SatelliteManager - * @hide */ + @FlaggedApi(com.android.internal.telephony.flags.Flags.FLAG_SATELLITE_STATE_CHANGE_LISTENER) public static final String SATELLITE_SERVICE = "satellite"; /** diff --git a/core/java/android/content/ContextWrapper.java b/core/java/android/content/ContextWrapper.java index 79fa6ea4d157..23d17cb5ce50 100644 --- a/core/java/android/content/ContextWrapper.java +++ b/core/java/android/content/ContextWrapper.java @@ -843,6 +843,13 @@ public class ContextWrapper extends Context { mBase.unregisterReceiver(receiver); } + /** @hide */ + @Override + @NonNull + public List<IntentFilter> getRegisteredIntentFilters(@NonNull BroadcastReceiver receiver) { + return mBase.getRegisteredIntentFilters(receiver); + } + @Override public @Nullable ComponentName startService(Intent service) { return mBase.startService(service); diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index 9bec5981261e..6fa5a9b82858 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -41,10 +41,7 @@ import android.app.Activity; import android.app.ActivityThread; import android.app.AppGlobals; import android.app.StatusBarManager; -import android.app.compat.CompatChanges; import android.bluetooth.BluetoothDevice; -import android.compat.annotation.ChangeId; -import android.compat.annotation.Overridable; import android.compat.annotation.UnsupportedAppUsage; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; @@ -673,12 +670,6 @@ import java.util.TimeZone; @android.ravenwood.annotation.RavenwoodKeepWholeClass public class Intent implements Parcelable, Cloneable { private static final String TAG = "Intent"; - - /** @hide */ - @ChangeId - @Overridable - public static final long ENABLE_PREVENT_INTENT_REDIRECT = 29076063L; - private static final String ATTR_ACTION = "action"; private static final String TAG_CATEGORIES = "categories"; private static final String ATTR_CATEGORY = "category"; @@ -908,7 +899,7 @@ public class Intent implements Parcelable, Cloneable { boolean isForeign = (intent.mLocalFlags & LOCAL_FLAG_FROM_PARCEL) != 0; boolean isWithoutTrustedCreatorToken = (intent.mLocalFlags & Intent.LOCAL_FLAG_TRUSTED_CREATOR_TOKEN_PRESENT) == 0; - if (isForeign && isWithoutTrustedCreatorToken) { + if (isForeign && isWithoutTrustedCreatorToken && preventIntentRedirect()) { intent.addExtendedFlags(EXTENDED_FLAG_MISSING_CREATOR_OR_INVALID_TOKEN); } } @@ -12255,7 +12246,7 @@ public class Intent implements Parcelable, Cloneable { * @hide */ public void collectExtraIntentKeys() { - if (!isPreventIntentRedirectEnabled()) return; + if (!preventIntentRedirect()) return; if (mExtras != null && !mExtras.isEmpty()) { for (String key : mExtras.keySet()) { @@ -12272,14 +12263,6 @@ public class Intent implements Parcelable, Cloneable { } } - /** - * @hide - */ - public static boolean isPreventIntentRedirectEnabled() { - return preventIntentRedirect() && CompatChanges.isChangeEnabled( - ENABLE_PREVENT_INTENT_REDIRECT); - } - /** @hide */ public void checkCreatorToken() { if (mExtras == null) return; @@ -12368,7 +12351,7 @@ public class Intent implements Parcelable, Cloneable { out.writeInt(0); } - if (isPreventIntentRedirectEnabled()) { + if (preventIntentRedirect()) { if (mCreatorTokenInfo == null) { out.writeInt(0); } else { @@ -12435,7 +12418,7 @@ public class Intent implements Parcelable, Cloneable { mOriginalIntent = new Intent(in); } - if (isPreventIntentRedirectEnabled()) { + if (preventIntentRedirect()) { if (in.readInt() != 0) { mCreatorTokenInfo = new CreatorTokenInfo(); mCreatorTokenInfo.mCreatorToken = in.readStrongBinder(); diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig index 35f9cff1aedb..528bde80cd3d 100644 --- a/core/java/android/content/pm/multiuser.aconfig +++ b/core/java/android/content/pm/multiuser.aconfig @@ -536,7 +536,7 @@ flag { flag { name: "ignore_restrictions_when_deleting_private_profile" - namespace: "multiuser" + namespace: "profile_experiences" description: "Ignore any user restrictions when deleting private profiles." bug: "350953833" metadata { diff --git a/core/java/android/content/res/flags.aconfig b/core/java/android/content/res/flags.aconfig index 0af2f2576137..e98fc0c9d02c 100644 --- a/core/java/android/content/res/flags.aconfig +++ b/core/java/android/content/res/flags.aconfig @@ -74,3 +74,12 @@ flag { description: "Feature flag for passing a dimension to create an frro" bug: "369672322" } + +flag { + name: "rro_control_for_android_no_overlayable" + is_exported: true + namespace: "resource_manager" + description: "Allow enabling and disabling RROs targeting android package with no overlayable" + bug: "364035303" +} + diff --git a/core/java/android/database/sqlite/SQLiteConnection.java b/core/java/android/database/sqlite/SQLiteConnection.java index d77e6280d19c..75c7e267d477 100644 --- a/core/java/android/database/sqlite/SQLiteConnection.java +++ b/core/java/android/database/sqlite/SQLiteConnection.java @@ -241,7 +241,8 @@ public final class SQLiteConnection implements CancellationSignal.OnCancelListen NoPreloadHolder.DEBUG_SQL_STATEMENTS, NoPreloadHolder.DEBUG_SQL_TIME, mConfiguration.lookasideSlotSize, mConfiguration.lookasideSlotCount); } catch (SQLiteCantOpenDatabaseException e) { - final StringBuilder message = new StringBuilder(e.getMessage()) + final StringBuilder message = new StringBuilder("Cannot open database ") + .append("[").append(e.getMessage()).append("]") .append(" '").append(file).append("'") .append(" with flags 0x") .append(Integer.toHexString(mConfiguration.openFlags)); diff --git a/core/java/android/hardware/SensorEvent.java b/core/java/android/hardware/SensorEvent.java index e8d7e1efe315..a7e7a57e35d3 100644 --- a/core/java/android/hardware/SensorEvent.java +++ b/core/java/android/hardware/SensorEvent.java @@ -150,6 +150,7 @@ public class SensorEvent { * All values are in micro-Tesla (uT) and measure the ambient magnetic field * in the X, Y and Z axis. * + * * <h4>{@link android.hardware.Sensor#TYPE_GYROSCOPE Sensor.TYPE_GYROSCOPE}: * </h4> All values are in radians/second and measure the rate of rotation * around the device's local X, Y and Z axis. The coordinate system is the @@ -406,10 +407,9 @@ public class SensorEvent { * </h4> * * <ul> - * <li> values[0]: ambient (room) temperature in degree Celsius.</li> + * <li> values[0]: Ambient (room) temperature in degrees Celsius</li> * </ul> * - * * <h4>{@link android.hardware.Sensor#TYPE_MAGNETIC_FIELD_UNCALIBRATED * Sensor.TYPE_MAGNETIC_FIELD_UNCALIBRATED}:</h4> * Similar to {@link android.hardware.Sensor#TYPE_MAGNETIC_FIELD}, @@ -549,8 +549,20 @@ public class SensorEvent { * <li> values[0]: 1.0 </li> * </ul> * - * <h4>{@link android.hardware.Sensor#TYPE_HEART_BEAT - * Sensor.TYPE_HEART_BEAT}:</h4> + * <h4>{@link android.hardware.Sensor#TYPE_STEP_COUNTER Sensor.TYPE_STEP_COUNTER}:</h4> + * + * <ul> + * <li>values[0]: Number of steps taken by the user since the last reboot while the sensor is + * activated</li> + * </ul> + * + * <h4>{@link android.hardware.Sensor#TYPE_STEP_DETECTOR Sensor.TYPE_STEP_DETECTOR}:</h4> + * + * <ul> + * <li>values[0]: Always set to 1.0, representing a single step detected event</li> + * </ul> + * + * <h4>{@link android.hardware.Sensor#TYPE_HEART_BEAT Sensor.TYPE_HEART_BEAT}:</h4> * * A sensor of this type returns an event everytime a heart beat peak is * detected. diff --git a/core/java/android/hardware/biometrics/BiometricPrompt.java b/core/java/android/hardware/biometrics/BiometricPrompt.java index e3fdd267623e..3ff21d85507b 100644 --- a/core/java/android/hardware/biometrics/BiometricPrompt.java +++ b/core/java/android/hardware/biometrics/BiometricPrompt.java @@ -22,7 +22,6 @@ import static android.Manifest.permission.USE_BIOMETRIC; import static android.Manifest.permission.USE_BIOMETRIC_INTERNAL; import static android.hardware.biometrics.BiometricManager.Authenticators; import static android.hardware.biometrics.Flags.FLAG_ADD_KEY_AGREEMENT_CRYPTO_OBJECT; -import static android.hardware.biometrics.Flags.FLAG_CUSTOM_BIOMETRIC_PROMPT; import static android.hardware.biometrics.Flags.FLAG_GET_OP_ID_CRYPTO_OBJECT; import static android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE; @@ -200,7 +199,6 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan * @param logoRes A drawable resource of the logo that will be shown on the prompt. * @return This builder. */ - @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED) @NonNull public BiometricPrompt.Builder setLogoRes(@DrawableRes int logoRes) { @@ -226,7 +224,6 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan * @param logoBitmap A bitmap drawable of the logo that will be shown on the prompt. * @return This builder. */ - @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED) @NonNull public BiometricPrompt.Builder setLogoBitmap(@NonNull Bitmap logoBitmap) { @@ -250,7 +247,6 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan * @return This builder. * @throws IllegalArgumentException If logo description is null. */ - @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED) @NonNull public BiometricPrompt.Builder setLogoDescription(@NonNull String logoDescription) { @@ -342,7 +338,6 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan * @param view The customized view information. * @return This builder. */ - @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) @NonNull public BiometricPrompt.Builder setContentView(@NonNull PromptContentView view) { mPromptInfo.setContentView(view); @@ -851,7 +846,6 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan * * @return The drawable resource of the logo, or 0 if the prompt has no logo resource set. */ - @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED) @DrawableRes public int getLogoRes() { @@ -864,7 +858,6 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan * * @return The logo bitmap of the prompt, or null if the prompt has no logo bitmap set. */ - @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED) @Nullable public Bitmap getLogoBitmap() { @@ -879,7 +872,6 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan * @return The logo description of the prompt, or null if the prompt has no logo description * set. */ - @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) @RequiresPermission(SET_BIOMETRIC_DIALOG_ADVANCED) @Nullable public String getLogoDescription() { @@ -939,7 +931,6 @@ public class BiometricPrompt implements BiometricAuthenticator, BiometricConstan * * @return The content view for the prompt, or null if the prompt has no content view. */ - @FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) @Nullable public PromptContentView getContentView() { return mPromptInfo.getContentView(); diff --git a/core/java/android/hardware/biometrics/PromptContentItem.java b/core/java/android/hardware/biometrics/PromptContentItem.java index c47b37aca2ea..0c41782da9fb 100644 --- a/core/java/android/hardware/biometrics/PromptContentItem.java +++ b/core/java/android/hardware/biometrics/PromptContentItem.java @@ -16,14 +16,9 @@ package android.hardware.biometrics; -import static android.hardware.biometrics.Flags.FLAG_CUSTOM_BIOMETRIC_PROMPT; - -import android.annotation.FlaggedApi; - /** * An item shown on {@link PromptContentView}. */ -@FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) public interface PromptContentItem { } diff --git a/core/java/android/hardware/biometrics/PromptContentItemBulletedText.java b/core/java/android/hardware/biometrics/PromptContentItemBulletedText.java index 25e5cca485d2..a026498b6e81 100644 --- a/core/java/android/hardware/biometrics/PromptContentItemBulletedText.java +++ b/core/java/android/hardware/biometrics/PromptContentItemBulletedText.java @@ -16,9 +16,6 @@ package android.hardware.biometrics; -import static android.hardware.biometrics.Flags.FLAG_CUSTOM_BIOMETRIC_PROMPT; - -import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.os.Parcel; import android.os.Parcelable; @@ -26,7 +23,6 @@ import android.os.Parcelable; /** * A list item with bulleted text shown on {@link PromptVerticalListContentView}. */ -@FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) public final class PromptContentItemBulletedText implements PromptContentItemParcelable { private final String mText; diff --git a/core/java/android/hardware/biometrics/PromptContentItemParcelable.java b/core/java/android/hardware/biometrics/PromptContentItemParcelable.java index 668912cfdf24..1860aae9993a 100644 --- a/core/java/android/hardware/biometrics/PromptContentItemParcelable.java +++ b/core/java/android/hardware/biometrics/PromptContentItemParcelable.java @@ -16,15 +16,11 @@ package android.hardware.biometrics; -import static android.hardware.biometrics.Flags.FLAG_CUSTOM_BIOMETRIC_PROMPT; - -import android.annotation.FlaggedApi; import android.os.Parcelable; /** * A parcelable {@link PromptContentItem}. */ -@FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) sealed interface PromptContentItemParcelable extends PromptContentItem, Parcelable permits PromptContentItemPlainText, PromptContentItemBulletedText { } diff --git a/core/java/android/hardware/biometrics/PromptContentItemPlainText.java b/core/java/android/hardware/biometrics/PromptContentItemPlainText.java index 7919256f9c6d..a5e612060575 100644 --- a/core/java/android/hardware/biometrics/PromptContentItemPlainText.java +++ b/core/java/android/hardware/biometrics/PromptContentItemPlainText.java @@ -16,9 +16,6 @@ package android.hardware.biometrics; -import static android.hardware.biometrics.Flags.FLAG_CUSTOM_BIOMETRIC_PROMPT; - -import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.os.Parcel; import android.os.Parcelable; @@ -26,7 +23,6 @@ import android.os.Parcelable; /** * A list item with plain text shown on {@link PromptVerticalListContentView}. */ -@FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) public final class PromptContentItemPlainText implements PromptContentItemParcelable { private final String mText; diff --git a/core/java/android/hardware/biometrics/PromptContentView.java b/core/java/android/hardware/biometrics/PromptContentView.java index ff9313e8d3f0..0836d72ccf18 100644 --- a/core/java/android/hardware/biometrics/PromptContentView.java +++ b/core/java/android/hardware/biometrics/PromptContentView.java @@ -16,13 +16,8 @@ package android.hardware.biometrics; -import static android.hardware.biometrics.Flags.FLAG_CUSTOM_BIOMETRIC_PROMPT; - -import android.annotation.FlaggedApi; - /** * Contains the information of the template of content view for Biometric Prompt. */ -@FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) public interface PromptContentView { } diff --git a/core/java/android/hardware/biometrics/PromptContentViewParcelable.java b/core/java/android/hardware/biometrics/PromptContentViewParcelable.java index b5982d4f7251..6e035634df78 100644 --- a/core/java/android/hardware/biometrics/PromptContentViewParcelable.java +++ b/core/java/android/hardware/biometrics/PromptContentViewParcelable.java @@ -16,15 +16,11 @@ package android.hardware.biometrics; -import static android.hardware.biometrics.Flags.FLAG_CUSTOM_BIOMETRIC_PROMPT; - -import android.annotation.FlaggedApi; import android.os.Parcelable; /** * A parcelable {@link PromptContentView}. */ -@FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) sealed interface PromptContentViewParcelable extends PromptContentView, Parcelable permits PromptVerticalListContentView, PromptContentViewWithMoreOptionsButton { } diff --git a/core/java/android/hardware/biometrics/PromptContentViewWithMoreOptionsButton.java b/core/java/android/hardware/biometrics/PromptContentViewWithMoreOptionsButton.java index 4b9d5ce7beb0..aa0ce5843a34 100644 --- a/core/java/android/hardware/biometrics/PromptContentViewWithMoreOptionsButton.java +++ b/core/java/android/hardware/biometrics/PromptContentViewWithMoreOptionsButton.java @@ -17,10 +17,8 @@ package android.hardware.biometrics; import static android.Manifest.permission.SET_BIOMETRIC_DIALOG_ADVANCED; -import static android.hardware.biometrics.Flags.FLAG_CUSTOM_BIOMETRIC_PROMPT; import android.annotation.CallbackExecutor; -import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; @@ -61,7 +59,6 @@ import java.util.concurrent.Executor; * .build(); * </pre> */ -@FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) public final class PromptContentViewWithMoreOptionsButton implements PromptContentViewParcelable { private static final String TAG = "PromptContentViewWithMoreOptionsButton"; @VisibleForTesting diff --git a/core/java/android/hardware/biometrics/PromptInfo.java b/core/java/android/hardware/biometrics/PromptInfo.java index e23ffeb6a0d2..3e304e4a5915 100644 --- a/core/java/android/hardware/biometrics/PromptInfo.java +++ b/core/java/android/hardware/biometrics/PromptInfo.java @@ -217,7 +217,7 @@ public class PromptInfo implements Parcelable { * Returns if the PromptContentViewWithMoreOptionsButton is set. */ public boolean isContentViewMoreOptionsButtonUsed() { - return Flags.customBiometricPrompt() && mContentView != null + return mContentView != null && mContentView instanceof PromptContentViewWithMoreOptionsButton; } diff --git a/core/java/android/hardware/biometrics/PromptVerticalListContentView.java b/core/java/android/hardware/biometrics/PromptVerticalListContentView.java index 86006f84fe60..2a521d1b1c2d 100644 --- a/core/java/android/hardware/biometrics/PromptVerticalListContentView.java +++ b/core/java/android/hardware/biometrics/PromptVerticalListContentView.java @@ -16,9 +16,6 @@ package android.hardware.biometrics; -import static android.hardware.biometrics.Flags.FLAG_CUSTOM_BIOMETRIC_PROMPT; - -import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; import android.os.Parcel; @@ -48,7 +45,6 @@ import java.util.List; * .build(); * </pre> */ -@FlaggedApi(FLAG_CUSTOM_BIOMETRIC_PROMPT) public final class PromptVerticalListContentView implements PromptContentViewParcelable { private static final String TAG = "PromptVerticalListContentView"; @VisibleForTesting diff --git a/core/java/android/hardware/biometrics/flags.aconfig b/core/java/android/hardware/biometrics/flags.aconfig index 52a4898209ce..7a23033754c5 100644 --- a/core/java/android/hardware/biometrics/flags.aconfig +++ b/core/java/android/hardware/biometrics/flags.aconfig @@ -54,3 +54,20 @@ flag { description: "This flag is for API changes related to Identity Check" bug: "373424727" } + +flag { + name: "private_space_bp" + namespace: "biometrics_framework" + description: "Feature flag for biometric prompt improvements in private space" + bug: "365554098" +} + +flag { + name: "effective_user_bp" + namespace: "biometrics_framework" + description: "Feature flag for using effective user in biometric prompt" + bug: "365094949" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/android/hardware/camera2/CameraCharacteristics.java b/core/java/android/hardware/camera2/CameraCharacteristics.java index 1137e1e89f67..16d82caa6c1a 100644 --- a/core/java/android/hardware/camera2/CameraCharacteristics.java +++ b/core/java/android/hardware/camera2/CameraCharacteristics.java @@ -775,6 +775,46 @@ public final class CameraCharacteristics extends CameraMetadata<CameraCharacteri new Key<int[]>("android.colorCorrection.availableAberrationModes", int[].class); /** + * <p>The range of supported color temperature values for + * {@link CaptureRequest#COLOR_CORRECTION_COLOR_TEMPERATURE android.colorCorrection.colorTemperature}.</p> + * <p>This key lists the valid range of color temperature values for + * {@link CaptureRequest#COLOR_CORRECTION_COLOR_TEMPERATURE android.colorCorrection.colorTemperature} supported by this camera device.</p> + * <p>This key will be null on devices that do not support CCT mode for + * {@link CaptureRequest#COLOR_CORRECTION_MODE android.colorCorrection.mode}.</p> + * <p><b>Range of valid values:</b><br></p> + * <p>The minimum supported range will be [2856K,6500K]. The maximum supported + * range will be [1000K,40000K].</p> + * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p> + * + * @see CaptureRequest#COLOR_CORRECTION_COLOR_TEMPERATURE + * @see CaptureRequest#COLOR_CORRECTION_MODE + */ + @PublicKey + @NonNull + @FlaggedApi(Flags.FLAG_COLOR_TEMPERATURE) + public static final Key<android.util.Range<Integer>> COLOR_CORRECTION_COLOR_TEMPERATURE_RANGE = + new Key<android.util.Range<Integer>>("android.colorCorrection.colorTemperatureRange", new TypeReference<android.util.Range<Integer>>() {{ }}); + + /** + * <p>List of color correction modes for {@link CaptureRequest#COLOR_CORRECTION_MODE android.colorCorrection.mode} that are + * supported by this camera device.</p> + * <p>This key lists the valid modes for {@link CaptureRequest#COLOR_CORRECTION_MODE android.colorCorrection.mode}. If no + * color correction modes are available for a device, this key will be null.</p> + * <p>Camera devices that have a FULL hardware level will always include at least + * FAST, HIGH_QUALITY, and TRANSFORM_MATRIX modes.</p> + * <p><b>Range of valid values:</b><br> + * Any value listed in {@link CaptureRequest#COLOR_CORRECTION_MODE android.colorCorrection.mode}</p> + * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p> + * + * @see CaptureRequest#COLOR_CORRECTION_MODE + */ + @PublicKey + @NonNull + @FlaggedApi(Flags.FLAG_COLOR_TEMPERATURE) + public static final Key<int[]> COLOR_CORRECTION_AVAILABLE_MODES = + new Key<int[]>("android.colorCorrection.availableModes", int[].class); + + /** * <p>List of auto-exposure antibanding modes for {@link CaptureRequest#CONTROL_AE_ANTIBANDING_MODE android.control.aeAntibandingMode} that are * supported by this camera device.</p> * <p>Not all of the auto-exposure anti-banding modes may be diff --git a/core/java/android/hardware/camera2/CameraMetadata.java b/core/java/android/hardware/camera2/CameraMetadata.java index acb48f328f1a..86bbd4a57a63 100644 --- a/core/java/android/hardware/camera2/CameraMetadata.java +++ b/core/java/android/hardware/camera2/CameraMetadata.java @@ -2169,6 +2169,22 @@ public abstract class CameraMetadata<TKey> { */ public static final int COLOR_CORRECTION_MODE_HIGH_QUALITY = 2; + /** + * <p>Use + * {@link CaptureRequest#COLOR_CORRECTION_COLOR_TEMPERATURE android.colorCorrection.colorTemperature} and + * {@link CaptureRequest#COLOR_CORRECTION_COLOR_TINT android.colorCorrection.colorTint} to adjust the white balance based + * on correlated color temperature.</p> + * <p>If AWB is enabled with <code>{@link CaptureRequest#CONTROL_AWB_MODE android.control.awbMode} != OFF</code>, then + * CCT is ignored.</p> + * + * @see CaptureRequest#COLOR_CORRECTION_COLOR_TEMPERATURE + * @see CaptureRequest#COLOR_CORRECTION_COLOR_TINT + * @see CaptureRequest#CONTROL_AWB_MODE + * @see CaptureRequest#COLOR_CORRECTION_MODE + */ + @FlaggedApi(Flags.FLAG_COLOR_TEMPERATURE) + public static final int COLOR_CORRECTION_MODE_CCT = 3; + // // Enumeration values for CaptureRequest#COLOR_CORRECTION_ABERRATION_MODE // diff --git a/core/java/android/hardware/camera2/CaptureRequest.java b/core/java/android/hardware/camera2/CaptureRequest.java index a5c5a9952879..8142bbe9b838 100644 --- a/core/java/android/hardware/camera2/CaptureRequest.java +++ b/core/java/android/hardware/camera2/CaptureRequest.java @@ -1086,11 +1086,17 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> * <li>{@link #COLOR_CORRECTION_MODE_HIGH_QUALITY HIGH_QUALITY}</li> * </ul> * + * <p><b>Available values for this device:</b><br> + * Starting from API level 36, {@link CameraCharacteristics#COLOR_CORRECTION_AVAILABLE_MODES android.colorCorrection.availableModes} + * can be used to check the list of supported values. Prior to API level 36, + * TRANSFORM_MATRIX, HIGH_QUALITY, and FAST are guaranteed to be available + * as valid modes on devices that support this key.</p> * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p> * <p><b>Full capability</b> - * Present on all camera devices that report being {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_FULL HARDWARE_LEVEL_FULL} devices in the * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p> * + * @see CameraCharacteristics#COLOR_CORRECTION_AVAILABLE_MODES * @see CaptureRequest#COLOR_CORRECTION_GAINS * @see CaptureRequest#COLOR_CORRECTION_TRANSFORM * @see CaptureRequest#CONTROL_AWB_MODE @@ -1195,6 +1201,60 @@ public final class CaptureRequest extends CameraMetadata<CaptureRequest.Key<?>> new Key<Integer>("android.colorCorrection.aberrationMode", int.class); /** + * <p>Specifies the color temperature for CCT mode in Kelvin + * to adjust the white balance of the image.</p> + * <p>Sets the color temperature in Kelvin units for when + * {@link CaptureRequest#COLOR_CORRECTION_MODE android.colorCorrection.mode} is CCT to adjust the + * white balance of the image.</p> + * <p>If CCT mode is enabled without a requested color temperature, + * a default value will be set by the camera device. The default value can be + * retrieved by checking the corresponding capture result. Color temperatures + * requested outside the advertised {@link CameraCharacteristics#COLOR_CORRECTION_COLOR_TEMPERATURE_RANGE android.colorCorrection.colorTemperatureRange} + * will be clamped.</p> + * <p><b>Units</b>: Kelvin</p> + * <p><b>Range of valid values:</b><br> + * {@link CameraCharacteristics#COLOR_CORRECTION_COLOR_TEMPERATURE_RANGE android.colorCorrection.colorTemperatureRange}</p> + * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p> + * + * @see CameraCharacteristics#COLOR_CORRECTION_COLOR_TEMPERATURE_RANGE + * @see CaptureRequest#COLOR_CORRECTION_MODE + */ + @PublicKey + @NonNull + @FlaggedApi(Flags.FLAG_COLOR_TEMPERATURE) + public static final Key<Integer> COLOR_CORRECTION_COLOR_TEMPERATURE = + new Key<Integer>("android.colorCorrection.colorTemperature", int.class); + + /** + * <p>Specifies the color tint for CCT mode to adjust the white + * balance of the image.</p> + * <p>Sets the color tint for when {@link CaptureRequest#COLOR_CORRECTION_MODE android.colorCorrection.mode} + * is CCT to adjust the white balance of the image.</p> + * <p>If CCT mode is enabled without a requested color tint, + * a default value will be set by the camera device. The default value can be + * retrieved by checking the corresponding capture result. Color tints requested + * outside the supported range will be clamped to the nearest limit (-50 or +50).</p> + * <p><b>Units</b>: D_uv defined as the distance from the Planckian locus on the CIE 1931 xy + * chromaticity diagram, with the range ±50 mapping to ±0.01 D_uv</p> + * <p><b>Range of valid values:</b><br> + * The supported range, -50 to +50, corresponds to a D_uv distance + * of ±0.01 below and above the Planckian locus. Some camera devices may have + * limitations to achieving the full ±0.01 D_uv range at some color temperatures + * (e.g., below 1500K). In these cases, the applied D_uv value may be clamped and + * the actual color tint will be reported in the {@link CaptureRequest#COLOR_CORRECTION_COLOR_TINT android.colorCorrection.colorTint} + * result.</p> + * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p> + * + * @see CaptureRequest#COLOR_CORRECTION_COLOR_TINT + * @see CaptureRequest#COLOR_CORRECTION_MODE + */ + @PublicKey + @NonNull + @FlaggedApi(Flags.FLAG_COLOR_TEMPERATURE) + public static final Key<Integer> COLOR_CORRECTION_COLOR_TINT = + new Key<Integer>("android.colorCorrection.colorTint", int.class); + + /** * <p>The desired setting for the camera device's auto-exposure * algorithm's antibanding compensation.</p> * <p>Some kinds of lighting fixtures, such as some fluorescent diff --git a/core/java/android/hardware/camera2/CaptureResult.java b/core/java/android/hardware/camera2/CaptureResult.java index a6bdb3f1bb07..ae72ca40fc5a 100644 --- a/core/java/android/hardware/camera2/CaptureResult.java +++ b/core/java/android/hardware/camera2/CaptureResult.java @@ -487,11 +487,17 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { * <li>{@link #COLOR_CORRECTION_MODE_HIGH_QUALITY HIGH_QUALITY}</li> * </ul> * + * <p><b>Available values for this device:</b><br> + * Starting from API level 36, {@link CameraCharacteristics#COLOR_CORRECTION_AVAILABLE_MODES android.colorCorrection.availableModes} + * can be used to check the list of supported values. Prior to API level 36, + * TRANSFORM_MATRIX, HIGH_QUALITY, and FAST are guaranteed to be available + * as valid modes on devices that support this key.</p> * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p> * <p><b>Full capability</b> - * Present on all camera devices that report being {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL_FULL HARDWARE_LEVEL_FULL} devices in the * {@link CameraCharacteristics#INFO_SUPPORTED_HARDWARE_LEVEL android.info.supportedHardwareLevel} key</p> * + * @see CameraCharacteristics#COLOR_CORRECTION_AVAILABLE_MODES * @see CaptureRequest#COLOR_CORRECTION_GAINS * @see CaptureRequest#COLOR_CORRECTION_TRANSFORM * @see CaptureRequest#CONTROL_AWB_MODE @@ -596,6 +602,60 @@ public class CaptureResult extends CameraMetadata<CaptureResult.Key<?>> { new Key<Integer>("android.colorCorrection.aberrationMode", int.class); /** + * <p>Specifies the color temperature for CCT mode in Kelvin + * to adjust the white balance of the image.</p> + * <p>Sets the color temperature in Kelvin units for when + * {@link CaptureRequest#COLOR_CORRECTION_MODE android.colorCorrection.mode} is CCT to adjust the + * white balance of the image.</p> + * <p>If CCT mode is enabled without a requested color temperature, + * a default value will be set by the camera device. The default value can be + * retrieved by checking the corresponding capture result. Color temperatures + * requested outside the advertised {@link CameraCharacteristics#COLOR_CORRECTION_COLOR_TEMPERATURE_RANGE android.colorCorrection.colorTemperatureRange} + * will be clamped.</p> + * <p><b>Units</b>: Kelvin</p> + * <p><b>Range of valid values:</b><br> + * {@link CameraCharacteristics#COLOR_CORRECTION_COLOR_TEMPERATURE_RANGE android.colorCorrection.colorTemperatureRange}</p> + * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p> + * + * @see CameraCharacteristics#COLOR_CORRECTION_COLOR_TEMPERATURE_RANGE + * @see CaptureRequest#COLOR_CORRECTION_MODE + */ + @PublicKey + @NonNull + @FlaggedApi(Flags.FLAG_COLOR_TEMPERATURE) + public static final Key<Integer> COLOR_CORRECTION_COLOR_TEMPERATURE = + new Key<Integer>("android.colorCorrection.colorTemperature", int.class); + + /** + * <p>Specifies the color tint for CCT mode to adjust the white + * balance of the image.</p> + * <p>Sets the color tint for when {@link CaptureRequest#COLOR_CORRECTION_MODE android.colorCorrection.mode} + * is CCT to adjust the white balance of the image.</p> + * <p>If CCT mode is enabled without a requested color tint, + * a default value will be set by the camera device. The default value can be + * retrieved by checking the corresponding capture result. Color tints requested + * outside the supported range will be clamped to the nearest limit (-50 or +50).</p> + * <p><b>Units</b>: D_uv defined as the distance from the Planckian locus on the CIE 1931 xy + * chromaticity diagram, with the range ±50 mapping to ±0.01 D_uv</p> + * <p><b>Range of valid values:</b><br> + * The supported range, -50 to +50, corresponds to a D_uv distance + * of ±0.01 below and above the Planckian locus. Some camera devices may have + * limitations to achieving the full ±0.01 D_uv range at some color temperatures + * (e.g., below 1500K). In these cases, the applied D_uv value may be clamped and + * the actual color tint will be reported in the {@link CaptureRequest#COLOR_CORRECTION_COLOR_TINT android.colorCorrection.colorTint} + * result.</p> + * <p><b>Optional</b> - The value for this key may be {@code null} on some devices.</p> + * + * @see CaptureRequest#COLOR_CORRECTION_COLOR_TINT + * @see CaptureRequest#COLOR_CORRECTION_MODE + */ + @PublicKey + @NonNull + @FlaggedApi(Flags.FLAG_COLOR_TEMPERATURE) + public static final Key<Integer> COLOR_CORRECTION_COLOR_TINT = + new Key<Integer>("android.colorCorrection.colorTint", int.class); + + /** * <p>The desired setting for the camera device's auto-exposure * algorithm's antibanding compensation.</p> * <p>Some kinds of lighting fixtures, such as some fluorescent diff --git a/core/java/android/hardware/camera2/MultiResolutionImageReader.java b/core/java/android/hardware/camera2/MultiResolutionImageReader.java index 116928b4283a..8ede7f378f1c 100644 --- a/core/java/android/hardware/camera2/MultiResolutionImageReader.java +++ b/core/java/android/hardware/camera2/MultiResolutionImageReader.java @@ -224,9 +224,8 @@ public class MultiResolutionImageReader implements AutoCloseable { * @see * android.hardware.camera2.params.MultiResolutionStreamConfigurationMap * - * @hide */ - @FlaggedApi(Flags.FLAG_MULTIRESOLUTION_IMAGEREADER_USAGE_CONFIG) + @FlaggedApi(Flags.FLAG_MULTIRESOLUTION_IMAGEREADER_USAGE_PUBLIC) public MultiResolutionImageReader( @NonNull Collection<MultiResolutionStreamInfo> streams, @Format int format, diff --git a/core/java/android/hardware/camera2/params/OutputConfiguration.java b/core/java/android/hardware/camera2/params/OutputConfiguration.java index 22dbf5bdde37..d38be9b7b694 100644 --- a/core/java/android/hardware/camera2/params/OutputConfiguration.java +++ b/core/java/android/hardware/camera2/params/OutputConfiguration.java @@ -1457,7 +1457,7 @@ public final class OutputConfiguration implements Parcelable { /** * Set the mirroring mode for a surface belonging to this OutputConfiguration * - * <p>This function is identical to {@link #setMirroMode(int)} if {@code surface} is + * <p>This function is identical to {@link #setMirrorMode(int)} if {@code surface} is * the only surface belonging to this OutputConfiguration.</p> * * <p>If this OutputConfiguration contains a deferred surface, the application can either diff --git a/core/java/android/hardware/display/DisplayManager.java b/core/java/android/hardware/display/DisplayManager.java index b0ea92d140a5..a81bcbcce9c9 100644 --- a/core/java/android/hardware/display/DisplayManager.java +++ b/core/java/android/hardware/display/DisplayManager.java @@ -580,7 +580,7 @@ public final class DisplayManager { EVENT_FLAG_DISPLAY_CONNECTION_CHANGED, }) @Retention(RetentionPolicy.SOURCE) - public @interface EventsMask {} + public @interface EventFlag {} /** * Event type for when a new display is added. @@ -774,7 +774,7 @@ public final class DisplayManager { * @param listener The listener to register. * @param handler The handler on which the listener should be invoked, or null * if the listener should be invoked on the calling thread's looper. - * @param eventsMask A bitmask of the event types for which this listener is subscribed. + * @param eventFlagsMask A bitmask of the event types for which this listener is subscribed. * * @see #EVENT_FLAG_DISPLAY_ADDED * @see #EVENT_FLAG_DISPLAY_CHANGED @@ -786,8 +786,8 @@ public final class DisplayManager { * @hide */ public void registerDisplayListener(@NonNull DisplayListener listener, - @Nullable Handler handler, @EventsMask long eventsMask) { - mGlobal.registerDisplayListener(listener, handler, eventsMask, + @Nullable Handler handler, @EventFlag long eventFlagsMask) { + mGlobal.registerDisplayListener(listener, handler, eventFlagsMask, ActivityThread.currentPackageName()); } diff --git a/core/java/android/hardware/display/DisplayManagerGlobal.java b/core/java/android/hardware/display/DisplayManagerGlobal.java index 6affd123bfde..3c6841cd8aeb 100644 --- a/core/java/android/hardware/display/DisplayManagerGlobal.java +++ b/core/java/android/hardware/display/DisplayManagerGlobal.java @@ -17,7 +17,7 @@ package android.hardware.display; -import static android.hardware.display.DisplayManager.EventsMask; +import static android.hardware.display.DisplayManager.EventFlag; import static android.view.Display.HdrCapabilities.HdrType; import android.Manifest; @@ -130,7 +130,7 @@ public final class DisplayManagerGlobal { private final IDisplayManager mDm; private DisplayManagerCallback mCallback; - private @EventsMask long mRegisteredEventsMask = 0; + private @EventFlag long mRegisteredEventFlagsMask = 0; private final CopyOnWriteArrayList<DisplayListenerDelegate> mDisplayListeners = new CopyOnWriteArrayList<>(); @@ -346,10 +346,11 @@ public final class DisplayManagerGlobal { * @param packageName of the calling package. */ public void registerDisplayListener(@NonNull DisplayListener listener, - @Nullable Handler handler, @EventsMask long eventsMask, String packageName) { + @Nullable Handler handler, @EventFlag long eventFlagsMask, + String packageName) { Looper looper = getLooperForHandler(handler); Handler springBoard = new Handler(looper); - registerDisplayListener(listener, new HandlerExecutor(springBoard), eventsMask, + registerDisplayListener(listener, new HandlerExecutor(springBoard), eventFlagsMask, packageName); } @@ -358,32 +359,32 @@ public final class DisplayManagerGlobal { * * @param listener The listener that will be called when display changes occur. * @param executor Executor for the thread that will be receiving the callbacks. Cannot be null. - * @param eventsMask Mask of events to be listened to. + * @param eventFlagsMask Flag of events to be listened to. * @param packageName of the calling package. */ public void registerDisplayListener(@NonNull DisplayListener listener, - @NonNull Executor executor, @EventsMask long eventsMask, String packageName) { + @NonNull Executor executor, @EventFlag long eventFlagsMask, String packageName) { if (listener == null) { throw new IllegalArgumentException("listener must not be null"); } - if (eventsMask == 0) { + if (eventFlagsMask == 0) { throw new IllegalArgumentException("The set of events to listen to must not be empty."); } if (extraLogging()) { Slog.i(TAG, "Registering Display Listener: " - + Long.toBinaryString(eventsMask) + ", packageName: " + packageName); + + Long.toBinaryString(eventFlagsMask) + ", packageName: " + packageName); } synchronized (mLock) { int index = findDisplayListenerLocked(listener); if (index < 0) { - mDisplayListeners.add(new DisplayListenerDelegate(listener, executor, eventsMask, - packageName)); + mDisplayListeners.add(new DisplayListenerDelegate(listener, executor, + eventFlagsMask, packageName)); registerCallbackIfNeededLocked(); } else { - mDisplayListeners.get(index).setEventsMask(eventsMask); + mDisplayListeners.get(index).setEventFlagsMask(eventFlagsMask); } updateCallbackIfNeededLocked(); maybeLogAllDisplayListeners(); @@ -455,12 +456,12 @@ public final class DisplayManagerGlobal { return -1; } - @EventsMask - private int calculateEventsMaskLocked() { + @EventFlag + private int calculateEventFlagsMaskLocked() { int mask = 0; final int numListeners = mDisplayListeners.size(); for (int i = 0; i < numListeners; i++) { - mask |= mDisplayListeners.get(i).mEventsMask; + mask |= mDisplayListeners.get(i).mEventFlagsMask; } if (mDispatchNativeCallbacks) { mask |= DisplayManager.EVENT_FLAG_DISPLAY_ADDED @@ -478,14 +479,14 @@ public final class DisplayManagerGlobal { } private void updateCallbackIfNeededLocked() { - int mask = calculateEventsMaskLocked(); + int mask = calculateEventFlagsMaskLocked(); if (DEBUG) { - Log.d(TAG, "Mask for listener: " + mask); + Log.d(TAG, "Flag for listener: " + mask); } - if (mask != mRegisteredEventsMask) { + if (mask != mRegisteredEventFlagsMask) { try { mDm.registerCallbackWithEventMask(mCallback, mask); - mRegisteredEventsMask = mask; + mRegisteredEventFlagsMask = mask; } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } @@ -1276,7 +1277,7 @@ public final class DisplayManagerGlobal { private static final class DisplayListenerDelegate { public final DisplayListener mListener; - public volatile long mEventsMask; + public volatile long mEventFlagsMask; private final DisplayInfo mDisplayInfo = new DisplayInfo(); private final Executor mExecutor; @@ -1284,10 +1285,10 @@ public final class DisplayManagerGlobal { private final String mPackageName; DisplayListenerDelegate(DisplayListener listener, @NonNull Executor executor, - @EventsMask long eventsMask, String packageName) { + @EventFlag long eventFlag, String packageName) { mExecutor = executor; mListener = listener; - mEventsMask = eventsMask; + mEventFlagsMask = eventFlag; mPackageName = packageName; } @@ -1309,16 +1310,16 @@ public final class DisplayManagerGlobal { mGenerationId.incrementAndGet(); } - void setEventsMask(@EventsMask long newEventsMask) { - mEventsMask = newEventsMask; + void setEventFlagsMask(@EventFlag long newEventsFlag) { + mEventFlagsMask = newEventsFlag; } - private void handleDisplayEventInner(int displayId, @DisplayEvent int event, + private void handleDisplayEventInner(int displayId, @DisplayEvent int eventFlagsMask, @Nullable DisplayInfo info, boolean forceUpdate) { if (extraLogging()) { - Slog.i(TAG, "DLD(" + eventToString(event) + Slog.i(TAG, "DLD(" + eventToString(eventFlagsMask) + ", display=" + displayId - + ", mEventsMask=" + Long.toBinaryString(mEventsMask) + + ", mEventsFlagMask=" + Long.toBinaryString(mEventFlagsMask) + ", mPackageName=" + mPackageName + ", displayInfo=" + info + ", listener=" + mListener.getClass() + ")"); @@ -1326,18 +1327,18 @@ public final class DisplayManagerGlobal { if (DEBUG) { Trace.beginSection( TextUtils.trimToSize( - "DLD(" + eventToString(event) + "DLD(" + eventToString(eventFlagsMask) + ", display=" + displayId + ", listener=" + mListener.getClass() + ")", 127)); } - switch (event) { + switch (eventFlagsMask) { case EVENT_DISPLAY_ADDED: - if ((mEventsMask & DisplayManager.EVENT_FLAG_DISPLAY_ADDED) != 0) { + if ((mEventFlagsMask & DisplayManager.EVENT_FLAG_DISPLAY_ADDED) != 0) { mListener.onDisplayAdded(displayId); } break; case EVENT_DISPLAY_CHANGED: - if ((mEventsMask & DisplayManager.EVENT_FLAG_DISPLAY_CHANGED) != 0) { + if ((mEventFlagsMask & DisplayManager.EVENT_FLAG_DISPLAY_CHANGED) != 0) { if (info != null && (forceUpdate || !info.equals(mDisplayInfo))) { if (extraLogging()) { Slog.i(TAG, "Sending onDisplayChanged: Display Changed. Info: " @@ -1349,27 +1350,29 @@ public final class DisplayManagerGlobal { } break; case EVENT_DISPLAY_BRIGHTNESS_CHANGED: - if ((mEventsMask & DisplayManager.EVENT_FLAG_DISPLAY_BRIGHTNESS) != 0) { + if ((mEventFlagsMask & DisplayManager.EVENT_FLAG_DISPLAY_BRIGHTNESS) != 0) { mListener.onDisplayChanged(displayId); } break; case EVENT_DISPLAY_REMOVED: - if ((mEventsMask & DisplayManager.EVENT_FLAG_DISPLAY_REMOVED) != 0) { + if ((mEventFlagsMask & DisplayManager.EVENT_FLAG_DISPLAY_REMOVED) != 0) { mListener.onDisplayRemoved(displayId); } break; case EVENT_DISPLAY_HDR_SDR_RATIO_CHANGED: - if ((mEventsMask & DisplayManager.EVENT_FLAG_HDR_SDR_RATIO_CHANGED) != 0) { + if ((mEventFlagsMask & DisplayManager.EVENT_FLAG_HDR_SDR_RATIO_CHANGED) != 0) { mListener.onDisplayChanged(displayId); } break; case EVENT_DISPLAY_CONNECTED: - if ((mEventsMask & DisplayManager.EVENT_FLAG_DISPLAY_CONNECTION_CHANGED) != 0) { + if ((mEventFlagsMask & DisplayManager.EVENT_FLAG_DISPLAY_CONNECTION_CHANGED) + != 0) { mListener.onDisplayConnected(displayId); } break; case EVENT_DISPLAY_DISCONNECTED: - if ((mEventsMask & DisplayManager.EVENT_FLAG_DISPLAY_CONNECTION_CHANGED) != 0) { + if ((mEventFlagsMask & DisplayManager.EVENT_FLAG_DISPLAY_CONNECTION_CHANGED) + != 0) { mListener.onDisplayDisconnected(displayId); } break; @@ -1381,7 +1384,7 @@ public final class DisplayManagerGlobal { @Override public String toString() { - return "mask: {" + mEventsMask + "}, for " + mListener.getClass(); + return "mEventFlagsMask: {" + mEventFlagsMask + "}, for " + mListener.getClass(); } } diff --git a/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java index 75ffcc3a8863..399184cfaecb 100644 --- a/core/java/android/hardware/display/DisplayManagerInternal.java +++ b/core/java/android/hardware/display/DisplayManagerInternal.java @@ -491,10 +491,16 @@ public abstract class DisplayManagerInternal { public static final int POLICY_DIM = 2; // Policy: Make the screen bright as usual. public static final int POLICY_BRIGHT = 3; + // The maximum policy constant. Useful for iterating through all constants in tests. + public static final int POLICY_MAX = POLICY_BRIGHT; // The basic overall policy to apply: off, doze, dim or bright. public int policy; + // The reason behind the current policy. + @Display.StateReason + public int policyReason; + // If true, the proximity sensor overrides the screen state when an object is // nearby, turning it off temporarily until the object is moved away. public boolean useProximitySensor; @@ -541,6 +547,7 @@ public abstract class DisplayManagerInternal { public DisplayPowerRequest() { policy = POLICY_BRIGHT; + policyReason = Display.STATE_REASON_DEFAULT_POLICY; useProximitySensor = false; screenBrightnessOverride = PowerManager.BRIGHTNESS_INVALID_FLOAT; screenAutoBrightnessAdjustmentOverride = Float.NaN; @@ -561,6 +568,7 @@ public abstract class DisplayManagerInternal { public void copyFrom(DisplayPowerRequest other) { policy = other.policy; + policyReason = other.policyReason; useProximitySensor = other.useProximitySensor; screenBrightnessOverride = other.screenBrightnessOverride; screenBrightnessOverrideTag = other.screenBrightnessOverrideTag; diff --git a/core/java/android/hardware/display/VirtualDisplayConfig.java b/core/java/android/hardware/display/VirtualDisplayConfig.java index b0994e6145ef..49944c76eb99 100644 --- a/core/java/android/hardware/display/VirtualDisplayConfig.java +++ b/core/java/android/hardware/display/VirtualDisplayConfig.java @@ -60,6 +60,7 @@ public final class VirtualDisplayConfig implements Parcelable { private final float mRequestedRefreshRate; private final boolean mIsHomeSupported; private final DisplayCutout mDisplayCutout; + private final boolean mIgnoreActivitySizeRestrictions; private VirtualDisplayConfig( @NonNull String name, @@ -74,7 +75,8 @@ public final class VirtualDisplayConfig implements Parcelable { @NonNull ArraySet<String> displayCategories, float requestedRefreshRate, boolean isHomeSupported, - @Nullable DisplayCutout displayCutout) { + @Nullable DisplayCutout displayCutout, + boolean ignoreActivitySizeRestrictions) { mName = name; mWidth = width; mHeight = height; @@ -88,6 +90,7 @@ public final class VirtualDisplayConfig implements Parcelable { mRequestedRefreshRate = requestedRefreshRate; mIsHomeSupported = isHomeSupported; mDisplayCutout = displayCutout; + mIgnoreActivitySizeRestrictions = ignoreActivitySizeRestrictions; } /** @@ -193,6 +196,20 @@ public final class VirtualDisplayConfig implements Parcelable { } /** + * Whether this virtual display ignores fixed orientation, aspect ratio and resizability + * of apps. + * + * @see Builder#setIgnoreActivitySizeRestrictions(boolean) + * @hide + */ + @FlaggedApi(com.android.window.flags.Flags.FLAG_VDM_FORCE_APP_UNIVERSAL_RESIZABLE_API) + @SystemApi + public boolean isIgnoreActivitySizeRestrictions() { + return mIgnoreActivitySizeRestrictions + && com.android.window.flags.Flags.vdmForceAppUniversalResizableApi(); + } + + /** * Returns the display categories. * * @see Builder#setDisplayCategories @@ -227,6 +244,7 @@ public final class VirtualDisplayConfig implements Parcelable { dest.writeFloat(mRequestedRefreshRate); dest.writeBoolean(mIsHomeSupported); DisplayCutout.ParcelableWrapper.writeCutoutToParcel(mDisplayCutout, dest, flags); + dest.writeBoolean(mIgnoreActivitySizeRestrictions); } @Override @@ -253,6 +271,7 @@ public final class VirtualDisplayConfig implements Parcelable { && Objects.equals(mDisplayCategories, that.mDisplayCategories) && mRequestedRefreshRate == that.mRequestedRefreshRate && mIsHomeSupported == that.mIsHomeSupported + && mIgnoreActivitySizeRestrictions == that.mIgnoreActivitySizeRestrictions && Objects.equals(mDisplayCutout, that.mDisplayCutout); } @@ -261,7 +280,8 @@ public final class VirtualDisplayConfig implements Parcelable { int hashCode = Objects.hash( mName, mWidth, mHeight, mDensityDpi, mFlags, mSurface, mUniqueId, mDisplayIdToMirror, mWindowManagerMirroringEnabled, mDisplayCategories, - mRequestedRefreshRate, mIsHomeSupported, mDisplayCutout); + mRequestedRefreshRate, mIsHomeSupported, mDisplayCutout, + mIgnoreActivitySizeRestrictions); return hashCode; } @@ -282,6 +302,7 @@ public final class VirtualDisplayConfig implements Parcelable { + " mRequestedRefreshRate=" + mRequestedRefreshRate + " mIsHomeSupported=" + mIsHomeSupported + " mDisplayCutout=" + mDisplayCutout + + " mIgnoreActivitySizeRestrictions=" + mIgnoreActivitySizeRestrictions + ")"; } @@ -299,6 +320,7 @@ public final class VirtualDisplayConfig implements Parcelable { mRequestedRefreshRate = in.readFloat(); mIsHomeSupported = in.readBoolean(); mDisplayCutout = DisplayCutout.ParcelableWrapper.readCutoutFromParcel(in); + mIgnoreActivitySizeRestrictions = in.readBoolean(); } @NonNull @@ -332,6 +354,7 @@ public final class VirtualDisplayConfig implements Parcelable { private float mRequestedRefreshRate = 0.0f; private boolean mIsHomeSupported = false; private DisplayCutout mDisplayCutout = null; + private boolean mIgnoreActivitySizeRestrictions = false; /** * Creates a new Builder. @@ -506,6 +529,24 @@ public final class VirtualDisplayConfig implements Parcelable { } /** + * Sets whether this display ignores fixed orientation, aspect ratio and resizability + * of apps. + * + * <p>Note: setting to {@code true} requires the display to have + * {@link DisplayManager#VIRTUAL_DISPLAY_FLAG_TRUSTED}. If this is false, this property + * is ignored.</p> + * + * @hide + */ + @FlaggedApi(com.android.window.flags.Flags.FLAG_VDM_FORCE_APP_UNIVERSAL_RESIZABLE_API) + @SystemApi + @NonNull + public Builder setIgnoreActivitySizeRestrictions(boolean enabled) { + mIgnoreActivitySizeRestrictions = enabled; + return this; + } + + /** * Builds the {@link VirtualDisplayConfig} instance. */ @NonNull @@ -523,7 +564,8 @@ public final class VirtualDisplayConfig implements Parcelable { mDisplayCategories, mRequestedRefreshRate, mIsHomeSupported, - mDisplayCutout); + mDisplayCutout, + mIgnoreActivitySizeRestrictions); } } } diff --git a/core/java/android/hardware/input/AidlInputGestureData.aidl b/core/java/android/hardware/input/AidlInputGestureData.aidl new file mode 100644 index 000000000000..137f672bf59c --- /dev/null +++ b/core/java/android/hardware/input/AidlInputGestureData.aidl @@ -0,0 +1,31 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.hardware.input; + +/** @hide */ +@JavaDerive(equals=true) +parcelable AidlInputGestureData { + int keycode; + int modifierState; + int gestureType; + + // App launch parameters: Only set if gestureType is KEY_GESTURE_TYPE_LAUNCH_APPLICATION + String appLaunchCategory; + String appLaunchRole; + String appLaunchPackageName; + String appLaunchClassName; +} diff --git a/core/java/android/hardware/input/IInputManager.aidl b/core/java/android/hardware/input/IInputManager.aidl index 102f56e4672b..bce95187515a 100644 --- a/core/java/android/hardware/input/IInputManager.aidl +++ b/core/java/android/hardware/input/IInputManager.aidl @@ -17,6 +17,7 @@ package android.hardware.input; import android.graphics.Rect; +import android.hardware.input.AidlInputGestureData; import android.hardware.input.HostUsiVersion; import android.hardware.input.InputDeviceIdentifier; import android.hardware.input.KeyboardLayout; @@ -261,4 +262,21 @@ interface IInputManager { @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " + "android.Manifest.permission.MANAGE_KEY_GESTURES)") void unregisterKeyGestureHandler(IKeyGestureHandler handler); + + @PermissionManuallyEnforced + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " + + "android.Manifest.permission.MANAGE_KEY_GESTURES)") + int addCustomInputGesture(in AidlInputGestureData data); + + @PermissionManuallyEnforced + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " + + "android.Manifest.permission.MANAGE_KEY_GESTURES)") + int removeCustomInputGesture(in AidlInputGestureData data); + + @PermissionManuallyEnforced + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(value = " + + "android.Manifest.permission.MANAGE_KEY_GESTURES)") + void removeAllCustomInputGestures(); + + AidlInputGestureData[] getCustomInputGestures(); } diff --git a/core/java/android/hardware/input/InputGestureData.java b/core/java/android/hardware/input/InputGestureData.java new file mode 100644 index 000000000000..5ab73cee9641 --- /dev/null +++ b/core/java/android/hardware/input/InputGestureData.java @@ -0,0 +1,249 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.hardware.input; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.view.KeyEvent; + +import java.util.Objects; + +/** + * Data class to store input gesture data. + * + * <p> + * All input gestures are of type Trigger -> Action(Key gesture type, app data). And currently types + * of triggers supported are: + * - KeyTrigger (Keycode + modifierState) + * - TODO(b/365064144): Add Touchpad gesture based trigger + * </p> + * @hide + */ +public final class InputGestureData { + + @NonNull + private final AidlInputGestureData mInputGestureData; + + public InputGestureData(AidlInputGestureData inputGestureData) { + this.mInputGestureData = inputGestureData; + validate(); + } + + /** Returns the trigger information for this input gesture */ + public Trigger getTrigger() { + if (mInputGestureData.keycode != KeyEvent.KEYCODE_UNKNOWN) { + return new KeyTrigger(mInputGestureData.keycode, mInputGestureData.modifierState); + } + throw new RuntimeException("InputGestureData is corrupted, invalid trigger type!"); + } + + /** Returns the action to perform for this input gesture */ + public Action getAction() { + return new Action(mInputGestureData.gestureType, getAppLaunchData()); + } + + private void validate() { + Trigger trigger = getTrigger(); + Action action = getAction(); + if (trigger == null) { + throw new IllegalArgumentException("No trigger found"); + } + if (action.keyGestureType == KeyGestureEvent.KEY_GESTURE_TYPE_UNSPECIFIED) { + throw new IllegalArgumentException("No system action found"); + } + if (action.keyGestureType == KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION + && action.appLaunchData == null) { + throw new IllegalArgumentException( + "No app launch data for system action launch application"); + } + } + + public AidlInputGestureData getAidlData() { + return mInputGestureData; + } + + @Nullable + private AppLaunchData getAppLaunchData() { + if (mInputGestureData.gestureType != KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION) { + return null; + } + return AppLaunchData.createLaunchData(mInputGestureData.appLaunchCategory, + mInputGestureData.appLaunchRole, mInputGestureData.appLaunchPackageName, + mInputGestureData.appLaunchClassName); + } + + /** Builder class for creating {@link InputGestureData} */ + public static class Builder { + @Nullable + private Trigger mTrigger = null; + private int mKeyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_UNSPECIFIED; + @Nullable + private AppLaunchData mAppLaunchData = null; + + /** Set input gesture trigger data for key based gestures */ + public Builder setTrigger(Trigger trigger) { + mTrigger = trigger; + return this; + } + + /** Set input gesture system action */ + public Builder setKeyGestureType(@KeyGestureEvent.KeyGestureType int keyGestureType) { + mKeyGestureType = keyGestureType; + return this; + } + + /** Set input gesture system action as launching a target app */ + public Builder setAppLaunchData(@NonNull AppLaunchData appLaunchData) { + mKeyGestureType = KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION; + mAppLaunchData = appLaunchData; + return this; + } + + /** Creates {@link android.hardware.input.InputGestureData} based on data provided */ + public InputGestureData build() throws IllegalArgumentException { + if (mTrigger == null) { + throw new IllegalArgumentException("No trigger found"); + } + if (mKeyGestureType == KeyGestureEvent.KEY_GESTURE_TYPE_UNSPECIFIED) { + throw new IllegalArgumentException("No system action found"); + } + if (mKeyGestureType == KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION + && mAppLaunchData == null) { + throw new IllegalArgumentException( + "No app launch data for system action launch application"); + } + AidlInputGestureData data = new AidlInputGestureData(); + if (mTrigger instanceof KeyTrigger keyTrigger) { + data.keycode = keyTrigger.getKeycode(); + data.modifierState = keyTrigger.getModifierState(); + } else { + throw new IllegalArgumentException("Invalid trigger type!"); + } + data.gestureType = mKeyGestureType; + if (mAppLaunchData != null) { + if (mAppLaunchData instanceof AppLaunchData.CategoryData categoryData) { + data.appLaunchCategory = categoryData.getCategory(); + } else if (mAppLaunchData instanceof AppLaunchData.RoleData roleData) { + data.appLaunchRole = roleData.getRole(); + } else if (mAppLaunchData instanceof AppLaunchData.ComponentData componentData) { + data.appLaunchPackageName = componentData.getPackageName(); + data.appLaunchClassName = componentData.getClassName(); + } else { + throw new IllegalArgumentException("AppLaunchData type is invalid!"); + } + } + return new InputGestureData(data); + } + } + + @Override + public String toString() { + return "InputGestureData { " + + "trigger = " + getTrigger() + + ", action = " + getAction() + + " }"; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + InputGestureData that = (InputGestureData) o; + return mInputGestureData.keycode == that.mInputGestureData.keycode + && mInputGestureData.modifierState == that.mInputGestureData.modifierState + && mInputGestureData.gestureType == that.mInputGestureData.gestureType + && Objects.equals(mInputGestureData.appLaunchCategory, that.mInputGestureData.appLaunchCategory) + && Objects.equals(mInputGestureData.appLaunchRole, that.mInputGestureData.appLaunchRole) + && Objects.equals(mInputGestureData.appLaunchPackageName, that.mInputGestureData.appLaunchPackageName) + && Objects.equals(mInputGestureData.appLaunchPackageName, that.mInputGestureData.appLaunchPackageName); + } + + @Override + public int hashCode() { + int _hash = 1; + _hash = 31 * _hash + mInputGestureData.keycode; + _hash = 31 * _hash + mInputGestureData.modifierState; + _hash = 31 * _hash + mInputGestureData.gestureType; + _hash = 31 * _hash + (mInputGestureData.appLaunchCategory != null + ? mInputGestureData.appLaunchCategory.hashCode() : 0); + _hash = 31 * _hash + (mInputGestureData.appLaunchRole != null + ? mInputGestureData.appLaunchRole.hashCode() : 0); + _hash = 31 * _hash + (mInputGestureData.appLaunchPackageName != null + ? mInputGestureData.appLaunchPackageName.hashCode() : 0); + _hash = 31 * _hash + (mInputGestureData.appLaunchPackageName != null + ? mInputGestureData.appLaunchPackageName.hashCode() : 0); + return _hash; + } + + public interface Trigger { + } + + /** Creates a input gesture trigger based on a key press */ + public static Trigger createKeyTrigger(int keycode, int modifierState) { + return new KeyTrigger(keycode, modifierState); + } + + /** Key based input gesture trigger */ + public static class KeyTrigger implements Trigger { + private static final int SHORTCUT_META_MASK = + KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON | KeyEvent.META_ALT_ON + | KeyEvent.META_SHIFT_ON; + private final int mKeycode; + private final int mModifierState; + + private KeyTrigger(int keycode, int modifierState) { + if (keycode <= KeyEvent.KEYCODE_UNKNOWN || keycode > KeyEvent.getMaxKeyCode()) { + throw new IllegalArgumentException("Invalid keycode = " + keycode); + } + mKeycode = keycode; + mModifierState = modifierState; + } + + public int getKeycode() { + return mKeycode; + } + + public int getModifierState() { + return mModifierState; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof KeyTrigger that)) return false; + return mKeycode == that.mKeycode && mModifierState == that.mModifierState; + } + + @Override + public int hashCode() { + return Objects.hash(mKeycode, mModifierState); + } + + @Override + public String toString() { + return "KeyTrigger{" + + "mKeycode=" + KeyEvent.keyCodeToString(mKeycode) + + ", mModifierState=" + mModifierState + + '}'; + } + } + + /** Data for action to perform when input gesture is triggered */ + public record Action(@KeyGestureEvent.KeyGestureType int keyGestureType, + @Nullable AppLaunchData appLaunchData) { + } +} diff --git a/core/java/android/hardware/input/InputManager.java b/core/java/android/hardware/input/InputManager.java index 22728f7a5ad3..876ba1021917 100644 --- a/core/java/android/hardware/input/InputManager.java +++ b/core/java/android/hardware/input/InputManager.java @@ -18,6 +18,7 @@ package android.hardware.input; import static com.android.input.flags.Flags.FLAG_INPUT_DEVICE_VIEW_BEHAVIOR_API; import static com.android.input.flags.Flags.FLAG_DEVICE_ASSOCIATIONS; +import static com.android.hardware.input.Flags.enableCustomizableInputGestures; import static com.android.hardware.input.Flags.keyboardLayoutPreviewFlag; import static com.android.hardware.input.Flags.keyboardGlyphMap; @@ -258,6 +259,52 @@ public final class InputManager { } /** + * Custom input gesture error: Input gesture already exists + * + * @hide + */ + public static final int CUSTOM_INPUT_GESTURE_RESULT_SUCCESS = 1; + + /** + * Custom input gesture error: Input gesture already exists + * + * @hide + */ + public static final int CUSTOM_INPUT_GESTURE_RESULT_ERROR_ALREADY_EXISTS = 2; + + /** + * Custom input gesture error: Input gesture does not exist + * + * @hide + */ + public static final int CUSTOM_INPUT_GESTURE_RESULT_ERROR_DOES_NOT_EXIST = 3; + + /** + * Custom input gesture error: Input gesture is reserved for system action + * + * @hide + */ + public static final int CUSTOM_INPUT_GESTURE_RESULT_ERROR_RESERVED_GESTURE = 4; + + /** + * Custom input gesture error: Failure error code for all other errors/warnings + * + * @hide + */ + public static final int CUSTOM_INPUT_GESTURE_RESULT_ERROR_OTHER = 5; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(prefix = { "CUSTOM_INPUT_GESTURE_RESULT_" }, value = { + CUSTOM_INPUT_GESTURE_RESULT_SUCCESS, + CUSTOM_INPUT_GESTURE_RESULT_ERROR_ALREADY_EXISTS, + CUSTOM_INPUT_GESTURE_RESULT_ERROR_DOES_NOT_EXIST, + CUSTOM_INPUT_GESTURE_RESULT_ERROR_RESERVED_GESTURE, + CUSTOM_INPUT_GESTURE_RESULT_ERROR_OTHER, + }) + public @interface CustomInputGestureResult {} + + /** * Switch State: Unknown. * * The system has yet to report a valid value for the switch. @@ -1432,6 +1479,84 @@ public final class InputManager { mGlobal.unregisterKeyGestureEventHandler(handler); } + /** Adds a new custom input gesture + * + * @param inputGestureData gesture data to add as custom gesture + * + * @hide + */ + @RequiresPermission(Manifest.permission.MANAGE_KEY_GESTURES) + @CustomInputGestureResult + public int addCustomInputGesture(@NonNull InputGestureData inputGestureData) { + if (!enableCustomizableInputGestures()) { + return CUSTOM_INPUT_GESTURE_RESULT_ERROR_OTHER; + } + try { + return mIm.addCustomInputGesture(inputGestureData.getAidlData()); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + return CUSTOM_INPUT_GESTURE_RESULT_ERROR_OTHER; + } + + /** Removes an existing custom gesture + * + * <p> NOTE: Should not be used to remove system gestures. This API is only to be used to + * remove gestures added using {@link #addCustomInputGesture(InputGestureData)} + * + * @param inputGestureData gesture data for the existing custom gesture to remove + * + * @hide + */ + @RequiresPermission(Manifest.permission.MANAGE_KEY_GESTURES) + @CustomInputGestureResult + public int removeCustomInputGesture(@NonNull InputGestureData inputGestureData) { + if (!enableCustomizableInputGestures()) { + return CUSTOM_INPUT_GESTURE_RESULT_ERROR_OTHER; + } + try { + return mIm.removeCustomInputGesture(inputGestureData.getAidlData()); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + return CUSTOM_INPUT_GESTURE_RESULT_ERROR_OTHER; + } + + /** Removes all custom input gestures + * + * @hide + */ + @RequiresPermission(Manifest.permission.MANAGE_KEY_GESTURES) + public void removeAllCustomInputGestures() { + if (!enableCustomizableInputGestures()) { + return; + } + try { + mIm.removeAllCustomInputGestures(); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + + /** Get all custom input gestures + * + * @hide + */ + public List<InputGestureData> getCustomInputGestures() { + List<InputGestureData> result = new ArrayList<>(); + if (!enableCustomizableInputGestures()) { + return result; + } + try { + for (AidlInputGestureData data : mIm.getCustomInputGestures()) { + result.add(new InputGestureData(data)); + } + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + return result; + } + /** * 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 diff --git a/core/java/android/hardware/input/KeyGestureEvent.java b/core/java/android/hardware/input/KeyGestureEvent.java index 0f290d90770f..9d42b67bf5a8 100644 --- a/core/java/android/hardware/input/KeyGestureEvent.java +++ b/core/java/android/hardware/input/KeyGestureEvent.java @@ -113,6 +113,10 @@ public final class KeyGestureEvent { public static final int KEY_GESTURE_TYPE_TOGGLE_BOUNCE_KEYS = 65; public static final int KEY_GESTURE_TYPE_TOGGLE_SLOW_KEYS = 66; public static final int KEY_GESTURE_TYPE_TOGGLE_MOUSE_KEYS = 67; + public static final int KEY_GESTURE_TYPE_SNAP_LEFT_FREEFORM_WINDOW = 68; + public static final int KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW = 69; + public static final int KEY_GESTURE_TYPE_MAXIMIZE_FREEFORM_WINDOW = 70; + public static final int KEY_GESTURE_TYPE_RESTORE_FREEFORM_WINDOW_SIZE = 71; public static final int FLAG_CANCELLED = 1; @@ -194,7 +198,11 @@ public final class KeyGestureEvent { KEY_GESTURE_TYPE_TOGGLE_STICKY_KEYS, KEY_GESTURE_TYPE_TOGGLE_BOUNCE_KEYS, KEY_GESTURE_TYPE_TOGGLE_SLOW_KEYS, - KEY_GESTURE_TYPE_TOGGLE_MOUSE_KEYS + KEY_GESTURE_TYPE_TOGGLE_MOUSE_KEYS, + KEY_GESTURE_TYPE_SNAP_LEFT_FREEFORM_WINDOW, + KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW, + KEY_GESTURE_TYPE_MAXIMIZE_FREEFORM_WINDOW, + KEY_GESTURE_TYPE_RESTORE_FREEFORM_WINDOW_SIZE, }) @Retention(RetentionPolicy.SOURCE) public @interface KeyGestureType { @@ -541,6 +549,14 @@ public final class KeyGestureEvent { return FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__DESKTOP_MODE; case KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION: return FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__MULTI_WINDOW_NAVIGATION; + case KEY_GESTURE_TYPE_SNAP_LEFT_FREEFORM_WINDOW: + return FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__SNAP_LEFT_FREEFORM_WINDOW; + case KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW: + return FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__SNAP_RIGHT_FREEFORM_WINDOW; + case KEY_GESTURE_TYPE_MAXIMIZE_FREEFORM_WINDOW: + return FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__MAXIMIZE_FREEFORM_WINDOW; + case KEY_GESTURE_TYPE_RESTORE_FREEFORM_WINDOW_SIZE: + return FrameworkStatsLog.KEYBOARD_SYSTEMS_EVENT_REPORTED__KEYBOARD_SYSTEM_EVENT__RESTORE_FREEFORM_WINDOW_SIZE; default: return LOG_EVENT_UNSPECIFIED; } @@ -749,6 +765,14 @@ public final class KeyGestureEvent { return "KEY_GESTURE_TYPE_TOGGLE_SLOW_KEYS"; case KEY_GESTURE_TYPE_TOGGLE_MOUSE_KEYS: return "KEY_GESTURE_TYPE_TOGGLE_MOUSE_KEYS"; + case KEY_GESTURE_TYPE_SNAP_LEFT_FREEFORM_WINDOW: + return "KEY_GESTURE_TYPE_SNAP_LEFT_FREEFORM_WINDOW"; + case KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW: + return "KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW"; + case KEY_GESTURE_TYPE_MAXIMIZE_FREEFORM_WINDOW: + return "KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW"; + case KEY_GESTURE_TYPE_RESTORE_FREEFORM_WINDOW_SIZE: + return "KEY_GESTURE_TYPE_RESTORE_FREEFORM_WINDOW_SIZE"; default: return Integer.toHexString(value); } diff --git a/core/java/android/hardware/input/KeyGlyphMap.java b/core/java/android/hardware/input/KeyGlyphMap.java index 6a16502e2612..f82d1cf276b9 100644 --- a/core/java/android/hardware/input/KeyGlyphMap.java +++ b/core/java/android/hardware/input/KeyGlyphMap.java @@ -34,6 +34,7 @@ import android.view.KeyEvent; import java.util.Arrays; import java.util.HashMap; import java.util.Map; +import java.util.Objects; /** * This class provides access to device specific key glyphs, modifier glyphs and device specific @@ -107,7 +108,54 @@ public final class KeyGlyphMap implements Parcelable { /** * Defines a key combination that includes a keycode and modifier state. */ - public record KeyCombination(int modifierState, int keycode) {} + public static class KeyCombination implements Parcelable { + private final int mModifierState; + private final int mKeycode; + + public KeyCombination(int modifierState, int keycode) { + this.mModifierState = modifierState; + this.mKeycode = keycode; + } + + public KeyCombination(Parcel in) { + this(in.readInt(), in.readInt()); + } + + public static final Creator<KeyCombination> CREATOR = new Creator<>() { + @Override + public KeyCombination createFromParcel(Parcel in) { + return new KeyCombination(in); + } + + @Override + public KeyCombination[] newArray(int size) { + return new KeyCombination[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@androidx.annotation.NonNull Parcel dest, int flags) { + dest.writeInt(mModifierState); + dest.writeInt(mKeycode); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof KeyCombination that)) return false; + return mModifierState == that.mModifierState && mKeycode == that.mKeycode; + } + + @Override + public int hashCode() { + return Objects.hash(mModifierState, mKeycode); + } + } /** * Returns keycodes generated from the functional row defined for the keyboard. diff --git a/core/java/android/hardware/input/input_framework.aconfig b/core/java/android/hardware/input/input_framework.aconfig index 12068558f5ac..71c91e913376 100644 --- a/core/java/android/hardware/input/input_framework.aconfig +++ b/core/java/android/hardware/input/input_framework.aconfig @@ -1,7 +1,10 @@ package: "com.android.hardware.input" container: "system" -# Project link: https://gantry.corp.google.com/projects/android_platform_input_native/changes +# Project link: https://gantry.corp.google.com/projects/android_platform_input/changes + +# NOTE: the input_native namespace is deprecated. New flags should be added to the input namespace +# instead. flag { namespace: "input_native" @@ -143,6 +146,13 @@ flag { } flag { + name: "enable_customizable_input_gestures" + namespace: "input" + description: "Enables keyboard shortcut customization support" + bug: "365064144" +} + +flag { name: "override_power_key_behavior_in_focused_window" namespace: "input_native" description: "Allows privileged focused windows to capture power key events." diff --git a/core/java/android/hardware/usb/UsbManager.java b/core/java/android/hardware/usb/UsbManager.java index 41f344a03e77..92608d048135 100644 --- a/core/java/android/hardware/usb/UsbManager.java +++ b/core/java/android/hardware/usb/UsbManager.java @@ -21,6 +21,7 @@ import static android.hardware.usb.UsbPortStatus.DATA_STATUS_DISABLED_FORCE; import android.Manifest; import android.annotation.CallbackExecutor; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.LongDef; import android.annotation.NonNull; @@ -37,6 +38,7 @@ import android.content.ComponentName; import android.content.Context; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; +import android.hardware.usb.flags.Flags; import android.hardware.usb.gadget.GadgetFunction; import android.hardware.usb.gadget.UsbSpeed; import android.os.Binder; @@ -509,7 +511,8 @@ public class UsbManager { * * @hide */ - @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + @FlaggedApi(Flags.FLAG_EXPOSE_USB_SPEED_SYSTEM_API) + @SystemApi public static final int USB_DATA_TRANSFER_RATE_UNKNOWN = -1; /** @@ -517,7 +520,8 @@ public class UsbManager { * * @hide */ - @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + @FlaggedApi(Flags.FLAG_EXPOSE_USB_SPEED_SYSTEM_API) + @SystemApi public static final int USB_DATA_TRANSFER_RATE_LOW_SPEED = 2; /** @@ -525,7 +529,8 @@ public class UsbManager { * * @hide */ - @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + @FlaggedApi(Flags.FLAG_EXPOSE_USB_SPEED_SYSTEM_API) + @SystemApi public static final int USB_DATA_TRANSFER_RATE_FULL_SPEED = 12; /** @@ -533,7 +538,8 @@ public class UsbManager { * * @hide */ - @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + @FlaggedApi(Flags.FLAG_EXPOSE_USB_SPEED_SYSTEM_API) + @SystemApi public static final int USB_DATA_TRANSFER_RATE_HIGH_SPEED = 480; /** @@ -541,7 +547,8 @@ public class UsbManager { * * @hide */ - @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + @FlaggedApi(Flags.FLAG_EXPOSE_USB_SPEED_SYSTEM_API) + @SystemApi public static final int USB_DATA_TRANSFER_RATE_5G = 5 * 1024; /** @@ -549,7 +556,8 @@ public class UsbManager { * * @hide */ - @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + @FlaggedApi(Flags.FLAG_EXPOSE_USB_SPEED_SYSTEM_API) + @SystemApi public static final int USB_DATA_TRANSFER_RATE_10G = 10 * 1024; /** @@ -557,7 +565,8 @@ public class UsbManager { * * @hide */ - @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + @FlaggedApi(Flags.FLAG_EXPOSE_USB_SPEED_SYSTEM_API) + @SystemApi public static final int USB_DATA_TRANSFER_RATE_20G = 20 * 1024; /** @@ -565,7 +574,8 @@ public class UsbManager { * * @hide */ - @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + @FlaggedApi(Flags.FLAG_EXPOSE_USB_SPEED_SYSTEM_API) + @SystemApi public static final int USB_DATA_TRANSFER_RATE_40G = 40 * 1024; /** @@ -1292,7 +1302,8 @@ public class UsbManager { * * @hide */ - @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + @FlaggedApi(Flags.FLAG_EXPOSE_USB_SPEED_SYSTEM_API) + @SystemApi @RequiresPermission(Manifest.permission.MANAGE_USB) public int getUsbBandwidthMbps() { int usbSpeed; diff --git a/core/java/android/hardware/usb/flags/usb_framework_flags.aconfig b/core/java/android/hardware/usb/flags/usb_framework_flags.aconfig index 40e5ffb141ab..3b7a9e95c521 100644 --- a/core/java/android/hardware/usb/flags/usb_framework_flags.aconfig +++ b/core/java/android/hardware/usb/flags/usb_framework_flags.aconfig @@ -24,3 +24,10 @@ flag { description: "Feature flag to enable interface name as a parameter for device filter" bug: "312828160" } + +flag { + name: "expose_usb_speed_system_api" + namespace: "usb" + description: "Feature flag to enable exposing usb speed system api" + bug: "373653182" +} diff --git a/core/java/android/net/flags.aconfig b/core/java/android/net/flags.aconfig index 48eb9680e647..f7dc7906d50d 100644 --- a/core/java/android/net/flags.aconfig +++ b/core/java/android/net/flags.aconfig @@ -5,13 +5,6 @@ container: "system" # Flags used for module APIs must be in aconfig files under each modules flag { - name: "ipsec_transform_state" - namespace: "core_networking_ipsec" - description: "The flag controls the access for getIpSecTransformState and IpSecTransformState" - bug: "308011229" -} - -flag { name: "powered_off_finding_platform" namespace: "nearby" description: "Controls whether the Powered Off Finding feature is enabled" diff --git a/core/java/android/net/vcn/flags.aconfig b/core/java/android/net/vcn/flags.aconfig index 5b306243fc36..1adefe5a0b86 100644 --- a/core/java/android/net/vcn/flags.aconfig +++ b/core/java/android/net/vcn/flags.aconfig @@ -10,8 +10,26 @@ flag { } flag { + name: "mainline_vcn_module_api" + namespace: "vcn" + description: "Expose APIs from VCN for mainline migration" + is_exported: true + bug: "376339506" +} + +flag { name: "safe_mode_timeout_config" namespace: "vcn" description: "Feature flag for adjustable safe mode timeout" bug: "317406085" +} + +flag { + name: "fix_config_garbage_collection" + namespace: "vcn" + description: "Handle race condition in subscription change" + bug: "370862489" + metadata { + purpose: PURPOSE_BUGFIX + } }
\ No newline at end of file diff --git a/core/java/android/os/CombinedMessageQueue/MessageQueue.java b/core/java/android/os/CombinedMessageQueue/MessageQueue.java new file mode 100644 index 000000000000..66f4198ad31c --- /dev/null +++ b/core/java/android/os/CombinedMessageQueue/MessageQueue.java @@ -0,0 +1,2516 @@ +/* + * Copyright (C) 2006 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.os; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.TestApi; +import android.compat.annotation.UnsupportedAppUsage; +import android.os.Process; +import android.os.UserHandle; +import android.ravenwood.annotation.RavenwoodKeepWholeClass; +import android.ravenwood.annotation.RavenwoodRedirect; +import android.ravenwood.annotation.RavenwoodRedirectionClass; +import android.util.Log; +import android.util.Printer; +import android.util.SparseArray; +import android.util.proto.ProtoOutputStream; + +import dalvik.annotation.optimization.NeverCompile; + +import java.io.FileDescriptor; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.VarHandle; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.concurrent.ConcurrentSkipListSet; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; + +/** + * Low-level class holding the list of messages to be dispatched by a + * {@link Looper}. Messages are not added directly to a MessageQueue, + * but rather through {@link Handler} objects associated with the Looper. + * + * <p>You can retrieve the MessageQueue for the current thread with + * {@link Looper#myQueue() Looper.myQueue()}. + */ +@RavenwoodKeepWholeClass +@RavenwoodRedirectionClass("MessageQueue_host") +public final class MessageQueue { + private static final String TAG_L = "LegacyMessageQueue"; + private static final String TAG_C = "ConcurrentMessageQueue"; + private static final boolean DEBUG = false; + private static final boolean TRACE = false; + + // True if the message queue can be quit. + @UnsupportedAppUsage + private final boolean mQuitAllowed; + + @UnsupportedAppUsage + @SuppressWarnings("unused") + private long mPtr; // used by native code + + @UnsupportedAppUsage + Message mMessages; + private Message mLast; + @UnsupportedAppUsage + private final ArrayList<IdleHandler> mIdleHandlers = new ArrayList<IdleHandler>(); + private SparseArray<FileDescriptorRecord> mFileDescriptorRecords; + private IdleHandler[] mPendingIdleHandlers; + private boolean mQuitting; + + // Indicates whether next() is blocked waiting in pollOnce() with a non-zero timeout. + private boolean mBlocked; + + // Tracks the number of async message. We use this in enqueueMessage() to avoid searching the + // queue for async messages when inserting a message at the tail. + private int mAsyncMessageCount; + + /* + * Select between two implementations of message queue. The legacy implementation is used + * by default as it provides maximum compatibility with applications and tests that + * reach into MessageQueue via the mMessages field. The concurrent implemmentation is used for + * system processes and provides a higher level of concurrency and higher enqueue throughput + * than the legacy implementation. + */ + private static boolean sUseConcurrent; + + private static boolean sUseConcurrentInitialized = false; + + @RavenwoodRedirect + private native static long nativeInit(); + @RavenwoodRedirect + private native static void nativeDestroy(long ptr); + @UnsupportedAppUsage + @RavenwoodRedirect + private native void nativePollOnce(long ptr, int timeoutMillis); /*non-static for callbacks*/ + @RavenwoodRedirect + private native static void nativeWake(long ptr); + @RavenwoodRedirect + private native static boolean nativeIsPolling(long ptr); + @RavenwoodRedirect + private native static void nativeSetFileDescriptorEvents(long ptr, int fd, int events); + + MessageQueue(boolean quitAllowed) { + if (!sUseConcurrentInitialized) { + sUseConcurrent = UserHandle.isCore(Process.myUid()); + sUseConcurrentInitialized = true; + } + mQuitAllowed = quitAllowed; + mPtr = nativeInit(); + } + + @Override + protected void finalize() throws Throwable { + try { + dispose(); + } finally { + super.finalize(); + } + } + + // Disposes of the underlying message queue. + // Must only be called on the looper thread or the finalizer. + private void dispose() { + if (mPtr != 0) { + nativeDestroy(mPtr); + mPtr = 0; + } + } + + private static final class MatchDeliverableMessages extends MessageCompare { + @Override + public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r, + long when) { + if (m.when <= when) { + return true; + } + return false; + } + } + private final MatchDeliverableMessages mMatchDeliverableMessages = + new MatchDeliverableMessages(); + /** + * Returns true if the looper has no pending messages which are due to be processed. + * + * <p>This method is safe to call from any thread. + * + * @return True if the looper is idle. + */ + public boolean isIdle() { + if (sUseConcurrent) { + final long now = SystemClock.uptimeMillis(); + + if (stackHasMessages(null, 0, null, null, now, mMatchDeliverableMessages, false)) { + return false; + } + + MessageNode msgNode = null; + MessageNode asyncMsgNode = null; + + if (!mPriorityQueue.isEmpty()) { + try { + msgNode = mPriorityQueue.first(); + } catch (NoSuchElementException e) { } + } + + if (!mAsyncPriorityQueue.isEmpty()) { + try { + asyncMsgNode = mAsyncPriorityQueue.first(); + } catch (NoSuchElementException e) { } + } + + if ((msgNode != null && msgNode.getWhen() <= now) + || (asyncMsgNode != null && asyncMsgNode.getWhen() <= now)) { + return false; + } + + return true; + } else { + synchronized (this) { + final long now = SystemClock.uptimeMillis(); + return mMessages == null || now < mMessages.when; + } + } + } + + /** + * Add a new {@link IdleHandler} to this message queue. This may be + * removed automatically for you by returning false from + * {@link IdleHandler#queueIdle IdleHandler.queueIdle()} when it is + * invoked, or explicitly removing it with {@link #removeIdleHandler}. + * + * <p>This method is safe to call from any thread. + * + * @param handler The IdleHandler to be added. + */ + public void addIdleHandler(@NonNull IdleHandler handler) { + if (handler == null) { + throw new NullPointerException("Can't add a null IdleHandler"); + } + if (sUseConcurrent) { + synchronized (mIdleHandlersLock) { + mIdleHandlers.add(handler); + } + } else { + synchronized (this) { + mIdleHandlers.add(handler); + } + } + } + + /** + * Remove an {@link IdleHandler} from the queue that was previously added + * with {@link #addIdleHandler}. If the given object is not currently + * in the idle list, nothing is done. + * + * <p>This method is safe to call from any thread. + * + * @param handler The IdleHandler to be removed. + */ + public void removeIdleHandler(@NonNull IdleHandler handler) { + if (sUseConcurrent) { + synchronized (mIdleHandlersLock) { + mIdleHandlers.remove(handler); + } + } else { + synchronized (this) { + mIdleHandlers.remove(handler); + } + } + } + + /** + * Returns whether this looper's thread is currently polling for more work to do. + * This is a good signal that the loop is still alive rather than being stuck + * handling a callback. Note that this method is intrinsically racy, since the + * state of the loop can change before you get the result back. + * + * <p>This method is safe to call from any thread. + * + * @return True if the looper is currently polling for events. + * @hide + */ + public boolean isPolling() { + if (sUseConcurrent) { + // If the loop is quitting then it must not be idling. + // We can assume mPtr != 0 when sQuitting is false. + return !((boolean) sQuitting.getVolatile(this)) && nativeIsPolling(mPtr); + } else { + synchronized (this) { + return isPollingLocked(); + } + } + } + + private boolean isPollingLocked() { + // If the loop is quitting then it must not be idling. + // We can assume mPtr != 0 when mQuitting is false. + return !mQuitting && nativeIsPolling(mPtr); + } + + /** + * Adds a file descriptor listener to receive notification when file descriptor + * related events occur. + * <p> + * If the file descriptor has already been registered, the specified events + * and listener will replace any that were previously associated with it. + * It is not possible to set more than one listener per file descriptor. + * </p><p> + * It is important to always unregister the listener when the file descriptor + * is no longer of use. + * </p> + * + * @param fd The file descriptor for which a listener will be registered. + * @param events The set of events to receive: a combination of the + * {@link OnFileDescriptorEventListener#EVENT_INPUT}, + * {@link OnFileDescriptorEventListener#EVENT_OUTPUT}, and + * {@link OnFileDescriptorEventListener#EVENT_ERROR} event masks. If the requested + * set of events is zero, then the listener is unregistered. + * @param listener The listener to invoke when file descriptor events occur. + * + * @see OnFileDescriptorEventListener + * @see #removeOnFileDescriptorEventListener + */ + @android.ravenwood.annotation.RavenwoodThrow(blockedBy = android.os.ParcelFileDescriptor.class) + public void addOnFileDescriptorEventListener(@NonNull FileDescriptor fd, + @OnFileDescriptorEventListener.Events int events, + @NonNull OnFileDescriptorEventListener listener) { + if (fd == null) { + throw new IllegalArgumentException("fd must not be null"); + } + if (listener == null) { + throw new IllegalArgumentException("listener must not be null"); + } + + if (sUseConcurrent) { + synchronized (mFileDescriptorRecordsLock) { + updateOnFileDescriptorEventListenerLocked(fd, events, listener); + } + } else { + synchronized (this) { + updateOnFileDescriptorEventListenerLocked(fd, events, listener); + } + } + } + + /** + * Removes a file descriptor listener. + * <p> + * This method does nothing if no listener has been registered for the + * specified file descriptor. + * </p> + * + * @param fd The file descriptor whose listener will be unregistered. + * + * @see OnFileDescriptorEventListener + * @see #addOnFileDescriptorEventListener + */ + @android.ravenwood.annotation.RavenwoodThrow(blockedBy = android.os.ParcelFileDescriptor.class) + public void removeOnFileDescriptorEventListener(@NonNull FileDescriptor fd) { + if (fd == null) { + throw new IllegalArgumentException("fd must not be null"); + } + if (sUseConcurrent) { + synchronized (mFileDescriptorRecordsLock) { + updateOnFileDescriptorEventListenerLocked(fd, 0, null); + } + } else { + synchronized (this) { + updateOnFileDescriptorEventListenerLocked(fd, 0, null); + } + } + } + + @android.ravenwood.annotation.RavenwoodThrow(blockedBy = android.os.ParcelFileDescriptor.class) + private void updateOnFileDescriptorEventListenerLocked(FileDescriptor fd, int events, + OnFileDescriptorEventListener listener) { + final int fdNum = fd.getInt$(); + + int index = -1; + FileDescriptorRecord record = null; + if (mFileDescriptorRecords != null) { + index = mFileDescriptorRecords.indexOfKey(fdNum); + if (index >= 0) { + record = mFileDescriptorRecords.valueAt(index); + if (record != null && record.mEvents == events) { + return; + } + } + } + + if (events != 0) { + events |= OnFileDescriptorEventListener.EVENT_ERROR; + if (record == null) { + if (mFileDescriptorRecords == null) { + mFileDescriptorRecords = new SparseArray<FileDescriptorRecord>(); + } + record = new FileDescriptorRecord(fd, events, listener); + mFileDescriptorRecords.put(fdNum, record); + } else { + record.mListener = listener; + record.mEvents = events; + record.mSeq += 1; + } + nativeSetFileDescriptorEvents(mPtr, fdNum, events); + } else if (record != null) { + record.mEvents = 0; + mFileDescriptorRecords.removeAt(index); + nativeSetFileDescriptorEvents(mPtr, fdNum, 0); + } + } + + // Called from native code. + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + private int dispatchEvents(int fd, int events) { + // Get the file descriptor record and any state that might change. + final FileDescriptorRecord record; + final int oldWatchedEvents; + final OnFileDescriptorEventListener listener; + final int seq; + if (sUseConcurrent) { + synchronized (mFileDescriptorRecordsLock) { + record = mFileDescriptorRecords.get(fd); + if (record == null) { + return 0; // spurious, no listener registered + } + + oldWatchedEvents = record.mEvents; + events &= oldWatchedEvents; // filter events based on current watched set + if (events == 0) { + return oldWatchedEvents; // spurious, watched events changed + } + + listener = record.mListener; + seq = record.mSeq; + } + } else { + synchronized (this) { + record = mFileDescriptorRecords.get(fd); + if (record == null) { + return 0; // spurious, no listener registered + } + + oldWatchedEvents = record.mEvents; + events &= oldWatchedEvents; // filter events based on current watched set + if (events == 0) { + return oldWatchedEvents; // spurious, watched events changed + } + + listener = record.mListener; + seq = record.mSeq; + } + } + // Invoke the listener outside of the lock. + int newWatchedEvents = listener.onFileDescriptorEvents( + record.mDescriptor, events); + if (newWatchedEvents != 0) { + newWatchedEvents |= OnFileDescriptorEventListener.EVENT_ERROR; + } + + // Update the file descriptor record if the listener changed the set of + // events to watch and the listener itself hasn't been updated since. + if (newWatchedEvents != oldWatchedEvents) { + synchronized (this) { + int index = mFileDescriptorRecords.indexOfKey(fd); + if (index >= 0 && mFileDescriptorRecords.valueAt(index) == record + && record.mSeq == seq) { + record.mEvents = newWatchedEvents; + if (newWatchedEvents == 0) { + mFileDescriptorRecords.removeAt(index); + } + } + } + } + + // Return the new set of events to watch for native code to take care of. + return newWatchedEvents; + } + + private static final AtomicLong mMessagesDelivered = new AtomicLong(); + + /* This is only read/written from the Looper thread. For use with Concurrent MQ */ + private int mNextPollTimeoutMillis; + private boolean mMessageDirectlyQueued; + private Message nextMessage() { + int i = 0; + + while (true) { + if (DEBUG) { + Log.d(TAG_C, "nextMessage loop #" + i); + i++; + } + + mDrainingLock.lock(); + mNextIsDrainingStack = true; + mDrainingLock.unlock(); + + /* + * Set our state to active, drain any items from the stack into our priority queues + */ + StackNode oldTop; + oldTop = swapAndSetStackStateActive(); + drainStack(oldTop); + + mDrainingLock.lock(); + mNextIsDrainingStack = false; + mDrainCompleted.signalAll(); + mDrainingLock.unlock(); + + /* + * The objective of this next block of code is to: + * - find a message to return (if any is ready) + * - find a next message we would like to return, after scheduling. + * - we make our scheduling decision based on this next message (if it exists). + * + * We have two queues to juggle and the presence of barriers throws an additional + * wrench into our plans. + * + * The last wrinkle is that remove() may delete items from underneath us. If we hit + * that case, we simply restart the loop. + */ + + /* Get the first node from each queue */ + Iterator<MessageNode> queueIter = mPriorityQueue.iterator(); + MessageNode msgNode = iterateNext(queueIter); + Iterator<MessageNode> asyncQueueIter = mAsyncPriorityQueue.iterator(); + MessageNode asyncMsgNode = iterateNext(asyncQueueIter); + + if (DEBUG) { + if (msgNode != null) { + Message msg = msgNode.mMessage; + Log.d(TAG_C, "Next found node what: " + msg.what + " when: " + msg.when + + " seq: " + msgNode.mInsertSeq + "barrier: " + + msgNode.isBarrier() + " now: " + SystemClock.uptimeMillis()); + } + if (asyncMsgNode != null) { + Message msg = asyncMsgNode.mMessage; + Log.d(TAG_C, "Next found async node what: " + msg.what + " when: " + msg.when + + " seq: " + asyncMsgNode.mInsertSeq + "barrier: " + + asyncMsgNode.isBarrier() + " now: " + + SystemClock.uptimeMillis()); + } + } + + /* + * the node which we will return, null if none are ready + */ + MessageNode found = null; + /* + * The node from which we will determine our next wakeup time. + * Null indicates there is no next message ready. If we found a node, + * we can leave this null as Looper will call us again after delivering + * the message. + */ + MessageNode next = null; + + long now = SystemClock.uptimeMillis(); + /* + * If we have a barrier we should return the async node (if it exists and is ready) + */ + if (msgNode != null && msgNode.isBarrier()) { + if (asyncMsgNode != null && now >= asyncMsgNode.getWhen()) { + found = asyncMsgNode; + } else { + next = asyncMsgNode; + } + } else { /* No barrier. */ + MessageNode earliest; + /* + * If we have two messages, pick the earliest option from either queue. + * Otherwise grab whichever node is non-null. If both are null we'll fall through. + */ + earliest = pickEarliestNode(msgNode, asyncMsgNode); + + if (earliest != null) { + if (now >= earliest.getWhen()) { + found = earliest; + } else { + next = earliest; + } + } + } + + if (DEBUG) { + if (found != null) { + Message msg = found.mMessage; + Log.d(TAG_C, " Will deliver node what: " + msg.what + " when: " + msg.when + + " seq: " + found.mInsertSeq + " barrier: " + found.isBarrier() + + " async: " + found.isAsync() + " now: " + + SystemClock.uptimeMillis()); + } else { + Log.d(TAG_C, "No node to deliver"); + } + if (next != null) { + Message msg = next.mMessage; + Log.d(TAG_C, "Next node what: " + msg.what + " when: " + msg.when + " seq: " + + next.mInsertSeq + " barrier: " + next.isBarrier() + " async: " + + next.isAsync() + + " now: " + SystemClock.uptimeMillis()); + } else { + Log.d(TAG_C, "No next node"); + } + } + + /* + * If we have a found message, we will get called again so there's no need to set state. + * In that case we can leave our state as ACTIVE. + * + * Otherwise we should determine how to park the thread. + */ + StateNode nextOp = sStackStateActive; + if (found == null) { + if (next == null) { + /* No message to deliver, sleep indefinitely */ + mNextPollTimeoutMillis = -1; + nextOp = sStackStateParked; + if (DEBUG) { + Log.d(TAG_C, "nextMessage next state is StackStateParked"); + } + } else { + /* Message not ready, or we found one to deliver already, set a timeout */ + long nextMessageWhen = next.getWhen(); + if (nextMessageWhen > now) { + mNextPollTimeoutMillis = (int) Math.min(nextMessageWhen - now, + Integer.MAX_VALUE); + } else { + mNextPollTimeoutMillis = 0; + } + + mStackStateTimedPark.mWhenToWake = now + mNextPollTimeoutMillis; + nextOp = mStackStateTimedPark; + if (DEBUG) { + Log.d(TAG_C, "nextMessage next state is StackStateTimedParked timeout ms " + + mNextPollTimeoutMillis + " mWhenToWake: " + + mStackStateTimedPark.mWhenToWake + " now " + now); + } + } + } + + /* + * Try to swap our state from Active back to Park or TimedPark. If we raced with + * enqueue, loop back around to pick up any new items. + */ + if (sState.compareAndSet(this, sStackStateActive, nextOp)) { + mMessageCounts.clearCounts(); + if (found != null) { + if (!removeFromPriorityQueue(found)) { + /* + * RemoveMessages() might be able to pull messages out from under us + * However we can detect that here and just loop around if it happens. + */ + continue; + } + + if (TRACE) { + Trace.setCounter("MQ.Delivered", mMessagesDelivered.incrementAndGet()); + } + return found.mMessage; + } + return null; + } + } + } + + private Message nextConcurrent() { + final long ptr = mPtr; + if (ptr == 0) { + return null; + } + + mNextPollTimeoutMillis = 0; + int pendingIdleHandlerCount = -1; // -1 only during first iteration + while (true) { + if (mNextPollTimeoutMillis != 0) { + Binder.flushPendingCommands(); + } + + mMessageDirectlyQueued = false; + nativePollOnce(ptr, mNextPollTimeoutMillis); + + Message msg = nextMessage(); + if (msg != null) { + msg.markInUse(); + return msg; + } + + if ((boolean) sQuitting.getVolatile(this)) { + return null; + } + + synchronized (mIdleHandlersLock) { + // If first time idle, then get the number of idlers to run. + // Idle handles only run if the queue is empty or if the first message + // in the queue (possibly a barrier) is due to be handled in the future. + if (pendingIdleHandlerCount < 0 + && isIdle()) { + pendingIdleHandlerCount = mIdleHandlers.size(); + } + if (pendingIdleHandlerCount <= 0) { + // No idle handlers to run. Loop and wait some more. + continue; + } + + if (mPendingIdleHandlers == null) { + mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)]; + } + mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers); + } + + // Run the idle handlers. + // We only ever reach this code block during the first iteration. + for (int i = 0; i < pendingIdleHandlerCount; i++) { + final IdleHandler idler = mPendingIdleHandlers[i]; + mPendingIdleHandlers[i] = null; // release the reference to the handler + + boolean keep = false; + try { + keep = idler.queueIdle(); + } catch (Throwable t) { + Log.wtf(TAG_C, "IdleHandler threw exception", t); + } + + if (!keep) { + synchronized (mIdleHandlersLock) { + mIdleHandlers.remove(idler); + } + } + } + + // Reset the idle handler count to 0 so we do not run them again. + pendingIdleHandlerCount = 0; + + // While calling an idle handler, a new message could have been delivered + // so go back and look again for a pending message without waiting. + mNextPollTimeoutMillis = 0; + } + } + + @UnsupportedAppUsage + Message next() { + if (sUseConcurrent) { + return nextConcurrent(); + } + + // Return here if the message loop has already quit and been disposed. + // This can happen if the application tries to restart a looper after quit + // which is not supported. + final long ptr = mPtr; + if (ptr == 0) { + return null; + } + + int pendingIdleHandlerCount = -1; // -1 only during first iteration + int nextPollTimeoutMillis = 0; + for (;;) { + if (nextPollTimeoutMillis != 0) { + Binder.flushPendingCommands(); + } + + nativePollOnce(ptr, nextPollTimeoutMillis); + + synchronized (this) { + // Try to retrieve the next message. Return if found. + final long now = SystemClock.uptimeMillis(); + Message prevMsg = null; + Message msg = mMessages; + if (msg != null && msg.target == null) { + // Stalled by a barrier. Find the next asynchronous message in the queue. + do { + prevMsg = msg; + msg = msg.next; + } while (msg != null && !msg.isAsynchronous()); + } + if (msg != null) { + if (now < msg.when) { + // Next message is not ready. Set a timeout to wake up when it is ready. + nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE); + } else { + // Got a message. + mBlocked = false; + if (prevMsg != null) { + prevMsg.next = msg.next; + if (prevMsg.next == null) { + mLast = prevMsg; + } + } else { + mMessages = msg.next; + if (msg.next == null) { + mLast = null; + } + } + msg.next = null; + if (DEBUG) Log.v(TAG_L, "Returning message: " + msg); + msg.markInUse(); + if (msg.isAsynchronous()) { + mAsyncMessageCount--; + } + if (TRACE) { + Trace.setCounter("MQ.Delivered", mMessagesDelivered.incrementAndGet()); + } + return msg; + } + } else { + // No more messages. + nextPollTimeoutMillis = -1; + } + + // Process the quit message now that all pending messages have been handled. + if (mQuitting) { + dispose(); + return null; + } + + // If first time idle, then get the number of idlers to run. + // Idle handles only run if the queue is empty or if the first message + // in the queue (possibly a barrier) is due to be handled in the future. + if (pendingIdleHandlerCount < 0 + && (mMessages == null || now < mMessages.when)) { + pendingIdleHandlerCount = mIdleHandlers.size(); + } + if (pendingIdleHandlerCount <= 0) { + // No idle handlers to run. Loop and wait some more. + mBlocked = true; + continue; + } + + if (mPendingIdleHandlers == null) { + mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)]; + } + mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers); + } + + // Run the idle handlers. + // We only ever reach this code block during the first iteration. + for (int i = 0; i < pendingIdleHandlerCount; i++) { + final IdleHandler idler = mPendingIdleHandlers[i]; + mPendingIdleHandlers[i] = null; // release the reference to the handler + + boolean keep = false; + try { + keep = idler.queueIdle(); + } catch (Throwable t) { + Log.wtf(TAG_L, "IdleHandler threw exception", t); + } + + if (!keep) { + synchronized (this) { + mIdleHandlers.remove(idler); + } + } + } + + // Reset the idle handler count to 0 so we do not run them again. + pendingIdleHandlerCount = 0; + + // While calling an idle handler, a new message could have been delivered + // so go back and look again for a pending message without waiting. + nextPollTimeoutMillis = 0; + } + } + + void quit(boolean safe) { + if (!mQuitAllowed) { + throw new IllegalStateException("Main thread not allowed to quit."); + } + + if (sUseConcurrent) { + synchronized (mIdleHandlersLock) { + if (sQuitting.compareAndSet(this, false, true)) { + if (safe) { + removeAllFutureMessages(); + } else { + removeAllMessages(); + } + + // We can assume mPtr != 0 because sQuitting was previously false. + nativeWake(mPtr); + } + } + } else { + synchronized (this) { + if (mQuitting) { + return; + } + mQuitting = true; + + if (safe) { + removeAllFutureMessagesLocked(); + } else { + removeAllMessagesLocked(); + } + + // We can assume mPtr != 0 because mQuitting was previously false. + nativeWake(mPtr); + } + } + } + + /** + * Posts a synchronization barrier to the Looper's message queue. + * + * Message processing occurs as usual until the message queue encounters the + * synchronization barrier that has been posted. When the barrier is encountered, + * later synchronous messages in the queue are stalled (prevented from being executed) + * until the barrier is released by calling {@link #removeSyncBarrier} and specifying + * the token that identifies the synchronization barrier. + * + * This method is used to immediately postpone execution of all subsequently posted + * synchronous messages until a condition is met that releases the barrier. + * Asynchronous messages (see {@link Message#isAsynchronous} are exempt from the barrier + * and continue to be processed as usual. + * + * This call must be always matched by a call to {@link #removeSyncBarrier} with + * the same token to ensure that the message queue resumes normal operation. + * Otherwise the application will probably hang! + * + * @return A token that uniquely identifies the barrier. This token must be + * passed to {@link #removeSyncBarrier} to release the barrier. + * + * @hide + */ + @UnsupportedAppUsage + @TestApi + public int postSyncBarrier() { + return postSyncBarrier(SystemClock.uptimeMillis()); + } + + private int postSyncBarrier(long when) { + // Enqueue a new sync barrier token. + // We don't need to wake the queue because the purpose of a barrier is to stall it. + if (sUseConcurrent) { + final int token = mNextBarrierTokenAtomic.getAndIncrement(); + + // b/376573804: apps and tests may expect to be able to use reflection + // to read this value. Make some effort to support this legacy use case. + mNextBarrierToken = token + 1; + + final Message msg = Message.obtain(); + + msg.markInUse(); + msg.arg1 = token; + + if (!enqueueMessageUnchecked(msg, when)) { + Log.wtf(TAG_C, "Unexpected error while adding sync barrier!"); + return -1; + } + + return token; + } + + synchronized (this) { + final int token = mNextBarrierToken++; + final Message msg = Message.obtain(); + msg.markInUse(); + msg.when = when; + msg.arg1 = token; + + if (Flags.messageQueueTailTracking() && mLast != null && mLast.when <= when) { + /* Message goes to tail of list */ + mLast.next = msg; + mLast = msg; + msg.next = null; + return token; + } + + Message prev = null; + Message p = mMessages; + if (when != 0) { + while (p != null && p.when <= when) { + prev = p; + p = p.next; + } + } + + if (p == null) { + /* We reached the tail of the list, or list is empty. */ + mLast = msg; + } + + if (prev != null) { // invariant: p == prev.next + msg.next = p; + prev.next = msg; + } else { + msg.next = p; + mMessages = msg; + } + return token; + } + } + + private static final class MatchBarrierToken extends MessageCompare { + int mBarrierToken; + + MatchBarrierToken(int token) { + super(); + mBarrierToken = token; + } + + @Override + public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r, + long when) { + if (m.target == null && m.arg1 == mBarrierToken) { + return true; + } + return false; + } + } + + /** + * Removes a synchronization barrier. + * + * @param token The synchronization barrier token that was returned by + * {@link #postSyncBarrier}. + * + * @throws IllegalStateException if the barrier was not found. + * + * @hide + */ + @UnsupportedAppUsage + @TestApi + public void removeSyncBarrier(int token) { + // Remove a sync barrier token from the queue. + // If the queue is no longer stalled by a barrier then wake it. + if (sUseConcurrent) { + boolean removed; + MessageNode first; + final MatchBarrierToken matchBarrierToken = new MatchBarrierToken(token); + + try { + /* Retain the first element to see if we are currently stuck on a barrier. */ + first = mPriorityQueue.first(); + } catch (NoSuchElementException e) { + /* The queue is empty */ + first = null; + } + + removed = findOrRemoveMessages(null, 0, null, null, 0, matchBarrierToken, true); + if (removed && first != null) { + Message m = first.mMessage; + if (m.target == null && m.arg1 == token) { + /* Wake up next() in case it was sleeping on this barrier. */ + nativeWake(mPtr); + } + } else if (!removed) { + throw new IllegalStateException("The specified message queue synchronization " + + " barrier token has not been posted or has already been removed."); + } + return; + } + + synchronized (this) { + Message prev = null; + Message p = mMessages; + while (p != null && (p.target != null || p.arg1 != token)) { + prev = p; + p = p.next; + } + if (p == null) { + throw new IllegalStateException("The specified message queue synchronization " + + " barrier token has not been posted or has already been removed."); + } + final boolean needWake; + if (prev != null) { + prev.next = p.next; + if (prev.next == null) { + mLast = prev; + } + needWake = false; + } else { + mMessages = p.next; + if (mMessages == null) { + mLast = null; + } + needWake = mMessages == null || mMessages.target != null; + } + p.recycleUnchecked(); + + // If the loop is quitting then it is already awake. + // We can assume mPtr != 0 when mQuitting is false. + if (needWake && !mQuitting) { + nativeWake(mPtr); + } + } + } + + boolean enqueueMessage(Message msg, long when) { + if (msg.target == null) { + throw new IllegalArgumentException("Message must have a target."); + } + + if (sUseConcurrent) { + if (msg.isInUse()) { + throw new IllegalStateException(msg + " This message is already in use."); + } + + return enqueueMessageUnchecked(msg, when); + } + + synchronized (this) { + if (msg.isInUse()) { + throw new IllegalStateException(msg + " This message is already in use."); + } + + if (mQuitting) { + IllegalStateException e = new IllegalStateException( + msg.target + " sending message to a Handler on a dead thread"); + Log.w(TAG_L, e.getMessage(), e); + msg.recycle(); + return false; + } + + msg.markInUse(); + msg.when = when; + Message p = mMessages; + boolean needWake; + if (p == null || when == 0 || when < p.when) { + // New head, wake up the event queue if blocked. + msg.next = p; + mMessages = msg; + needWake = mBlocked; + if (p == null) { + mLast = mMessages; + } + } else { + // Message is to be inserted at tail or middle of queue. Usually we don't have to + // wake up the event queue unless there is a barrier at the head of the queue and + // the message is the earliest asynchronous message in the queue. + needWake = mBlocked && p.target == null && msg.isAsynchronous(); + + // For readability, we split this portion of the function into two blocks based on + // whether tail tracking is enabled. This has a minor implication for the case + // where tail tracking is disabled. See the comment below. + if (Flags.messageQueueTailTracking()) { + if (when >= mLast.when) { + needWake = needWake && mAsyncMessageCount == 0; + msg.next = null; + mLast.next = msg; + mLast = msg; + } else { + // Inserted within the middle of the queue. + Message prev; + for (;;) { + prev = p; + p = p.next; + if (p == null || when < p.when) { + break; + } + if (needWake && p.isAsynchronous()) { + needWake = false; + } + } + if (p == null) { + /* Inserting at tail of queue */ + mLast = msg; + } + msg.next = p; // invariant: p == prev.next + prev.next = msg; + } + } else { + Message prev; + for (;;) { + prev = p; + p = p.next; + if (p == null || when < p.when) { + break; + } + if (needWake && p.isAsynchronous()) { + needWake = false; + } + } + msg.next = p; // invariant: p == prev.next + prev.next = msg; + + /* + * If this block is executing then we have a build without tail tracking - + * specifically: Flags.messageQueueTailTracking() == false. This is determined + * at build time so the flag won't change on us during runtime. + * + * Since we don't want to pepper the code with extra checks, we only check + * for tail tracking when we might use mLast. Otherwise, we continue to update + * mLast as the tail of the list. + * + * In this case however we are not maintaining mLast correctly. Since we never + * use it, this is fine. However, we run the risk of leaking a reference. + * So set mLast to null in this case to avoid any Message leaks. The other + * sites will never use the value so we are safe against null pointer derefs. + */ + mLast = null; + } + } + + if (msg.isAsynchronous()) { + mAsyncMessageCount++; + } + + // We can assume mPtr != 0 because mQuitting is false. + if (needWake) { + nativeWake(mPtr); + } + } + return true; + } + + private static final class MatchHandlerWhatAndObject extends MessageCompare { + @Override + public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r, + long when) { + if (m.target == h && m.what == what && (object == null || m.obj == object)) { + return true; + } + return false; + } + } + private final MatchHandlerWhatAndObject mMatchHandlerWhatAndObject = + new MatchHandlerWhatAndObject(); + boolean hasMessages(Handler h, int what, Object object) { + if (h == null) { + return false; + } + if (sUseConcurrent) { + return findOrRemoveMessages(h, what, object, null, 0, mMatchHandlerWhatAndObject, + false); + } + synchronized (this) { + Message p = mMessages; + while (p != null) { + if (p.target == h && p.what == what && (object == null || p.obj == object)) { + return true; + } + p = p.next; + } + return false; + } + } + + private static final class MatchHandlerWhatAndObjectEquals extends MessageCompare { + @Override + public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r, + long when) { + if (m.target == h && m.what == what && (object == null || object.equals(m.obj))) { + return true; + } + return false; + } + } + private final MatchHandlerWhatAndObjectEquals mMatchHandlerWhatAndObjectEquals = + new MatchHandlerWhatAndObjectEquals(); + boolean hasEqualMessages(Handler h, int what, Object object) { + if (h == null) { + return false; + } + if (sUseConcurrent) { + return findOrRemoveMessages(h, what, object, null, 0, mMatchHandlerWhatAndObjectEquals, + false); + + } + synchronized (this) { + Message p = mMessages; + while (p != null) { + if (p.target == h && p.what == what && (object == null || object.equals(p.obj))) { + return true; + } + p = p.next; + } + return false; + } + } + + private static final class MatchHandlerRunnableAndObject extends MessageCompare { + @Override + public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r, + long when) { + if (m.target == h && m.callback == r && (object == null || m.obj == object)) { + return true; + } + return false; + } + } + private final MatchHandlerRunnableAndObject mMatchHandlerRunnableAndObject = + new MatchHandlerRunnableAndObject(); + @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) + boolean hasMessages(Handler h, Runnable r, Object object) { + if (h == null) { + return false; + } + if (sUseConcurrent) { + return findOrRemoveMessages(h, -1, object, r, 0, mMatchHandlerRunnableAndObject, + false); + } + + synchronized (this) { + Message p = mMessages; + while (p != null) { + if (p.target == h && p.callback == r && (object == null || p.obj == object)) { + return true; + } + p = p.next; + } + return false; + } + } + + private static final class MatchHandler extends MessageCompare { + @Override + public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r, + long when) { + if (m.target == h) { + return true; + } + return false; + } + } + private final MatchHandler mMatchHandler = new MatchHandler(); + boolean hasMessages(Handler h) { + if (h == null) { + return false; + } + if (sUseConcurrent) { + return findOrRemoveMessages(h, -1, null, null, 0, mMatchHandler, false); + } + synchronized (this) { + Message p = mMessages; + while (p != null) { + if (p.target == h) { + return true; + } + p = p.next; + } + return false; + } + } + + void removeMessages(Handler h, int what, Object object) { + if (h == null) { + return; + } + if (sUseConcurrent) { + findOrRemoveMessages(h, what, object, null, 0, mMatchHandlerWhatAndObject, true); + return; + } + synchronized (this) { + Message p = mMessages; + + // Remove all messages at front. + while (p != null && p.target == h && p.what == what + && (object == null || p.obj == object)) { + Message n = p.next; + mMessages = n; + if (p.isAsynchronous()) { + mAsyncMessageCount--; + } + p.recycleUnchecked(); + p = n; + } + + if (p == null) { + mLast = mMessages; + } + + // Remove all messages after front. + while (p != null) { + Message n = p.next; + if (n != null) { + if (n.target == h && n.what == what + && (object == null || n.obj == object)) { + Message nn = n.next; + if (n.isAsynchronous()) { + mAsyncMessageCount--; + } + n.recycleUnchecked(); + p.next = nn; + if (p.next == null) { + mLast = p; + } + continue; + } + } + p = n; + } + } + } + + void removeEqualMessages(Handler h, int what, Object object) { + if (h == null) { + return; + } + + if (sUseConcurrent) { + findOrRemoveMessages(h, what, object, null, 0, mMatchHandlerWhatAndObjectEquals, true); + return; + } + + synchronized (this) { + Message p = mMessages; + + // Remove all messages at front. + while (p != null && p.target == h && p.what == what + && (object == null || object.equals(p.obj))) { + Message n = p.next; + mMessages = n; + if (p.isAsynchronous()) { + mAsyncMessageCount--; + } + p.recycleUnchecked(); + p = n; + } + + if (p == null) { + mLast = mMessages; + } + + // Remove all messages after front. + while (p != null) { + Message n = p.next; + if (n != null) { + if (n.target == h && n.what == what + && (object == null || object.equals(n.obj))) { + Message nn = n.next; + if (n.isAsynchronous()) { + mAsyncMessageCount--; + } + n.recycleUnchecked(); + p.next = nn; + if (p.next == null) { + mLast = p; + } + continue; + } + } + p = n; + } + } + } + + void removeMessages(Handler h, Runnable r, Object object) { + if (h == null || r == null) { + return; + } + + if (sUseConcurrent) { + findOrRemoveMessages(h, -1, object, r, 0, mMatchHandlerRunnableAndObject, true); + return; + } + synchronized (this) { + Message p = mMessages; + + // Remove all messages at front. + while (p != null && p.target == h && p.callback == r + && (object == null || p.obj == object)) { + Message n = p.next; + mMessages = n; + if (p.isAsynchronous()) { + mAsyncMessageCount--; + } + p.recycleUnchecked(); + p = n; + } + + if (p == null) { + mLast = mMessages; + } + + // Remove all messages after front. + while (p != null) { + Message n = p.next; + if (n != null) { + if (n.target == h && n.callback == r + && (object == null || n.obj == object)) { + Message nn = n.next; + if (n.isAsynchronous()) { + mAsyncMessageCount--; + } + n.recycleUnchecked(); + p.next = nn; + if (p.next == null) { + mLast = p; + } + continue; + } + } + p = n; + } + } + } + + private static final class MatchHandlerRunnableAndObjectEquals extends MessageCompare { + @Override + public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r, + long when) { + if (m.target == h && m.callback == r && (object == null || object.equals(m.obj))) { + return true; + } + return false; + } + } + private final MatchHandlerRunnableAndObjectEquals mMatchHandlerRunnableAndObjectEquals = + new MatchHandlerRunnableAndObjectEquals(); + void removeEqualMessages(Handler h, Runnable r, Object object) { + if (h == null || r == null) { + return; + } + + if (sUseConcurrent) { + findOrRemoveMessages(h, -1, object, r, 0, mMatchHandlerRunnableAndObjectEquals, true); + return; + } + synchronized (this) { + Message p = mMessages; + + // Remove all messages at front. + while (p != null && p.target == h && p.callback == r + && (object == null || object.equals(p.obj))) { + Message n = p.next; + mMessages = n; + if (p.isAsynchronous()) { + mAsyncMessageCount--; + } + p.recycleUnchecked(); + p = n; + } + + if (p == null) { + mLast = mMessages; + } + + // Remove all messages after front. + while (p != null) { + Message n = p.next; + if (n != null) { + if (n.target == h && n.callback == r + && (object == null || object.equals(n.obj))) { + Message nn = n.next; + if (n.isAsynchronous()) { + mAsyncMessageCount--; + } + n.recycleUnchecked(); + p.next = nn; + if (p.next == null) { + mLast = p; + } + continue; + } + } + p = n; + } + } + } + + private static final class MatchHandlerAndObject extends MessageCompare { + @Override + public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r, + long when) { + if (m.target == h && (object == null || m.obj == object)) { + return true; + } + return false; + } + } + private final MatchHandlerAndObject mMatchHandlerAndObject = new MatchHandlerAndObject(); + void removeCallbacksAndMessages(Handler h, Object object) { + if (h == null) { + return; + } + + if (sUseConcurrent) { + findOrRemoveMessages(h, -1, object, null, 0, mMatchHandlerAndObject, true); + return; + } + synchronized (this) { + Message p = mMessages; + + // Remove all messages at front. + while (p != null && p.target == h + && (object == null || p.obj == object)) { + Message n = p.next; + mMessages = n; + if (p.isAsynchronous()) { + mAsyncMessageCount--; + } + p.recycleUnchecked(); + p = n; + } + + if (p == null) { + mLast = mMessages; + } + + // Remove all messages after front. + while (p != null) { + Message n = p.next; + if (n != null) { + if (n.target == h && (object == null || n.obj == object)) { + Message nn = n.next; + if (n.isAsynchronous()) { + mAsyncMessageCount--; + } + n.recycleUnchecked(); + p.next = nn; + if (p.next == null) { + mLast = p; + } + continue; + } + } + p = n; + } + } + } + + private static final class MatchHandlerAndObjectEquals extends MessageCompare { + @Override + public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r, + long when) { + if (m.target == h && (object == null || object.equals(m.obj))) { + return true; + } + return false; + } + } + private final MatchHandlerAndObjectEquals mMatchHandlerAndObjectEquals = + new MatchHandlerAndObjectEquals(); + void removeCallbacksAndEqualMessages(Handler h, Object object) { + if (h == null) { + return; + } + + if (sUseConcurrent) { + findOrRemoveMessages(h, -1, object, null, 0, mMatchHandlerAndObjectEquals, true); + return; + } + synchronized (this) { + Message p = mMessages; + + // Remove all messages at front. + while (p != null && p.target == h + && (object == null || object.equals(p.obj))) { + Message n = p.next; + mMessages = n; + if (p.isAsynchronous()) { + mAsyncMessageCount--; + } + p.recycleUnchecked(); + p = n; + } + + if (p == null) { + mLast = mMessages; + } + + // Remove all messages after front. + while (p != null) { + Message n = p.next; + if (n != null) { + if (n.target == h && (object == null || object.equals(n.obj))) { + Message nn = n.next; + if (n.isAsynchronous()) { + mAsyncMessageCount--; + } + n.recycleUnchecked(); + p.next = nn; + if (p.next == null) { + mLast = p; + } + continue; + } + } + p = n; + } + } + } + + private void removeAllMessagesLocked() { + Message p = mMessages; + while (p != null) { + Message n = p.next; + p.recycleUnchecked(); + p = n; + } + mMessages = null; + mLast = null; + mAsyncMessageCount = 0; + } + + private void removeAllFutureMessagesLocked() { + final long now = SystemClock.uptimeMillis(); + Message p = mMessages; + if (p != null) { + if (p.when > now) { + removeAllMessagesLocked(); + } else { + Message n; + for (;;) { + n = p.next; + if (n == null) { + return; + } + if (n.when > now) { + break; + } + p = n; + } + p.next = null; + mLast = p; + + do { + p = n; + n = p.next; + if (p.isAsynchronous()) { + mAsyncMessageCount--; + } + p.recycleUnchecked(); + } while (n != null); + } + } + } + + private static final class MatchAllMessages extends MessageCompare { + @Override + public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r, + long when) { + return true; + } + } + private final MatchAllMessages mMatchAllMessages = new MatchAllMessages(); + private void removeAllMessages() { + findOrRemoveMessages(null, -1, null, null, 0, mMatchAllMessages, true); + } + + private static final class MatchAllFutureMessages extends MessageCompare { + @Override + public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r, + long when) { + if (m.when > when) { + return true; + } + return false; + } + } + private final MatchAllFutureMessages mMatchAllFutureMessages = new MatchAllFutureMessages(); + private void removeAllFutureMessages() { + findOrRemoveMessages(null, -1, null, null, SystemClock.uptimeMillis(), + mMatchAllFutureMessages, true); + } + + @NeverCompile + private void printPriorityQueueNodes() { + Iterator<MessageNode> iterator = mPriorityQueue.iterator(); + + Log.d(TAG_C, "* Dump priority queue"); + while (iterator.hasNext()) { + MessageNode msgNode = iterator.next(); + Log.d(TAG_C, "** MessageNode what: " + msgNode.mMessage.what + " when " + + msgNode.mMessage.when + " seq: " + msgNode.mInsertSeq); + } + } + + @NeverCompile + private int dumpPriorityQueue(ConcurrentSkipListSet<MessageNode> queue, Printer pw, + String prefix, Handler h, int n) { + int count = 0; + long now = SystemClock.uptimeMillis(); + + for (MessageNode msgNode : queue) { + Message msg = msgNode.mMessage; + if (h == null || h == msg.target) { + pw.println(prefix + "Message " + (n + count) + ": " + msg.toString(now)); + } + count++; + } + return count; + } + + @NeverCompile + void dump(Printer pw, String prefix, Handler h) { + if (sUseConcurrent) { + long now = SystemClock.uptimeMillis(); + int n = 0; + + pw.println(prefix + "(MessageQueue is using Concurrent implementation)"); + + StackNode node = (StackNode) sState.getVolatile(this); + while (node != null) { + if (node.isMessageNode()) { + Message msg = ((MessageNode) node).mMessage; + if (h == null || h == msg.target) { + pw.println(prefix + "Message " + n + ": " + msg.toString(now)); + } + node = ((MessageNode) node).mNext; + } else { + pw.println(prefix + "State: " + node); + node = null; + } + n++; + } + + pw.println(prefix + "PriorityQueue Messages: "); + n += dumpPriorityQueue(mPriorityQueue, pw, prefix, h, n); + pw.println(prefix + "AsyncPriorityQueue Messages: "); + n += dumpPriorityQueue(mAsyncPriorityQueue, pw, prefix, h, n); + + pw.println(prefix + "(Total messages: " + n + ", polling=" + isPolling() + + ", quitting=" + (boolean) sQuitting.getVolatile(this) + ")"); + return; + } + + synchronized (this) { + pw.println(prefix + "(MessageQueue is using Legacy implementation)"); + long now = SystemClock.uptimeMillis(); + int n = 0; + for (Message msg = mMessages; msg != null; msg = msg.next) { + if (h == null || h == msg.target) { + pw.println(prefix + "Message " + n + ": " + msg.toString(now)); + } + n++; + } + pw.println(prefix + "(Total messages: " + n + ", polling=" + isPollingLocked() + + ", quitting=" + mQuitting + ")"); + } + } + + @NeverCompile + private int dumpPriorityQueue(ConcurrentSkipListSet<MessageNode> queue, + ProtoOutputStream proto) { + int count = 0; + + for (MessageNode msgNode : queue) { + Message msg = msgNode.mMessage; + msg.dumpDebug(proto, MessageQueueProto.MESSAGES); + count++; + } + return count; + } + + @NeverCompile + void dumpDebug(ProtoOutputStream proto, long fieldId) { + if (sUseConcurrent) { + final long messageQueueToken = proto.start(fieldId); + + StackNode node = (StackNode) sState.getVolatile(this); + while (node.isMessageNode()) { + Message msg = ((MessageNode) node).mMessage; + msg.dumpDebug(proto, MessageQueueProto.MESSAGES); + node = ((MessageNode) node).mNext; + } + + dumpPriorityQueue(mPriorityQueue, proto); + dumpPriorityQueue(mAsyncPriorityQueue, proto); + + proto.write(MessageQueueProto.IS_POLLING_LOCKED, isPolling()); + proto.write(MessageQueueProto.IS_QUITTING, (boolean) sQuitting.getVolatile(this)); + proto.end(messageQueueToken); + return; + } + + final long messageQueueToken = proto.start(fieldId); + synchronized (this) { + for (Message msg = mMessages; msg != null; msg = msg.next) { + msg.dumpDebug(proto, MessageQueueProto.MESSAGES); + } + proto.write(MessageQueueProto.IS_POLLING_LOCKED, isPollingLocked()); + proto.write(MessageQueueProto.IS_QUITTING, mQuitting); + } + proto.end(messageQueueToken); + } + + /** + * Callback interface for discovering when a thread is going to block + * waiting for more messages. + */ + public static interface IdleHandler { + /** + * Called when the message queue has run out of messages and will now + * wait for more. Return true to keep your idle handler active, false + * to have it removed. This may be called if there are still messages + * pending in the queue, but they are all scheduled to be dispatched + * after the current time. + */ + boolean queueIdle(); + } + + /** + * A listener which is invoked when file descriptor related events occur. + */ + public interface OnFileDescriptorEventListener { + /** + * File descriptor event: Indicates that the file descriptor is ready for input + * operations, such as reading. + * <p> + * The listener should read all available data from the file descriptor + * then return <code>true</code> to keep the listener active or <code>false</code> + * to remove the listener. + * </p><p> + * In the case of a socket, this event may be generated to indicate + * that there is at least one incoming connection that the listener + * should accept. + * </p><p> + * This event will only be generated if the {@link #EVENT_INPUT} event mask was + * specified when the listener was added. + * </p> + */ + public static final int EVENT_INPUT = 1 << 0; + + /** + * File descriptor event: Indicates that the file descriptor is ready for output + * operations, such as writing. + * <p> + * The listener should write as much data as it needs. If it could not + * write everything at once, then it should return <code>true</code> to + * keep the listener active. Otherwise, it should return <code>false</code> + * to remove the listener then re-register it later when it needs to write + * something else. + * </p><p> + * This event will only be generated if the {@link #EVENT_OUTPUT} event mask was + * specified when the listener was added. + * </p> + */ + public static final int EVENT_OUTPUT = 1 << 1; + + /** + * File descriptor event: Indicates that the file descriptor encountered a + * fatal error. + * <p> + * File descriptor errors can occur for various reasons. One common error + * is when the remote peer of a socket or pipe closes its end of the connection. + * </p><p> + * This event may be generated at any time regardless of whether the + * {@link #EVENT_ERROR} event mask was specified when the listener was added. + * </p> + */ + public static final int EVENT_ERROR = 1 << 2; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef(flag = true, prefix = { "EVENT_" }, value = { + EVENT_INPUT, + EVENT_OUTPUT, + EVENT_ERROR + }) + public @interface Events {} + + /** + * Called when a file descriptor receives events. + * + * @param fd The file descriptor. + * @param events The set of events that occurred: a combination of the + * {@link #EVENT_INPUT}, {@link #EVENT_OUTPUT}, and {@link #EVENT_ERROR} event masks. + * @return The new set of events to watch, or 0 to unregister the listener. + * + * @see #EVENT_INPUT + * @see #EVENT_OUTPUT + * @see #EVENT_ERROR + */ + @Events int onFileDescriptorEvents(@NonNull FileDescriptor fd, @Events int events); + } + + private static final class FileDescriptorRecord { + public final FileDescriptor mDescriptor; + public int mEvents; + public OnFileDescriptorEventListener mListener; + public int mSeq; + + public FileDescriptorRecord(FileDescriptor descriptor, + int events, OnFileDescriptorEventListener listener) { + mDescriptor = descriptor; + mEvents = events; + mListener = listener; + } + } + + /** + * ConcurrentMessageQueue specific classes methods and variables + */ + /* Helper to choose the correct queue to insert into. */ + private void insertIntoPriorityQueue(MessageNode msgNode) { + if (msgNode.isAsync()) { + mAsyncPriorityQueue.add(msgNode); + } else { + mPriorityQueue.add(msgNode); + } + } + + private boolean removeFromPriorityQueue(MessageNode msgNode) { + if (msgNode.isAsync()) { + return mAsyncPriorityQueue.remove(msgNode); + } else { + return mPriorityQueue.remove(msgNode); + } + } + + private MessageNode pickEarliestNode(MessageNode nodeA, MessageNode nodeB) { + if (nodeA != null && nodeB != null) { + if (nodeA.compareTo(nodeB) < 0) { + return nodeA; + } + return nodeB; + } + + return nodeA != null ? nodeA : nodeB; + } + + private MessageNode iterateNext(Iterator<MessageNode> iter) { + if (iter.hasNext()) { + try { + return iter.next(); + } catch (NoSuchElementException e) { + /* The queue is empty - this can happen if we race with remove */ + } + } + return null; + } + + /* Move any non-cancelled messages into the priority queue */ + private void drainStack(StackNode oldTop) { + while (oldTop.isMessageNode()) { + MessageNode oldTopMessageNode = (MessageNode) oldTop; + if (oldTopMessageNode.removeFromStack()) { + insertIntoPriorityQueue(oldTopMessageNode); + } + MessageNode inserted = oldTopMessageNode; + oldTop = oldTopMessageNode.mNext; + /* + * removeMessages can walk this list while we are consuming it. + * Set our next pointer to null *after* we add the message to our + * priority queue. This way removeMessages() will always find the + * message, either in our list or in the priority queue. + */ + inserted.mNext = null; + } + } + + /* Set the stack state to Active, return a list of nodes to walk. */ + private StackNode swapAndSetStackStateActive() { + while (true) { + /* Set stack state to Active, get node list to walk later */ + StackNode current = (StackNode) sState.getVolatile(this); + if (current == sStackStateActive + || sState.compareAndSet(this, current, sStackStateActive)) { + return current; + } + } + } + private StateNode getStateNode(StackNode node) { + if (node.isMessageNode()) { + return ((MessageNode) node).mBottomOfStack; + } + return (StateNode) node; + } + + private void waitForDrainCompleted() { + mDrainingLock.lock(); + while (mNextIsDrainingStack) { + mDrainCompleted.awaitUninterruptibly(); + } + mDrainingLock.unlock(); + } + + @IntDef(value = { + STACK_NODE_MESSAGE, + STACK_NODE_ACTIVE, + STACK_NODE_PARKED, + STACK_NODE_TIMEDPARK}) + @Retention(RetentionPolicy.SOURCE) + private @interface StackNodeType {} + + /* + * Stack node types. STACK_NODE_MESSAGE indicates a node containing a message. + * The other types indicate what state our Looper thread is in. The bottom of + * the stack is always a single state node. Message nodes are added on top. + */ + private static final int STACK_NODE_MESSAGE = 0; + /* + * Active state indicates that next() is processing messages + */ + private static final int STACK_NODE_ACTIVE = 1; + /* + * Parked state indicates that the Looper thread is sleeping indefinitely (nothing to deliver) + */ + private static final int STACK_NODE_PARKED = 2; + /* + * Timed Park state indicates that the Looper thread is sleeping, waiting for a message + * deadline + */ + private static final int STACK_NODE_TIMEDPARK = 3; + + /* Describes a node in the Treiber stack */ + static class StackNode { + @StackNodeType + private final int mType; + + StackNode(@StackNodeType int type) { + mType = type; + } + + @StackNodeType + final int getNodeType() { + return mType; + } + + final boolean isMessageNode() { + return mType == STACK_NODE_MESSAGE; + } + } + + static final class MessageNode extends StackNode implements Comparable<MessageNode> { + private final Message mMessage; + volatile StackNode mNext; + StateNode mBottomOfStack; + boolean mWokeUp; + final long mInsertSeq; + private static final VarHandle sRemovedFromStack; + private volatile boolean mRemovedFromStackValue; + static { + try { + MethodHandles.Lookup l = MethodHandles.lookup(); + sRemovedFromStack = l.findVarHandle(MessageQueue.MessageNode.class, + "mRemovedFromStackValue", boolean.class); + } catch (Exception e) { + Log.wtf(TAG_C, "VarHandle lookup failed with exception: " + e); + throw new ExceptionInInitializerError(e); + } + } + + MessageNode(@NonNull Message message, long insertSeq) { + super(STACK_NODE_MESSAGE); + mMessage = message; + mInsertSeq = insertSeq; + } + + long getWhen() { + return mMessage.when; + } + + boolean isRemovedFromStack() { + return mRemovedFromStackValue; + } + + boolean removeFromStack() { + return sRemovedFromStack.compareAndSet(this, false, true); + } + + boolean isAsync() { + return mMessage.isAsynchronous(); + } + + boolean isBarrier() { + return mMessage.target == null; + } + + @Override + public int compareTo(@NonNull MessageNode messageNode) { + Message other = messageNode.mMessage; + + int compared = Long.compare(mMessage.when, other.when); + if (compared == 0) { + compared = Long.compare(mInsertSeq, messageNode.mInsertSeq); + } + return compared; + } + } + + static class StateNode extends StackNode { + StateNode(int type) { + super(type); + } + } + + static final class TimedParkStateNode extends StateNode { + long mWhenToWake; + + TimedParkStateNode() { + super(STACK_NODE_TIMEDPARK); + } + } + + private static final StateNode sStackStateActive = new StateNode(STACK_NODE_ACTIVE); + private static final StateNode sStackStateParked = new StateNode(STACK_NODE_PARKED); + private final TimedParkStateNode mStackStateTimedPark = new TimedParkStateNode(); + + /* This is the top of our treiber stack. */ + private static final VarHandle sState; + static { + try { + MethodHandles.Lookup l = MethodHandles.lookup(); + sState = l.findVarHandle(MessageQueue.class, "mStateValue", + MessageQueue.StackNode.class); + } catch (Exception e) { + Log.wtf(TAG_C, "VarHandle lookup failed with exception: " + e); + throw new ExceptionInInitializerError(e); + } + } + + private volatile StackNode mStateValue = sStackStateParked; + private final ConcurrentSkipListSet<MessageNode> mPriorityQueue = + new ConcurrentSkipListSet<MessageNode>(); + private final ConcurrentSkipListSet<MessageNode> mAsyncPriorityQueue = + new ConcurrentSkipListSet<MessageNode>(); + + /* + * This helps us ensure that messages with the same timestamp are inserted in FIFO order. + * Increments on each insert, starting at 0. MessageNode.compareTo() will compare sequences + * when delivery timestamps are identical. + */ + private static final VarHandle sNextInsertSeq; + private volatile long mNextInsertSeqValue = 0; + /* + * The exception to the FIFO order rule is sendMessageAtFrontOfQueue(). + * Those messages must be in LIFO order. + * Decrements on each front of queue insert. + */ + private static final VarHandle sNextFrontInsertSeq; + private volatile long mNextFrontInsertSeqValue = -1; + static { + try { + MethodHandles.Lookup l = MethodHandles.lookup(); + sNextInsertSeq = l.findVarHandle(MessageQueue.class, "mNextInsertSeqValue", + long.class); + sNextFrontInsertSeq = l.findVarHandle(MessageQueue.class, "mNextFrontInsertSeqValue", + long.class); + } catch (Exception e) { + Log.wtf(TAG_C, "VarHandle lookup failed with exception: " + e); + throw new ExceptionInInitializerError(e); + } + + } + + /* + * Tracks the number of queued and cancelled messages in our stack. + * + * On item cancellation, determine whether to wake next() to flush tombstoned messages. + * We track queued and cancelled counts as two ints packed into a single long. + */ + private static final class MessageCounts { + private static VarHandle sCounts; + private volatile long mCountsValue = 0; + static { + try { + MethodHandles.Lookup l = MethodHandles.lookup(); + sCounts = l.findVarHandle(MessageQueue.MessageCounts.class, "mCountsValue", + long.class); + } catch (Exception e) { + Log.wtf(TAG_C, "VarHandle lookup failed with exception: " + e); + throw new ExceptionInInitializerError(e); + } + } + + /* We use a special value to indicate when next() has been woken for flush. */ + private static final long AWAKE = Long.MAX_VALUE; + /* + * Minimum number of messages in the stack which we need before we consider flushing + * tombstoned items. + */ + private static final int MESSAGE_FLUSH_THRESHOLD = 10; + + private static int numQueued(long val) { + return (int) (val >>> Integer.SIZE); + } + + private static int numCancelled(long val) { + return (int) val; + } + + private static long combineCounts(int queued, int cancelled) { + return ((long) queued << Integer.SIZE) | (long) cancelled; + } + + public void incrementQueued() { + while (true) { + long oldVal = mCountsValue; + int queued = numQueued(oldVal); + int cancelled = numCancelled(oldVal); + /* Use Math.max() to avoid overflow of queued count */ + long newVal = combineCounts(Math.max(queued + 1, queued), cancelled); + + /* Don't overwrite 'AWAKE' state */ + if (oldVal == AWAKE || sCounts.compareAndSet(this, oldVal, newVal)) { + break; + } + } + } + + public boolean incrementCancelled() { + while (true) { + long oldVal = mCountsValue; + if (oldVal == AWAKE) { + return false; + } + int queued = numQueued(oldVal); + int cancelled = numCancelled(oldVal); + boolean needsPurge = queued > MESSAGE_FLUSH_THRESHOLD + && (queued >> 1) < cancelled; + long newVal; + if (needsPurge) { + newVal = AWAKE; + } else { + newVal = combineCounts(queued, + Math.max(cancelled + 1, cancelled)); + } + + if (sCounts.compareAndSet(this, oldVal, newVal)) { + return needsPurge; + } + } + } + + public void clearCounts() { + mCountsValue = 0; + } + } + + private final MessageCounts mMessageCounts = new MessageCounts(); + + private final Object mIdleHandlersLock = new Object(); + private final Object mFileDescriptorRecordsLock = new Object(); + + private static final VarHandle sQuitting; + private boolean mQuittingValue = false; + static { + try { + MethodHandles.Lookup l = MethodHandles.lookup(); + sQuitting = l.findVarHandle(MessageQueue.class, "mQuittingValue", boolean.class); + } catch (Exception e) { + Log.wtf(TAG_C, "VarHandle lookup failed with exception: " + e); + throw new ExceptionInInitializerError(e); + } + } + + // The next barrier token. + // Barriers are indicated by messages with a null target whose arg1 field carries the token. + private final AtomicInteger mNextBarrierTokenAtomic = new AtomicInteger(1); + + // Must retain this for compatibility reasons. + @UnsupportedAppUsage + private int mNextBarrierToken; + + /* Protects mNextIsDrainingStack */ + private final ReentrantLock mDrainingLock = new ReentrantLock(); + private boolean mNextIsDrainingStack = false; + private final Condition mDrainCompleted = mDrainingLock.newCondition(); + + private boolean enqueueMessageUnchecked(@NonNull Message msg, long when) { + if ((boolean) sQuitting.getVolatile(this)) { + IllegalStateException e = new IllegalStateException( + msg.target + " sending message to a Handler on a dead thread"); + Log.w(TAG_C, e.getMessage(), e); + msg.recycleUnchecked(); + return false; + } + + long seq = when != 0 ? ((long) sNextInsertSeq.getAndAdd(this, 1L) + 1L) + : ((long) sNextFrontInsertSeq.getAndAdd(this, -1L) - 1L); + /* TODO: Add a MessageNode member to Message so we can avoid this allocation */ + MessageNode node = new MessageNode(msg, seq); + msg.when = when; + msg.markInUse(); + + if (DEBUG) { + Log.d(TAG_C, "Insert message what: " + msg.what + " when: " + msg.when + " seq: " + + node.mInsertSeq + " barrier: " + node.isBarrier() + " async: " + + node.isAsync() + " now: " + SystemClock.uptimeMillis()); + } + + final Looper myLooper = Looper.myLooper(); + /* If we are running on the looper thread we can add directly to the priority queue */ + if (myLooper != null && myLooper.getQueue() == this) { + node.removeFromStack(); + insertIntoPriorityQueue(node); + /* + * We still need to do this even though we are the current thread, + * otherwise next() may sleep indefinitely. + */ + if (!mMessageDirectlyQueued) { + mMessageDirectlyQueued = true; + nativeWake(mPtr); + } + return true; + } + + while (true) { + StackNode old = (StackNode) sState.getVolatile(this); + boolean wakeNeeded; + boolean inactive; + + node.mNext = old; + switch (old.getNodeType()) { + case STACK_NODE_ACTIVE: + /* + * The worker thread is currently active and will process any elements added to + * the stack before parking again. + */ + node.mBottomOfStack = (StateNode) old; + inactive = false; + node.mWokeUp = true; + wakeNeeded = false; + break; + + case STACK_NODE_PARKED: + node.mBottomOfStack = (StateNode) old; + inactive = true; + node.mWokeUp = true; + wakeNeeded = true; + break; + + case STACK_NODE_TIMEDPARK: + node.mBottomOfStack = (StateNode) old; + inactive = true; + wakeNeeded = mStackStateTimedPark.mWhenToWake >= node.getWhen(); + node.mWokeUp = wakeNeeded; + break; + + default: + MessageNode oldMessage = (MessageNode) old; + + node.mBottomOfStack = oldMessage.mBottomOfStack; + int bottomType = node.mBottomOfStack.getNodeType(); + inactive = bottomType >= STACK_NODE_PARKED; + wakeNeeded = (bottomType == STACK_NODE_TIMEDPARK + && mStackStateTimedPark.mWhenToWake >= node.getWhen() + && !oldMessage.mWokeUp); + node.mWokeUp = oldMessage.mWokeUp || wakeNeeded; + break; + } + if (sState.compareAndSet(this, old, node)) { + if (inactive) { + if (wakeNeeded) { + nativeWake(mPtr); + } else { + mMessageCounts.incrementQueued(); + } + } + return true; + } + } + } + + /* + * This class is used to find matches for hasMessages() and removeMessages() + */ + private abstract static class MessageCompare { + public abstract boolean compareMessage(Message m, Handler h, int what, Object object, + Runnable r, long when); + } + + private boolean stackHasMessages(Handler h, int what, Object object, Runnable r, long when, + MessageCompare compare, boolean removeMatches) { + boolean found = false; + StackNode top = (StackNode) sState.getVolatile(this); + StateNode bottom = getStateNode(top); + + /* + * If the top node is a state node, there are no reachable messages. + * If it's anything other than Active, we can quit as we know that next() is not + * consuming items. + * If the top node is Active then we know that next() is currently consuming items. + * In that case we should wait next() has drained the stack. + */ + if (top == bottom) { + if (bottom != sStackStateActive) { + return false; + } + waitForDrainCompleted(); + return false; + } + + /* + * We have messages that we may tombstone. Walk the stack until we hit the bottom or we + * hit a null pointer. + * If we hit the bottom, we are done. + * If we hit a null pointer, then the stack is being consumed by next() and we must cycle + * until the stack has been drained. + */ + MessageNode p = (MessageNode) top; + + while (true) { + if (compare.compareMessage(p.mMessage, h, what, object, r, when)) { + found = true; + if (DEBUG) { + Log.d(TAG_C, "stackHasMessages node matches"); + } + if (removeMatches) { + if (p.removeFromStack()) { + p.mMessage.recycleUnchecked(); + if (mMessageCounts.incrementCancelled()) { + nativeWake(mPtr); + } + } + } else { + return true; + } + } + + StackNode n = p.mNext; + if (n == null) { + /* Next() is walking the stack, we must re-sample */ + if (DEBUG) { + Log.d(TAG_C, "stackHasMessages next() is walking the stack, we must re-sample"); + } + waitForDrainCompleted(); + break; + } + if (!n.isMessageNode()) { + /* We reached the end of the stack */ + return found; + } + p = (MessageNode) n; + } + + return found; + } + + private boolean priorityQueueHasMessage(ConcurrentSkipListSet<MessageNode> queue, Handler h, + int what, Object object, Runnable r, long when, MessageCompare compare, + boolean removeMatches) { + Iterator<MessageNode> iterator = queue.iterator(); + boolean found = false; + + while (iterator.hasNext()) { + MessageNode msg = iterator.next(); + + if (compare.compareMessage(msg.mMessage, h, what, object, r, when)) { + if (removeMatches) { + found = true; + if (queue.remove(msg)) { + msg.mMessage.recycleUnchecked(); + } + } else { + return true; + } + } + } + return found; + } + + private boolean findOrRemoveMessages(Handler h, int what, Object object, Runnable r, long when, + MessageCompare compare, boolean removeMatches) { + boolean foundInStack, foundInQueue; + + foundInStack = stackHasMessages(h, what, object, r, when, compare, removeMatches); + foundInQueue = priorityQueueHasMessage(mPriorityQueue, h, what, object, r, when, compare, + removeMatches); + foundInQueue |= priorityQueueHasMessage(mAsyncPriorityQueue, h, what, object, r, when, + compare, removeMatches); + + return foundInStack || foundInQueue; + } + +} diff --git a/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java b/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java index 8eaadde5a1d0..9db88d17614b 100644 --- a/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java +++ b/core/java/android/os/ConcurrentMessageQueue/MessageQueue.java @@ -382,6 +382,18 @@ public final class MessageQueue { } } + private static final class MatchDeliverableMessages extends MessageCompare { + @Override + public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r, + long when) { + if (m.when <= when) { + return true; + } + return false; + } + } + private final MatchDeliverableMessages mMatchDeliverableMessages = + new MatchDeliverableMessages(); /** * Returns true if the looper has no pending messages which are due to be processed. * @@ -390,6 +402,12 @@ public final class MessageQueue { * @return True if the looper is idle. */ public boolean isIdle() { + final long now = SystemClock.uptimeMillis(); + + if (stackHasMessages(null, 0, null, null, now, mMatchDeliverableMessages, false)) { + return false; + } + MessageNode msgNode = null; MessageNode asyncMsgNode = null; @@ -405,7 +423,6 @@ public final class MessageQueue { } catch (NoSuchElementException e) { } } - final long now = SystemClock.uptimeMillis(); if ((msgNode != null && msgNode.getWhen() <= now) || (asyncMsgNode != null && asyncMsgNode.getWhen() <= now)) { return false; @@ -967,7 +984,7 @@ public final class MessageQueue { return token; } - private class MatchBarrierToken extends MessageCompare { + private static final class MatchBarrierToken extends MessageCompare { int mBarrierToken; MatchBarrierToken(int token) { @@ -1148,7 +1165,7 @@ public final class MessageQueue { return foundInStack || foundInQueue; } - private static class MatchHandlerWhatAndObject extends MessageCompare { + private static final class MatchHandlerWhatAndObject extends MessageCompare { @Override public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r, long when) { @@ -1168,7 +1185,7 @@ public final class MessageQueue { return findOrRemoveMessages(h, what, object, null, 0, mMatchHandlerWhatAndObject, false); } - private static class MatchHandlerWhatAndObjectEquals extends MessageCompare { + private static final class MatchHandlerWhatAndObjectEquals extends MessageCompare { @Override public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r, long when) { @@ -1189,7 +1206,7 @@ public final class MessageQueue { false); } - private static class MatchHandlerRunnableAndObject extends MessageCompare { + private static final class MatchHandlerRunnableAndObject extends MessageCompare { @Override public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r, long when) { @@ -1210,7 +1227,7 @@ public final class MessageQueue { return findOrRemoveMessages(h, -1, object, r, 0, mMatchHandlerRunnableAndObject, false); } - private static class MatchHandler extends MessageCompare { + private static final class MatchHandler extends MessageCompare { @Override public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r, long when) { @@ -1249,7 +1266,7 @@ public final class MessageQueue { findOrRemoveMessages(h, -1, object, r, 0, mMatchHandlerRunnableAndObject, true); } - private static class MatchHandlerRunnableAndObjectEquals extends MessageCompare { + private static final class MatchHandlerRunnableAndObjectEquals extends MessageCompare { @Override public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r, long when) { @@ -1268,7 +1285,7 @@ public final class MessageQueue { findOrRemoveMessages(h, -1, object, r, 0, mMatchHandlerRunnableAndObjectEquals, true); } - private static class MatchHandlerAndObject extends MessageCompare { + private static final class MatchHandlerAndObject extends MessageCompare { @Override public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r, long when) { @@ -1286,7 +1303,7 @@ public final class MessageQueue { findOrRemoveMessages(h, -1, object, null, 0, mMatchHandlerAndObject, true); } - private static class MatchHandlerAndObjectEquals extends MessageCompare { + private static final class MatchHandlerAndObjectEquals extends MessageCompare { @Override public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r, long when) { @@ -1305,7 +1322,7 @@ public final class MessageQueue { findOrRemoveMessages(h, -1, object, null, 0, mMatchHandlerAndObjectEquals, true); } - private static class MatchAllMessages extends MessageCompare { + private static final class MatchAllMessages extends MessageCompare { @Override public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r, long when) { @@ -1317,7 +1334,7 @@ public final class MessageQueue { findOrRemoveMessages(null, -1, null, null, 0, mMatchAllMessages, true); } - private static class MatchAllFutureMessages extends MessageCompare { + private static final class MatchAllFutureMessages extends MessageCompare { @Override public boolean compareMessage(Message m, Handler h, int what, Object object, Runnable r, long when) { diff --git a/core/java/android/os/Environment.java b/core/java/android/os/Environment.java index 89a5e5d6637d..69540c42a742 100644 --- a/core/java/android/os/Environment.java +++ b/core/java/android/os/Environment.java @@ -417,6 +417,14 @@ public class Environment { */ @SystemApi @FlaggedApi(android.crashrecovery.flags.Flags.FLAG_ENABLE_CRASHRECOVERY) + public static @NonNull File getDataSystemDeviceProtectedDirectory() { + return buildPath(getDataDirectory(), "system_de"); + } + + /** Use {@link #getDataSystemDeviceProtectedDirectory()} instead. + * {@hide} + */ + @Deprecated public static @NonNull File getDataSystemDeDirectory() { return buildPath(getDataDirectory(), "system_de"); } diff --git a/core/java/android/os/IpcDataCache.java b/core/java/android/os/IpcDataCache.java index 7875c23be038..8db1567336d3 100644 --- a/core/java/android/os/IpcDataCache.java +++ b/core/java/android/os/IpcDataCache.java @@ -23,6 +23,7 @@ import android.annotation.StringDef; import android.annotation.SystemApi; import android.annotation.TestApi; import android.app.PropertyInvalidatedCache; +import android.app.PropertyInvalidatedCache.Args; import android.text.TextUtils; import android.util.ArraySet; @@ -341,7 +342,7 @@ public class IpcDataCache<Query, Result> extends PropertyInvalidatedCache<Query, public IpcDataCache(int maxEntries, @NonNull @IpcDataCacheModule String module, @NonNull String api, @NonNull String cacheName, @NonNull QueryHandler<Query, Result> computer) { - super(maxEntries, module, api, cacheName, computer); + super(new Args(module).maxEntries(maxEntries).api(api), cacheName, computer); } /** @@ -563,7 +564,8 @@ public class IpcDataCache<Query, Result> extends PropertyInvalidatedCache<Query, * @hide */ public IpcDataCache(@NonNull Config config, @NonNull QueryHandler<Query, Result> computer) { - super(config.maxEntries(), config.module(), config.api(), config.name(), computer); + super(new Args(config.module()).maxEntries(config.maxEntries()).api(config.api()), + config.name(), computer); } /** diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java index 32db3bea7686..9d4ac2963b2d 100644 --- a/core/java/android/os/PowerManager.java +++ b/core/java/android/os/PowerManager.java @@ -517,9 +517,15 @@ public final class PowerManager { public static final int GO_TO_SLEEP_REASON_DEVICE_FOLD = 13; /** + * Go to sleep reason code: reason unknown. * @hide */ - public static final int GO_TO_SLEEP_REASON_MAX = GO_TO_SLEEP_REASON_DEVICE_FOLD; + public static final int GO_TO_SLEEP_REASON_UNKNOWN = 14; + + /** + * @hide + */ + public static final int GO_TO_SLEEP_REASON_MAX = GO_TO_SLEEP_REASON_UNKNOWN; /** * @hide @@ -540,6 +546,7 @@ public final class PowerManager { case GO_TO_SLEEP_REASON_QUIESCENT: return "quiescent"; case GO_TO_SLEEP_REASON_SLEEP_BUTTON: return "sleep_button"; case GO_TO_SLEEP_REASON_TIMEOUT: return "timeout"; + case GO_TO_SLEEP_REASON_UNKNOWN: return "unknown"; default: return Integer.toString(sleepReason); } } @@ -635,6 +642,7 @@ public final class PowerManager { GO_TO_SLEEP_REASON_QUIESCENT, GO_TO_SLEEP_REASON_SLEEP_BUTTON, GO_TO_SLEEP_REASON_TIMEOUT, + GO_TO_SLEEP_REASON_UNKNOWN, }) @Retention(RetentionPolicy.SOURCE) public @interface GoToSleepReason{} diff --git a/core/java/android/os/StrictMode.java b/core/java/android/os/StrictMode.java index 60a9e053e99d..90993e1850d4 100644 --- a/core/java/android/os/StrictMode.java +++ b/core/java/android/os/StrictMode.java @@ -20,7 +20,7 @@ import static android.annotation.SystemApi.Client.MODULE_LIBRARIES; import static com.android.internal.util.FrameworkStatsLog.UNSAFE_INTENT_EVENT_REPORTED__EVENT_TYPE__EXPLICIT_INTENT_FILTER_UNMATCH; import static com.android.internal.util.FrameworkStatsLog.UNSAFE_INTENT_EVENT_REPORTED__EVENT_TYPE__INTERNAL_NON_EXPORTED_COMPONENT_MATCH; import static com.android.internal.util.FrameworkStatsLog.UNSAFE_INTENT_EVENT_REPORTED__EVENT_TYPE__NULL_ACTION_MATCH; -import static com.android.window.flags.Flags.balStrictMode; +import static com.android.window.flags.Flags.balStrictModeRo; import android.animation.ValueAnimator; import android.annotation.FlaggedApi; @@ -33,6 +33,7 @@ import android.app.ActivityManager; import android.app.ActivityTaskManager; import android.app.ActivityThread; import android.app.IActivityManager; +import android.app.IActivityTaskManager; import android.app.IBackgroundActivityLaunchCallback; import android.app.IUnsafeIntentStrictModeCallback; import android.app.PendingIntent; @@ -912,7 +913,7 @@ public final class StrictMode { if (targetSdk >= Build.VERSION_CODES.S) { detectUnsafeIntentLaunch(); } - if (balStrictMode() && targetSdk > Build.VERSION_CODES.VANILLA_ICE_CREAM) { + if (balStrictModeRo() && targetSdk > Build.VERSION_CODES.VANILLA_ICE_CREAM) { detectBlockedBackgroundActivityLaunch(); } @@ -1168,7 +1169,7 @@ public final class StrictMode { * the home button while the app tries to start a new activity. */ @SuppressWarnings("BuilderSetStyle") - @FlaggedApi(Flags.FLAG_BAL_STRICT_MODE) + @FlaggedApi(Flags.FLAG_BAL_STRICT_MODE_RO) public @NonNull Builder detectBlockedBackgroundActivityLaunch() { return enable(DETECT_VM_BACKGROUND_ACTIVITY_LAUNCH_ABORTED); } @@ -1180,7 +1181,7 @@ public final class StrictMode { * This disables the effect of {@link #detectBlockedBackgroundActivityLaunch()}. */ @SuppressWarnings("BuilderSetStyle") - @FlaggedApi(Flags.FLAG_BAL_STRICT_MODE) + @FlaggedApi(Flags.FLAG_BAL_STRICT_MODE_RO) public @NonNull Builder ignoreBlockedBackgroundActivityLaunch() { return disable(DETECT_VM_BACKGROUND_ACTIVITY_LAUNCH_ABORTED); } @@ -2189,8 +2190,11 @@ public final class StrictMode { private static void registerBackgroundActivityLaunchCallback() { try { - ActivityTaskManager.getService().registerBackgroundActivityStartCallback( + IActivityTaskManager service = ActivityTaskManager.getService(); + if (service != null) { + service.registerBackgroundActivityStartCallback( new BackgroundActivityLaunchCallback()); + } } catch (DeadObjectException e) { // ignore } catch (RemoteException e) { diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java index fa99f3563de9..4bc8fe0a974c 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -5502,10 +5502,14 @@ public class UserManager { Manifest.permission.CREATE_USERS, Manifest.permission.QUERY_USERS}, conditional = true) public @NonNull int[] getProfileIds(@UserIdInt int userId, boolean enabledOnly) { - try { - return mService.getProfileIds(userId, enabledOnly); - } catch (RemoteException re) { - throw re.rethrowFromSystemServer(); + if (android.multiuser.Flags.cacheProfileIdsReadOnly()) { + return enabledOnly ? getEnabledProfileIds(userId) : getProfileIdsWithDisabled(userId); + } else { + try { + return mService.getProfileIds(userId, enabledOnly); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } } } @@ -5518,8 +5522,14 @@ public class UserManager { Manifest.permission.MANAGE_USERS, Manifest.permission.CREATE_USERS, Manifest.permission.QUERY_USERS}, conditional = true) + @CachedProperty(api = "user_manager_users") public int[] getProfileIdsWithDisabled(@UserIdInt int userId) { - return getProfileIds(userId, false /* enabledOnly */); + if (android.multiuser.Flags.cacheProfileIdsReadOnly()) { + return UserManagerCache.getProfileIdsWithDisabled( + (Integer userIdentifuer) -> mService.getProfileIds(userIdentifuer, false), userId); + } else { + return getProfileIds(userId, false /* enabledOnly */); + } } /** @@ -5530,8 +5540,21 @@ public class UserManager { Manifest.permission.MANAGE_USERS, Manifest.permission.CREATE_USERS, Manifest.permission.QUERY_USERS}, conditional = true) + @CachedProperty(api = "user_manager_users_enabled") public int[] getEnabledProfileIds(@UserIdInt int userId) { - return getProfileIds(userId, true /* enabledOnly */); + if (android.multiuser.Flags.cacheProfileIdsReadOnly()) { + return UserManagerCache.getEnabledProfileIds( + (Integer userIdentifuer) -> mService.getProfileIds(userIdentifuer, true), userId); + } else { + return getProfileIds(userId, true /* enabledOnly */); + } + } + + /** @hide */ + public static final void invalidateEnabledProfileIds() { + if (android.multiuser.Flags.cacheProfileIdsReadOnly()) { + UserManagerCache.invalidateEnabledProfileIds(); + } } /** @@ -6443,6 +6466,21 @@ public class UserManager { if (android.multiuser.Flags.cacheProfileParentReadOnly()) { UserManagerCache.invalidateProfileParent(); } + invalidateEnabledProfileIds(); + } + + /** + * Invalidate caches when related to specific user info flags change. + * + * @param flag a combination of FLAG_ constants, from the list in + * {@link UserInfo#UserInfoFlag}, whose value has changed and the associated + * invalidations must therefore be performed. + * @hide + */ + public static final void invalidateOnUserInfoFlagChange(@UserInfoFlag int flags) { + if ((flags & UserInfo.FLAG_DISABLED) > 0) { + invalidateEnabledProfileIds(); + } } /** diff --git a/core/java/android/os/VibrationEffect.java b/core/java/android/os/VibrationEffect.java index a3844e244edb..0cffd9f990fd 100644 --- a/core/java/android/os/VibrationEffect.java +++ b/core/java/android/os/VibrationEffect.java @@ -1740,7 +1740,9 @@ public abstract class VibrationEffect implements Parcelable { * * <p>The waveform envelope builder offers more flexibility for creating waveform effects, * allowing control over vibration amplitude and frequency via smooth transitions between - * values. + * values. The waveform will start the first transition from the vibrator off state, using + * the same frequency of the first control point. To provide a different initial vibration + * frequency, use {@link #startWaveformEnvelope(float)}. * * <p>Note: To check whether waveform envelope effects are supported, use * {@link Vibrator#areEnvelopeEffectsSupported()}. @@ -1754,6 +1756,32 @@ public abstract class VibrationEffect implements Parcelable { } /** + * Start building a waveform vibration with an initial frequency. + * + * <p>The waveform envelope builder offers more flexibility for creating waveform effects, + * allowing control over vibration amplitude and frequency via smooth transitions between + * values. + * + * <p>This is the same as {@link #startWaveformEnvelope()}, but the waveform will start + * vibrating at given frequency, in hertz, while it transitions to the new amplitude and + * frequency of the first control point. + * + * <p>Note: To check whether waveform envelope effects are supported, use + * {@link Vibrator#areEnvelopeEffectsSupported()}. + * + * @param initialFrequencyHz The starting frequency of the vibration, in hertz. Must be greater + * than zero. + * + * @see VibrationEffect.WaveformEnvelopeBuilder + */ + @FlaggedApi(Flags.FLAG_NORMALIZED_PWLE_EFFECTS) + @NonNull + public static VibrationEffect.WaveformEnvelopeBuilder startWaveformEnvelope( + @FloatRange(from = 0) float initialFrequencyHz) { + return new WaveformEnvelopeBuilder(initialFrequencyHz); + } + + /** * A builder for waveform effects described by its envelope. * * <p>Waveform effect envelopes are defined by one or more control points describing a target @@ -1810,6 +1838,10 @@ public abstract class VibrationEffect implements Parcelable { private WaveformEnvelopeBuilder() {} + private WaveformEnvelopeBuilder(float initialFrequency) { + mLastFrequencyHz = initialFrequency; + } + /** * Adds a new control point to the end of this waveform envelope. * @@ -1841,7 +1873,7 @@ public abstract class VibrationEffect implements Parcelable { @FloatRange(from = 0, to = 1) float amplitude, @FloatRange(from = 0) float frequencyHz, int timeMillis) { - if (mSegments.isEmpty()) { + if (mLastFrequencyHz == 0) { mLastFrequencyHz = frequencyHz; } diff --git a/core/java/android/os/vibrator/PwlePoint.java b/core/java/android/os/vibrator/PwlePoint.java new file mode 100644 index 000000000000..ea3ae6c4649d --- /dev/null +++ b/core/java/android/os/vibrator/PwlePoint.java @@ -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 android.os.vibrator; + +import java.util.Objects; + +/** + * A {@link PwlePoint} represents a single point in an envelope vibration effect. Defined by its + * amplitude, frequency and time to transition to this point from the previous one in the envelope. + * + * @hide + */ +public final class PwlePoint { + private final float mAmplitude; + private final float mFrequencyHz; + private final int mTimeMillis; + + /** @hide */ + public PwlePoint(float amplitude, float frequencyHz, int timeMillis) { + mAmplitude = amplitude; + mFrequencyHz = frequencyHz; + mTimeMillis = timeMillis; + } + + public float getAmplitude() { + return mAmplitude; + } + + public float getFrequencyHz() { + return mFrequencyHz; + } + + public int getTimeMillis() { + return mTimeMillis; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof PwlePoint)) { + return false; + } + PwlePoint other = (PwlePoint) obj; + return Float.compare(mAmplitude, other.mAmplitude) == 0 + && Float.compare(mFrequencyHz, other.mFrequencyHz) == 0 + && mTimeMillis == other.mTimeMillis; + } + + @Override + public int hashCode() { + return Objects.hash(mAmplitude, mFrequencyHz, mTimeMillis); + } +} diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig index 1d654e1322c7..6a4932211f27 100644 --- a/core/java/android/permission/flags.aconfig +++ b/core/java/android/permission/flags.aconfig @@ -239,6 +239,13 @@ flag { } flag { + name: "use_frozen_aware_remote_callback_list" + namespace: "permissions" + description: "Whether to use the new frozen-aware RemoteCallbackList API for op noted callbacks." + bug: "361157077" +} + +flag { name: "wallet_role_icon_property_enabled" is_exported: true namespace: "wallet_integration" @@ -307,3 +314,21 @@ flag { description: "Enable AppOp mode caching in AppOpsManager" bug: "366013082" } + +flag { + name: "permission_tree_apis_deprecated" + is_fixed_read_only: true + is_exported: true + namespace: "permissions" + description: "This flag is used to deprecate permission tree related APIs" + bug: "376535612" +} + +flag { + name: "enable_otp_in_text_classifiers" + is_fixed_read_only: true + is_exported: true + namespace: "permissions" + description: "Enables ExtServices to leverage TextClassifier for OTP detection" + bug: "351976749" +} diff --git a/core/java/android/provider/ContactsContract.java b/core/java/android/provider/ContactsContract.java index 8afc1779ed00..1b289fdabbd1 100644 --- a/core/java/android/provider/ContactsContract.java +++ b/core/java/android/provider/ContactsContract.java @@ -9383,7 +9383,14 @@ public final class ContactsContract { * @param resolver the ContentResolver to query. * @return the default account for new contacts, or null if it's not set or set to NULL * account. + * + * @deprecated This API is only supported up to Android version + * * {@link Build.VERSION_CODES#VANILLA_ICE_CREAM}. On later versions, + * {@link ContactsContract.RawContacts.DefaultAccount#getDefaultAccountForNewContacts} + * should be used. */ + @Deprecated + @FlaggedApi(Flags.FLAG_NEW_DEFAULT_ACCOUNT_API_ENABLED) @Nullable public static Account getDefaultAccount(@NonNull ContentResolver resolver) { Bundle response = resolver.call(ContactsContract.AUTHORITY_URI, @@ -9404,7 +9411,14 @@ public final class ContactsContract { * @param resolver the ContentResolver to query. * @param account the account to be set to default. * @hide + * + * @deprecated This API is only supported up to Android version + * * {@link Build.VERSION_CODES#VANILLA_ICE_CREAM}. On later versions, + * {@link ContactsContract.RawContacts.DefaultAccount#setDefaultAccountForNewContacts} + * should be used. */ + @Deprecated + @FlaggedApi(Flags.FLAG_NEW_DEFAULT_ACCOUNT_API_ENABLED) @SystemApi @RequiresPermission(android.Manifest.permission.SET_DEFAULT_ACCOUNT_FOR_CONTACTS) public static void setDefaultAccount(@NonNull ContentResolver resolver, diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 8efbc9c92675..d19681c86320 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -6332,6 +6332,27 @@ public final class Settings { public static final String SCREEN_FLASH_NOTIFICATION_COLOR = "screen_flash_notification_color_global"; + + /** + * A semi-colon separated list of Bluetooth hearing devices' local ambient volume. + * Each entry is encoded as a key=value list, separated by commas. Ex: + * + * "addr=XX:XX:XX:00:11,ambient=20,group_ambient=30;addr=XX:XX:XX:00:22,ambient=50" + * + * The following keys are supported: + * <pre> + * addr (String) + * ambient (int) + * group_ambient (int) + * control_expanded (boolean) + * </pre> + * + * Each entry must contains "addr" attribute, otherwise it'll be ignored. + * @hide + */ + public static final String HEARING_DEVICE_LOCAL_AMBIENT_VOLUME = + "hearing_device_local_ambient_volume"; + /** * IMPORTANT: If you add a new public settings you also have to add it to * PUBLIC_SETTINGS below. If the new setting is hidden you have to add @@ -6476,6 +6497,7 @@ public final class Settings { PRIVATE_SETTINGS.add(DEFAULT_DEVICE_FONT_SCALE); PRIVATE_SETTINGS.add(MOUSE_REVERSE_VERTICAL_SCROLLING); PRIVATE_SETTINGS.add(MOUSE_SWAP_PRIMARY_BUTTON); + PRIVATE_SETTINGS.add(HEARING_DEVICE_LOCAL_AMBIENT_VOLUME); } /** diff --git a/core/java/android/security/advancedprotection/AdvancedProtectionFeature.aidl b/core/java/android/security/advancedprotection/AdvancedProtectionFeature.aidl new file mode 100644 index 000000000000..3ecef0248e1f --- /dev/null +++ b/core/java/android/security/advancedprotection/AdvancedProtectionFeature.aidl @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.security.advancedprotection; + +/** + * Represents an advanced protection feature providing protections + * @hide + */ +parcelable AdvancedProtectionFeature;
\ No newline at end of file diff --git a/core/java/android/security/advancedprotection/AdvancedProtectionFeature.java b/core/java/android/security/advancedprotection/AdvancedProtectionFeature.java new file mode 100644 index 000000000000..a086bf7f8b08 --- /dev/null +++ b/core/java/android/security/advancedprotection/AdvancedProtectionFeature.java @@ -0,0 +1,77 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.security.advancedprotection; + +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; +import android.security.Flags; + +/** + * An advanced protection feature providing protections. + * @hide + */ +@FlaggedApi(Flags.FLAG_AAPM_API) +@SystemApi +public final class AdvancedProtectionFeature implements Parcelable { + private final String mId; + + /** + * Create an object identifying an Advanced Protection feature for AdvancedProtectionManager + * @param id A unique ID to identify this feature. It is used by Settings screens to display + * information about this feature. + */ + public AdvancedProtectionFeature(@NonNull String id) { + mId = id; + } + + private AdvancedProtectionFeature(Parcel in) { + mId = in.readString8(); + } + + /** + * @return the unique ID representing this feature + */ + @NonNull + public String getId() { + return mId; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeString8(mId); + } + + @NonNull + public static final Parcelable.Creator<AdvancedProtectionFeature> CREATOR = + new Parcelable.Creator<>() { + public AdvancedProtectionFeature createFromParcel(Parcel in) { + return new AdvancedProtectionFeature(in); + } + + public AdvancedProtectionFeature[] newArray(int size) { + return new AdvancedProtectionFeature[size]; + } + }; +} diff --git a/core/java/android/security/advancedprotection/AdvancedProtectionManager.java b/core/java/android/security/advancedprotection/AdvancedProtectionManager.java index 43b6ebe2a735..6f3e3d8f0d3b 100644 --- a/core/java/android/security/advancedprotection/AdvancedProtectionManager.java +++ b/core/java/android/security/advancedprotection/AdvancedProtectionManager.java @@ -29,6 +29,7 @@ import android.os.RemoteException; import android.security.Flags; import android.util.Log; +import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; @@ -41,7 +42,7 @@ import java.util.concurrent.Executor; */ @FlaggedApi(Flags.FLAG_AAPM_API) @SystemService(Context.ADVANCED_PROTECTION_SERVICE) -public class AdvancedProtectionManager { +public final class AdvancedProtectionManager { private static final String TAG = "AdvancedProtectionMgr"; private final ConcurrentHashMap<Callback, IAdvancedProtectionCallback> @@ -147,6 +148,22 @@ public class AdvancedProtectionManager { } /** + * Returns the list of advanced protection features which are available on this device. + * + * @hide + */ + @SystemApi + @NonNull + @RequiresPermission(Manifest.permission.SET_ADVANCED_PROTECTION_MODE) + public List<AdvancedProtectionFeature> getAdvancedProtectionFeatures() { + try { + return mService.getAdvancedProtectionFeatures(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * A callback class for monitoring changes to Advanced Protection state * * <p>To register a callback, implement this interface, and register it with diff --git a/core/java/android/security/advancedprotection/IAdvancedProtectionService.aidl b/core/java/android/security/advancedprotection/IAdvancedProtectionService.aidl index ef0abf4f23a0..68307632027a 100644 --- a/core/java/android/security/advancedprotection/IAdvancedProtectionService.aidl +++ b/core/java/android/security/advancedprotection/IAdvancedProtectionService.aidl @@ -16,6 +16,7 @@ package android.security.advancedprotection; +import android.security.advancedprotection.AdvancedProtectionFeature; import android.security.advancedprotection.IAdvancedProtectionCallback; /** @@ -32,4 +33,6 @@ interface IAdvancedProtectionService { void unregisterAdvancedProtectionCallback(IAdvancedProtectionCallback callback); @EnforcePermission("SET_ADVANCED_PROTECTION_MODE") void setAdvancedProtectionEnabled(boolean enabled); + @EnforcePermission("SET_ADVANCED_PROTECTION_MODE") + List<AdvancedProtectionFeature> getAdvancedProtectionFeatures(); }
\ No newline at end of file diff --git a/core/java/android/security/responsible_apis_flags.aconfig b/core/java/android/security/responsible_apis_flags.aconfig index dec28c34ae5b..5995760a41ec 100644 --- a/core/java/android/security/responsible_apis_flags.aconfig +++ b/core/java/android/security/responsible_apis_flags.aconfig @@ -87,6 +87,14 @@ flag { } flag { + name: "prevent_intent_redirect_show_toast" + namespace: "responsible_apis" + description: "Prevent intent redirect attacks by showing a toast when activity start is blocked" + bug: "361143368" + is_fixed_read_only: true +} + +flag { name: "enable_intent_matching_flags" is_exported: true namespace: "permissions" diff --git a/core/java/android/service/contextualsearch/OWNERS b/core/java/android/service/contextualsearch/OWNERS index b7238721bc60..c435bd87be21 100644 --- a/core/java/android/service/contextualsearch/OWNERS +++ b/core/java/android/service/contextualsearch/OWNERS @@ -1,2 +1,3 @@ srazdan@google.com -hackz@google.com +hyunyoungs@google.com +awickham@google.com diff --git a/core/java/android/service/games/GameSession.java b/core/java/android/service/games/GameSession.java index e8d53d351795..531e0b11996a 100644 --- a/core/java/android/service/games/GameSession.java +++ b/core/java/android/service/games/GameSession.java @@ -516,6 +516,8 @@ public abstract class GameSession { options, future); + trampolineIntent.collectExtraIntentKeys(); + try { int result = ActivityTaskManager.getService().startActivityFromGameSession( mContext.getIApplicationThread(), mContext.getPackageName(), "GameSession", diff --git a/core/java/android/service/notification/NotificationListenerService.java b/core/java/android/service/notification/NotificationListenerService.java index bd9ab86fa8d1..a8ab2115d97f 100644 --- a/core/java/android/service/notification/NotificationListenerService.java +++ b/core/java/android/service/notification/NotificationListenerService.java @@ -17,6 +17,7 @@ package android.service.notification; import android.annotation.CurrentTimeMillisLong; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -882,6 +883,35 @@ public abstract class NotificationListenerService extends Service { } } + /** + * Creates a conversation notification channel for a given package for a given user. + * + * <p>This method will throw a security exception if you don't have access to notifications + * for the given user.</p> + * <p>The caller must have {@link CompanionDeviceManager#getAssociations() an associated + * device} or be the notification assistant in order to use this method. + * + * @param pkg The package the channel belongs to. + * @param user The user the channel belongs to. + * @param parentChannelId The parent channel id of the conversation channel belongs to. + * @param conversationId The conversation id of the conversation channel. + * + * @return The created conversation channel. + */ + @FlaggedApi(Flags.FLAG_NOTIFICATION_CONVERSATION_CHANNEL_MANAGEMENT) + public final @Nullable NotificationChannel createConversationNotificationChannelForPackage( + @NonNull String pkg, @NonNull UserHandle user, @NonNull String parentChannelId, + @NonNull String conversationId) { + if (!isBound()) return null; + try { + return getNotificationInterface() + .createConversationNotificationChannelForPackageFromPrivilegedListener( + mWrapper, pkg, user, parentChannelId, conversationId); + } catch (RemoteException e) { + Log.v(TAG, "Unable to contact notification manager", e); + throw e.rethrowFromSystemServer(); + } + } /** * Updates a notification channel for a given package for a given user. This should only be used @@ -890,7 +920,7 @@ public abstract class NotificationListenerService extends Service { * <p>This method will throw a security exception if you don't have access to notifications * for the given user.</p> * <p>The caller must have {@link CompanionDeviceManager#getAssociations() an associated - * device} in order to use this method. + * device} or be the notification assistant in order to use this method. * * @param pkg The package the channel belongs to. * @param user The user the channel belongs to. diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java index e1732559e262..24328eb1e825 100644 --- a/core/java/android/service/notification/ZenModeConfig.java +++ b/core/java/android/service/notification/ZenModeConfig.java @@ -24,12 +24,12 @@ import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_FULL_SCRE import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_LIGHTS; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_SCREEN_OFF; +import static android.app.backup.NotificationLoggingConstants.DATA_TYPE_ZEN_RULES; import static android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; import static android.service.notification.SystemZenRules.PACKAGE_ANDROID; import static android.service.notification.ZenAdapters.peopleTypeToPrioritySenders; import static android.service.notification.ZenAdapters.prioritySendersToPeopleType; import static android.service.notification.ZenAdapters.zenPolicyConversationSendersToNotificationPolicy; -import static android.service.notification.ZenModeConfig.EventInfo.REPLY_YES_OR_MAYBE; import static android.service.notification.ZenPolicy.PEOPLE_TYPE_STARRED; import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_ALARMS; import static android.service.notification.ZenPolicy.PRIORITY_CATEGORY_CALLS; @@ -56,6 +56,7 @@ import android.app.AutomaticZenRule; import android.app.Flags; import android.app.NotificationManager; import android.app.NotificationManager.Policy; +import android.app.backup.BackupRestoreEventLogger; import android.compat.annotation.UnsupportedAppUsage; import android.content.ComponentName; import android.content.Context; @@ -957,8 +958,9 @@ public class ZenModeConfig implements Parcelable { } } - public static ZenModeConfig readXml(TypedXmlPullParser parser) - throws XmlPullParserException, IOException { + public static ZenModeConfig readXml(TypedXmlPullParser parser, + @Nullable BackupRestoreEventLogger logger) throws XmlPullParserException, IOException { + int readRuleCount = 0; int type = parser.getEventType(); if (type != XmlPullParser.START_TAG) return null; String tag = parser.getName(); @@ -1048,6 +1050,8 @@ public class ZenModeConfig implements Parcelable { readManualRule = true; if (rt.manualRule.zenPolicy == null) { readManualRuleWithoutPolicy = true; + } else { + readRuleCount++; } } else if (AUTOMATIC_TAG.equals(tag) || (Flags.modesApi() && AUTOMATIC_DELETED_TAG.equals(tag))) { @@ -1062,6 +1066,7 @@ public class ZenModeConfig implements Parcelable { } } else if (AUTOMATIC_TAG.equals(tag)) { rt.automaticRules.put(id, automaticRule); + readRuleCount++; } } } else if (STATE_TAG.equals(tag)) { @@ -1085,8 +1090,17 @@ public class ZenModeConfig implements Parcelable { } rt.manualRule.condition = new Condition(rt.manualRule.conditionId, "", Condition.STATE_TRUE); + readRuleCount++; } } + + if (!Flags.modesUi()){ + readRuleCount++; + } + + if (logger != null) { + logger.logItemsRestored(DATA_TYPE_ZEN_RULES, readRuleCount); + } return rt; } } @@ -1110,8 +1124,9 @@ public class ZenModeConfig implements Parcelable { * @throws IOException */ - public void writeXml(TypedXmlSerializer out, Integer version, boolean forBackup) - throws IOException { + public void writeXml(TypedXmlSerializer out, Integer version, boolean forBackup, + @Nullable BackupRestoreEventLogger logger) throws IOException { + int writtenRuleCount = 0; int xmlVersion = getCurrentXmlVersion(); out.startTag(null, ZEN_TAG); out.attribute(null, ZEN_ATT_VERSION, version == null @@ -1147,6 +1162,7 @@ public class ZenModeConfig implements Parcelable { writeRuleXml(manualRule, out, forBackup); out.endTag(null, MANUAL_TAG); } + writtenRuleCount++; final int N = automaticRules.size(); for (int i = 0; i < N; i++) { final String id = automaticRules.keyAt(i); @@ -1155,6 +1171,7 @@ public class ZenModeConfig implements Parcelable { out.attribute(null, RULE_ATT_ID, id); writeRuleXml(automaticRule, out, forBackup); out.endTag(null, AUTOMATIC_TAG); + writtenRuleCount++; } if (Flags.modesApi() && !forBackup) { for (int i = 0; i < deletedRules.size(); i++) { @@ -1171,6 +1188,9 @@ public class ZenModeConfig implements Parcelable { out.endTag(null, STATE_TAG); out.endTag(null, ZEN_TAG); + if (logger != null) { + logger.logItemsBackedUp(DATA_TYPE_ZEN_RULES, writtenRuleCount); + } } @NonNull diff --git a/core/java/android/service/notification/flags.aconfig b/core/java/android/service/notification/flags.aconfig index 51961a85d307..34e311f8c932 100644 --- a/core/java/android/service/notification/flags.aconfig +++ b/core/java/android/service/notification/flags.aconfig @@ -57,4 +57,12 @@ flag { namespace: "systemui" description: "Guards the new FLAG_SILENT Notification flag" bug: "336488844" +} + +flag { + name: "notification_conversation_channel_management" + is_exported: true + namespace: "systemui" + description: "Allows the NAS to create and modify conversation notifications" + bug: "373599715" }
\ No newline at end of file diff --git a/core/java/android/service/wallpaper/IWallpaperEngine.aidl b/core/java/android/service/wallpaper/IWallpaperEngine.aidl index f1ae22eca873..e9e26466339c 100644 --- a/core/java/android/service/wallpaper/IWallpaperEngine.aidl +++ b/core/java/android/service/wallpaper/IWallpaperEngine.aidl @@ -22,6 +22,7 @@ import android.graphics.Rect; import android.graphics.RectF; import android.view.MotionEvent; import android.view.SurfaceControl; +import android.app.wallpaper.WallpaperDescription; import android.os.Bundle; /** @@ -50,4 +51,5 @@ interface IWallpaperEngine { SurfaceControl mirrorSurfaceControl(); oneway void applyDimming(float dimAmount); oneway void setWallpaperFlags(int which); + @nullable WallpaperDescription onApplyWallpaper(int which); } diff --git a/core/java/android/service/wallpaper/IWallpaperService.aidl b/core/java/android/service/wallpaper/IWallpaperService.aidl index 3262f3a9fdfb..f76e6cee13f0 100644 --- a/core/java/android/service/wallpaper/IWallpaperService.aidl +++ b/core/java/android/service/wallpaper/IWallpaperService.aidl @@ -17,6 +17,7 @@ package android.service.wallpaper; import android.app.WallpaperInfo; +import android.app.wallpaper.WallpaperDescription; import android.graphics.Rect; import android.service.wallpaper.IWallpaperConnection; @@ -27,6 +28,6 @@ oneway interface IWallpaperService { void attach(IWallpaperConnection connection, IBinder windowToken, int windowType, boolean isPreview, int reqWidth, int reqHeight, in Rect padding, int displayId, int which, - in WallpaperInfo info); + in WallpaperInfo info, in @nullable WallpaperDescription description); void detach(IBinder windowToken); } diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java index 2ab16e91d987..131fdc895841 100644 --- a/core/java/android/service/wallpaper/WallpaperService.java +++ b/core/java/android/service/wallpaper/WallpaperService.java @@ -16,6 +16,7 @@ package android.service.wallpaper; +import static android.app.Flags.FLAG_LIVE_WALLPAPER_CONTENT_HANDLING; import static android.app.WallpaperManager.COMMAND_FREEZE; import static android.app.WallpaperManager.COMMAND_UNFREEZE; import static android.app.WallpaperManager.SetWallpaperFlags; @@ -50,6 +51,7 @@ import android.app.WallpaperColors; import android.app.WallpaperInfo; import android.app.WallpaperManager; import android.app.compat.CompatChanges; +import android.app.wallpaper.WallpaperDescription; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledAfter; import android.compat.annotation.EnabledSince; @@ -221,14 +223,14 @@ public abstract class WallpaperService extends Service { /** * Wear products currently force a slight scaling transition to wallpapers - * when the QSS is opened. However, on Wear 6 (SDK 35) and above, 1P watch faces + * when the QSS is opened. However, on Wear 7 (SDK 37) and above, 1P watch faces * will be expected to either implement their own scaling, or to override this * method to allow the WallpaperController to continue to scale for them. * * @hide */ @ChangeId - @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM) + @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.BAKLAVA) public static final long WEAROS_WALLPAPER_HANDLES_SCALING = 272527315L; static final class WallpaperCommand { @@ -922,6 +924,24 @@ public abstract class WallpaperService extends Service { } /** + * Called when the wallpaper preview rendered by this engine is about to be persisted as + * a selected wallpaper. The returned WallpaperDescription (if any) will be persisted by + * the system and passed into subsequent calls to + * {@link WallpaperService#onCreateEngine(WallpaperDescription)}. This allows the Engine + * to perform any necessary bookkeeping before a wallpaper being previewed is set on + * the device, and update the description if necessary. + * + * @param which Specifies wallpaper destination: home, lock, or both + * @return the description of the applied wallpaper, or {@code null} if description is + * unchanged + */ + @Nullable + @FlaggedApi(FLAG_LIVE_WALLPAPER_CONTENT_HANDLING) + public WallpaperDescription onApplyWallpaper(@SetWallpaperFlags int which) { + return null; + } + + /** * Notifies the engine that wallpaper colors changed significantly. * This will trigger a {@link #onComputeColors()} call. */ @@ -2449,6 +2469,7 @@ public abstract class WallpaperService extends Service { final Display mDisplay; final WallpaperManager mWallpaperManager; @Nullable final WallpaperInfo mInfo; + @NonNull final WallpaperDescription mDescription; Engine mEngine; @SetWallpaperFlags int mWhich; @@ -2456,7 +2477,8 @@ public abstract class WallpaperService extends Service { IWallpaperEngineWrapper(WallpaperService service, IWallpaperConnection conn, IBinder windowToken, int windowType, boolean isPreview, int reqWidth, int reqHeight, Rect padding, - int displayId, @SetWallpaperFlags int which, @Nullable WallpaperInfo info) { + int displayId, @SetWallpaperFlags int which, @Nullable WallpaperInfo info, + @NonNull WallpaperDescription description) { mWallpaperManager = getSystemService(WallpaperManager.class); mCaller = new HandlerCaller(service, service.onProvideEngineLooper(), this, true); mConnection = conn; @@ -2469,6 +2491,7 @@ public abstract class WallpaperService extends Service { mDisplayId = displayId; mWhich = which; mInfo = info; + mDescription = description; // Create a display context before onCreateEngine. mDisplayManager = getSystemService(DisplayManager.class); @@ -2593,9 +2616,19 @@ public abstract class WallpaperService extends Service { return mEngine == null ? null : SurfaceControl.mirrorSurface(mEngine.mSurfaceControl); } + @Nullable + public WallpaperDescription onApplyWallpaper(@SetWallpaperFlags int which) { + return mEngine != null ? mEngine.onApplyWallpaper(which) : null; + } + private void doAttachEngine() { Trace.beginSection("WPMS.onCreateEngine"); - Engine engine = onCreateEngine(); + Engine engine; + if (mDescription != null) { + engine = onCreateEngine(mDescription); + } else { + engine = onCreateEngine(); + } Trace.endSection(); mEngine = engine; Trace.beginSection("WPMS.mConnection.attachEngine-" + mDisplayId); @@ -2804,11 +2837,13 @@ public abstract class WallpaperService extends Service { @Override public void attach(IWallpaperConnection conn, IBinder windowToken, int windowType, boolean isPreview, int reqWidth, int reqHeight, Rect padding, - int displayId, @SetWallpaperFlags int which, @Nullable WallpaperInfo info) { + int displayId, @SetWallpaperFlags int which, WallpaperInfo info, + @NonNull WallpaperDescription description) { Trace.beginSection("WPMS.ServiceWrapper.attach"); IWallpaperEngineWrapper engineWrapper = new IWallpaperEngineWrapper(mTarget, conn, windowToken, windowType, - isPreview, reqWidth, reqHeight, padding, displayId, which, info); + isPreview, reqWidth, reqHeight, padding, displayId, which, info, + description); synchronized (mActiveEngines) { mActiveEngines.put(windowToken, engineWrapper); } @@ -2883,6 +2918,19 @@ public abstract class WallpaperService extends Service { @MainThread public abstract Engine onCreateEngine(); + /** + * Creates a new engine instance to show the given content. See also {@link #onCreateEngine()}. + * + * @param description content to display + * @return the rendering engine + */ + @FlaggedApi(FLAG_LIVE_WALLPAPER_CONTENT_HANDLING) + @MainThread + @Nullable + public Engine onCreateEngine(@NonNull WallpaperDescription description) { + return onCreateEngine(); + } + @Override protected void dump(FileDescriptor fd, PrintWriter out, String[] args) { out.print("State of wallpaper "); out.print(this); out.println(":"); diff --git a/core/java/android/telephony/TelephonyCallback.java b/core/java/android/telephony/TelephonyCallback.java index 46e27dc60adc..64a5533cbe69 100644 --- a/core/java/android/telephony/TelephonyCallback.java +++ b/core/java/android/telephony/TelephonyCallback.java @@ -1715,11 +1715,10 @@ public class TelephonyCallback { * @see TelephonyManager#EMERGENCY_CALLBACK_MODE_SMS * * @param timerDuration is the time remaining in the emergency callback mode. - * @param subId The subscription ID used to start the emergency callback mode. + * @param subscriptionId The subscription ID used to start the emergency callback mode. */ - @RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE) void onCallbackModeStarted(@TelephonyManager.EmergencyCallbackModeType int type, - @NonNull Duration timerDuration, int subId); + @NonNull Duration timerDuration, int subscriptionId); /** * Indicates that emergency callback mode has been re-started. @@ -1734,11 +1733,10 @@ public class TelephonyCallback { * @see TelephonyManager#EMERGENCY_CALLBACK_MODE_SMS * * @param timerDuration is the time remaining in the emergency callback mode. - * @param subId The subscription ID used to restart the emergency callback mode. + * @param subscriptionId The subscription ID used to restart the emergency callback mode. */ - @RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE) void onCallbackModeRestarted(@TelephonyManager.EmergencyCallbackModeType int type, - @NonNull Duration timerDuration, int subId); + @NonNull Duration timerDuration, int subscriptionId); /** * Indicates that emergency callback mode has been stopped. @@ -1759,11 +1757,10 @@ public class TelephonyCallback { * @see TelephonyManager#STOP_REASON_TIMER_EXPIRED * @see TelephonyManager#STOP_REASON_USER_ACTION * - * @param subId is the current subscription used the emergency callback mode. + * @param subscriptionId is the current subscription used the emergency callback mode. */ - @RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE) void onCallbackModeStopped(@TelephonyManager.EmergencyCallbackModeType int type, - @TelephonyManager.EmergencyCallbackModeStopReason int reason, int subId); + @TelephonyManager.EmergencyCallbackModeStopReason int reason, int subscriptionId); } /** @@ -2192,7 +2189,7 @@ public class TelephonyCallback { @RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public void onCallbackModeStarted(@TelephonyManager.EmergencyCallbackModeType int type, - long durationMillis, int subId) { + long durationMillis, int subscriptionId) { if (!Flags.emergencyCallbackModeNotification()) return; EmergencyCallbackModeListener listener = @@ -2203,12 +2200,12 @@ public class TelephonyCallback { final Duration timerDuration = Duration.ofMillis(durationMillis); Binder.withCleanCallingIdentity( () -> mExecutor.execute(() -> listener.onCallbackModeStarted(type, - timerDuration, subId))); + timerDuration, subscriptionId))); } @RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public void onCallbackModeRestarted(@TelephonyManager.EmergencyCallbackModeType int type, - long durationMillis, int subId) { + long durationMillis, int subscriptionId) { if (!Flags.emergencyCallbackModeNotification()) return; EmergencyCallbackModeListener listener = @@ -2219,12 +2216,12 @@ public class TelephonyCallback { final Duration timerDuration = Duration.ofMillis(durationMillis); Binder.withCleanCallingIdentity( () -> mExecutor.execute(() -> listener.onCallbackModeRestarted(type, - timerDuration, subId))); + timerDuration, subscriptionId))); } @RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public void onCallbackModeStopped(@TelephonyManager.EmergencyCallbackModeType int type, - @TelephonyManager.EmergencyCallbackModeStopReason int reason, int subId) { + @TelephonyManager.EmergencyCallbackModeStopReason int reason, int subscriptionId) { if (!Flags.emergencyCallbackModeNotification()) return; EmergencyCallbackModeListener listener = @@ -2235,7 +2232,7 @@ public class TelephonyCallback { Binder.withCleanCallingIdentity( () -> mExecutor.execute(() -> listener.onCallbackModeStopped(type, reason, - subId))); + subscriptionId))); } public void onCarrierRoamingNtnModeChanged(boolean active) { diff --git a/core/java/android/telephony/TelephonyRegistryManager.java b/core/java/android/telephony/TelephonyRegistryManager.java index 4d50a450490e..1dab2cf75594 100644 --- a/core/java/android/telephony/TelephonyRegistryManager.java +++ b/core/java/android/telephony/TelephonyRegistryManager.java @@ -47,6 +47,8 @@ import android.telephony.emergency.EmergencyNumber; import android.telephony.ims.ImsCallSession; import android.telephony.ims.ImsReasonInfo; import android.telephony.ims.MediaQualityStatus; +import android.telephony.satellite.SatelliteStateChangeListener; +import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; @@ -55,12 +57,14 @@ import com.android.internal.listeners.ListenerExecutor; import com.android.internal.telephony.ICarrierConfigChangeListener; import com.android.internal.telephony.ICarrierPrivilegesCallback; import com.android.internal.telephony.IOnSubscriptionsChangedListener; +import com.android.internal.telephony.ISatelliteStateChangeListener; import com.android.internal.telephony.ITelephonyRegistry; import com.android.server.telecom.flags.Flags; import java.lang.ref.WeakReference; import java.util.Arrays; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.WeakHashMap; @@ -1482,6 +1486,111 @@ public class TelephonyRegistryManager { pkgName, attributionTag, callback, new int[0], notifyNow); } + @NonNull + @GuardedBy("sSatelliteStateChangeListeners") + private static final Map<SatelliteStateChangeListener, + WeakReference<SatelliteStateChangeListenerWrapper>> + sSatelliteStateChangeListeners = new ArrayMap<>(); + + /** + * Register a {@link SatelliteStateChangeListener} to receive notification when Satellite state + * has changed. + * + * @param executor The {@link Executor} where the {@code listener} will be invoked + * @param listener The listener to monitor the satellite state change + * @hide + */ + public void addSatelliteStateChangeListener(@NonNull @CallbackExecutor Executor executor, + @NonNull SatelliteStateChangeListener listener) { + if (listener == null || executor == null) { + throw new IllegalArgumentException("Listener and executor must be non-null"); + } + + synchronized (sSatelliteStateChangeListeners) { + WeakReference<SatelliteStateChangeListenerWrapper> existing = + sSatelliteStateChangeListeners.get(listener); + if (existing != null && existing.get() != null) { + Log.d(TAG, "addSatelliteStateChangeListener: listener already registered"); + return; + } + SatelliteStateChangeListenerWrapper wrapper = + new SatelliteStateChangeListenerWrapper(executor, listener); + try { + sRegistry.addSatelliteStateChangeListener( + wrapper, + mContext.getOpPackageName(), + mContext.getAttributionTag()); + sSatelliteStateChangeListeners.put(listener, new WeakReference<>(wrapper)); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } + + /** + * Unregister a {@link SatelliteStateChangeListener} to stop receiving notification when + * satellite state has changed. + * + * @param listener The listener previously registered with addSatelliteStateChangeListener. + * @hide + */ + public void removeSatelliteStateChangeListener(@NonNull SatelliteStateChangeListener listener) { + if (listener == null) { + throw new IllegalArgumentException("listener must be non-null"); + } + + synchronized (sSatelliteStateChangeListeners) { + WeakReference<SatelliteStateChangeListenerWrapper> ref = + sSatelliteStateChangeListeners.get(listener); + if (ref == null) return; + SatelliteStateChangeListenerWrapper wrapper = ref.get(); + if (wrapper == null) return; + try { + sRegistry.removeSatelliteStateChangeListener(wrapper, mContext.getOpPackageName()); + sSatelliteStateChangeListeners.remove(listener); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } + + /** + * Notify the registrants that the satellite state has changed. + * + * @param isEnabled True if the satellite modem is enabled, false otherwise + * @hide + */ + public void notifySatelliteStateChanged(boolean isEnabled) { + try { + sRegistry.notifySatelliteStateChanged(isEnabled); + } catch (RemoteException ex) { + // system process is dead + throw ex.rethrowFromSystemServer(); + } + } + + private static class SatelliteStateChangeListenerWrapper extends + ISatelliteStateChangeListener.Stub implements ListenerExecutor { + @NonNull private final WeakReference<SatelliteStateChangeListener> mListener; + @NonNull private final Executor mExecutor; + + SatelliteStateChangeListenerWrapper(@NonNull Executor executor, + @NonNull SatelliteStateChangeListener listener) { + mExecutor = executor; + mListener = new WeakReference<>(listener); + } + + @Override + public void onSatelliteEnabledStateChanged(boolean isEnabled) { + Binder.withCleanCallingIdentity( + () -> + executeSafely( + mExecutor, + mListener::get, + sscl -> sscl.onEnabledStateChanged(isEnabled))); + } + } + private static class CarrierPrivilegesCallbackWrapper extends ICarrierPrivilegesCallback.Stub implements ListenerExecutor { @NonNull private final WeakReference<CarrierPrivilegesCallback> mCallback; diff --git a/core/java/android/text/flags/flags.aconfig b/core/java/android/text/flags/flags.aconfig index 09b2201efb4e..e830d89d3116 100644 --- a/core/java/android/text/flags/flags.aconfig +++ b/core/java/android/text/flags/flags.aconfig @@ -195,3 +195,10 @@ flag { description: "Feature flag for adding a TYPE_DURATION to TtsSpan" bug: "337103893" } + +flag { + name: "deprecate_elegant_text_height_api" + namespace: "text" + description: "Deprecate the Paint#elegantTextHeight API and stick it to true" + bug: "349519475" +} diff --git a/core/java/android/tracing/TEST_MAPPING b/core/java/android/tracing/TEST_MAPPING index b51d19da97a5..545ba1190d2c 100644 --- a/core/java/android/tracing/TEST_MAPPING +++ b/core/java/android/tracing/TEST_MAPPING @@ -1,5 +1,5 @@ { - "postsubmit": [ + "presubmit": [ { "name": "TracingTests" }, diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java index e940e55bd38b..910e644f7b6f 100644 --- a/core/java/android/view/Display.java +++ b/core/java/android/view/Display.java @@ -22,6 +22,7 @@ import static android.hardware.flags.Flags.FLAG_OVERLAYPROPERTIES_CLASS_API; import static com.android.server.display.feature.flags.Flags.FLAG_HIGHEST_HDR_SDR_RATIO_API; import static com.android.server.display.feature.flags.Flags.FLAG_ENABLE_HAS_ARR_SUPPORT; +import static com.android.server.display.feature.flags.Flags.FLAG_ENABLE_GET_SUGGESTED_FRAME_RATE; import android.Manifest; import android.annotation.FlaggedApi; @@ -1278,6 +1279,60 @@ public final class Display { } } + @FlaggedApi(FLAG_ENABLE_GET_SUGGESTED_FRAME_RATE) + public static final int FRAME_RATE_CATEGORY_NORMAL = 0; + @FlaggedApi(FLAG_ENABLE_GET_SUGGESTED_FRAME_RATE) + public static final int FRAME_RATE_CATEGORY_HIGH = 1; + + /** + * @hide + */ + @IntDef(prefix = {"FRAME_RATE_CATEGORY_"}, + value = { + FRAME_RATE_CATEGORY_NORMAL, + FRAME_RATE_CATEGORY_HIGH + }) + @Retention(RetentionPolicy.SOURCE) + public @interface FrameRateCategory {} + + /** + * <p> Gets the display-defined frame rate given a descriptive frame rate category. </p> + * + * <p> For example, an animation that does not require fast render rates can use + * the {@link #FRAME_RATE_CATEGORY_NORMAL} to get the suggested frame rate. + * The suggested frame rate then can be used in the + * {@link Surface.FrameRateParams.Builder#setDesiredRateRange} for desiredMinRate. + * + * <pre>{@code + * float desiredMinRate = display.getSuggestedFrameRate(FRAME_RATE_CATEGORY_NORMAL); + * Surface.FrameRateParams params = new Surface.FrameRateParams.Builder(). + * setDesiredRateRange(desiredMinRate, Float.MAX).build(); + * surface.setFrameRate(params); + * }</pre> + * </p> + * + * @param category either {@link #FRAME_RATE_CATEGORY_NORMAL} + * or {@link #FRAME_RATE_CATEGORY_HIGH} + * + * @see Surface#setFrameRate(Surface.FrameRateParams) + * @see SurfaceControl.Transaction#setFrameRate(SurfaceControl, Surface.FrameRateParams) + * @throws IllegalArgumentException when category is not {@link #FRAME_RATE_CATEGORY_NORMAL} + * or {@link #FRAME_RATE_CATEGORY_HIGH} + */ + @FlaggedApi(FLAG_ENABLE_GET_SUGGESTED_FRAME_RATE) + public float getSuggestedFrameRate(@FrameRateCategory int category) { + synchronized (mLock) { + updateDisplayInfoLocked(); + if (category == FRAME_RATE_CATEGORY_HIGH) { + return mDisplayInfo.frameRateCategoryRate.getHigh(); + } else if (category == FRAME_RATE_CATEGORY_NORMAL) { + return mDisplayInfo.frameRateCategoryRate.getNormal(); + } else { + throw new IllegalArgumentException("Invalid FrameRateCategory provided"); + } + } + } + /** * <p> Returns true if the connected display can be switched into a mode with minimal * post processing. </p> diff --git a/core/java/android/view/DisplayInfo.java b/core/java/android/view/DisplayInfo.java index 26fce904eb5e..8f112f338a00 100644 --- a/core/java/android/view/DisplayInfo.java +++ b/core/java/android/view/DisplayInfo.java @@ -204,6 +204,12 @@ public final class DisplayInfo implements Parcelable { public boolean hasArrSupport; /** + * Represents frame rate for the FrameRateCategory Normal and High. + * @see android.view.Display#getSuggestedFrameRate(int) for more details. + */ + public FrameRateCategoryRate frameRateCategoryRate; + + /** * The default display mode. */ public int defaultModeId; @@ -443,6 +449,7 @@ public final class DisplayInfo implements Parcelable { && modeId == other.modeId && renderFrameRate == other.renderFrameRate && hasArrSupport == other.hasArrSupport + && Objects.equals(frameRateCategoryRate, other.frameRateCategoryRate) && defaultModeId == other.defaultModeId && userPreferredModeId == other.userPreferredModeId && Arrays.equals(supportedModes, other.supportedModes) @@ -505,6 +512,7 @@ public final class DisplayInfo implements Parcelable { modeId = other.modeId; renderFrameRate = other.renderFrameRate; hasArrSupport = other.hasArrSupport; + frameRateCategoryRate = other.frameRateCategoryRate; defaultModeId = other.defaultModeId; userPreferredModeId = other.userPreferredModeId; supportedModes = Arrays.copyOf(other.supportedModes, other.supportedModes.length); @@ -562,6 +570,8 @@ public final class DisplayInfo implements Parcelable { modeId = source.readInt(); renderFrameRate = source.readFloat(); hasArrSupport = source.readBoolean(); + frameRateCategoryRate = source.readParcelable(null, + android.view.FrameRateCategoryRate.class); defaultModeId = source.readInt(); userPreferredModeId = source.readInt(); int nModes = source.readInt(); @@ -636,6 +646,7 @@ public final class DisplayInfo implements Parcelable { dest.writeInt(modeId); dest.writeFloat(renderFrameRate); dest.writeBoolean(hasArrSupport); + dest.writeParcelable(frameRateCategoryRate, flags); dest.writeInt(defaultModeId); dest.writeInt(userPreferredModeId); dest.writeInt(supportedModes.length); @@ -883,6 +894,8 @@ public final class DisplayInfo implements Parcelable { sb.append(renderFrameRate); sb.append(", hasArrSupport "); sb.append(hasArrSupport); + sb.append(", frameRateCategoryRate "); + sb.append(frameRateCategoryRate); sb.append(", defaultMode "); sb.append(defaultModeId); sb.append(", userPreferredModeId "); diff --git a/core/java/android/view/FrameRateCategoryRate.java b/core/java/android/view/FrameRateCategoryRate.java new file mode 100644 index 000000000000..3c674b8966d1 --- /dev/null +++ b/core/java/android/view/FrameRateCategoryRate.java @@ -0,0 +1,113 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * A class to create and manage FrameRateCategoryRate for + * categories Normal and High. + * @hide + */ +public class FrameRateCategoryRate implements Parcelable { + + private final float mNormal; + private final float mHigh; + + /** + * Creates a FrameRateCategoryRate with the provided rates + * for the categories Normal and High respectively; + * + * @param normal rate for the category Normal. + * @param high rate for the category High. + * @hide + */ + public FrameRateCategoryRate(float normal, float high) { + this.mNormal = normal; + this.mHigh = high; + } + + /** + * @return the value for the category normal; + * @hide + */ + public float getNormal() { + return mNormal; + } + + /** + * @return the value for the category high; + * @hide + */ + public float getHigh() { + return mHigh; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (!(o instanceof FrameRateCategoryRate)) { + return false; + } + FrameRateCategoryRate that = (FrameRateCategoryRate) o; + return mNormal == that.mNormal && mHigh == that.mHigh; + + } + + @Override + public int hashCode() { + int result = 1; + result = 31 * result + Float.hashCode(mNormal); + result = 31 * result + Float.hashCode(mHigh); + return result; + } + + @Override + public String toString() { + return "FrameRateCategoryRate {" + + "normal=" + mNormal + + ", high=" + mHigh + + '}'; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeFloat(mNormal); + dest.writeFloat(mHigh); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator<FrameRateCategoryRate> CREATOR = + new Creator<>() { + @Override + public FrameRateCategoryRate createFromParcel(Parcel in) { + return new FrameRateCategoryRate(in.readFloat(), in.readFloat()); + } + + @Override + public FrameRateCategoryRate[] newArray(int size) { + return new FrameRateCategoryRate[size]; + } + }; +} diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java index ece2a6068934..b8b22e283175 100644 --- a/core/java/android/view/SurfaceControl.java +++ b/core/java/android/view/SurfaceControl.java @@ -87,6 +87,7 @@ import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; import java.util.Arrays; +import java.util.List; import java.util.Objects; import java.util.concurrent.Executor; import java.util.function.Consumer; @@ -463,7 +464,7 @@ public final class SurfaceControl implements Parcelable { /** * Called when new jank classifications are available. */ - void onJankDataAvailable(JankData[] jankData); + void onJankDataAvailable(@NonNull List<JankData> jankData); } @@ -1798,6 +1799,7 @@ public final class SurfaceControl implements Parcelable { public int activeDisplayModeId; public float renderFrameRate; public boolean hasArrSupport; + public FrameRateCategoryRate frameRateCategoryRate; public int[] supportedColorModes; public int activeColorMode; @@ -1816,6 +1818,7 @@ public final class SurfaceControl implements Parcelable { + ", activeDisplayModeId=" + activeDisplayModeId + ", renderFrameRate=" + renderFrameRate + ", hasArrSupport=" + hasArrSupport + + ", frameRateCategoryRate=" + frameRateCategoryRate + ", supportedColorModes=" + Arrays.toString(supportedColorModes) + ", activeColorMode=" + activeColorMode + ", hdrCapabilities=" + hdrCapabilities @@ -1836,13 +1839,15 @@ public final class SurfaceControl implements Parcelable { && activeColorMode == that.activeColorMode && Objects.equals(hdrCapabilities, that.hdrCapabilities) && preferredBootDisplayMode == that.preferredBootDisplayMode - && hasArrSupport == that.hasArrSupport; + && hasArrSupport == that.hasArrSupport + && Objects.equals(frameRateCategoryRate, that.frameRateCategoryRate); } @Override public int hashCode() { return Objects.hash(Arrays.hashCode(supportedDisplayModes), activeDisplayModeId, - renderFrameRate, activeColorMode, hdrCapabilities, hasArrSupport); + renderFrameRate, activeColorMode, hdrCapabilities, hasArrSupport, + frameRateCategoryRate); } } @@ -2682,7 +2687,9 @@ public final class SurfaceControl implements Parcelable { * Adds a callback to be informed about SF's jank classification for this surface. * @hide */ - public OnJankDataListenerRegistration addJankDataListener(OnJankDataListener listener) { + @NonNull + public OnJankDataListenerRegistration addOnJankDataListener( + @NonNull OnJankDataListener listener) { return new OnJankDataListenerRegistration(this, listener); } @@ -3455,15 +3462,15 @@ public final class SurfaceControl implements Parcelable { * @return this This transaction for chaining * @hide */ - public @NonNull Transaction setCrop(@NonNull SurfaceControl sc, float top, float left, - float bottom, float right) { + public @NonNull Transaction setCrop(@NonNull SurfaceControl sc, float left, float top, + float right, float bottom) { checkPreconditions(sc); if (SurfaceControlRegistry.sCallStackDebuggingEnabled) { SurfaceControlRegistry.getProcessInstance().checkCallStackDebugging( "setCrop", this, sc, "crop={" + top + ", " + left + ", " + bottom + ", " + right + "}"); } - nativeSetCrop(mNativeObject, sc.mNativeObject, top, left, bottom, right); + nativeSetCrop(mNativeObject, sc.mNativeObject, left, top, right, bottom); return this; } diff --git a/core/java/android/view/SurfaceView.java b/core/java/android/view/SurfaceView.java index 9cad3e58fa02..4df7649e1c9b 100644 --- a/core/java/android/view/SurfaceView.java +++ b/core/java/android/view/SurfaceView.java @@ -40,6 +40,7 @@ import android.graphics.Matrix; import android.graphics.Paint; import android.graphics.PixelFormat; import android.graphics.Rect; +import android.graphics.RectF; import android.graphics.Region; import android.graphics.RenderNode; import android.hardware.input.InputManager; @@ -1666,7 +1667,7 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall } private final Rect mRTLastReportedPosition = new Rect(); - private final Rect mRTLastSetCrop = new Rect(); + private final RectF mRTLastSetCrop = new RectF(); private class SurfaceViewPositionUpdateListener implements RenderNode.PositionUpdateListener { private final int mRtSurfaceWidth; @@ -1711,16 +1712,18 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall @Override public void positionChanged(long frameNumber, int left, int top, int right, int bottom, - int clipLeft, int clipTop, int clipRight, int clipBottom) { + int clipLeft, int clipTop, int clipRight, int clipBottom, + int nodeWidth, int nodeHeight) { try { if (DEBUG_POSITION) { Log.d(TAG, String.format( "%d updateSurfacePosition RenderWorker, frameNr = %d, " + "position = [%d, %d, %d, %d] clip = [%d, %d, %d, %d] " - + "surfaceSize = %dx%d", + + "surfaceSize = %dx%d renderNodeSize = %d%d", System.identityHashCode(SurfaceView.this), frameNumber, left, top, right, bottom, clipLeft, clipTop, clipRight, clipBottom, - mRtSurfaceWidth, mRtSurfaceHeight)); + mRtSurfaceWidth, mRtSurfaceHeight, + nodeWidth, nodeHeight)); } synchronized (mSurfaceControlLock) { if (mSurfaceControl == null) return; @@ -1735,14 +1738,29 @@ public class SurfaceView extends View implements ViewRootImpl.SurfaceChangedCall mRTLastReportedPosition.top /*positionTop*/, postScaleX, postScaleY); - mRTLastSetCrop.set(clipLeft, clipTop, clipRight, clipBottom); + // The computed crop is in view-relative dimensions, however we need it to be + // in buffer-relative dimensions. So scale the crop by the ratio between + // the view's unscaled width/height (nodeWidth/Height), and the surface's + // width/height + // That is, if the Surface has a fixed size of 50x50, the SurfaceView is laid + // out to a size of 100x100, and the SurfaceView is ultimately scaled to + // 1000x1000, then we need to scale the crop by just the 2x from surface + // domain to SV domain. + final float surfaceToNodeScaleX = (float) mRtSurfaceWidth / (float) nodeWidth; + final float surfaceToNodeScaleY = (float) mRtSurfaceHeight / (float) nodeHeight; + mRTLastSetCrop.set(clipLeft * surfaceToNodeScaleX, + clipTop * surfaceToNodeScaleY, + clipRight * surfaceToNodeScaleX, + clipBottom * surfaceToNodeScaleY); + if (DEBUG_POSITION) { - Log.d(TAG, String.format("Setting layer crop = [%d, %d, %d, %d] " + Log.d(TAG, String.format("Setting layer crop = [%f, %f, %f, %f] " + "from scale %f, %f", mRTLastSetCrop.left, mRTLastSetCrop.top, mRTLastSetCrop.right, mRTLastSetCrop.bottom, - postScaleX, postScaleY)); + surfaceToNodeScaleX, surfaceToNodeScaleY)); } - mPositionChangedTransaction.setCrop(mSurfaceControl, mRTLastSetCrop); + mPositionChangedTransaction.setCrop(mSurfaceControl, mRTLastSetCrop.left, + mRTLastSetCrop.top, mRTLastSetCrop.right, mRTLastSetCrop.bottom); if (mRTLastSetCrop.isEmpty()) { mPositionChangedTransaction.hide(mSurfaceControl); } else { diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index d5fc26281fc6..e49eec69fff5 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -30,7 +30,9 @@ import static android.view.Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE; import static android.view.Surface.FRAME_RATE_COMPATIBILITY_GTE; import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD; import static android.view.accessibility.AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED; +import static android.view.accessibility.Flags.FLAG_SUPPLEMENTAL_DESCRIPTION; import static android.view.accessibility.Flags.removeChildHoverCheckForTouchExploration; +import static android.view.accessibility.Flags.supplementalDescription; import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_INVALID_BOUNDS; import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_MISSING_WINDOW; import static android.view.displayhash.DisplayHashResultCallback.DISPLAY_HASH_ERROR_NOT_VISIBLE_ON_SCREEN; @@ -40,7 +42,6 @@ import static android.view.displayhash.DisplayHashResultCallback.EXTRA_DISPLAY_H import static android.view.flags.Flags.FLAG_SENSITIVE_CONTENT_APP_PROTECTION_API; import static android.view.flags.Flags.FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY; import static android.view.flags.Flags.FLAG_VIEW_VELOCITY_API; -import static android.view.flags.Flags.calculateBoundsInParentFromBoundsInScreen; import static android.view.flags.Flags.enableUseMeasureCacheDuringForceLayout; import static android.view.flags.Flags.sensitiveContentAppProtection; import static android.view.flags.Flags.toolkitFrameRateAnimationBugfix25q1; @@ -51,6 +52,7 @@ import static android.view.flags.Flags.toolkitFrameRateVelocityMappingReadOnly; import static android.view.flags.Flags.toolkitFrameRateViewEnablingReadOnly; import static android.view.flags.Flags.toolkitMetricsForFrameRateDecision; import static android.view.flags.Flags.toolkitSetFrameRateReadOnly; +import static android.view.flags.Flags.toolkitViewgroupSetRequestedFrameRateApi; import static android.view.flags.Flags.viewVelocityApi; import static android.view.inputmethod.Flags.FLAG_HOME_SCREEN_HANDWRITING_DELEGATOR; import static android.view.inputmethod.Flags.initiationWithoutInputConnection; @@ -968,13 +970,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, private static boolean sAlwaysRemeasureExactly = false; /** - * When true calculates the bounds in parent from bounds in screen relative to its parents. - * This addresses the deprecated API (setBoundsInParent) in Compose, which causes empty - * getBoundsInParent call for Compose apps. - */ - private static boolean sCalculateBoundsInParentFromBoundsInScreenFlagValue = false; - - /** * When true makes it possible to use onMeasure caches also when the force layout flag is * enabled. This helps avoiding multiple measures in the same frame with the same dimensions. */ @@ -2475,6 +2470,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, toolkitFrameRateVelocityMappingReadOnly(); private static boolean sToolkitFrameRateAnimationBugfix25q1FlagValue = toolkitFrameRateAnimationBugfix25q1(); + private static boolean sToolkitViewGroupFrameRateApiFlagValue = + toolkitViewgroupSetRequestedFrameRateApi(); // Used to set frame rate compatibility. @Surface.FrameRateCompatibility int mFrameRateCompatibility = @@ -2564,8 +2561,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, sToolkitSetFrameRateReadOnlyFlagValue = toolkitSetFrameRateReadOnly(); sToolkitMetricsForFrameRateDecisionFlagValue = toolkitMetricsForFrameRateDecision(); - sCalculateBoundsInParentFromBoundsInScreenFlagValue = - calculateBoundsInParentFromBoundsInScreen(); sUseMeasureCacheDuringForceLayoutFlagValue = enableUseMeasureCacheDuringForceLayout(); } @@ -3815,6 +3810,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * 1 PFLAG4_HAS_DRAWN * 1 PFLAG4_HAS_MOVED * 1 PFLAG4_HAS_VIEW_PROPERTY_INVALIDATION + * 1 PFLAG4_FORCED_OVERRIDE_FRAME_RATE + * 1 PFLAG4_SELF_REQUESTED_FRAME_RATE * |-------|-------|-------|-------| */ @@ -3965,6 +3962,17 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ private static final int PFLAG4_HAS_VIEW_PROPERTY_INVALIDATION = 0x20000000; + /** + * When set, this indicates whether the frame rate of the children should be + * forcibly overridden, even if it has been explicitly configured by a user request. + */ + private static final int PFLAG4_FORCED_OVERRIDE_FRAME_RATE = 0x40000000; + + /** + * When set, this indicates that the frame rate is configured based on a user request. + */ + private static final int PFLAG4_SELF_REQUESTED_FRAME_RATE = 0x80000000; + /* End of masks for mPrivateFlags4 */ /** @hide */ @@ -4874,6 +4882,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, private CharSequence mContentDescription; /** + * Brief supplemental information for view and is primarily used for accessibility support. + */ + private CharSequence mSupplementalDescription; + + /** * If this view represents a distinct part of the window, it can have a title that labels the * area. */ @@ -6544,6 +6557,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback, case R.styleable.View_contentSensitivity: setContentSensitivity(a.getInt(attr, CONTENT_SENSITIVITY_AUTO)); break; + default: { + if (supplementalDescription()) { + if (attr == com.android.internal.R.styleable.View_supplementalDescription) { + setSupplementalDescription(a.getString(attr)); + } + } + } } } @@ -9786,7 +9806,7 @@ public class View implements Drawable.Callback, KeyEvent.Callback, structure.setChildCount(1); final ViewStructure root = structure.newChild(0); if (info != null) { - populateVirtualStructure(root, provider, info, null, forAutofill); + populateVirtualStructure(root, provider, info, forAutofill); info.recycle(); } else { Log.w(AUTOFILL_LOG_TAG, "AccessibilityNodeInfo is null."); @@ -11085,19 +11105,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback, private void populateVirtualStructure(ViewStructure structure, AccessibilityNodeProvider provider, AccessibilityNodeInfo info, - @Nullable AccessibilityNodeInfo parentInfo, boolean forAutofill) { + boolean forAutofill) { structure.setId(AccessibilityNodeInfo.getVirtualDescendantId(info.getSourceNodeId()), null, null, info.getViewIdResourceName()); Rect rect = structure.getTempRect(); - // The bounds in parent for Jetpack Compose views aren't set as setBoundsInParent is - // deprecated, and only setBoundsInScreen is called. - // The bounds in parent can be calculated by diff'ing the child view's bounds in screen with - // the parent's. - if (sCalculateBoundsInParentFromBoundsInScreenFlagValue) { - getBoundsInParent(info, parentInfo, rect); - } else { - info.getBoundsInParent(rect); - } + info.getBoundsInParent(rect); structure.setDimens(rect.left, rect.top, 0, 0, rect.width(), rect.height()); structure.setVisibility(VISIBLE); structure.setEnabled(info.isEnabled()); @@ -11181,32 +11193,13 @@ public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityNodeInfo.getVirtualDescendantId(info.getChildId(i))); if (cinfo != null) { ViewStructure child = structure.newChild(i); - populateVirtualStructure(child, provider, cinfo, info, forAutofill); + populateVirtualStructure(child, provider, cinfo, forAutofill); cinfo.recycle(); } } } } - private void getBoundsInParent(@NonNull AccessibilityNodeInfo info, - @Nullable AccessibilityNodeInfo parentInfo, @NonNull Rect rect) { - info.getBoundsInParent(rect); - // Fallback to calculate bounds in parent by diffing the bounds in - // screen if it's all 0. - if ((rect.left | rect.top | rect.right | rect.bottom) == 0) { - if (parentInfo != null) { - Rect parentBoundsInScreen = parentInfo.getBoundsInScreen(); - Rect boundsInScreen = info.getBoundsInScreen(); - rect.set(boundsInScreen.left - parentBoundsInScreen.left, - boundsInScreen.top - parentBoundsInScreen.top, - boundsInScreen.right - parentBoundsInScreen.left, - boundsInScreen.bottom - parentBoundsInScreen.top); - } else { - info.getBoundsInScreen(rect); - } - } - } - /** * Dispatch creation of {@link ViewStructure} down the hierarchy. The default * implementation calls {@link #onProvideStructure} and @@ -11843,6 +11836,39 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** + * Returns the {@link View}'s supplemental description. + * <p> + * A supplemental description provides + * brief supplemental information for this node, such as the purpose of the node when + * that purpose is not conveyed within its textual representation. For example, if a + * dropdown select has a purpose of setting font family, the supplemental description + * could be "font family". If this node has children, its supplemental description serves + * as additional information and is not intended to replace any existing information + * in the subtree. This is different from the {@link #getContentDescription()} in that + * this description is purely supplemental while a content description may be used + * to replace a description for a node or its subtree that an assistive technology + * would otherwise compute based on other properties of the node and its descendants. + * + * <p> + * <strong>Note:</strong> Do not override this method, as it will have no + * effect on the supplemental description presented to accessibility services. + * You must call {@link #setSupplementalDescription(CharSequence)} to modify the + * supplemental description. + * + * @return the supplemental description + * @see #setSupplementalDescription(CharSequence) + * @see #getContentDescription() + * @attr ref android.R.styleable#View_supplementalDescription + */ + @FlaggedApi(FLAG_SUPPLEMENTAL_DESCRIPTION) + @ViewDebug.ExportedProperty(category = "accessibility") + @InspectableProperty + @Nullable + public CharSequence getSupplementalDescription() { + return mSupplementalDescription; + } + + /** * Sets the {@link View}'s state description. * <p> * A state description briefly describes the states of the view and is primarily used @@ -11929,6 +11955,53 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** + * Sets the {@link View}'s supplemental description. + * <p> + * A supplemental description provides + * brief supplemental information for this node, such as the purpose of the node when + * that purpose is not conveyed within its textual representation. For example, if a + * dropdown select has a purpose of setting font family, the supplemental description + * could be "font family". If this node has children, its supplemental description serves + * as additional information and is not intended to replace any existing information + * in the subtree. This is different from the {@link #setContentDescription(CharSequence)} + * in that this description is purely supplemental while a content description may be used + * to replace a description for a node or its subtree that an assistive technology + * would otherwise compute based on other properties of the node and its descendants. + * + * <p> + * This should omit role or state. Role refers to the kind of user-interface element the View + * is, such as a Button or Checkbox. State refers to a frequently changing property of the View, + * such as an On/Off state of a button or the audio level of a volume slider. + * + * @param supplementalDescription The supplemental description. + * @see #getSupplementalDescription() + * @see #setContentDescription(CharSequence) + * @see #setStateDescription(CharSequence) for state changes. + * @attr ref android.R.styleable#View_supplementalDescription + */ + @FlaggedApi(FLAG_SUPPLEMENTAL_DESCRIPTION) + @RemotableViewMethod + public void setSupplementalDescription(@Nullable CharSequence supplementalDescription) { + if (mSupplementalDescription == null) { + if (supplementalDescription == null) { + return; + } + } else if (mSupplementalDescription.equals(supplementalDescription)) { + return; + } + mSupplementalDescription = supplementalDescription; + final boolean nonEmptyDesc = supplementalDescription != null + && !supplementalDescription.isEmpty(); + if (nonEmptyDesc && getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) { + setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); + notifySubtreeAccessibilityStateChangedIfNeeded(); + } else { + notifyViewAccessibilityStateChangedIfNeeded( + AccessibilityEvent.CONTENT_CHANGE_TYPE_SUPPLEMENTAL_DESCRIPTION); + } + } + + /** * Sets the id of a view that screen readers are requested to visit after this view. * * <p> @@ -34235,9 +34308,20 @@ public class View implements Drawable.Callback, KeyEvent.Callback, */ @FlaggedApi(FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY) public void setRequestedFrameRate(float frameRate) { + // Skip setting the frame rate if it's currently in forced override mode. + if (sToolkitViewGroupFrameRateApiFlagValue && getForcedOverrideFrameRateFlag()) { + return; + } + if (sToolkitSetFrameRateReadOnlyFlagValue) { mPreferredFrameRate = frameRate; } + + if (sToolkitViewGroupFrameRateApiFlagValue) { + // If frameRate is Float.NaN, it means it's set to the default value. + // We only want to make the flag true, when the value is not Float.nan + setSelfRequestedFrameRateFlag(!Float.isNaN(mPreferredFrameRate)); + } } /** @@ -34261,4 +34345,35 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } return 0; } + + void overrideFrameRate(float frameRate, boolean forceOverride) { + setForcedOverrideFrameRateFlag(forceOverride); + if (forceOverride || !getSelfRequestedFrameRateFlag()) { + mPreferredFrameRate = frameRate; + } + } + + void setForcedOverrideFrameRateFlag(boolean forcedOverride) { + if (forcedOverride) { + mPrivateFlags4 |= PFLAG4_FORCED_OVERRIDE_FRAME_RATE; + } else { + mPrivateFlags4 &= ~PFLAG4_FORCED_OVERRIDE_FRAME_RATE; + } + } + + boolean getForcedOverrideFrameRateFlag() { + return (mPrivateFlags4 & PFLAG4_FORCED_OVERRIDE_FRAME_RATE) != 0; + } + + void setSelfRequestedFrameRateFlag(boolean forcedOverride) { + if (forcedOverride) { + mPrivateFlags4 |= PFLAG4_SELF_REQUESTED_FRAME_RATE; + } else { + mPrivateFlags4 &= ~PFLAG4_SELF_REQUESTED_FRAME_RATE; + } + } + + boolean getSelfRequestedFrameRateFlag() { + return (mPrivateFlags4 & PFLAG4_SELF_REQUESTED_FRAME_RATE) != 0; + } } diff --git a/core/java/android/view/ViewGroup.java b/core/java/android/view/ViewGroup.java index 22374173b988..4a9916c007c4 100644 --- a/core/java/android/view/ViewGroup.java +++ b/core/java/android/view/ViewGroup.java @@ -18,9 +18,12 @@ package android.view; import static android.view.WindowInsetsAnimation.Callback.DISPATCH_MODE_CONTINUE_ON_SUBTREE; import static android.view.WindowInsetsAnimation.Callback.DISPATCH_MODE_STOP; +import static android.view.flags.Flags.FLAG_TOOLKIT_VIEWGROUP_SET_REQUESTED_FRAME_RATE_API; +import static android.view.flags.Flags.toolkitViewgroupSetRequestedFrameRateApi; import android.animation.LayoutTransition; import android.annotation.CallSuper; +import android.annotation.FlaggedApi; import android.annotation.IdRes; import android.annotation.NonNull; import android.annotation.Nullable; @@ -448,6 +451,14 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager private static final int FLAG_SHOW_CONTEXT_MENU_WITH_COORDS = 0x20000000; /** + * When set, this indicates that the frame rate is passed down from the parent. + */ + private static final int FLAG_PROPAGATED_FRAME_RATE = 0x40000000; + + private static boolean sToolkitViewGroupFrameRateApiFlagValue = + toolkitViewgroupSetRequestedFrameRateApi(); + + /** * Indicates which types of drawing caches are to be kept in memory. * This field should be made private, so it is hidden from the SDK. * {@hide} @@ -5361,6 +5372,12 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } touchAccessibilityNodeProviderIfNeeded(child); + + // If a propagated value exists, pass it to the child. + if (sToolkitViewGroupFrameRateApiFlagValue && !Float.isNaN(getRequestedFrameRate()) + && (mGroupFlags & FLAG_PROPAGATED_FRAME_RATE) != 0) { + child.overrideFrameRate(getRequestedFrameRate(), getForcedOverrideFrameRateFlag()); + } } /** @@ -9465,4 +9482,75 @@ public abstract class ViewGroup extends View implements ViewParent, ViewManager } return null; } + + /** + * You can set the preferred frame rate for a ViewGroup using a positive number + * or by specifying the preferred frame rate category using constants, including + * REQUESTED_FRAME_RATE_CATEGORY_NO_PREFERENCE, REQUESTED_FRAME_RATE_CATEGORY_LOW, + * REQUESTED_FRAME_RATE_CATEGORY_NORMAL, REQUESTED_FRAME_RATE_CATEGORY_HIGH. + * Keep in mind that the preferred frame rate affects the frame rate for the next frame, + * so use this method carefully. It's important to note that the preference is valid as + * long as the ViewGroup is invalidated. Please also be aware that the requested frame rate + * will not propagate to child views. + * + * @param frameRate the preferred frame rate of the ViewGroup. + */ + @Override + @FlaggedApi(FLAG_TOOLKIT_VIEWGROUP_SET_REQUESTED_FRAME_RATE_API) + public void setRequestedFrameRate(float frameRate) { + if (sToolkitViewGroupFrameRateApiFlagValue) { + if (getForcedOverrideFrameRateFlag()) { + return; + } + super.setRequestedFrameRate(frameRate); + // If frameRate is Float.NaN, it means it's set to the default value. + // We only want to make the flag true, when the value is not Float.nan + setSelfRequestedFrameRateFlag(!Float.isNaN(getRequestedFrameRate())); + mGroupFlags &= ~FLAG_PROPAGATED_FRAME_RATE; + } + } + + /** + * You can set the preferred frame rate for a ViewGroup and its children using a positive number + * or by specifying the preferred frame rate category using constants, including + * REQUESTED_FRAME_RATE_CATEGORY_NO_PREFERENCE, REQUESTED_FRAME_RATE_CATEGORY_LOW, + * REQUESTED_FRAME_RATE_CATEGORY_NORMAL, REQUESTED_FRAME_RATE_CATEGORY_HIGH. + * Keep in mind that the preferred frame rate affects the frame rate for the next frame, + * so use this method carefully. It's important to note that the preference is valid as + * long as the ViewGroup or any of its children is invalidated. + * To undo the frame rate propagation, call the API with REQUESTED_FRAME_RATE_CATEGORY_DEFAULT. + * + * @param frameRate the preferred frame rate of the ViewGroup. + * @param forceOverride indicate whether it should override the frame rate of + * all the children with the given frame rate. + */ + @FlaggedApi(FLAG_TOOLKIT_VIEWGROUP_SET_REQUESTED_FRAME_RATE_API) + public void propagateRequestedFrameRate(float frameRate, boolean forceOverride) { + if (sToolkitViewGroupFrameRateApiFlagValue) { + // Skip setting the frame rate if it's currently in forced override mode. + if (getForcedOverrideFrameRateFlag()) { + return; + } + + // frame rate could be set previously with setRequestedFrameRate + // or propagateRequestedFrameRate + setSelfRequestedFrameRateFlag(false); + overrideFrameRate(frameRate, forceOverride); + setSelfRequestedFrameRateFlag(true); + } + } + + @Override + void overrideFrameRate(float frameRate, boolean forceOverride) { + // if it's in forceOverrid mode or has no self requested frame rate, + // it will override the frame rate. + if (forceOverride || !getSelfRequestedFrameRateFlag()) { + super.overrideFrameRate(frameRate, forceOverride); + mGroupFlags |= FLAG_PROPAGATED_FRAME_RATE; + + for (int i = 0; i < getChildCount(); i++) { + getChildAt(i).overrideFrameRate(frameRate, forceOverride); + } + } + } } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index d57a88075f8a..3ce6870bf2ca 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -1132,6 +1132,8 @@ public final class ViewRootImpl implements ViewParent, // time for evaluating the interval between current time and // the time when frame rate was set previously. private static final int FRAME_RATE_SETTING_REEVALUATE_TIME = 100; + // timeout for surface replaced. + private static final int FRAME_RATE_SURFACE_REPLACED_TIME = 3000; /* * The variables below are used to update frame rate category @@ -3831,6 +3833,9 @@ public final class ViewRootImpl implements ViewParent, if (surfaceReplaced) { mSurfaceReplaced = true; mSurfaceSequenceId++; + mHandler.removeMessages(MSG_SURFACE_REPLACED_TIMEOUT); + mHandler.sendEmptyMessageDelayed(MSG_SURFACE_REPLACED_TIMEOUT, + FRAME_RATE_SURFACE_REPLACED_TIME); } if (alwaysConsumeSystemBarsChanged) { mAttachInfo.mAlwaysConsumeSystemBars = mPendingAlwaysConsumeSystemBars; @@ -6544,7 +6549,7 @@ public final class ViewRootImpl implements ViewParent, } Configuration globalConfig = mergedConfiguration.getGlobalConfiguration(); - final Configuration overrideConfig = mergedConfiguration.getOverrideConfiguration(); + Configuration overrideConfig = mergedConfiguration.getOverrideConfiguration(); if (DEBUG_CONFIGURATION) Log.v(mTag, "Applying new config to window " + mWindowAttributes.getTitle() + ", globalConfig: " + globalConfig @@ -6553,7 +6558,9 @@ public final class ViewRootImpl implements ViewParent, final CompatibilityInfo ci = mDisplay.getDisplayAdjustments().getCompatibilityInfo(); if (!ci.equals(CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO)) { globalConfig = new Configuration(globalConfig); + overrideConfig = new Configuration(overrideConfig); ci.applyToConfiguration(mNoncompatDensity, globalConfig); + ci.applyToConfiguration(mNoncompatDensity, overrideConfig); } synchronized (sConfigCallbacks) { @@ -6694,6 +6701,7 @@ public final class ViewRootImpl implements ViewParent, private static final int MSG_CHECK_INVALIDATION_IDLE = 40; private static final int MSG_REFRESH_POINTER_ICON = 41; private static final int MSG_FRAME_RATE_SETTING = 42; + private static final int MSG_SURFACE_REPLACED_TIMEOUT = 43; final class ViewRootHandler extends Handler { @Override @@ -6765,6 +6773,8 @@ public final class ViewRootImpl implements ViewParent, return "MSG_TOUCH_BOOST_TIMEOUT"; case MSG_FRAME_RATE_SETTING: return "MSG_FRAME_RATE_SETTING"; + case MSG_SURFACE_REPLACED_TIMEOUT: + return "MSG_SURFACE_REPLACED_TIMEOUT"; } return super.getMessageName(message); } @@ -7036,6 +7046,9 @@ public final class ViewRootImpl implements ViewParent, */ mIsFrameRateBoosting = false; mIsTouchBoosting = false; + if (!mDrawnThisFrame) { + setPreferredFrameRateCategory(FRAME_RATE_CATEGORY_NO_PREFERENCE); + } break; case MSG_REFRESH_POINTER_ICON: if (mPointerIconEvent == null) { @@ -7047,6 +7060,9 @@ public final class ViewRootImpl implements ViewParent, mPreferredFrameRate = 0; mFrameRateCompatibility = FRAME_RATE_COMPATIBILITY_FIXED_SOURCE; break; + case MSG_SURFACE_REPLACED_TIMEOUT: + mSurfaceReplaced = false; + break; } } } @@ -9323,7 +9339,7 @@ public final class ViewRootImpl implements ViewParent, boolean insetsPending) throws RemoteException { final WindowConfiguration winConfigFromAm = getConfiguration().windowConfiguration; final WindowConfiguration winConfigFromWm = - mLastReportedMergedConfiguration.getGlobalConfiguration().windowConfiguration; + mLastReportedMergedConfiguration.getMergedConfiguration().windowConfiguration; final WindowConfiguration winConfig = getCompatWindowConfiguration(); final int measuredWidth = mMeasuredWidth; final int measuredHeight = mMeasuredHeight; @@ -13390,6 +13406,7 @@ public final class ViewRootImpl implements ViewParent, private void removeVrrMessages() { mHandler.removeMessages(MSG_TOUCH_BOOST_TIMEOUT); mHandler.removeMessages(MSG_FRAME_RATE_SETTING); + mHandler.removeMessages(MSG_SURFACE_REPLACED_TIMEOUT); if (mInvalidationIdleMessagePosted && sSurfaceFlingerBugfixFlagValue) { mInvalidationIdleMessagePosted = false; mHandler.removeMessages(MSG_CHECK_INVALIDATION_IDLE); diff --git a/core/java/android/view/accessibility/AccessibilityEvent.java b/core/java/android/view/accessibility/AccessibilityEvent.java index 4e9d054dae07..c690787e4a33 100644 --- a/core/java/android/view/accessibility/AccessibilityEvent.java +++ b/core/java/android/view/accessibility/AccessibilityEvent.java @@ -810,6 +810,20 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par @FlaggedApi(Flags.FLAG_A11Y_EXPANSION_STATE_API) public static final int CONTENT_CHANGE_TYPE_EXPANDED = 1 << 14; + /** + * Change type for {@link #TYPE_WINDOW_CONTENT_CHANGED} event: + * The source node changed its supplemental description, which is returned by + * {@link AccessibilityNodeInfo#getSupplementalDescription()}. + * The view changing its supplemental description should call + * {@link AccessibilityNodeInfo#setSupplementalDescription(CharSequence)} and + * then send this event. + * + * @see AccessibilityNodeInfo#getSupplementalDescription() + * @see AccessibilityNodeInfo#setSupplementalDescription(CharSequence) + */ + @FlaggedApi(Flags.FLAG_SUPPLEMENTAL_DESCRIPTION) + public static final int CONTENT_CHANGE_TYPE_SUPPLEMENTAL_DESCRIPTION = 1 << 15; + // Speech state change types. /** Change type for {@link #TYPE_SPEECH_STATE_CHANGE} event: another service is speaking. */ @@ -942,6 +956,7 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par CONTENT_CHANGE_TYPE_CONTENT_INVALID, CONTENT_CHANGE_TYPE_ERROR, CONTENT_CHANGE_TYPE_ENABLED, + CONTENT_CHANGE_TYPE_SUPPLEMENTAL_DESCRIPTION, }) public @interface ContentChangeTypes {} @@ -1222,7 +1237,14 @@ public final class AccessibilityEvent extends AccessibilityRecord implements Par return "CONTENT_CHANGE_TYPE_CONTENT_INVALID"; case CONTENT_CHANGE_TYPE_ERROR: return "CONTENT_CHANGE_TYPE_ERROR"; case CONTENT_CHANGE_TYPE_ENABLED: return "CONTENT_CHANGE_TYPE_ENABLED"; - default: return Integer.toHexString(type); + default: { + if (Flags.supplementalDescription()) { + if (type == CONTENT_CHANGE_TYPE_SUPPLEMENTAL_DESCRIPTION) { + return "CONTENT_CHANGE_TYPE_SUPPLEMENTAL_DESCRIPTION"; + } + } + return Integer.toHexString(type); + } } } diff --git a/core/java/android/view/accessibility/AccessibilityNodeInfo.java b/core/java/android/view/accessibility/AccessibilityNodeInfo.java index f70f7e6a59c0..5e5f33ef41f8 100644 --- a/core/java/android/view/accessibility/AccessibilityNodeInfo.java +++ b/core/java/android/view/accessibility/AccessibilityNodeInfo.java @@ -997,6 +997,8 @@ public class AccessibilityNodeInfo implements Parcelable { private static final int BOOLEAN_PROPERTY_SUPPORTS_GRANULAR_SCROLLING = 1 << 26; + private static final int BOOLEAN_PROPERTY_FIELD_REQUIRED = 1 << 27; + /** * Bits that provide the id of a virtual descendant of a view. */ @@ -1085,6 +1087,7 @@ public class AccessibilityNodeInfo implements Parcelable { private CharSequence mPaneTitle; private CharSequence mStateDescription; private CharSequence mContentDescription; + private CharSequence mSupplementalDescription; private CharSequence mTooltipText; private String mViewIdResourceName; private String mUniqueId; @@ -2543,6 +2546,32 @@ public class AccessibilityNodeInfo implements Parcelable { } /** + * Gets whether a node representing a form field requires input or selection. + * + * @return {@code true} if {@code this} node represents a form field that requires input or + * selection, {@code false} otherwise. + */ + @FlaggedApi(Flags.FLAG_A11Y_IS_REQUIRED_API) + public boolean isFieldRequired() { + return getBooleanProperty(BOOLEAN_PROPERTY_FIELD_REQUIRED); + } + + /** + * Sets whether {@code this} node represents a form field that requires input or selection. + * + * <p><strong>Note:</strong> Cannot be called from an AccessibilityService. This class is made + * immutable before being delivered to an AccessibilityService. + * + * @param required {@code true} if input or selection of this node should be required, {@code + * false} otherwise. + * @throws IllegalStateException If called from an AccessibilityService + */ + @FlaggedApi(Flags.FLAG_A11Y_IS_REQUIRED_API) + public void setFieldRequired(boolean required) { + setBooleanProperty(BOOLEAN_PROPERTY_FIELD_REQUIRED, required); + } + + /** * Gets whether this node is focusable. * * <p>In the View system, this typically maps to {@link View#isFocusable()}. @@ -3686,6 +3715,27 @@ public class AccessibilityNodeInfo implements Parcelable { return mContentDescription; } + /** + * Gets the supplemental description of this node. A supplemental description provides + * brief supplemental information for this node, such as the purpose of the node when + * that purpose is not conveyed within its textual representation. For example, if a + * dropdown select has a purpose of setting font family, the supplemental description + * could be "font family". If this node has children, its supplemental description serves + * as additional information and is not intended to replace any existing information + * in the subtree. This is different from the {@link #getContentDescription()} in that + * this description is purely supplemental while a content description may be used + * to replace a description for a node or its subtree that an assistive technology + * would otherwise compute based on other properties of the node and its descendants. + * + * @return The supplemental description. + * @see #setSupplementalDescription(CharSequence) + * @see #getContentDescription() + */ + @FlaggedApi(Flags.FLAG_SUPPLEMENTAL_DESCRIPTION) + @Nullable + public CharSequence getSupplementalDescription() { + return mSupplementalDescription; + } /** * Sets the state description of this node. @@ -3724,6 +3774,35 @@ public class AccessibilityNodeInfo implements Parcelable { } /** + * Sets the supplemental description of this node. A supplemental description provides + * brief supplemental information for this node, such as the purpose of the node when + * that purpose is not conveyed within its textual representation. For example, if a + * dropdown select has a purpose of setting font family, the supplemental description + * could be "font family". If this node has children, its supplemental description serves + * as additional information and is not intended to replace any existing information + * in the subtree. This is different from the {@link #setContentDescription(CharSequence)} + * in that this description is purely supplemental while a content description may be used + * to replace a description for a node or its subtree that an assistive technology + * would otherwise compute based on other properties of the node and its descendants. + * <p> + * <strong>Note:</strong> Cannot be called from an + * {@link android.accessibilityservice.AccessibilityService}. + * This class is made immutable before being delivered to an AccessibilityService. + * + * @param supplementalDescription The supplemental description. + * + * @throws IllegalStateException If called from an AccessibilityService. + * @see #getSupplementalDescription() + * @see #setContentDescription(CharSequence) + */ + @FlaggedApi(Flags.FLAG_SUPPLEMENTAL_DESCRIPTION) + public void setSupplementalDescription(@Nullable CharSequence supplementalDescription) { + enforceNotSealed(); + mSupplementalDescription = (supplementalDescription == null) ? null + : supplementalDescription.subSequence(0, supplementalDescription.length()); + } + + /** * Gets the tooltip text of this node. * * @return The tooltip text. @@ -4657,6 +4736,10 @@ public class AccessibilityNodeInfo implements Parcelable { nonDefaultFields |= bitAt(fieldIndex); } fieldIndex++; + if (!Objects.equals(mSupplementalDescription, DEFAULT.mSupplementalDescription)) { + nonDefaultFields |= bitAt(fieldIndex); + } + fieldIndex++; if (!Objects.equals(mPaneTitle, DEFAULT.mPaneTitle)) { nonDefaultFields |= bitAt(fieldIndex); } @@ -4843,6 +4926,9 @@ public class AccessibilityNodeInfo implements Parcelable { if (isBitSet(nonDefaultFields, fieldIndex++)) { parcel.writeCharSequence(mContentDescription); } + if (isBitSet(nonDefaultFields, fieldIndex++)) { + parcel.writeCharSequence(mSupplementalDescription); + } if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeCharSequence(mPaneTitle); if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeCharSequence(mTooltipText); if (isBitSet(nonDefaultFields, fieldIndex++)) parcel.writeCharSequence(mContainerTitle); @@ -4950,6 +5036,7 @@ public class AccessibilityNodeInfo implements Parcelable { mError = other.mError; mStateDescription = other.mStateDescription; mContentDescription = other.mContentDescription; + mSupplementalDescription = other.mSupplementalDescription; mPaneTitle = other.mPaneTitle; mTooltipText = other.mTooltipText; mContainerTitle = other.mContainerTitle; @@ -5119,6 +5206,9 @@ public class AccessibilityNodeInfo implements Parcelable { if (isBitSet(nonDefaultFields, fieldIndex++)) { mContentDescription = parcel.readCharSequence(); } + if (isBitSet(nonDefaultFields, fieldIndex++)) { + mSupplementalDescription = parcel.readCharSequence(); + } if (isBitSet(nonDefaultFields, fieldIndex++)) mPaneTitle = parcel.readCharSequence(); if (isBitSet(nonDefaultFields, fieldIndex++)) mTooltipText = parcel.readCharSequence(); if (isBitSet(nonDefaultFields, fieldIndex++)) mContainerTitle = parcel.readCharSequence(); @@ -5483,6 +5573,9 @@ public class AccessibilityNodeInfo implements Parcelable { builder.append("; maxTextLength: ").append(mMaxTextLength); builder.append("; stateDescription: ").append(mStateDescription); builder.append("; contentDescription: ").append(mContentDescription); + if (Flags.supplementalDescription()) { + builder.append("; supplementalDescription: ").append(mSupplementalDescription); + } builder.append("; tooltipText: ").append(mTooltipText); builder.append("; containerTitle: ").append(mContainerTitle); builder.append("; viewIdResName: ").append(mViewIdResourceName); @@ -5491,6 +5584,9 @@ public class AccessibilityNodeInfo implements Parcelable { builder.append("; checkable: ").append(isCheckable()); builder.append("; checked: ").append(isChecked()); + if (Flags.a11yIsRequiredApi()) { + builder.append("; required: ").append(isFieldRequired()); + } builder.append("; focusable: ").append(isFocusable()); builder.append("; focused: ").append(isFocused()); builder.append("; selected: ").append(isSelected()); @@ -6417,6 +6513,19 @@ public class AccessibilityNodeInfo implements Parcelable { /** Range type: percent with values from zero to one hundred. */ public static final int RANGE_TYPE_PERCENT = 2; + /** + * Range type: indeterminate. + * + * A {@link RangeInfo} type used to represent a node which may typically expose range + * information but is presently in an indeterminate state, such as a {@link + * android.widget.ProgressBar} representing a loading operation of unknown duration. + * When using this type, the {@code min}, {@code max}, and {@code current} values used to + * construct an instance may be ignored. It is recommended to use {@code Float.NaN} for + * these values. + */ + @FlaggedApi(Flags.FLAG_INDETERMINATE_RANGE_INFO) + public static final int RANGE_TYPE_INDETERMINATE = 3; + private int mType; private float mMin; private float mMax; diff --git a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig index 820a1fba11ad..c07da410c7f9 100644 --- a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig +++ b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig @@ -40,6 +40,13 @@ flag { } flag { + name: "a11y_character_in_window_api" + namespace: "accessibility" + description: "Enables new extra data key for an AccessibilityService to request character bounds in unmagnified window coordinates." + bug: "375429616" +} + +flag { namespace: "accessibility" name: "allow_shortcut_chooser_on_lockscreen" description: "Allows the a11y shortcut disambig dialog to appear on the lockscreen" @@ -215,6 +222,13 @@ flag { } flag { + name: "supplemental_description" + namespace: "accessibility" + description: "Feature flag for supplemental description api" + bug: "375266174" +} + +flag { name: "support_multiple_labeledby" namespace: "accessibility" description: "Feature flag for supporting multiple labels in AccessibilityNodeInfo labeledby api" @@ -244,3 +258,10 @@ flag { purpose: PURPOSE_BUGFIX } } + + flag { + name: "indeterminate_range_info" + namespace: "accessibility" + description: "Creates a way to create an INDETERMINATE RangeInfo" + bug: "376108874" + } diff --git a/core/java/android/view/flags/refresh_rate_flags.aconfig b/core/java/android/view/flags/refresh_rate_flags.aconfig index c31df73fbeae..1c7570efc26b 100644 --- a/core/java/android/view/flags/refresh_rate_flags.aconfig +++ b/core/java/android/view/flags/refresh_rate_flags.aconfig @@ -122,6 +122,14 @@ flag { } flag { + name: "toolkit_viewgroup_set_requested_frame_rate_api" + namespace: "toolkit" + description: "Feature flag to introduce new frame rate setting APIs on ViewGroup" + bug: "335874198" + is_fixed_read_only: true +} + +flag { name: "toolkit_frame_rate_touch_boost_25q1" namespace: "toolkit" description: "Feature flag to not suppress touch boost for specific windowTypes in VRR V QPR2" diff --git a/core/java/android/webkit/IWebViewUpdateService.aidl b/core/java/android/webkit/IWebViewUpdateService.aidl index aeb746cbbe5e..39092fe48a6e 100644 --- a/core/java/android/webkit/IWebViewUpdateService.aidl +++ b/core/java/android/webkit/IWebViewUpdateService.aidl @@ -72,16 +72,6 @@ interface IWebViewUpdateService { PackageInfo getCurrentWebViewPackage(); /** - * Used by Settings to determine whether multiprocess is enabled. - */ - boolean isMultiProcessEnabled(); - - /** - * Used by Settings to enable/disable multiprocess. - */ - void enableMultiProcess(boolean enable); - - /** * Used by Settings to get the default WebView package. */ WebViewProviderInfo getDefaultWebViewPackage(); diff --git a/core/java/android/webkit/WebViewDelegate.java b/core/java/android/webkit/WebViewDelegate.java index 4c5802ccfcf5..778a51e67f57 100644 --- a/core/java/android/webkit/WebViewDelegate.java +++ b/core/java/android/webkit/WebViewDelegate.java @@ -28,7 +28,6 @@ import android.content.pm.ApplicationInfo; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.RecordingCanvas; -import android.os.RemoteException; import android.os.SystemProperties; import android.os.Trace; import android.util.SparseArray; @@ -217,19 +216,7 @@ public final class WebViewDelegate { * Returns whether WebView should run in multiprocess mode. */ public boolean isMultiProcessEnabled() { - if (Flags.updateServiceV2()) { - return true; - } else if (Flags.updateServiceIpcWrapper()) { - // We don't want to support this method in the new wrapper because updateServiceV2 is - // intended to ship in the same release (or sooner). It's only possible to disable it - // with an obscure adb command, so just return true here too. - return true; - } - try { - return WebViewFactory.getUpdateService().isMultiProcessEnabled(); - } catch (RemoteException e) { - throw e.rethrowFromSystemServer(); - } + return true; } /** diff --git a/core/java/android/webkit/WebViewFactory.java b/core/java/android/webkit/WebViewFactory.java index e10a3983f0b0..de303f80ff9e 100644 --- a/core/java/android/webkit/WebViewFactory.java +++ b/core/java/android/webkit/WebViewFactory.java @@ -16,8 +16,6 @@ package android.webkit; -import static android.webkit.Flags.updateServiceV2; - import android.annotation.NonNull; import android.annotation.SystemApi; import android.annotation.UptimeMillisLong; @@ -490,7 +488,7 @@ public final class WebViewFactory { Trace.traceEnd(Trace.TRACE_TAG_WEBVIEW); } - if (updateServiceV2() && !isInstalledPackage(newPackageInfo)) { + if (!isInstalledPackage(newPackageInfo)) { throw new MissingWebViewPackageException( TextUtils.formatSimple( "Current WebView Package (%s) is not installed for the current " @@ -498,7 +496,7 @@ public final class WebViewFactory { newPackageInfo.packageName)); } - if (updateServiceV2() && !isEnabledPackage(newPackageInfo)) { + if (!isEnabledPackage(newPackageInfo)) { throw new MissingWebViewPackageException( TextUtils.formatSimple( "Current WebView Package (%s) is not enabled for the current user", diff --git a/core/java/android/webkit/WebViewUpdateService.java b/core/java/android/webkit/WebViewUpdateService.java index 644d917c8be6..a26ba3f5829a 100644 --- a/core/java/android/webkit/WebViewUpdateService.java +++ b/core/java/android/webkit/WebViewUpdateService.java @@ -16,14 +16,18 @@ package android.webkit; +import android.annotation.FlaggedApi; import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.compat.annotation.UnsupportedAppUsage; import android.os.RemoteException; /** + * @deprecated Use the {@link WebViewUpdateManager} class instead. * @hide */ +@FlaggedApi(Flags.FLAG_UPDATE_SERVICE_IPC_WRAPPER) +@Deprecated @SystemApi public final class WebViewUpdateService { diff --git a/core/java/android/webkit/WebViewZygote.java b/core/java/android/webkit/WebViewZygote.java index 668cd0152846..10d16af8cb8c 100644 --- a/core/java/android/webkit/WebViewZygote.java +++ b/core/java/android/webkit/WebViewZygote.java @@ -16,8 +16,6 @@ package android.webkit; -import static android.webkit.Flags.updateServiceV2; - import android.content.pm.PackageInfo; import android.os.Build; import android.os.ChildZygoteProcess; @@ -52,13 +50,6 @@ public class WebViewZygote { @GuardedBy("sLock") private static PackageInfo sPackage; - /** - * Flag for whether multi-process WebView is enabled. If this is {@code false}, the zygote will - * not be started. Should be removed entirely after we remove the updateServiceV2 flag. - */ - @GuardedBy("sLock") - private static boolean sMultiprocessEnabled = false; - public static ZygoteProcess getProcess() { synchronized (sLock) { if (sZygote != null) return sZygote; @@ -76,40 +67,13 @@ public class WebViewZygote { public static boolean isMultiprocessEnabled() { synchronized (sLock) { - if (updateServiceV2()) { - return sPackage != null; - } else { - return sMultiprocessEnabled && sPackage != null; - } - } - } - - public static void setMultiprocessEnabled(boolean enabled) { - if (updateServiceV2()) { - throw new IllegalStateException( - "setMultiprocessEnabled shouldn't be called if update_service_v2 flag is set."); - } - synchronized (sLock) { - sMultiprocessEnabled = enabled; - - // When multi-process is disabled, kill the zygote. When it is enabled, - // the zygote will be started when it is first needed in getProcess(). - if (!enabled) { - stopZygoteLocked(); - } + return sPackage != null; } } static void onWebViewProviderChanged(PackageInfo packageInfo) { synchronized (sLock) { sPackage = packageInfo; - - // If multi-process is not enabled, then do not start the zygote service. - // Only check sMultiprocessEnabled if updateServiceV2 is not enabled. - if (!updateServiceV2() && !sMultiprocessEnabled) { - return; - } - stopZygoteLocked(); } } diff --git a/core/java/android/widget/TextClock.java b/core/java/android/widget/TextClock.java index 255bd679dc35..65cd190725a4 100644 --- a/core/java/android/widget/TextClock.java +++ b/core/java/android/widget/TextClock.java @@ -45,6 +45,7 @@ import android.view.inspector.InspectableProperty; import com.android.internal.R; import com.android.internal.util.Preconditions; +import java.time.DateTimeException; import java.time.Duration; import java.time.Instant; import java.time.ZoneId; @@ -291,11 +292,26 @@ public class TextClock extends TextView { } private void createTime(String timeZone) { - if (timeZone != null) { - mTime = Calendar.getInstance(TimeZone.getTimeZone(timeZone)); + TimeZone tz = null; + if (timeZone == null) { + tz = TimeZone.getDefault(); + // Note that mTimeZone should always be null if timeZone is. } else { - mTime = Calendar.getInstance(); + tz = TimeZone.getTimeZone(timeZone); + try { + // Try converting this TZ to a zoneId to make sure it's valid. This + // performs a different set of checks than TimeZone.getTimeZone so + // we can avoid exceptions later when we do need this conversion. + tz.toZoneId(); + } catch (DateTimeException ex) { + // If we're here, the user supplied timezone is invalid, so reset + // mTimeZone to something sane. + tz = TimeZone.getDefault(); + mTimeZone = tz.getID(); + } } + + mTime = Calendar.getInstance(tz); } /** diff --git a/core/java/android/window/BackNavigationInfo.java b/core/java/android/window/BackNavigationInfo.java index 6cefc4d2fecf..4b7bacb23e5d 100644 --- a/core/java/android/window/BackNavigationInfo.java +++ b/core/java/android/window/BackNavigationInfo.java @@ -87,6 +87,13 @@ public final class BackNavigationInfo implements Parcelable { */ public static final String KEY_GESTURE_FINISHED = "GestureFinished"; + /** + * Touch gestured has transferred to embedded window, Shell should pilfer pointers so the + * embedded won't receive motion events. + * @hide + */ + public static final String KEY_TOUCH_GESTURE_TRANSFERRED = "TouchGestureTransferred"; + /** * Defines the type of back destinations a back even can lead to. This is used to define the @@ -119,7 +126,7 @@ public final class BackNavigationInfo implements Parcelable { @NonNull private final Rect mTouchableRegion; - private final boolean mAppProgressGenerationAllowed; + private boolean mAppProgressGenerationAllowed; private final int mFocusedTaskId; /** @@ -253,6 +260,14 @@ public final class BackNavigationInfo implements Parcelable { } /** + * Force disable app to intercept back progress event. + * @hide + */ + public void disableAppProgressGenerationAllowed() { + mAppProgressGenerationAllowed = false; + } + + /** * Callback to be called when the back preview is finished in order to notify the server that * it can clean up the resources created for the animation. * @hide diff --git a/core/java/android/window/DesktopModeFlags.java b/core/java/android/window/DesktopModeFlags.java index 22eec2940e19..dae87ddcb1bd 100644 --- a/core/java/android/window/DesktopModeFlags.java +++ b/core/java/android/window/DesktopModeFlags.java @@ -70,7 +70,12 @@ public enum DesktopModeFlags { ENABLE_DESKTOP_WINDOWING_ENTER_TRANSITIONS(Flags::enableDesktopWindowingTransitions, false), ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS(Flags::enableDesktopWindowingExitTransitions, false), ENABLE_WINDOWING_TRANSITION_HANDLERS_OBSERVERS( - Flags::enableWindowingTransitionHandlersObservers, false); + Flags::enableWindowingTransitionHandlersObservers, false), + ENABLE_DESKTOP_APP_LAUNCH_ALTTAB_TRANSITIONS( + Flags::enableDesktopAppLaunchAlttabTransitions, false), + ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS( + Flags::enableDesktopAppLaunchTransitions, false), + ENABLE_DESKTOP_WINDOWING_PERSISTENCE(Flags::enableDesktopWindowingPersistence, false); private static final String TAG = "DesktopModeFlagsUtil"; // Function called to obtain aconfig flag value. diff --git a/core/java/android/window/OnBackInvokedCallbackInfo.java b/core/java/android/window/OnBackInvokedCallbackInfo.java index bb5fe96fdec1..44c7bd9ec612 100644 --- a/core/java/android/window/OnBackInvokedCallbackInfo.java +++ b/core/java/android/window/OnBackInvokedCallbackInfo.java @@ -16,6 +16,8 @@ package android.window; +import static android.window.SystemOverrideOnBackInvokedCallback.OVERRIDE_UNDEFINED; + import android.annotation.NonNull; import android.os.Parcel; import android.os.Parcelable; @@ -29,19 +31,23 @@ public final class OnBackInvokedCallbackInfo implements Parcelable { private final IOnBackInvokedCallback mCallback; private @OnBackInvokedDispatcher.Priority int mPriority; private final boolean mIsAnimationCallback; + private final @SystemOverrideOnBackInvokedCallback.OverrideBehavior int mOverrideBehavior; public OnBackInvokedCallbackInfo(@NonNull IOnBackInvokedCallback callback, int priority, - boolean isAnimationCallback) { + boolean isAnimationCallback, + int overrideBehavior) { mCallback = callback; mPriority = priority; mIsAnimationCallback = isAnimationCallback; + mOverrideBehavior = overrideBehavior; } private OnBackInvokedCallbackInfo(@NonNull Parcel in) { mCallback = IOnBackInvokedCallback.Stub.asInterface(in.readStrongBinder()); mPriority = in.readInt(); mIsAnimationCallback = in.readBoolean(); + mOverrideBehavior = in.readInt(); } @Override @@ -54,6 +60,7 @@ public final class OnBackInvokedCallbackInfo implements Parcelable { dest.writeStrongInterface(mCallback); dest.writeInt(mPriority); dest.writeBoolean(mIsAnimationCallback); + dest.writeInt(mOverrideBehavior); } public static final Creator<OnBackInvokedCallbackInfo> CREATOR = @@ -70,7 +77,8 @@ public final class OnBackInvokedCallbackInfo implements Parcelable { }; public boolean isSystemCallback() { - return mPriority == OnBackInvokedDispatcher.PRIORITY_SYSTEM; + return mPriority == OnBackInvokedDispatcher.PRIORITY_SYSTEM + || mOverrideBehavior != OVERRIDE_UNDEFINED; } @NonNull @@ -87,12 +95,18 @@ public final class OnBackInvokedCallbackInfo implements Parcelable { return mIsAnimationCallback; } + @SystemOverrideOnBackInvokedCallback.OverrideBehavior + public int getOverrideBehavior() { + return mOverrideBehavior; + } + @Override public String toString() { return "OnBackInvokedCallbackInfo{" + "mCallback=" + mCallback + ", mPriority=" + mPriority + ", mIsAnimationCallback=" + mIsAnimationCallback + + ", mOverrideBehavior=" + mOverrideBehavior + '}'; } } diff --git a/core/java/android/window/SystemOnBackInvokedCallbacks.java b/core/java/android/window/SystemOnBackInvokedCallbacks.java new file mode 100644 index 000000000000..f67520b1de96 --- /dev/null +++ b/core/java/android/window/SystemOnBackInvokedCallbacks.java @@ -0,0 +1,168 @@ +/* + * 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.window; + +import android.annotation.FlaggedApi; +import android.annotation.NonNull; +import android.app.Activity; +import android.util.ArrayMap; + +import com.android.window.flags.Flags; + +import java.lang.ref.WeakReference; + +/** + * Utility class providing {@link OnBackInvokedCallback}s to override the default behavior when + * system back is invoked. e.g. {@link Activity#finish} + * + * <p>By registering these callbacks with the {@link OnBackInvokedDispatcher}, the system can + * trigger specific behaviors and play corresponding ahead-of-time animations when the back + * gesture is invoked. + * + * <p>For example, to trigger the {@link Activity#moveTaskToBack} behavior: + * <pre> + * OnBackInvokedDispatcher dispatcher = activity.getOnBackInvokedDispatcher(); + * dispatcher.registerOnBackInvokedCallback( + * OnBackInvokedDispatcher.PRIORITY_DEFAULT, + * SystemOnBackInvokedCallbacks.moveTaskToBackCallback(activity)); + * </pre> + */ +@SuppressWarnings("SingularCallback") +@FlaggedApi(Flags.FLAG_PREDICTIVE_BACK_SYSTEM_OVERRIDE_CALLBACK) +public final class SystemOnBackInvokedCallbacks { + private static final OverrideCallbackFactory<Activity> sMoveTaskToBackFactory = new + MoveTaskToBackCallbackFactory(); + private static final OverrideCallbackFactory<Activity> sFinishAndRemoveTaskFactory = new + FinishAndRemoveTaskCallbackFactory(); + + private SystemOnBackInvokedCallbacks() { + throw new UnsupportedOperationException("This is a utility class and cannot be " + + "instantiated"); + } + + /** + * <p>Get a callback to triggers {@link Activity#moveTaskToBack(boolean)} on the associated + * {@link Activity}, moving the task containing the activity to the background. The system + * will play the corresponding transition animation, regardless of whether the activity + * is the root activity of the task.</p> + * + * @param activity The associated {@link Activity} + * @see Activity#moveTaskToBack(boolean) + */ + @FlaggedApi(Flags.FLAG_PREDICTIVE_BACK_SYSTEM_OVERRIDE_CALLBACK) + @NonNull + public static OnBackInvokedCallback moveTaskToBackCallback(@NonNull Activity activity) { + return sMoveTaskToBackFactory.getOverrideCallback(activity); + } + + /** + * <p>Get a callback to triggers {@link Activity#finishAndRemoveTask()} on the associated + * {@link Activity}. If the activity is the root activity of its task, the entire task + * will be removed from the recents task. The activity will be finished in all cases. + * The system will play the corresponding transition animation.</p> + * + * @param activity The associated {@link Activity} + * @see Activity#finishAndRemoveTask() + */ + @FlaggedApi(Flags.FLAG_PREDICTIVE_BACK_SYSTEM_OVERRIDE_CALLBACK) + @NonNull + public static OnBackInvokedCallback finishAndRemoveTaskCallback(@NonNull Activity activity) { + return sFinishAndRemoveTaskFactory.getOverrideCallback(activity); + } + + /** + * Abstract factory for creating system override {@link SystemOverrideOnBackInvokedCallback} + * instances. + * + * <p>Concrete implementations of this factory are responsible for creating callbacks that + * override the default system back navigation behavior. These callbacks should be used + * exclusively for system overrides and should never be invoked directly.</p> + */ + private abstract static class OverrideCallbackFactory<TYPE> { + private final ArrayMap<WeakReference<TYPE>, + WeakReference<SystemOverrideOnBackInvokedCallback>> mObjectMap = new ArrayMap<>(); + + protected abstract SystemOverrideOnBackInvokedCallback createCallback( + @NonNull TYPE context); + + @NonNull SystemOverrideOnBackInvokedCallback getOverrideCallback(@NonNull TYPE object) { + if (object == null) { + throw new NullPointerException("Input object cannot be null"); + } + synchronized (mObjectMap) { + WeakReference<SystemOverrideOnBackInvokedCallback> callback = null; + for (int i = mObjectMap.size() - 1; i >= 0; --i) { + final WeakReference<TYPE> next = mObjectMap.keyAt(i); + if (next.get() == object) { + callback = mObjectMap.get(next); + break; + } + } + if (callback != null) { + return callback.get(); + } + final SystemOverrideOnBackInvokedCallback contextCallback = createCallback(object); + if (contextCallback != null) { + mObjectMap.put(new WeakReference<>(object), + new WeakReference<>(contextCallback)); + } + return contextCallback; + } + } + } + + private static class MoveTaskToBackCallbackFactory extends OverrideCallbackFactory<Activity> { + @Override + protected SystemOverrideOnBackInvokedCallback createCallback(Activity activity) { + final WeakReference<Activity> activityRef = new WeakReference<>(activity); + return new SystemOverrideOnBackInvokedCallback() { + @Override + public void onBackInvoked() { + if (activityRef.get() != null) { + activityRef.get().moveTaskToBack(true /* nonRoot */); + } + } + + @Override + public int overrideBehavior() { + return OVERRIDE_MOVE_TASK_TO_BACK; + } + }; + } + } + + private static class FinishAndRemoveTaskCallbackFactory extends + OverrideCallbackFactory<Activity> { + @Override + protected SystemOverrideOnBackInvokedCallback createCallback(Activity activity) { + final WeakReference<Activity> activityRef = new WeakReference<>(activity); + return new SystemOverrideOnBackInvokedCallback() { + @Override + public void onBackInvoked() { + if (activityRef.get() != null) { + activityRef.get().finishAndRemoveTask(); + } + } + + @Override + public int overrideBehavior() { + return OVERRIDE_FINISH_AND_REMOVE_TASK; + } + }; + } + } +} diff --git a/core/java/android/window/SystemOverrideOnBackInvokedCallback.java b/core/java/android/window/SystemOverrideOnBackInvokedCallback.java new file mode 100644 index 000000000000..3360a199682e --- /dev/null +++ b/core/java/android/window/SystemOverrideOnBackInvokedCallback.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.window; + +import android.annotation.IntDef; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Non-default ahead-of-time system OnBackInvokedCallback. + * @hide + */ +public interface SystemOverrideOnBackInvokedCallback extends OnBackInvokedCallback { + /** + * No override request + */ + int OVERRIDE_UNDEFINED = 0; + + /** + * Navigating back will bring the task to back + */ + int OVERRIDE_MOVE_TASK_TO_BACK = 1; + + /** + * Navigating back will finish activity, and remove the task if this activity is root activity. + */ + int OVERRIDE_FINISH_AND_REMOVE_TASK = 2; + + /** @hide */ + @IntDef({ + OVERRIDE_UNDEFINED, + OVERRIDE_MOVE_TASK_TO_BACK, + OVERRIDE_FINISH_AND_REMOVE_TASK, + }) + @Retention(RetentionPolicy.SOURCE) + @interface OverrideBehavior { + } + + /** + * @return Override type of this callback. + */ + @OverrideBehavior + default int overrideBehavior() { + return OVERRIDE_UNDEFINED; + } +} diff --git a/core/java/android/window/WindowContainerTransaction.java b/core/java/android/window/WindowContainerTransaction.java index 34abf3114925..3fe63ab17248 100644 --- a/core/java/android/window/WindowContainerTransaction.java +++ b/core/java/android/window/WindowContainerTransaction.java @@ -1867,27 +1867,33 @@ public final class WindowContainerTransaction implements Parcelable { switch (type) { case HIERARCHY_OP_TYPE_REPARENT: return "reparent"; case HIERARCHY_OP_TYPE_REORDER: return "reorder"; - case HIERARCHY_OP_TYPE_CHILDREN_TASKS_REPARENT: return "ChildrenTasksReparent"; - case HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT: return "SetLaunchRoot"; - case HIERARCHY_OP_TYPE_SET_ADJACENT_ROOTS: return "SetAdjacentRoot"; - case HIERARCHY_OP_TYPE_LAUNCH_TASK: return "LaunchTask"; - case HIERARCHY_OP_TYPE_SET_LAUNCH_ADJACENT_FLAG_ROOT: return "SetAdjacentFlagRoot"; + case HIERARCHY_OP_TYPE_CHILDREN_TASKS_REPARENT: return "childrenTasksReparent"; + case HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT: return "setLaunchRoot"; + case HIERARCHY_OP_TYPE_SET_ADJACENT_ROOTS: return "setAdjacentRoot"; + case HIERARCHY_OP_TYPE_LAUNCH_TASK: return "launchTask"; + case HIERARCHY_OP_TYPE_SET_LAUNCH_ADJACENT_FLAG_ROOT: return "setAdjacentFlagRoot"; case HIERARCHY_OP_TYPE_SET_DISABLE_LAUNCH_ADJACENT: - return "SetDisableLaunchAdjacent"; - case HIERARCHY_OP_TYPE_PENDING_INTENT: return "PendingIntent"; - case HIERARCHY_OP_TYPE_START_SHORTCUT: return "StartShortcut"; + return "setDisableLaunchAdjacent"; + case HIERARCHY_OP_TYPE_PENDING_INTENT: return "pendingIntent"; + case HIERARCHY_OP_TYPE_START_SHORTCUT: return "startShortcut"; + case HIERARCHY_OP_TYPE_RESTORE_TRANSIENT_ORDER: return "restoreTransientOrder"; case HIERARCHY_OP_TYPE_ADD_INSETS_FRAME_PROVIDER: return "addInsetsFrameProvider"; case HIERARCHY_OP_TYPE_REMOVE_INSETS_FRAME_PROVIDER: return "removeInsetsFrameProvider"; case HIERARCHY_OP_TYPE_SET_ALWAYS_ON_TOP: return "setAlwaysOnTop"; - case HIERARCHY_OP_TYPE_REMOVE_TASK: return "RemoveTask"; + case HIERARCHY_OP_TYPE_REMOVE_TASK: return "removeTask"; case HIERARCHY_OP_TYPE_FINISH_ACTIVITY: return "finishActivity"; - case HIERARCHY_OP_TYPE_CLEAR_ADJACENT_ROOTS: return "ClearAdjacentRoot"; + case HIERARCHY_OP_TYPE_CLEAR_ADJACENT_ROOTS: return "clearAdjacentRoot"; case HIERARCHY_OP_TYPE_SET_REPARENT_LEAF_TASK_IF_RELAUNCH: return "setReparentLeafTaskIfRelaunch"; case HIERARCHY_OP_TYPE_ADD_TASK_FRAGMENT_OPERATION: return "addTaskFragmentOperation"; + case HIERARCHY_OP_TYPE_MOVE_PIP_ACTIVITY_TO_PINNED_TASK: + return "movePipActivityToPinnedTask"; + case HIERARCHY_OP_TYPE_SET_IS_TRIMMABLE: return "setIsTrimmable"; + case HIERARCHY_OP_TYPE_RESTORE_BACK_NAVIGATION: return "restoreBackNav"; case HIERARCHY_OP_TYPE_SET_EXCLUDE_INSETS_TYPES: return "setExcludeInsetsTypes"; + case HIERARCHY_OP_TYPE_SET_KEYGUARD_STATE: return "setKeyguardState"; default: return "HOP(" + type + ")"; } } diff --git a/core/java/android/window/WindowOnBackInvokedDispatcher.java b/core/java/android/window/WindowOnBackInvokedDispatcher.java index c9d458f22463..0ea4bb41d3a4 100644 --- a/core/java/android/window/WindowOnBackInvokedDispatcher.java +++ b/core/java/android/window/WindowOnBackInvokedDispatcher.java @@ -16,6 +16,9 @@ package android.window; +import static android.window.SystemOverrideOnBackInvokedCallback.OVERRIDE_UNDEFINED; + +import static com.android.window.flags.Flags.predictiveBackSystemOverrideCallback; import static com.android.window.flags.Flags.predictiveBackPrioritySystemNavigationObserver; import static com.android.window.flags.Flags.predictiveBackTimestampApi; @@ -201,6 +204,15 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { mImeDispatcher.registerOnBackInvokedCallback(priority, callback); return; } + if (predictiveBackPrioritySystemNavigationObserver() + && predictiveBackSystemOverrideCallback()) { + if (priority == PRIORITY_SYSTEM_NAVIGATION_OBSERVER + && callback instanceof SystemOverrideOnBackInvokedCallback) { + Log.e(TAG, "System override callbacks cannot be registered to " + + "NAVIGATION_OBSERVER"); + return; + } + } if (predictiveBackPrioritySystemNavigationObserver()) { if (priority == PRIORITY_SYSTEM_NAVIGATION_OBSERVER) { registerSystemNavigationObserverCallback(callback); @@ -365,7 +377,8 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { public void tryInvokeSystemNavigationObserverCallback() { OnBackInvokedCallback topCallback = getTopCallback(); Integer callbackPriority = mAllCallbacks.getOrDefault(topCallback, null); - if (callbackPriority != null && callbackPriority == PRIORITY_SYSTEM) { + final boolean isSystemOverride = topCallback instanceof SystemOverrideOnBackInvokedCallback; + if ((callbackPriority != null && callbackPriority == PRIORITY_SYSTEM) || isSystemOverride) { invokeSystemNavigationObserverCallback(); } } @@ -384,14 +397,22 @@ public class WindowOnBackInvokedDispatcher implements OnBackInvokedDispatcher { OnBackInvokedCallbackInfo callbackInfo = null; if (callback != null) { int priority = mAllCallbacks.get(callback); + int overrideAnimation = OVERRIDE_UNDEFINED; + if (callback instanceof SystemOverrideOnBackInvokedCallback) { + overrideAnimation = ((SystemOverrideOnBackInvokedCallback) callback) + .overrideBehavior(); + } + final boolean isSystemCallback = priority == PRIORITY_SYSTEM + || overrideAnimation != OVERRIDE_UNDEFINED; final IOnBackInvokedCallback iCallback = new OnBackInvokedCallbackWrapper(callback, mTouchTracker, mProgressAnimator, mHandler, this::callOnKeyPreIme, this::invokeSystemNavigationObserverCallback, - /*isSystemCallback*/ priority == PRIORITY_SYSTEM); + isSystemCallback /*isSystemCallback*/); callbackInfo = new OnBackInvokedCallbackInfo( iCallback, priority, - callback instanceof OnBackAnimationCallback); + callback instanceof OnBackAnimationCallback, + overrideAnimation); } mWindowSession.setOnBackInvokedCallbackInfo(mWindow, callbackInfo); } catch (RemoteException e) { diff --git a/core/java/android/window/flags/responsible_apis.aconfig b/core/java/android/window/flags/responsible_apis.aconfig index 57aacffd4ffc..fd5de91c80ca 100644 --- a/core/java/android/window/flags/responsible_apis.aconfig +++ b/core/java/android/window/flags/responsible_apis.aconfig @@ -9,13 +9,6 @@ flag { } flag { - name: "bal_require_opt_in_same_uid" - namespace: "responsible_apis" - description: "Require the PendingIntent creator/sender to opt in if it is the same UID" - bug: "296478951" -} - -flag { name: "bal_dont_bring_existing_background_task_stack_to_fg" namespace: "responsible_apis" description: "When starting a PendingIntent with ONLY creator privileges, don't bring the existing task stack to foreground" @@ -71,6 +64,7 @@ flag { bug: "339720406" } +# replaced by bal_strict_mode_ro flag { name: "bal_strict_mode" namespace: "responsible_apis" @@ -79,6 +73,14 @@ flag { } flag { + name: "bal_strict_mode_ro" + namespace: "responsible_apis" + description: "Strict mode flag" + bug: "324089586" + is_fixed_read_only: true +} + +flag { name: "bal_reduce_grace_period" namespace: "responsible_apis" description: "Changes to reduce or ideally remove the grace period exemption." diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig index ed7a796488af..015614e4a734 100644 --- a/core/java/android/window/flags/windowing_frontend.aconfig +++ b/core/java/android/window/flags/windowing_frontend.aconfig @@ -49,6 +49,17 @@ flag { } flag { + name: "respect_animation_clip" + namespace: "windowing_frontend" + description: "Fix missing clip transformation of animation" + bug: "376601866" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "edge_to_edge_by_default" namespace: "windowing_frontend" description: "Make app go edge-to-edge by default when targeting SDK 35 or greater" @@ -348,3 +359,22 @@ flag { bug: "372230928" is_fixed_read_only: true } + +flag { + name: "disallow_app_progress_embedded_window" + namespace: "windowing_frontend" + description: "Pilfer pointers when app transfer input gesture to embedded window." + bug: "365504126" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "predictive_back_system_override_callback" + namespace: "windowing_frontend" + description: "Provide pre-make predictive back API extension" + is_fixed_read_only: true + bug: "362938401" +}
\ No newline at end of file diff --git a/core/java/com/android/internal/app/SuggestedLocaleAdapter.java b/core/java/com/android/internal/app/SuggestedLocaleAdapter.java index be3f10acbed3..091975cf855a 100644 --- a/core/java/com/android/internal/app/SuggestedLocaleAdapter.java +++ b/core/java/com/android/internal/app/SuggestedLocaleAdapter.java @@ -28,6 +28,7 @@ import android.widget.BaseAdapter; import android.widget.Filter; import android.widget.Filterable; import android.widget.LinearLayout; +import android.widget.RadioButton; import android.widget.TextView; import com.android.internal.R; @@ -242,11 +243,7 @@ public class SuggestedLocaleAdapter extends BaseAdapter implements Filterable { break; case TYPE_SYSTEM_LANGUAGE_FOR_APP_LANGUAGE_PICKER: TextView title; - LocaleStore.LocaleInfo info = (LocaleStore.LocaleInfo) getItem(position); - if (info == null) { - throw new NullPointerException("Non header locale cannot be null."); - } - if (info.isAppCurrentLocale()) { + if (mHasSpecificAppPackageName) { title = itemView.findViewById(R.id.language_picker_item); } else { title = itemView.findViewById(R.id.locale); @@ -254,11 +251,22 @@ public class SuggestedLocaleAdapter extends BaseAdapter implements Filterable { title.setText(R.string.system_locale_title); break; case TYPE_CURRENT_LOCALE: - updateTextView(itemView, - itemView.findViewById(R.id.language_picker_item), position); + updateTextView( + itemView, itemView.findViewById(R.id.language_picker_item), position); break; default: - updateTextView(itemView, itemView.findViewById(R.id.locale), position); + LocaleStore.LocaleInfo localeInfo = (LocaleStore.LocaleInfo) getItem(position); + if (localeInfo == null) { + throw new NullPointerException("Non header locale cannot be null."); + } + if (mHasSpecificAppPackageName && localeInfo.isSuggested() && !mCountryMode) { + updateTextView( + itemView, + itemView.findViewById(R.id.language_picker_item), + position); + } else { + updateTextView(itemView, itemView.findViewById(R.id.locale), position); + } break; } return itemView; @@ -280,20 +288,21 @@ public class SuggestedLocaleAdapter extends BaseAdapter implements Filterable { } break; case TYPE_SYSTEM_LANGUAGE_FOR_APP_LANGUAGE_PICKER: - if (((LocaleStore.LocaleInfo) getItem(position)).isAppCurrentLocale()) { - shouldReuseView = convertView instanceof LinearLayout - && convertView.findViewById(R.id.language_picker_item) != null; - if (!shouldReuseView) { - updatedView = mInflater.inflate( - R.layout.app_language_picker_current_locale_item, - parent, false); + if (mHasSpecificAppPackageName) { + updatedView = mInflater.inflate( + R.layout.app_language_picker_locale_item, parent, false); + RadioButton option = updatedView.findViewById(R.id.checkbox); + if (((LocaleStore.LocaleInfo) getItem(position)).isAppCurrentLocale()) { + option.setChecked(true); + } else { + option.setChecked(false); } } else { shouldReuseView = convertView instanceof TextView - && convertView.findViewById(R.id.locale) != null; + && convertView.findViewById(R.id.locale) != null; if (!shouldReuseView) { - updatedView = mInflater.inflate( - R.layout.language_picker_item, parent, false); + updatedView = + mInflater.inflate(R.layout.language_picker_item, parent, false); } } break; @@ -302,14 +311,30 @@ public class SuggestedLocaleAdapter extends BaseAdapter implements Filterable { && convertView.findViewById(R.id.language_picker_item) != null; if (!shouldReuseView) { updatedView = mInflater.inflate( - R.layout.app_language_picker_current_locale_item, parent, false); + R.layout.app_language_picker_locale_item, parent, false); + RadioButton option = updatedView.findViewById(R.id.checkbox); + option.setChecked(true); } break; default: - shouldReuseView = convertView instanceof TextView + LocaleStore.LocaleInfo localeInfo = (LocaleStore.LocaleInfo) getItem(position); + if (mHasSpecificAppPackageName + && localeInfo.isSuggested() && !mCountryMode) { + shouldReuseView = convertView instanceof LinearLayout + && convertView.findViewById(R.id.language_picker_item) != null; + if ((!shouldReuseView)) { + updatedView = mInflater.inflate( + R.layout.app_language_picker_locale_item, parent, false); + RadioButton option = updatedView.findViewById(R.id.checkbox); + option.setChecked(false); + } + } else { + shouldReuseView = convertView instanceof TextView && convertView.findViewById(R.id.locale) != null; - if (!shouldReuseView) { - updatedView = mInflater.inflate(R.layout.language_picker_item, parent, false); + if ((!shouldReuseView)) { + updatedView = mInflater.inflate( + R.layout.language_picker_item, parent, false); + } } break; } diff --git a/core/java/com/android/internal/jank/FrameTracker.java b/core/java/com/android/internal/jank/FrameTracker.java index 0af4bea70e65..44c0bd01d545 100644 --- a/core/java/com/android/internal/jank/FrameTracker.java +++ b/core/java/com/android/internal/jank/FrameTracker.java @@ -54,6 +54,7 @@ import com.android.internal.util.FrameworkStatsLog; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.List; import java.util.concurrent.TimeUnit; /** @@ -448,7 +449,7 @@ public class FrameTracker implements HardwareRendererObserver.OnFrameMetricsAvai } @Override - public void onJankDataAvailable(SurfaceControl.JankData[] jankData) { + public void onJankDataAvailable(List<SurfaceControl.JankData> jankData) { postCallback(() -> { try { Trace.beginSection("FrameTracker#onJankDataAvailable"); @@ -832,7 +833,7 @@ public class FrameTracker implements HardwareRendererObserver.OnFrameMetricsAvai /** adds the jank listener to the given surface */ public SurfaceControl.OnJankDataListenerRegistration addJankStatsListener( SurfaceControl.OnJankDataListener listener, SurfaceControl surfaceControl) { - return surfaceControl.addJankDataListener(listener); + return surfaceControl.addOnJankDataListener(listener); } } diff --git a/core/java/com/android/internal/os/ZygoteInit.java b/core/java/com/android/internal/os/ZygoteInit.java index 2acda8ad71c1..e402ddfc637a 100644 --- a/core/java/com/android/internal/os/ZygoteInit.java +++ b/core/java/com/android/internal/os/ZygoteInit.java @@ -249,6 +249,10 @@ public class ZygoteInit { return isExperimentEnabled("profilesystemserver"); } + private static boolean shouldProfileBootClasspath() { + return isExperimentEnabled("profilebootclasspath"); + } + /** * Performs Zygote process initialization. Loads and initializes commonly used classes. * @@ -352,7 +356,7 @@ public class ZygoteInit { // If we are profiling the boot image, reset the Jit counters after preloading the // classes. We want to preload for performance, and we can use method counters to // infer what clases are used after calling resetJitCounters, for profile purposes. - if (isExperimentEnabled("profilebootclasspath")) { + if (shouldProfileBootClasspath()) { Trace.traceBegin(Trace.TRACE_TAG_DALVIK, "ResetJitCounters"); VMRuntime.resetJitCounters(); Trace.traceEnd(Trace.TRACE_TAG_DALVIK); @@ -460,12 +464,28 @@ public class ZygoteInit { ? String.join(":", systemServerClasspath, standaloneSystemServerJars) : systemServerClasspath; prepareSystemServerProfile(systemServerPaths); + try { + SystemProperties.set("debug.tracing.profile_system_server", "1"); + } catch (RuntimeException e) { + Slog.e(TAG, "Failed to set debug.tracing.profile_system_server", e); + } } catch (Exception e) { Log.wtf(TAG, "Failed to set up system server profile", e); } } } + // Zygote can't set system properties due to permission denied. We need to be in System + // Server to set system properties, so we do it here instead of the more natural place in + // preloadClasses. + if (shouldProfileBootClasspath()) { + try { + SystemProperties.set("debug.tracing.profile_boot_classpath", "1"); + } catch (RuntimeException e) { + Slog.e(TAG, "Failed to set debug.tracing.profile_boot_classpath", e); + } + } + if (parsedArgs.mInvokeWith != null) { String[] args = parsedArgs.mRemainingArgs; // If we have a non-null system server class path, we'll have to duplicate the diff --git a/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java b/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java index 032ac4283712..6b6b81f1f805 100644 --- a/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java +++ b/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java @@ -421,6 +421,10 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, @NonNull private String[] mUsesStaticLibrariesSorted; + private Map<String, Boolean> mFeatureFlagState = new ArrayMap<>(); + + private int mIntentMatchingFlags; + @NonNull public static PackageImpl forParsing(@NonNull String packageName, @NonNull String baseCodePath, @NonNull String codePath, @NonNull TypedArray manifestArray, boolean isCoreApp, @@ -2819,6 +2823,7 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, queriesProviders = Collections.unmodifiableSet(queriesProviders); mimeGroups = Collections.unmodifiableSet(mimeGroups); mKnownActivityEmbeddingCerts = Collections.unmodifiableSet(mKnownActivityEmbeddingCerts); + mFeatureFlagState = Collections.unmodifiableMap(mFeatureFlagState); } @Override @@ -3118,6 +3123,8 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, @Override public void writeToParcel(Parcel dest, int flags) { + writeFeatureFlagState(dest); + sForBoolean.parcel(this.supportsSmallScreens, dest, flags); sForBoolean.parcel(this.supportsNormalScreens, dest, flags); sForBoolean.parcel(this.supportsLargeScreens, dest, flags); @@ -3265,6 +3272,28 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, dest.writeLong(this.mBooleans); dest.writeLong(this.mBooleans2); dest.writeBoolean(this.mAllowCrossUidActivitySwitchFromBelow); + dest.writeInt(this.mIntentMatchingFlags); + } + + private void writeFeatureFlagState(@NonNull Parcel dest) { + // Use a string array to encode flag state. One string per flag in the form `<flag>=<value>` + // where value is 0 (disabled), 1 (enabled) or ? (unknown flag or value). + int featureFlagCount = this.mFeatureFlagState.size(); + String[] featureFlagStateAsArray = new String[featureFlagCount]; + var entryIterator = this.mFeatureFlagState.entrySet().iterator(); + for (int i = 0; i < featureFlagCount; i++) { + var entry = entryIterator.next(); + Boolean flagValue = entry.getValue(); + if (flagValue == null) { + featureFlagStateAsArray[i] = entry.getKey() + "=?"; + } else if (flagValue.booleanValue()) { + featureFlagStateAsArray[i] = entry.getKey() + "=1"; + } else { + featureFlagStateAsArray[i] = entry.getKey() + "=0"; + } + + } + dest.writeStringArray(featureFlagStateAsArray); } public PackageImpl(Parcel in) { @@ -3275,6 +3304,9 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, mCallback = callback; // We use the boot classloader for all classes that we load. final ClassLoader boot = Object.class.getClassLoader(); + + readFeatureFlagState(in); + this.supportsSmallScreens = sForBoolean.unparcel(in); this.supportsNormalScreens = sForBoolean.unparcel(in); this.supportsLargeScreens = sForBoolean.unparcel(in); @@ -3432,6 +3464,7 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, this.mBooleans = in.readLong(); this.mBooleans2 = in.readLong(); this.mAllowCrossUidActivitySwitchFromBelow = in.readBoolean(); + this.mIntentMatchingFlags = in.readInt(); assignDerivedFields(); assignDerivedFields2(); @@ -3440,6 +3473,27 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, // to mutate this instance before it's finalized. } + private void readFeatureFlagState(@NonNull Parcel in) { + // See comment in writeFeatureFlagState() for encoding of flag state. + String[] featureFlagStateAsArray = in.createStringArray(); + for (String s : featureFlagStateAsArray) { + int sepIndex = s.lastIndexOf('='); + if (sepIndex >= 0 && sepIndex == s.length() - 2) { + String flagPackageAndName = s.substring(0, sepIndex); + char c = s.charAt(sepIndex + 1); + Boolean flagValue = null; + if (c == '1') { + flagValue = Boolean.TRUE; + } else if (c == '0') { + flagValue = Boolean.FALSE; + } else if (c != '?') { + continue; + } + this.mFeatureFlagState.put(flagPackageAndName, flagValue); + } + } + } + @NonNull public static final Creator<PackageImpl> CREATOR = new Creator<PackageImpl>() { @Override @@ -3649,6 +3703,17 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, return this; } + @Override + public ParsingPackage setIntentMatchingFlags(int intentMatchingFlags) { + mIntentMatchingFlags = intentMatchingFlags; + return this; + } + + @Override + public int getIntentMatchingFlags() { + return mIntentMatchingFlags; + } + // The following methods are explicitly not inside any interface. These are hidden under // PackageImpl which is only accessible to the system server. This is to prevent/discourage // usage of these fields outside of the utility classes. @@ -3660,6 +3725,18 @@ public class PackageImpl implements ParsedPackage, AndroidPackageInternal, return mBaseAppDataDeviceProtectedDirForSystemUser; } + @Override + public PackageImpl addFeatureFlag( + @NonNull String flagPackageAndName, + @Nullable Boolean flagValue) { + mFeatureFlagState.put(flagPackageAndName, flagValue); + return this; + } + + public Map<String, Boolean> getFeatureFlagState() { + return mFeatureFlagState; + } + /** * Flags used for a internal bitset. These flags should never be persisted or exposed outside * of this class. It is expected that PackageCacher explicitly clears itself whenever the diff --git a/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java b/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java index 8faaf9584e54..70b7953ed364 100644 --- a/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java +++ b/core/java/com/android/internal/pm/pkg/component/AconfigFlags.java @@ -33,6 +33,7 @@ import android.util.Slog; import android.util.Xml; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.pm.pkg.parsing.ParsingPackage; import com.android.modules.utils.TypedXmlPullParser; import org.xmlpull.v1.XmlPullParser; @@ -199,7 +200,7 @@ public class AconfigFlags { * @return the current value of the given Aconfig flag, or null if there is no such flag */ @Nullable - private Boolean getFlagValue(@NonNull String flagPackageAndName) { + public Boolean getFlagValue(@NonNull String flagPackageAndName) { Boolean value = mFlagValues.get(flagPackageAndName); if (DEBUG) { Slog.v(LOG_TAG, "Aconfig flag value for " + flagPackageAndName + " = " + value); @@ -209,10 +210,13 @@ public class AconfigFlags { /** * Check if the element in {@code parser} should be skipped because of the feature flag. + * @param pkg The package being parsed * @param parser XML parser object currently parsing an element * @return true if the element is disabled because of its feature flag */ - public boolean skipCurrentElement(@NonNull XmlResourceParser parser) { + public boolean skipCurrentElement( + @NonNull ParsingPackage pkg, + @NonNull XmlResourceParser parser) { if (!Flags.manifestFlagging()) { return false; } @@ -227,18 +231,21 @@ public class AconfigFlags { featureFlag = featureFlag.substring(1).strip(); } final Boolean flagValue = getFlagValue(featureFlag); + boolean shouldSkip = false; if (flagValue == null) { Slog.w(LOG_TAG, "Skipping element " + parser.getName() + " due to unknown feature flag " + featureFlag); - return true; - } - // Skip if flag==false && attr=="flag" OR flag==true && attr=="!flag" (negated) - if (flagValue == negated) { + shouldSkip = true; + } else if (flagValue == negated) { + // Skip if flag==false && attr=="flag" OR flag==true && attr=="!flag" (negated) Slog.i(LOG_TAG, "Skipping element " + parser.getName() + " behind feature flag " + featureFlag + " = " + flagValue); - return true; + shouldSkip = true; + } + if (android.content.pm.Flags.includeFeatureFlagsInPackageCacher()) { + pkg.addFeatureFlag(featureFlag, flagValue); } - return false; + return shouldSkip; } /** diff --git a/core/java/com/android/internal/pm/pkg/component/ComponentParseUtils.java b/core/java/com/android/internal/pm/pkg/component/ComponentParseUtils.java index 8858f9492890..335dedd17f75 100644 --- a/core/java/com/android/internal/pm/pkg/component/ComponentParseUtils.java +++ b/core/java/com/android/internal/pm/pkg/component/ComponentParseUtils.java @@ -61,7 +61,7 @@ public class ComponentParseUtils { if (type != XmlPullParser.START_TAG) { continue; } - if (ParsingPackageUtils.getAconfigFlags().skipCurrentElement(parser)) { + if (ParsingPackageUtils.getAconfigFlags().skipCurrentElement(pkg, parser)) { continue; } diff --git a/core/java/com/android/internal/pm/pkg/component/InstallConstraintsTagParser.java b/core/java/com/android/internal/pm/pkg/component/InstallConstraintsTagParser.java index bb015812c225..5f48d16a7b87 100644 --- a/core/java/com/android/internal/pm/pkg/component/InstallConstraintsTagParser.java +++ b/core/java/com/android/internal/pm/pkg/component/InstallConstraintsTagParser.java @@ -54,7 +54,7 @@ public class InstallConstraintsTagParser { return input.skip("install-constraints cannot be used by this package"); } - ParseResult<Set<String>> prefixes = parseFingerprintPrefixes(input, res, parser); + ParseResult<Set<String>> prefixes = parseFingerprintPrefixes(input, pkg, res, parser); if (prefixes.isSuccess()) { if (validateFingerprintPrefixes(prefixes.getResult())) { return input.success(pkg); @@ -68,7 +68,7 @@ public class InstallConstraintsTagParser { } private static ParseResult<Set<String>> parseFingerprintPrefixes( - ParseInput input, Resources res, XmlResourceParser parser) + ParseInput input, ParsingPackage pkg, Resources res, XmlResourceParser parser) throws XmlPullParserException, IOException { Set<String> prefixes = new ArraySet<>(); int type; @@ -81,7 +81,7 @@ public class InstallConstraintsTagParser { } return input.success(prefixes); } else if (type == XmlPullParser.START_TAG) { - if (ParsingPackageUtils.getAconfigFlags().skipCurrentElement(parser)) { + if (ParsingPackageUtils.getAconfigFlags().skipCurrentElement(pkg, parser)) { continue; } if (parser.getName().equals(TAG_FINGERPRINT_PREFIX)) { diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedActivityUtils.java b/core/java/com/android/internal/pm/pkg/component/ParsedActivityUtils.java index 55baa532b434..219e885331ee 100644 --- a/core/java/com/android/internal/pm/pkg/component/ParsedActivityUtils.java +++ b/core/java/com/android/internal/pm/pkg/component/ParsedActivityUtils.java @@ -109,7 +109,8 @@ public class ParsedActivityUtils { R.styleable.AndroidManifestActivity_process, R.styleable.AndroidManifestActivity_roundIcon, R.styleable.AndroidManifestActivity_splitName, - R.styleable.AndroidManifestActivity_attributionTags); + R.styleable.AndroidManifestActivity_attributionTags, + R.styleable.AndroidManifestActivity_intentMatchingFlags); if (result.isError()) { return input.error(result); } @@ -310,7 +311,8 @@ public class ParsedActivityUtils { NOT_SET /*processAttr*/, R.styleable.AndroidManifestActivityAlias_roundIcon, NOT_SET /*splitNameAttr*/, - R.styleable.AndroidManifestActivityAlias_attributionTags); + R.styleable.AndroidManifestActivityAlias_attributionTags, + R.styleable.AndroidManifestActivityAlias_intentMatchingFlags); if (result.isError()) { return input.error(result); } @@ -393,7 +395,7 @@ public class ParsedActivityUtils { if (type != XmlPullParser.START_TAG) { continue; } - if (ParsingPackageUtils.getAconfigFlags().skipCurrentElement(parser)) { + if (ParsingPackageUtils.getAconfigFlags().skipCurrentElement(pkg, parser)) { continue; } diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedIntentInfoUtils.java b/core/java/com/android/internal/pm/pkg/component/ParsedIntentInfoUtils.java index da48b23a2b81..39d7af6c4c4a 100644 --- a/core/java/com/android/internal/pm/pkg/component/ParsedIntentInfoUtils.java +++ b/core/java/com/android/internal/pm/pkg/component/ParsedIntentInfoUtils.java @@ -99,7 +99,7 @@ public class ParsedIntentInfoUtils { if (type != XmlPullParser.START_TAG) { continue; } - if (ParsingPackageUtils.getAconfigFlags().skipCurrentElement(parser)) { + if (ParsingPackageUtils.getAconfigFlags().skipCurrentElement(pkg, parser)) { continue; } @@ -200,7 +200,7 @@ public class ParsedIntentInfoUtils { if (type != XmlPullParser.START_TAG) { continue; } - if (ParsingPackageUtils.getAconfigFlags().skipCurrentElement(parser)) { + if (ParsingPackageUtils.getAconfigFlags().skipCurrentElement(pkg, parser)) { continue; } diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedMainComponent.java b/core/java/com/android/internal/pm/pkg/component/ParsedMainComponent.java index 291ed0c44ddd..54e67360ab8b 100644 --- a/core/java/com/android/internal/pm/pkg/component/ParsedMainComponent.java +++ b/core/java/com/android/internal/pm/pkg/component/ParsedMainComponent.java @@ -45,4 +45,9 @@ public interface ParsedMainComponent extends ParsedComponent { @Nullable String getSplitName(); + + /** + * Returns the intent matching flags. + */ + int getIntentMatchingFlags(); } diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedMainComponentImpl.java b/core/java/com/android/internal/pm/pkg/component/ParsedMainComponentImpl.java index bb8f565d2032..678e9990f89c 100644 --- a/core/java/com/android/internal/pm/pkg/component/ParsedMainComponentImpl.java +++ b/core/java/com/android/internal/pm/pkg/component/ParsedMainComponentImpl.java @@ -24,7 +24,6 @@ import android.os.Parcel; import android.os.Parcelable; import android.text.TextUtils; -import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.DataClass; import com.android.internal.util.Parcelling.BuiltIn.ForInternedString; @@ -34,7 +33,6 @@ import libcore.util.EmptyArray; * @hide */ @DataClass(genGetters = true, genSetters = true, genBuilder = false, genParcelable = false) -@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) public class ParsedMainComponentImpl extends ParsedComponentImpl implements ParsedMainComponent, Parcelable { @@ -51,6 +49,30 @@ public class ParsedMainComponentImpl extends ParsedComponentImpl implements Pars @Nullable private String[] attributionTags; + private int mIntentMatchingFlags; + + /** + * Opt-out of all intent filter matching rules. The value corresponds to the <code>none</code> + * value of {@link android.R.attr#intentMatchingFlags} + * @hide + */ + public static final int INTENT_MATCHING_FLAGS_NONE = 1; + + /** + * Opt-in to enforce intent filter matching. The value corresponds to the + * <code>enforceIntentFilter</code> value of {@link android.R.attr#intentMatchingFlags} + * @hide + */ + public static final int INTENT_MATCHING_FLAGS_ENFORCE_INTENT_FILTER = 1 << 1; + + /** + * Allows intent filters to match actions even when the action value is null. The value + * corresponds to the <code>allowNullAction</code> value of + * {@link android.R.attr#intentMatchingFlags} + * @hide + */ + public static final int INTENT_MATCHING_FLAGS_ALLOW_NULL_ACTION = 1 << 2; + public ParsedMainComponentImpl() { } @@ -83,6 +105,20 @@ public class ParsedMainComponentImpl extends ParsedComponentImpl implements Pars return attributionTags == null ? EmptyArray.STRING : attributionTags; } + /** + * Sets the intent matching flags. This value is intended to be set from the "component" tags. + * @see android.R.styleable#AndroidManifestApplication_intentMatchingFlags + */ + public ParsedMainComponent setIntentMatchingFlags(int intentMatchingFlags) { + mIntentMatchingFlags = intentMatchingFlags; + return this; + } + + @Override + public int getIntentMatchingFlags() { + return this.mIntentMatchingFlags; + } + @Override public int describeContents() { return 0; @@ -98,6 +134,7 @@ public class ParsedMainComponentImpl extends ParsedComponentImpl implements Pars dest.writeInt(this.order); dest.writeString(this.splitName); dest.writeString8Array(this.attributionTags); + dest.writeInt(this.mIntentMatchingFlags); } protected ParsedMainComponentImpl(Parcel in) { @@ -109,6 +146,7 @@ public class ParsedMainComponentImpl extends ParsedComponentImpl implements Pars this.order = in.readInt(); this.splitName = in.readString(); this.attributionTags = in.createString8Array(); + this.mIntentMatchingFlags = in.readInt(); } public static final Parcelable.Creator<ParsedMainComponentImpl> CREATOR = @@ -139,6 +177,28 @@ public class ParsedMainComponentImpl extends ParsedComponentImpl implements Pars //@formatter:off + @android.annotation.IntDef(prefix = "INTENT_MATCHING_FLAGS_", value = { + INTENT_MATCHING_FLAGS_NONE, + INTENT_MATCHING_FLAGS_ENFORCE_INTENT_FILTER, + INTENT_MATCHING_FLAGS_ALLOW_NULL_ACTION + }) + @java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.SOURCE) + @DataClass.Generated.Member + public @interface IntentMatchingFlags {} + + @DataClass.Generated.Member + public static String intentMatchingFlagsToString(@IntentMatchingFlags int value) { + switch (value) { + case INTENT_MATCHING_FLAGS_NONE: + return "INTENT_MATCHING_FLAGS_NONE"; + case INTENT_MATCHING_FLAGS_ENFORCE_INTENT_FILTER: + return "INTENT_MATCHING_FLAGS_ENFORCE_INTENT_FILTER"; + case INTENT_MATCHING_FLAGS_ALLOW_NULL_ACTION: + return "INTENT_MATCHING_FLAGS_ALLOW_NULL_ACTION"; + default: return Integer.toHexString(value); + } + } + @DataClass.Generated.Member public ParsedMainComponentImpl( @Nullable String processName, @@ -147,7 +207,8 @@ public class ParsedMainComponentImpl extends ParsedComponentImpl implements Pars boolean exported, int order, @Nullable String splitName, - @Nullable String[] attributionTags) { + @Nullable String[] attributionTags, + int intentMatchingFlags) { this.processName = processName; this.directBootAware = directBootAware; this.enabled = enabled; @@ -155,6 +216,7 @@ public class ParsedMainComponentImpl extends ParsedComponentImpl implements Pars this.order = order; this.splitName = splitName; this.attributionTags = attributionTags; + this.mIntentMatchingFlags = intentMatchingFlags; // onConstructed(); // You can define this method to get a callback } @@ -226,10 +288,10 @@ public class ParsedMainComponentImpl extends ParsedComponentImpl implements Pars } @DataClass.Generated( - time = 1701447884766L, + time = 1729613643190L, codegenVersion = "1.0.23", sourceFile = "frameworks/base/core/java/com/android/internal/pm/pkg/component/ParsedMainComponentImpl.java", - inputSignatures = "private @android.annotation.Nullable @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInternedString.class) java.lang.String processName\nprivate boolean directBootAware\nprivate boolean enabled\nprivate boolean exported\nprivate int order\nprivate @android.annotation.Nullable java.lang.String splitName\nprivate @android.annotation.Nullable java.lang.String[] attributionTags\npublic static final android.os.Parcelable.Creator<com.android.internal.pm.pkg.component.ParsedMainComponentImpl> CREATOR\npublic com.android.internal.pm.pkg.component.ParsedMainComponentImpl setProcessName(java.lang.String)\npublic java.lang.String getClassName()\npublic @android.annotation.NonNull @java.lang.Override java.lang.String[] getAttributionTags()\npublic @java.lang.Override int describeContents()\npublic @java.lang.Override void writeToParcel(android.os.Parcel,int)\nclass ParsedMainComponentImpl extends com.android.internal.pm.pkg.component.ParsedComponentImpl implements [com.android.internal.pm.pkg.component.ParsedMainComponent, android.os.Parcelable]\n@com.android.internal.util.DataClass(genGetters=true, genSetters=true, genBuilder=false, genParcelable=false)") + inputSignatures = "private @android.annotation.Nullable @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInternedString.class) java.lang.String processName\nprivate boolean directBootAware\nprivate boolean enabled\nprivate boolean exported\nprivate int order\nprivate @android.annotation.Nullable java.lang.String splitName\nprivate @android.annotation.Nullable java.lang.String[] attributionTags\nprivate int mIntentMatchingFlags\npublic static final int INTENT_MATCHING_FLAGS_NONE\npublic static final int INTENT_MATCHING_FLAGS_ENFORCE_INTENT_FILTER\npublic static final int INTENT_MATCHING_FLAGS_ALLOW_NULL_ACTION\npublic static final android.os.Parcelable.Creator<com.android.internal.pm.pkg.component.ParsedMainComponentImpl> CREATOR\npublic com.android.internal.pm.pkg.component.ParsedMainComponentImpl setProcessName(java.lang.String)\npublic java.lang.String getClassName()\npublic @android.annotation.NonNull @java.lang.Override java.lang.String[] getAttributionTags()\npublic com.android.internal.pm.pkg.component.ParsedMainComponent setIntentMatchingFlags(int)\npublic @java.lang.Override int getIntentMatchingFlags()\npublic @java.lang.Override int describeContents()\npublic @java.lang.Override void writeToParcel(android.os.Parcel,int)\nclass ParsedMainComponentImpl extends com.android.internal.pm.pkg.component.ParsedComponentImpl implements [com.android.internal.pm.pkg.component.ParsedMainComponent, android.os.Parcelable]\n@com.android.internal.util.DataClass(genGetters=true, genSetters=true, genBuilder=false, genParcelable=false)") @Deprecated private void __metadata() {} diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedMainComponentUtils.java b/core/java/com/android/internal/pm/pkg/component/ParsedMainComponentUtils.java index 7e56180f72ce..4feb894448f6 100644 --- a/core/java/com/android/internal/pm/pkg/component/ParsedMainComponentUtils.java +++ b/core/java/com/android/internal/pm/pkg/component/ParsedMainComponentUtils.java @@ -18,6 +18,7 @@ package com.android.internal.pm.pkg.component; import static com.android.internal.pm.pkg.parsing.ParsingUtils.NOT_SET; +import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; import android.content.IntentFilter; @@ -39,7 +40,8 @@ import org.xmlpull.v1.XmlPullParserException; import java.io.IOException; /** @hide */ -class ParsedMainComponentUtils { +@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) +public class ParsedMainComponentUtils { private static final String TAG = ParsingUtils.TAG; @@ -47,10 +49,11 @@ class ParsedMainComponentUtils { @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) static <Component extends ParsedMainComponentImpl> ParseResult<Component> parseMainComponent( Component component, String tag, String[] separateProcesses, ParsingPackage pkg, - TypedArray array, int flags, boolean useRoundIcon, @Nullable String defaultSplitName, + TypedArray array, int flags, boolean useRoundIcon, @Nullable String defaultSplitName, @NonNull ParseInput input, int bannerAttr, int descriptionAttr, int directBootAwareAttr, int enabledAttr, int iconAttr, int labelAttr, int logoAttr, int nameAttr, - int processAttr, int roundIconAttr, int splitNameAttr, int attributionTagsAttr) { + int processAttr, int roundIconAttr, int splitNameAttr, int attributionTagsAttr, + int intentMatchingFlagsAttr) { ParseResult<Component> result = ParsedComponentUtils.parseComponent(component, tag, pkg, array, useRoundIcon, input, bannerAttr, descriptionAttr, iconAttr, labelAttr, logoAttr, nameAttr, roundIconAttr); @@ -107,6 +110,12 @@ class ParsedMainComponentUtils { } } + if (android.security.Flags.enableIntentMatchingFlags()) { + int resolvedFlags = resolveIntentMatchingFlags( + pkg.getIntentMatchingFlags(), array.getInt(intentMatchingFlagsAttr, 0)); + component.setIntentMatchingFlags(resolvedFlags); + } + return input.success(component); } @@ -147,4 +156,21 @@ class ParsedMainComponentUtils { return input.success(intentResult.getResult()); } + /** + * Resolves intent matching flags from the application and a component, prioritizing the + * component's flags. + * + * @param applicationFlags The flag value from the "application" tag. + * @param componentFlags The flag value from the "component" tags. + * @return The resolved intent matching flags. + */ + @FlaggedApi(android.security.Flags.FLAG_ENABLE_INTENT_MATCHING_FLAGS) + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) + public static int resolveIntentMatchingFlags(int applicationFlags, int componentFlags) { + if (componentFlags == 0) { + return applicationFlags; + } else { + return componentFlags; + } + } } diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedProviderUtils.java b/core/java/com/android/internal/pm/pkg/component/ParsedProviderUtils.java index 6af2a29822a2..5c39827a7ec7 100644 --- a/core/java/com/android/internal/pm/pkg/component/ParsedProviderUtils.java +++ b/core/java/com/android/internal/pm/pkg/component/ParsedProviderUtils.java @@ -79,7 +79,8 @@ public class ParsedProviderUtils { R.styleable.AndroidManifestProvider_process, R.styleable.AndroidManifestProvider_roundIcon, R.styleable.AndroidManifestProvider_splitName, - R.styleable.AndroidManifestProvider_attributionTags); + R.styleable.AndroidManifestProvider_attributionTags, + R.styleable.AndroidManifestProvider_intentMatchingFlags); if (result.isError()) { return input.error(result); } @@ -174,7 +175,7 @@ public class ParsedProviderUtils { if (type != XmlPullParser.START_TAG) { continue; } - if (ParsingPackageUtils.getAconfigFlags().skipCurrentElement(parser)) { + if (ParsingPackageUtils.getAconfigFlags().skipCurrentElement(pkg, parser)) { continue; } diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java b/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java index c68ea2dc8516..c469a7a5bc05 100644 --- a/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java +++ b/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java @@ -74,7 +74,8 @@ public class ParsedServiceUtils { R.styleable.AndroidManifestService_process, R.styleable.AndroidManifestService_roundIcon, R.styleable.AndroidManifestService_splitName, - R.styleable.AndroidManifestService_attributionTags + R.styleable.AndroidManifestService_attributionTags, + R.styleable.AndroidManifestService_intentMatchingFlags ); if (result.isError()) { @@ -138,7 +139,7 @@ public class ParsedServiceUtils { if (type != XmlPullParser.START_TAG) { continue; } - if (ParsingPackageUtils.getAconfigFlags().skipCurrentElement(parser)) { + if (ParsingPackageUtils.getAconfigFlags().skipCurrentElement(pkg, parser)) { continue; } diff --git a/core/java/com/android/internal/pm/pkg/parsing/ParsingPackage.java b/core/java/com/android/internal/pm/pkg/parsing/ParsingPackage.java index 5d185af17d48..f4bceb880617 100644 --- a/core/java/com/android/internal/pm/pkg/parsing/ParsingPackage.java +++ b/core/java/com/android/internal/pm/pkg/parsing/ParsingPackage.java @@ -123,6 +123,9 @@ public interface ParsingPackage { ParsingPackage addQueriesProvider(String authority); + /** Adds a feature flag (`android:featureFlag` attribute) encountered in the manifest. */ + ParsingPackage addFeatureFlag(@NonNull String flagPackageAndName, @Nullable Boolean flagValue); + /** Sets a process name -> {@link ParsedProcess} map coming from the <processes> tag. */ ParsingPackage setProcesses(@NonNull Map<String, ParsedProcess> processes); @@ -550,4 +553,15 @@ public interface ParsingPackage { boolean isNormalScreensSupported(); boolean isSmallScreensSupported(); + + /** + * Sets the intent matching flags. This value is intended to be set from the "application" tag. + * @see android.R.styleable#AndroidManifestApplication_intentMatchingFlags + */ + ParsingPackage setIntentMatchingFlags(int intentMatchingFlags); + + /** + * Returns the intent matching flags. + */ + int getIntentMatchingFlags(); } diff --git a/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java b/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java index 787006eb214c..5db7b4197658 100644 --- a/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java +++ b/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java @@ -763,7 +763,7 @@ public class ParsingPackageUtils { if (outerDepth + 1 < parser.getDepth() || type != XmlPullParser.START_TAG) { continue; } - if (sAconfigFlags.skipCurrentElement(parser)) { + if (sAconfigFlags.skipCurrentElement(pkg, parser)) { continue; } @@ -842,7 +842,7 @@ public class ParsingPackageUtils { if (type != XmlPullParser.START_TAG) { continue; } - if (sAconfigFlags.skipCurrentElement(parser)) { + if (sAconfigFlags.skipCurrentElement(pkg, parser)) { continue; } @@ -988,7 +988,7 @@ public class ParsingPackageUtils { if (type != XmlPullParser.START_TAG) { continue; } - if (sAconfigFlags.skipCurrentElement(parser)) { + if (sAconfigFlags.skipCurrentElement(pkg, parser)) { continue; } @@ -1610,7 +1610,7 @@ public class ParsingPackageUtils { if (type != XmlPullParser.START_TAG) { continue; } - if (sAconfigFlags.skipCurrentElement(parser)) { + if (sAconfigFlags.skipCurrentElement(pkg, parser)) { continue; } @@ -1853,7 +1853,7 @@ public class ParsingPackageUtils { if (type != XmlPullParser.START_TAG) { continue; } - if (sAconfigFlags.skipCurrentElement(parser)) { + if (sAconfigFlags.skipCurrentElement(pkg, parser)) { continue; } if (parser.getName().equals("intent")) { @@ -2187,6 +2187,8 @@ public class ParsingPackageUtils { pkg.setKnownActivityEmbeddingCerts(knownActivityEmbeddingCerts); } } + pkg.setIntentMatchingFlags( + sa.getInt(R.styleable.AndroidManifestApplication_intentMatchingFlags, 0)); } finally { sa.recycle(); } @@ -2202,7 +2204,7 @@ public class ParsingPackageUtils { if (type != XmlPullParser.START_TAG) { continue; } - if (sAconfigFlags.skipCurrentElement(parser)) { + if (sAconfigFlags.skipCurrentElement(pkg, parser)) { continue; } @@ -2620,7 +2622,7 @@ public class ParsingPackageUtils { } } - ParseResult<String[]> certResult = parseAdditionalCertificates(input, res, parser); + ParseResult<String[]> certResult = parseAdditionalCertificates(input, pkg, res, parser); if (certResult.isError()) { return input.error(certResult); } @@ -2674,7 +2676,8 @@ public class ParsingPackageUtils { // Fot apps targeting O-MR1 we require explicit enumeration of all certs. String[] additionalCertSha256Digests = EmptyArray.STRING; if (pkg.getTargetSdkVersion() >= Build.VERSION_CODES.O_MR1) { - ParseResult<String[]> certResult = parseAdditionalCertificates(input, res, parser); + ParseResult<String[]> certResult = + parseAdditionalCertificates(input, pkg, res, parser); if (certResult.isError()) { return input.error(certResult); } @@ -2782,7 +2785,7 @@ public class ParsingPackageUtils { } private static ParseResult<String[]> parseAdditionalCertificates(ParseInput input, - Resources resources, XmlResourceParser parser) + ParsingPackage pkg, Resources resources, XmlResourceParser parser) throws XmlPullParserException, IOException { String[] certSha256Digests = EmptyArray.STRING; final int depth = parser.getDepth(); @@ -2793,7 +2796,7 @@ public class ParsingPackageUtils { if (type != XmlPullParser.START_TAG) { continue; } - if (sAconfigFlags.skipCurrentElement(parser)) { + if (sAconfigFlags.skipCurrentElement(pkg, parser)) { continue; } diff --git a/core/java/com/android/internal/protolog/ProcessedPerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/ProcessedPerfettoProtoLogImpl.java index febe1f3a72ac..70d148a311f6 100644 --- a/core/java/com/android/internal/protolog/ProcessedPerfettoProtoLogImpl.java +++ b/core/java/com/android/internal/protolog/ProcessedPerfettoProtoLogImpl.java @@ -132,9 +132,9 @@ public class ProcessedPerfettoProtoLogImpl extends PerfettoProtoLogImpl { @Deprecated @Override void dumpViewerConfig() { - Log.d(LOG_TAG, "Dumping viewer config to trace"); + Log.d(LOG_TAG, "Dumping viewer config to trace from " + mViewerConfigFilePath); Utils.dumpViewerConfig(mDataSource, mViewerConfigInputStreamProvider); - Log.d(LOG_TAG, "Dumped viewer config to trace"); + Log.d(LOG_TAG, "Successfully dumped viewer config to trace from " + mViewerConfigFilePath); } @NonNull diff --git a/core/java/com/android/internal/protolog/TEST_MAPPING b/core/java/com/android/internal/protolog/TEST_MAPPING index b51d19da97a5..545ba1190d2c 100644 --- a/core/java/com/android/internal/protolog/TEST_MAPPING +++ b/core/java/com/android/internal/protolog/TEST_MAPPING @@ -1,5 +1,5 @@ { - "postsubmit": [ + "presubmit": [ { "name": "TracingTests" }, diff --git a/core/java/com/android/internal/telephony/ISatelliteStateChangeListener.aidl b/core/java/com/android/internal/telephony/ISatelliteStateChangeListener.aidl new file mode 100644 index 000000000000..4d195c2028b5 --- /dev/null +++ b/core/java/com/android/internal/telephony/ISatelliteStateChangeListener.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 com.android.internal.telephony; + +oneway interface ISatelliteStateChangeListener { + void onSatelliteEnabledStateChanged(boolean isEnabled); +}
\ No newline at end of file diff --git a/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl b/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl index ca75abdedfcc..1c76a6cd4bba 100644 --- a/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl +++ b/core/java/com/android/internal/telephony/ITelephonyRegistry.aidl @@ -38,6 +38,7 @@ import com.android.internal.telephony.ICarrierConfigChangeListener; import com.android.internal.telephony.ICarrierPrivilegesCallback; import com.android.internal.telephony.IPhoneStateListener; import com.android.internal.telephony.IOnSubscriptionsChangedListener; +import com.android.internal.telephony.ISatelliteStateChangeListener; interface ITelephonyRegistry { void addOnSubscriptionsChangedListener(String pkg, String featureId, @@ -124,4 +125,8 @@ interface ITelephonyRegistry { void notifyCarrierRoamingNtnModeChanged(int subId, in boolean active); void notifyCarrierRoamingNtnEligibleStateChanged(int subId, in boolean eligible); void notifyCarrierRoamingNtnAvailableServicesChanged(int subId, in int[] availableServices); + + void addSatelliteStateChangeListener(ISatelliteStateChangeListener listener, String pkg, String featureId); + void removeSatelliteStateChangeListener(ISatelliteStateChangeListener listener, String pkg); + void notifySatelliteStateChanged(boolean isEnabled); } diff --git a/services/core/java/com/android/server/FgThread.java b/core/java/com/android/server/FgThread.java index b4b6e3f1c3c1..f8a6bb06798e 100644 --- a/services/core/java/com/android/server/FgThread.java +++ b/core/java/com/android/server/FgThread.java @@ -30,7 +30,10 @@ import java.util.concurrent.Executor; * relatively long-running operations like saving state to disk (in addition to * simply being a background priority), which can cause operations scheduled on it * to be delayed for a user-noticeable amount of time. + * + * @hide */ +@android.ravenwood.annotation.RavenwoodKeepWholeClass public final class FgThread extends ServiceThread { private static final long SLOW_DISPATCH_THRESHOLD_MS = 100; private static final long SLOW_DELIVERY_THRESHOLD_MS = 200; diff --git a/services/core/java/com/android/server/ServiceThread.java b/core/java/com/android/server/ServiceThread.java index 6d8e49c7c869..86e507b76771 100644 --- a/services/core/java/com/android/server/ServiceThread.java +++ b/core/java/com/android/server/ServiceThread.java @@ -24,7 +24,10 @@ import android.os.StrictMode; /** * Special handler thread that we create for system services that require their own loopers. + * + * @hide */ +@android.ravenwood.annotation.RavenwoodKeepWholeClass public class ServiceThread extends HandlerThread { private static final String TAG = "ServiceThread"; diff --git a/core/java/com/android/server/pm/pkg/AndroidPackage.java b/core/java/com/android/server/pm/pkg/AndroidPackage.java index d430fe32a64e..53500596f938 100644 --- a/core/java/com/android/server/pm/pkg/AndroidPackage.java +++ b/core/java/com/android/server/pm/pkg/AndroidPackage.java @@ -1514,4 +1514,10 @@ public interface AndroidPackage { * @hide */ boolean isAllowCrossUidActivitySwitchFromBelow(); + + /** + * Returns the intent matching flags. + * @hide + */ + int getIntentMatchingFlags(); } diff --git a/services/core/java/com/android/server/servicewatcher/CurrentUserServiceSupplier.java b/core/java/com/android/server/servicewatcher/CurrentUserServiceSupplier.java index 152623090314..e8aeb8653d06 100644 --- a/services/core/java/com/android/server/servicewatcher/CurrentUserServiceSupplier.java +++ b/core/java/com/android/server/servicewatcher/CurrentUserServiceSupplier.java @@ -63,6 +63,8 @@ import java.util.Objects; * <p>Optionally, two permissions may be specified: (1) a caller permission - any service that does * not require callers to hold this permission is rejected (2) a service permission - any service * whose package does not hold this permission is rejected. + * + * @hide */ public final class CurrentUserServiceSupplier extends BroadcastReceiver implements ServiceSupplier<CurrentUserServiceSupplier.BoundServiceInfo> { diff --git a/services/core/java/com/android/server/servicewatcher/OWNERS b/core/java/com/android/server/servicewatcher/OWNERS index ced619f05f1d..ced619f05f1d 100644 --- a/services/core/java/com/android/server/servicewatcher/OWNERS +++ b/core/java/com/android/server/servicewatcher/OWNERS diff --git a/services/core/java/com/android/server/servicewatcher/ServiceWatcher.java b/core/java/com/android/server/servicewatcher/ServiceWatcher.java index 56367180ec15..831ff67a02a5 100644 --- a/services/core/java/com/android/server/servicewatcher/ServiceWatcher.java +++ b/core/java/com/android/server/servicewatcher/ServiceWatcher.java @@ -16,6 +16,10 @@ package com.android.server.servicewatcher; +import static android.content.Context.BIND_AUTO_CREATE; +import static android.content.Context.BIND_NOT_FOREGROUND; +import static android.content.Context.BIND_NOT_VISIBLE; + import android.annotation.Nullable; import android.annotation.UserIdInt; import android.content.ComponentName; @@ -52,6 +56,8 @@ import java.util.Objects; * whether any particular {@link BinderOperation} will succeed. Clients must ensure they do not rely * on this, and instead use {@link ServiceListener} notifications as necessary to recover from * failures. + * + * @hide */ public interface ServiceWatcher { @@ -144,6 +150,10 @@ public interface ServiceWatcher { protected final @Nullable String mAction; protected final int mUid; protected final ComponentName mComponentName; + private final int mFlags; + + private static final int DEFAULT_FLAGS = + BIND_AUTO_CREATE | BIND_NOT_FOREGROUND | BIND_NOT_VISIBLE; protected BoundServiceInfo(String action, ResolveInfo resolveInfo) { this(action, resolveInfo.serviceInfo.applicationInfo.uid, @@ -151,9 +161,14 @@ public interface ServiceWatcher { } protected BoundServiceInfo(String action, int uid, ComponentName componentName) { + this(action, uid, componentName, DEFAULT_FLAGS); + } + + protected BoundServiceInfo(String action, int uid, ComponentName componentName, int flags) { mAction = action; mUid = uid; mComponentName = Objects.requireNonNull(componentName); + mFlags = flags; } /** Returns the action associated with this bound service. */ @@ -171,6 +186,11 @@ public interface ServiceWatcher { return UserHandle.getUserId(mUid); } + /** Returns flags used when binding the service. */ + public int getFlags() { + return mFlags; + } + @Override public final boolean equals(Object o) { if (this == o) { @@ -183,12 +203,13 @@ public interface ServiceWatcher { BoundServiceInfo that = (BoundServiceInfo) o; return mUid == that.mUid && Objects.equals(mAction, that.mAction) - && mComponentName.equals(that.mComponentName); + && mComponentName.equals(that.mComponentName) + && mFlags == that.mFlags; } @Override public final int hashCode() { - return Objects.hash(mAction, mUid, mComponentName); + return Objects.hash(mAction, mUid, mComponentName, mFlags); } @Override @@ -256,4 +277,4 @@ public interface ServiceWatcher { * Dumps ServiceWatcher information. */ void dump(PrintWriter pw); -}
\ No newline at end of file +} diff --git a/services/core/java/com/android/server/servicewatcher/ServiceWatcherImpl.java b/core/java/com/android/server/servicewatcher/ServiceWatcherImpl.java index b1782693480a..ccbab9fdba12 100644 --- a/services/core/java/com/android/server/servicewatcher/ServiceWatcherImpl.java +++ b/core/java/com/android/server/servicewatcher/ServiceWatcherImpl.java @@ -16,10 +16,6 @@ package com.android.server.servicewatcher; -import static android.content.Context.BIND_AUTO_CREATE; -import static android.content.Context.BIND_NOT_FOREGROUND; -import static android.content.Context.BIND_NOT_VISIBLE; - import android.annotation.Nullable; import android.content.ComponentName; import android.content.Context; @@ -46,6 +42,8 @@ import java.util.Objects; * Implementation of ServiceWatcher. Keeping the implementation separate from the interface allows * us to store the generic relationship between the service supplier and the service listener, while * hiding the generics from clients, simplifying the API. + * + * @hide */ class ServiceWatcherImpl<TBoundServiceInfo extends BoundServiceInfo> implements ServiceWatcher, ServiceChangedListener { @@ -212,7 +210,7 @@ class ServiceWatcherImpl<TBoundServiceInfo extends BoundServiceInfo> implements mBoundServiceInfo.getComponentName()); try { if (!mContext.bindServiceAsUser(bindIntent, this, - BIND_AUTO_CREATE | BIND_NOT_FOREGROUND | BIND_NOT_VISIBLE, + mBoundServiceInfo.getFlags(), mHandler, UserHandle.of(mBoundServiceInfo.getUserId()))) { Log.e(TAG, "[" + mTag + "] unexpected bind failure - retrying later"); mRebinder = this::bind; diff --git a/core/jni/Android.bp b/core/jni/Android.bp index 2283b88e5d97..2bb6e71e9000 100644 --- a/core/jni/Android.bp +++ b/core/jni/Android.bp @@ -105,18 +105,7 @@ cc_library_shared_for_libandroid_runtime { ], shared_libs: [ - "libbase", - "libcutils", "libtracing_perfetto", - "libharfbuzz_ng", - "liblog", - "libmediautils", - "libminikin", - "libz", - "server_configurable_flags", - "libaconfig_storage_read_api_cc", - "android.database.sqlite-aconfig-cc", - "android.media.audiopolicy-aconfig-cc", ], static_libs: [ @@ -303,6 +292,14 @@ cc_library_shared_for_libandroid_runtime { ], shared_libs: [ + "libbase", + "libharfbuzz_ng", + "liblog", + "libmediautils", + "libminikin", + "libz", + "android.database.sqlite-aconfig-cc", + "android.media.audiopolicy-aconfig-cc", "audioclient-types-aidl-cpp", "audioflinger-aidl-cpp", "audiopolicy-types-aidl-cpp", @@ -412,20 +409,24 @@ cc_library_shared_for_libandroid_runtime { "frameworks/native/libs/nativebase/include", "frameworks/native/libs/nativewindow/include", ], - shared_libs: [ - "libicui18n", - "libicuuc", - ], static_libs: [ "libandroidfw", + "libbase", "libbinary_parse", + "libcutils", "libdng_sdk", "libft2", + "libharfbuzz_ng", "libhostgraphics", "libhwui", + "libicui18n", + "libicuuc", + "libicuuc_stubdata", "libimage_type_recognition", "libinput", "libjpeg", + "liblog", + "libminikin", "libnativehelper_jvm", "libpiex", "libpng", @@ -435,11 +436,18 @@ cc_library_shared_for_libandroid_runtime { "libwebp-decode", "libwebp-encode", "libwuffs_mirror_release_c", + "libz", "libimage_io", "libjpegdecoder", "libjpegencoder", "libultrahdr", + "server_configurable_flags", + ], + export_static_lib_headers: [ + "libnativehelper_jvm", + "libui-types", ], + stl: "libc++_static", }, host_linux: { srcs: [ @@ -465,14 +473,18 @@ cc_library_shared_for_libandroid_runtime { "libbinderthreadstateutils", "libsqlite", "libgui_window_info_static", - ], - shared_libs: [ - // libbinder needs to be shared since it has global state - // (e.g. gDefaultServiceManager) "libbinder", "libhidlbase", // libhwbinder is in here ], }, + linux_glibc_x86_64: { + ldflags: ["-static-libgcc"], + dist: { + targets: ["layoutlib"], + dir: "layoutlib_native/linux", + tag: "stripped_all", + }, + }, }, } diff --git a/core/jni/android_view_SurfaceControl.cpp b/core/jni/android_view_SurfaceControl.cpp index f162b7410b10..df87a69f02ce 100644 --- a/core/jni/android_view_SurfaceControl.cpp +++ b/core/jni/android_view_SurfaceControl.cpp @@ -117,6 +117,7 @@ static struct { jfieldID activeDisplayModeId; jfieldID renderFrameRate; jfieldID hasArrSupport; + jfieldID frameRateCategoryRate; jfieldID supportedColorModes; jfieldID activeColorMode; jfieldID hdrCapabilities; @@ -292,6 +293,16 @@ static struct { jfieldID frameNumber; } gStalledTransactionInfoClassInfo; +static struct { + jclass clazz; + jmethodID ctor; +} gFrameRateCategoryRateClassInfo; + +static struct { + jclass clazz; + jmethodID asList; +} gUtilArrays; + constexpr ui::Dataspace pickDataspaceFromColorMode(const ui::ColorMode colorMode) { switch (colorMode) { case ui::ColorMode::DISPLAY_P3: @@ -1388,6 +1399,13 @@ static jobject nativeGetStaticDisplayInfo(JNIEnv* env, jclass clazz, jlong id) { return object; } +static jobject convertFrameRateCategoryRateToJavaObject( + JNIEnv* env, const ui::FrameRateCategoryRate& frameRateCategoryRate) { + return env->NewObject(gFrameRateCategoryRateClassInfo.clazz, + gFrameRateCategoryRateClassInfo.ctor, frameRateCategoryRate.getNormal(), + frameRateCategoryRate.getHigh()); +} + static jobject convertDisplayModeToJavaObject(JNIEnv* env, const ui::DisplayMode& config) { jobject object = env->NewObject(gDisplayModeClassInfo.clazz, gDisplayModeClassInfo.ctor); env->SetIntField(object, gDisplayModeClassInfo.id, config.id); @@ -1456,6 +1474,8 @@ static jobject nativeGetDynamicDisplayInfo(JNIEnv* env, jclass clazz, jlong disp info.activeDisplayModeId); env->SetFloatField(object, gDynamicDisplayInfoClassInfo.renderFrameRate, info.renderFrameRate); env->SetBooleanField(object, gDynamicDisplayInfoClassInfo.hasArrSupport, info.hasArrSupport); + env->SetObjectField(object, gDynamicDisplayInfoClassInfo.frameRateCategoryRate, + convertFrameRateCategoryRateToJavaObject(env, info.frameRateCategoryRate)); jintArray colorModesArray = env->NewIntArray(info.supportedColorModes.size()); if (colorModesArray == NULL) { jniThrowException(env, "java/lang/OutOfMemoryError", NULL); @@ -2191,10 +2211,13 @@ public: env->SetObjectArrayElement(jJankDataArray, i, jJankData); env->DeleteLocalRef(jJankData); } - env->CallVoidMethod(target, - gJankDataListenerClassInfo.onJankDataAvailable, - jJankDataArray); + + jobject jJankDataList = + env->CallStaticObjectMethod(gUtilArrays.clazz, gUtilArrays.asList, jJankDataArray); env->DeleteLocalRef(jJankDataArray); + + env->CallVoidMethod(target, gJankDataListenerClassInfo.onJankDataAvailable, jJankDataList); + env->DeleteLocalRef(jJankDataList); env->DeleteLocalRef(target); return true; @@ -2666,6 +2689,15 @@ int register_android_view_SurfaceControl(JNIEnv* env) GetFieldIDOrDie(env, dynamicInfoClazz, "renderFrameRate", "F"); gDynamicDisplayInfoClassInfo.hasArrSupport = GetFieldIDOrDie(env, dynamicInfoClazz, "hasArrSupport", "Z"); + + gDynamicDisplayInfoClassInfo.frameRateCategoryRate = + GetFieldIDOrDie(env, dynamicInfoClazz, "frameRateCategoryRate", + "Landroid/view/FrameRateCategoryRate;"); + jclass frameRateCategoryRateClazz = FindClassOrDie(env, "android/view/FrameRateCategoryRate"); + gFrameRateCategoryRateClassInfo.clazz = MakeGlobalRefOrDie(env, frameRateCategoryRateClazz); + gFrameRateCategoryRateClassInfo.ctor = + GetMethodIDOrDie(env, frameRateCategoryRateClazz, "<init>", "(FF)V"); + gDynamicDisplayInfoClassInfo.supportedColorModes = GetFieldIDOrDie(env, dynamicInfoClazz, "supportedColorModes", "[I"); gDynamicDisplayInfoClassInfo.activeColorMode = @@ -2834,7 +2866,7 @@ int register_android_view_SurfaceControl(JNIEnv* env) gJankDataListenerClassInfo.clazz = MakeGlobalRefOrDie(env, onJankDataListenerClazz); gJankDataListenerClassInfo.onJankDataAvailable = GetMethodIDOrDie(env, onJankDataListenerClazz, "onJankDataAvailable", - "([Landroid/view/SurfaceControl$JankData;)V"); + "(Ljava/util/List;)V"); jclass transactionCommittedListenerClazz = FindClassOrDie(env, "android/view/SurfaceControl$TransactionCommittedListener"); @@ -2909,6 +2941,10 @@ int register_android_view_SurfaceControl(JNIEnv* env) gStalledTransactionInfoClassInfo.frameNumber = GetFieldIDOrDie(env, stalledTransactionInfoClazz, "frameNumber", "J"); + jclass utilArrays = FindClassOrDie(env, "java/util/Arrays"); + gUtilArrays.clazz = MakeGlobalRefOrDie(env, utilArrays); + gUtilArrays.asList = GetStaticMethodIDOrDie(env, utilArrays, "asList", + "([Ljava/lang/Object;)Ljava/util/List;"); return err; } diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 561d351ede90..0479318ad9ed 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -7023,7 +7023,6 @@ <!-- Allows an application to set the advanced features on BiometricDialog (SystemUI), including logo, logo description, and content view with more options button. <p>Not for use by third-party applications. - @FlaggedApi("android.hardware.biometrics.custom_biometric_prompt") --> <permission android:name="android.permission.SET_BIOMETRIC_DIALOG_ADVANCED" android:protectionLevel="signature|privileged" /> @@ -8536,10 +8535,26 @@ @hide --> <permission - android:name="android.permission.RECEIVE_PACKAGE_CHANGED_BROADCAST_ON_COMPONENT_STATE_CHANGED" + android:name="android.permission.INTERNAL_RECEIVE_PACKAGE_CHANGED_BROADCAST_ON_COMPONENT_STATE_CHANGED" android:protectionLevel="internal" android:featureFlag="android.content.pm.reduce_broadcasts_for_component_state_changes"/> + <uses-permission + android:name="android.permission.INTERNAL_RECEIVE_PACKAGE_CHANGED_BROADCAST_ON_COMPONENT_STATE_CHANGED" + android:featureFlag="android.content.pm.reduce_broadcasts_for_component_state_changes"/> + + <!-- @SystemApi + @FlaggedApi("android.media.tv.flags.kids_mode_tvdb_sharing") + This permission is required when accessing information related to + singleUser-ed TIS session. + <p>This should only be used by OEM. + <p>Protection level: signature|privileged|vendorPrivileged + @hide + --> + <permission android:name="android.permission.SINGLE_USER_TIS_ACCESS" + android:protectionLevel="signature|privileged|vendorPrivileged" + android:featureFlag="android.media.tv.flags.kids_mode_tvdb_sharing"/> + <!-- Attribution for Geofencing service. --> <attribution android:tag="GeofencingService" android:label="@string/geofencing_service"/> <!-- Attribution for Country Detector. --> diff --git a/core/res/res/layout/app_language_picker_current_locale_item.xml b/core/res/res/layout/app_language_picker_locale_item.xml index edd6d648ea99..bcad9ce751bc 100644 --- a/core/res/res/layout/app_language_picker_current_locale_item.xml +++ b/core/res/res/layout/app_language_picker_locale_item.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <!-- - ~ Copyright (C) 2022 The Android Open Source Project + ~ Copyright (C) 2024 The Android Open Source Project ~ ~ Licensed under the Apache License, Version 2.0 (the "License"); ~ you may not use this file except in compliance with the License. @@ -20,10 +20,27 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center_vertical"> + <LinearLayout + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:gravity="center" + android:minHeight="?android:attr/listPreferredItemHeight" + android:layout_marginStart="20dip"> + + <RadioButton + android:id="@+id/checkbox" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:background="@null" + android:focusable="false" + android:clickable="false" /> + + </LinearLayout> + <RelativeLayout android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginEnd="6dip" android:layout_marginTop="6dip" android:layout_marginBottom="6dip" android:layout_weight="1"> @@ -31,20 +48,4 @@ android:id="@+id/language_picker_item" layout="@layout/language_picker_item" /> </RelativeLayout> - - <LinearLayout - android:layout_width="wrap_content" - android:layout_height="match_parent" - android:gravity="center" - android:minHeight="?android:attr/listPreferredItemHeight"> - <ImageView - android:id="@+id/imageView" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_centerVertical="true" - android:layout_marginHorizontal="16dp" - android:src="@drawable/ic_check_24dp" - app:tint="?attr/colorAccentPrimaryVariant" - android:contentDescription="@*android:string/checked"/> - </LinearLayout> </LinearLayout> diff --git a/core/res/res/values-en-rCA/strings.xml b/core/res/res/values-en-rCA/strings.xml index ef420eb0a35a..3fac7360c8f2 100644 --- a/core/res/res/values-en-rCA/strings.xml +++ b/core/res/res/values-en-rCA/strings.xml @@ -1943,8 +1943,7 @@ <string name="zen_mode_implicit_deactivated" msgid="8688441768371501750">"Off"</string> <string name="zen_mode_trigger_summary_divider_text" msgid="7461583466043698862">", "</string> <string name="zen_mode_trigger_summary_range_symbol_combination" msgid="1804900738798069619">"<xliff:g id="START">%1$s</xliff:g> - <xliff:g id="END">%2$s</xliff:g>"</string> - <!-- no translation found for zen_mode_trigger_summary_range_words (7228261413029290750) --> - <skip /> + <string name="zen_mode_trigger_summary_range_words" msgid="7228261413029290750">"<xliff:g id="START">%1$s</xliff:g> to <xliff:g id="END">%2$s</xliff:g>"</string> <string name="zen_mode_trigger_event_calendar_any" msgid="2086784607921121803">"Any calendar"</string> <string name="muted_by" msgid="91464083490094950">"<xliff:g id="THIRD_PARTY">%1$s</xliff:g> is muting some sounds"</string> <string name="system_error_wipe_data" msgid="5910572292172208493">"There\'s an internal problem with your device, and it may be unstable until you factory data reset."</string> @@ -2426,14 +2425,10 @@ <string name="satellite_notification_manual_summary" msgid="901206289846283445">"Send and receive messages without a mobile or Wi-Fi network"</string> <string name="satellite_notification_open_message" msgid="4149234979688273729">"Open Messages"</string> <string name="satellite_notification_how_it_works" msgid="3132069321977520519">"How it works"</string> - <!-- no translation found for satellite_manual_selection_state_popup_title (8545991934926661974) --> - <skip /> - <!-- no translation found for satellite_manual_selection_state_popup_message (1928101658551382450) --> - <skip /> - <!-- no translation found for satellite_manual_selection_state_popup_ok (2459664752624985095) --> - <skip /> - <!-- no translation found for satellite_manual_selection_state_popup_cancel (973605633339469252) --> - <skip /> + <string name="satellite_manual_selection_state_popup_title" msgid="8545991934926661974">"Turn on \"Automatically select network\""</string> + <string name="satellite_manual_selection_state_popup_message" msgid="1928101658551382450">"Turn on \"Automatically select network\" in Settings so your phone can find a network that works with satellite"</string> + <string name="satellite_manual_selection_state_popup_ok" msgid="2459664752624985095">"Turn on"</string> + <string name="satellite_manual_selection_state_popup_cancel" msgid="973605633339469252">"Go back"</string> <string name="unarchival_session_app_label" msgid="6811856981546348205">"Pending..."</string> <string name="fingerprint_dangling_notification_title" msgid="7362075195588639989">"Set up Fingerprint Unlock again"</string> <string name="fingerprint_dangling_notification_msg_1" msgid="5851784577768803510">"<xliff:g id="FINGERPRINT">%s</xliff:g> can no longer be recognized."</string> diff --git a/core/res/res/values-it/strings.xml b/core/res/res/values-it/strings.xml index 051c61a26373..4070f1160aec 100644 --- a/core/res/res/values-it/strings.xml +++ b/core/res/res/values-it/strings.xml @@ -1944,8 +1944,7 @@ <string name="zen_mode_implicit_deactivated" msgid="8688441768371501750">"Off"</string> <string name="zen_mode_trigger_summary_divider_text" msgid="7461583466043698862">", "</string> <string name="zen_mode_trigger_summary_range_symbol_combination" msgid="1804900738798069619">"<xliff:g id="START">%1$s</xliff:g> - <xliff:g id="END">%2$s</xliff:g>"</string> - <!-- no translation found for zen_mode_trigger_summary_range_words (7228261413029290750) --> - <skip /> + <string name="zen_mode_trigger_summary_range_words" msgid="7228261413029290750">"Da <xliff:g id="START">%1$s</xliff:g> a <xliff:g id="END">%2$s</xliff:g>"</string> <string name="zen_mode_trigger_event_calendar_any" msgid="2086784607921121803">"Qualsiasi calendario"</string> <string name="muted_by" msgid="91464083490094950">"<xliff:g id="THIRD_PARTY">%1$s</xliff:g> sta disattivando alcuni suoni"</string> <string name="system_error_wipe_data" msgid="5910572292172208493">"Si è verificato un problema interno con il dispositivo, che potrebbe essere instabile fino al ripristino dei dati di fabbrica."</string> @@ -2427,14 +2426,10 @@ <string name="satellite_notification_manual_summary" msgid="901206289846283445">"Invia e ricevi messaggi senza una rete mobile o Wi-Fi"</string> <string name="satellite_notification_open_message" msgid="4149234979688273729">"Apri Messaggi"</string> <string name="satellite_notification_how_it_works" msgid="3132069321977520519">"Come funziona"</string> - <!-- no translation found for satellite_manual_selection_state_popup_title (8545991934926661974) --> - <skip /> - <!-- no translation found for satellite_manual_selection_state_popup_message (1928101658551382450) --> - <skip /> - <!-- no translation found for satellite_manual_selection_state_popup_ok (2459664752624985095) --> - <skip /> - <!-- no translation found for satellite_manual_selection_state_popup_cancel (973605633339469252) --> - <skip /> + <string name="satellite_manual_selection_state_popup_title" msgid="8545991934926661974">"Attiva \"Seleziona rete automaticamente\""</string> + <string name="satellite_manual_selection_state_popup_message" msgid="1928101658551382450">"Attiva \"Seleziona rete automaticamente\" nelle Impostazioni in modo che lo smartphone possa trovare una rete compatibile con il satellite"</string> + <string name="satellite_manual_selection_state_popup_ok" msgid="2459664752624985095">"Attiva"</string> + <string name="satellite_manual_selection_state_popup_cancel" msgid="973605633339469252">"Indietro"</string> <string name="unarchival_session_app_label" msgid="6811856981546348205">"In attesa…"</string> <string name="fingerprint_dangling_notification_title" msgid="7362075195588639989">"Riconfigura lo Sblocco con l\'Impronta"</string> <string name="fingerprint_dangling_notification_msg_1" msgid="5851784577768803510">"<xliff:g id="FINGERPRINT">%s</xliff:g> non può più essere riconosciuto."</string> diff --git a/core/res/res/values-ne/strings.xml b/core/res/res/values-ne/strings.xml index 2fea96252ebc..5684d780be8f 100644 --- a/core/res/res/values-ne/strings.xml +++ b/core/res/res/values-ne/strings.xml @@ -590,7 +590,7 @@ <string name="permdesc_changeWifiMulticastState" product="default" msgid="8296627590220222740">"तपाईँको फोन मात्र होइन, मल्टिकास्ट ठेगानाहरूको प्रयोग गरे Wi-Fi नेटवर्कका सबै उपकरणहरूमा पठाइएका प्याकेटहरू प्राप्त गर्न एपलाई अनुमति दिन्छ। यसले गैर-मल्टिकास्ट मोडभन्दा बढी उर्जा प्रयोग गर्छ।"</string> <string name="permlab_bluetoothAdmin" msgid="6490373569441946064">"ब्लुटुथ सेटिङहरूमा पहुँच गर्नुहोस्"</string> <string name="permdesc_bluetoothAdmin" product="tablet" msgid="5370837055438574863">"स्थानीय ब्लुटुथ ट्याब्लेटलाई कन्फिगर गर्नको लागि र टाढाका उपकरणहरूलाई पत्ता लगाउन र जोड्नको लागि एपलाई अनुमति दिन्छ।"</string> - <string name="permdesc_bluetoothAdmin" product="tv" msgid="1623992984547014588">"एपलाई तपाईंको Android टिभी डिभाइसको ब्लुटुथ कन्फिगर गर्ने तथा टाढा रहेका यन्त्रहरू पत्ता लगाई ती यन्त्रहरूसँग जोडा बनाउने अनुमति दिन्छ।"</string> + <string name="permdesc_bluetoothAdmin" product="tv" msgid="1623992984547014588">"एपलाई तपाईंको Android टिभी डिभाइसको ब्लुटुथ कन्फिगर गर्ने तथा टाढा रहेका यन्त्रहरू पत्ता लगाई ती यन्त्रहरूसँग कनेक्ट गर्ने अनुमति दिन्छ।"</string> <string name="permdesc_bluetoothAdmin" product="default" msgid="7381341743021234863">"एपलाई स्थानीय ब्लुटुथ फोन कन्फिगर गर्न र टाढाका उपकरणहरूसँग खोज गर्न र जोडी गर्न अनुमति दिन्छ।"</string> <string name="permlab_accessWimaxState" msgid="7029563339012437434">"WiMAXसँग जोड्नुहोस् वा छुटाउनुहोस्"</string> <string name="permdesc_accessWimaxState" msgid="5372734776802067708">"एपलाई वाइम्याक्स सक्षम छ कि छैन र जडान भएको कुनै पनि वाइम्याक्स नेटवर्कहरूको बारेमा जानकारी निर्धारिण गर्न अनुमति दिन्छ।"</string> diff --git a/core/res/res/values-pt-rPT/strings.xml b/core/res/res/values-pt-rPT/strings.xml index 388c5a9077ae..86e0fcdad8a1 100644 --- a/core/res/res/values-pt-rPT/strings.xml +++ b/core/res/res/values-pt-rPT/strings.xml @@ -1944,8 +1944,7 @@ <string name="zen_mode_implicit_deactivated" msgid="8688441768371501750">"Desativada"</string> <string name="zen_mode_trigger_summary_divider_text" msgid="7461583466043698862">", "</string> <string name="zen_mode_trigger_summary_range_symbol_combination" msgid="1804900738798069619">"<xliff:g id="START">%1$s</xliff:g> – <xliff:g id="END">%2$s</xliff:g>"</string> - <!-- no translation found for zen_mode_trigger_summary_range_words (7228261413029290750) --> - <skip /> + <string name="zen_mode_trigger_summary_range_words" msgid="7228261413029290750">"<xliff:g id="START">%1$s</xliff:g> – <xliff:g id="END">%2$s</xliff:g>"</string> <string name="zen_mode_trigger_event_calendar_any" msgid="2086784607921121803">"Qualquer calendário"</string> <string name="muted_by" msgid="91464083490094950">"<xliff:g id="THIRD_PARTY">%1$s</xliff:g> está a desativar alguns sons."</string> <string name="system_error_wipe_data" msgid="5910572292172208493">"Existe um problema interno no seu dispositivo e pode ficar instável até efetuar uma reposição de dados de fábrica."</string> @@ -2427,14 +2426,10 @@ <string name="satellite_notification_manual_summary" msgid="901206289846283445">"Envie e receba mensagens sem uma rede móvel ou Wi-Fi"</string> <string name="satellite_notification_open_message" msgid="4149234979688273729">"Abre a app Mensagens"</string> <string name="satellite_notification_how_it_works" msgid="3132069321977520519">"Como funciona"</string> - <!-- no translation found for satellite_manual_selection_state_popup_title (8545991934926661974) --> - <skip /> - <!-- no translation found for satellite_manual_selection_state_popup_message (1928101658551382450) --> - <skip /> - <!-- no translation found for satellite_manual_selection_state_popup_ok (2459664752624985095) --> - <skip /> - <!-- no translation found for satellite_manual_selection_state_popup_cancel (973605633339469252) --> - <skip /> + <string name="satellite_manual_selection_state_popup_title" msgid="8545991934926661974">"Ative a opção \"Selecionar rede automaticamente\""</string> + <string name="satellite_manual_selection_state_popup_message" msgid="1928101658551382450">"Ative a opção \"Selecionar rede automaticamente\" nas Definições para que o telemóvel possa encontrar uma rede que funcione com o satélite"</string> + <string name="satellite_manual_selection_state_popup_ok" msgid="2459664752624985095">"Ativar"</string> + <string name="satellite_manual_selection_state_popup_cancel" msgid="973605633339469252">"Retroceder"</string> <string name="unarchival_session_app_label" msgid="6811856981546348205">"Pendente…"</string> <string name="fingerprint_dangling_notification_title" msgid="7362075195588639989">"Configure o Desbloqueio por impressão digital novamente"</string> <string name="fingerprint_dangling_notification_msg_1" msgid="5851784577768803510">"Já não é possível reconhecer <xliff:g id="FINGERPRINT">%s</xliff:g>."</string> diff --git a/core/res/res/values-te/strings.xml b/core/res/res/values-te/strings.xml index 492dca723c21..1d9e00c012bb 100644 --- a/core/res/res/values-te/strings.xml +++ b/core/res/res/values-te/strings.xml @@ -1937,7 +1937,7 @@ <string name="zen_mode_default_weeknights_name" msgid="7902108149994062847">"వారపు రోజుల్లో రాత్రి"</string> <string name="zen_mode_default_weekends_name" msgid="4707200272709377930">"వారాంతం"</string> <string name="zen_mode_default_events_name" msgid="2280682960128512257">"ఈవెంట్"</string> - <string name="zen_mode_default_every_night_name" msgid="1467765312174275823">"నిద్ర"</string> + <string name="zen_mode_default_every_night_name" msgid="1467765312174275823">"స్లీప్ మోడ్"</string> <string name="zen_mode_implicit_trigger_description" msgid="5714956693073007111">"<xliff:g id="APP_NAME">%1$s</xliff:g> ద్వారా మేనేజ్ చేయబడుతోంది"</string> <string name="zen_mode_implicit_activated" msgid="2634285680776672994">"ఆన్లో ఉంది"</string> <string name="zen_mode_implicit_deactivated" msgid="8688441768371501750">"ఆఫ్లో ఉంది"</string> diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index d750ff6623f2..092d2a72580a 100644 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -3486,6 +3486,12 @@ representation this attribute can be used for providing such. --> <attr name="contentDescription" format="string" localization="suggested" /> + <!-- Provides brief supplemental information for the view, such as the purpose of + the view when that purpose is not conveyed within its textual representation. + This property is used primarily for accessibility. --> + <!-- @FlaggedApi("android.view.accessibility.supplemental_description") --> + <attr name="supplementalDescription" format="string" localization="suggested" /> + <!-- Sets the id of a view that screen readers are requested to visit after this view. Requests that a screen-reader visits the content of this view before the content of the one it precedes. This does nothing if either view is not important for accessibility. @@ -9238,6 +9244,8 @@ <flag name="home_screen" value="0x1" /> <flag name="keyguard" value="0x2" /> <flag name="searchbox" value="0x4" /> + <!-- @FlaggedApi("android.appwidget.flags.not_keyguard_category") --> + <flag name="not_keyguard" value="0x8" /> </attr> <!-- Flags indicating various features supported by the widget. These are hints to the widget host, and do not actually change the behavior of the widget. --> diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml index 0be33c2e7a03..4d73f228ad0c 100644 --- a/core/res/res/values/attrs_manifest.xml +++ b/core/res/res/values/attrs_manifest.xml @@ -1876,6 +1876,34 @@ @hide @SystemApi @FlaggedApi("android.content.res.manifest_flagging") --> <attr name="featureFlag" format="string" /> + <!-- This attribute provides a way to fine-tune how incoming intents are matched to application + components. By default, no special matching rules are applied. This attribute can be specified + on the {@code <application>} tag as well as at the component tags such as {@code <activity>}, + {@code <activity-alias>}, {@code <receiver>}, {@code <service>}, {@code <provider>} and the + attribute on the component can be used to override what's on the {@code <application>} tag. --> + <attr name="intentMatchingFlags"> + <!-- Disables all special matching rules for incoming intents. When specifying multiple + flags, conflicting values are resolved by giving precedence to the "none" flag. --> + <flag name="none" value="0x0001" /> + + <!-- Enforces stricter matching for incoming intents: + <ul> + <li>Explicit intents should match the target component's intent filter + <li>Intents without an action should not match any intent filter + </ul> + --> + <flag name="enforceIntentFilter" value="0x0002" /> + + <!-- Relaxes the matching rules to allow intents without a action to match. This flag to be + used in conjunction with enforceIntentFilter to achieve a specific behavior: + <ul> + <li>Explicit intents should match the target component's intent filter + <li>Intents without an action are allowed to match any intent filter + </ul> + --> + <flag name="allowNullAction" value="0x0004" /> + </attr> + <!-- The <code>manifest</code> tag is the root of an <code>AndroidManifest.xml</code> file, describing the contents of an Android package (.apk) file. One @@ -2243,6 +2271,8 @@ {@link android.window.OnBackInvokedCallback#onBackInvoked OnBackInvokedCallback.onBackInvoked()} on the focused window. --> <attr name="enableOnBackInvokedCallback" format="boolean"/> + + <attr name="intentMatchingFlags"/> </declare-styleable> <!-- An attribution is a logical part of an app and is identified by a tag. @@ -2930,6 +2960,7 @@ contained here. --> <attr name="attributionTags" /> <attr name="systemUserOnly" format="boolean" /> + <attr name="intentMatchingFlags"/> </declare-styleable> <!-- Attributes that can be supplied in an AndroidManifest.xml @@ -3089,6 +3120,7 @@ --> <attr name="allowSharedIsolatedProcess" format="boolean" /> <attr name="systemUserOnly" format="boolean" /> + <attr name="intentMatchingFlags"/> </declare-styleable> <!-- @hide The <code>apex-system-service</code> tag declares an apex system service @@ -3156,6 +3188,7 @@ Context.createAttributionContext() using the first attribution tag contained here. --> <attr name="attributionTags" /> + <attr name="intentMatchingFlags"/> </declare-styleable> <!-- The <code>activity</code> tag declares an @@ -3350,6 +3383,7 @@ URIs. --> <enum name="readAndWrite" value="4" /> </attr> + <attr name="intentMatchingFlags"/> </declare-styleable> <!-- The <code>activity-alias</code> tag declares a new @@ -3392,6 +3426,7 @@ <attr name="attributionTags" /> <attr name="allowUntrustedActivityEmbedding" /> <attr name="knownActivityEmbeddingCerts" /> + <attr name="intentMatchingFlags"/> </declare-styleable> <!-- The <code>meta-data</code> tag is used to attach additional diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml index 522dcfaf4729..db752065289e 100644 --- a/core/res/res/values/dimens.xml +++ b/core/res/res/values/dimens.xml @@ -820,17 +820,17 @@ <!-- The gap between segments in the notification progress bar --> <dimen name="notification_progress_segSeg_gap">2dp</dimen> <!-- The gap between a segment and a point in the notification progress bar --> - <dimen name="notification_progress_segPoint_gap">4dp</dimen> + <dimen name="notification_progress_segPoint_gap">8dp</dimen> <!-- The dash gap of the notification progress bar segments --> - <dimen name="notification_progress_segments_dash_gap">9dp</dimen> + <dimen name="notification_progress_segments_dash_gap">8dp</dimen> <!-- The dash width of the notification progress bar segments --> <dimen name="notification_progress_segments_dash_width">3dp</dimen> <!-- The height of the notification progress bar segments --> <dimen name="notification_progress_segments_height">6dp</dimen> <!-- The radius of the notification progress bar points --> - <dimen name="notification_progress_points_radius">10dp</dimen> + <dimen name="notification_progress_points_radius">6dp</dimen> <!-- The corner radius of the notification progress bar points drawn as rects --> - <dimen name="notification_progress_points_corner_radius">4dp</dimen> + <dimen name="notification_progress_points_corner_radius">2dp</dimen> <!-- The inset of the notification progress bar points drawn as rects --> <dimen name="notification_progress_points_inset">0dp</dimen> diff --git a/core/res/res/values/public-staging.xml b/core/res/res/values/public-staging.xml index 81215453b3ef..0c28ea406aa2 100644 --- a/core/res/res/values/public-staging.xml +++ b/core/res/res/values/public-staging.xml @@ -121,6 +121,10 @@ <!-- @FlaggedApi("android.permission.flags.replace_body_sensor_permission_enabled") @hide @SystemApi --> <public name="backgroundPermission"/> + <!-- @FlaggedApi(android.view.accessibility.supplemental_description) --> + <public name="supplementalDescription"/> + <!-- @FlaggedApi("android.security.enable_intent_matching_flags") --> + <public name="intentMatchingFlags"/> </staging-public-group> <staging-public-group type="id" first-id="0x01b60000"> diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index 0d4870b07216..d3ef07ca8122 100644 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -5337,7 +5337,9 @@ <!-- Zen mode - name of default automatic calendar time-based rule that is triggered every night (when sleeping). [CHAR LIMIT=40] --> <string name="zen_mode_default_every_night_name">Sleeping</string> - <!-- Zen mode - Trigger description of the rule, indicating which app owns it. [CHAR_LIMIT=100] --> + <!-- Implicit zen mode - Name of the rule, indicating which app owns it. [CHAR_LIMIT=30] --> + <string name="zen_mode_implicit_name">Do Not Disturb (<xliff:g id="app_name" example="Gmail">%1$s</xliff:g>)</string> + <!-- Implicit zen mode - Trigger description of the rule, indicating which app owns it. [CHAR_LIMIT=100] --> <string name="zen_mode_implicit_trigger_description">Managed by <xliff:g id="app_name">%1$s</xliff:g></string> <!-- Zen mode - Condition summary when a rule is activated due to a call to setInterruptionFilter(). [CHAR_LIMIT=NONE] --> <string name="zen_mode_implicit_activated">On</string> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index 51c405ae4ae8..515ebd54b5eb 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -2652,6 +2652,7 @@ <java-symbol type="string" name="zen_mode_default_weekends_name" /> <java-symbol type="string" name="zen_mode_default_events_name" /> <java-symbol type="string" name="zen_mode_default_every_night_name" /> + <java-symbol type="string" name="zen_mode_implicit_name" /> <java-symbol type="string" name="zen_mode_implicit_trigger_description" /> <java-symbol type="string" name="zen_mode_implicit_activated" /> <java-symbol type="string" name="zen_mode_implicit_deactivated" /> @@ -5263,7 +5264,7 @@ <java-symbol type="string" name="system_locale_title" /> <java-symbol type="layout" name="app_language_picker_system_default" /> <java-symbol type="layout" name="app_language_picker_system_current" /> - <java-symbol type="layout" name="app_language_picker_current_locale_item" /> + <java-symbol type="layout" name="app_language_picker_locale_item" /> <java-symbol type="id" name="system_locale_subtitle" /> <java-symbol type="id" name="language_picker_item" /> <java-symbol type="id" name="language_picker_header" /> diff --git a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsTest.java b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsTest.java index bdc4d2540bae..0f8dc13faabe 100644 --- a/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsTest.java +++ b/core/tests/BroadcastRadioTests/src/com/android/server/broadcastradio/aidl/ConversionUtilsTest.java @@ -20,13 +20,26 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyInt; import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyLong; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; +import android.annotation.Nullable; import android.app.compat.CompatChanges; +import android.hardware.broadcastradio.Alert; +import android.hardware.broadcastradio.AlertArea; +import android.hardware.broadcastradio.AlertCategory; +import android.hardware.broadcastradio.AlertCertainty; +import android.hardware.broadcastradio.AlertInfo; +import android.hardware.broadcastradio.AlertMessageType; +import android.hardware.broadcastradio.AlertSeverity; +import android.hardware.broadcastradio.AlertStatus; +import android.hardware.broadcastradio.AlertUrgency; import android.hardware.broadcastradio.AmFmBandRange; import android.hardware.broadcastradio.AmFmRegionConfig; import android.hardware.broadcastradio.ConfigFlag; +import android.hardware.broadcastradio.Coordinate; import android.hardware.broadcastradio.DabTableEntry; +import android.hardware.broadcastradio.Geocode; import android.hardware.broadcastradio.IdentifierType; import android.hardware.broadcastradio.Metadata; +import android.hardware.broadcastradio.Polygon; import android.hardware.broadcastradio.ProgramFilter; import android.hardware.broadcastradio.ProgramIdentifier; import android.hardware.broadcastradio.ProgramInfo; @@ -37,10 +50,13 @@ import android.hardware.radio.Announcement; import android.hardware.radio.Flags; import android.hardware.radio.ProgramList; import android.hardware.radio.ProgramSelector; +import android.hardware.radio.RadioAlert; import android.hardware.radio.RadioManager; import android.hardware.radio.RadioMetadata; import android.hardware.radio.UniqueProgramIdentifier; import android.os.ServiceSpecificException; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.SetFlagsRule; import android.util.ArrayMap; import android.util.ArraySet; @@ -148,6 +164,9 @@ public final class ConversionUtilsTest extends ExtendedRadioMockitoTestCase { private static final ProgramIdentifier TEST_HAL_HD_STATION_LOCATION_ID = AidlTestUtils.makeHalIdentifier(IdentifierType.HD_STATION_LOCATION, TEST_HD_LOCATION_VALUE); + private static final ProgramIdentifier TEST_HAL_HD_FM_FREQUENCY_ID = + AidlTestUtils.makeHalIdentifier(IdentifierType.AMFM_FREQUENCY_KHZ, + TEST_HD_FREQUENCY_VALUE); private static final UniqueProgramIdentifier TEST_DAB_UNIQUE_ID = new UniqueProgramIdentifier( TEST_DAB_SELECTOR); @@ -173,6 +192,57 @@ public final class ConversionUtilsTest extends ExtendedRadioMockitoTestCase { private static final Metadata TEST_HAL_HD_SUBCHANNELS = Metadata.hdSubChannelsAvailable( TEST_HD_SUBCHANNELS); + private static final int TEST_STATUS = RadioAlert.STATUS_ACTUAL; + private static final int TEST_HAL_STATUS = AlertStatus.ACTUAL; + private static final int TEST_TYPE = RadioAlert.MESSAGE_TYPE_ALERT; + private static final int TEST_HAL_TYPE = AlertMessageType.ALERT; + private static final int[] TEST_CATEGORY_ARRAY = new int[]{RadioAlert.CATEGORY_CBRNE, + RadioAlert.CATEGORY_GEO}; + private static final int[] TEST_HAL_CATEGORY_LIST = new int[]{AlertCategory.CBRNE, + AlertCategory.GEO}; + private static final int TEST_URGENCY = RadioAlert.URGENCY_FUTURE; + private static final int TEST_HAL_URGENCY = AlertUrgency.FUTURE; + private static final int TEST_SEVERITY = RadioAlert.SEVERITY_MINOR; + private static final int TEST_HAL_SEVERITY = AlertSeverity.MINOR; + private static final int TEST_CERTAINTY = RadioAlert.CERTAINTY_UNLIKELY; + private static final int TEST_HAL_CERTAINTY = AlertCertainty.UNLIKELY; + private static final String TEST_DESCRIPTION_MESSAGE = "Test Alert Description Message."; + private static final String TEST_GEOCODE_VALUE_NAME = "ZIP"; + private static final String TEST_GEOCODE_VALUE_1 = "10001"; + private static final String TEST_GEOCODE_VALUE_2 = "10002"; + private static final double TEST_POLYGON_LATITUDE_START = -38.47; + private static final double TEST_POLYGON_LONGITUDE_START = -120.14; + private static final RadioAlert.Coordinate TEST_POLYGON_COORDINATE_START = + new RadioAlert.Coordinate(TEST_POLYGON_LATITUDE_START, TEST_POLYGON_LONGITUDE_START); + private static final List<RadioAlert.Coordinate> TEST_COORDINATES = List.of( + TEST_POLYGON_COORDINATE_START, new RadioAlert.Coordinate(38.34, -119.95), + new RadioAlert.Coordinate(38.52, -119.74), new RadioAlert.Coordinate(38.62, -119.89), + TEST_POLYGON_COORDINATE_START); + private static final RadioAlert.Polygon TEST_POLYGON = new RadioAlert.Polygon(TEST_COORDINATES); + private static final Polygon TEST_HAL_POLYGON = createHalPolygon(TEST_COORDINATES); + private static final RadioAlert.Geocode TEST_GEOCODE_1 = new RadioAlert.Geocode( + TEST_GEOCODE_VALUE_NAME, TEST_GEOCODE_VALUE_1); + private static final RadioAlert.Geocode TEST_GEOCODE_2 = new RadioAlert.Geocode( + TEST_GEOCODE_VALUE_NAME, TEST_GEOCODE_VALUE_2); + private static final RadioAlert.AlertArea TEST_AREA = new RadioAlert.AlertArea( + List.of(TEST_POLYGON), List.of(TEST_GEOCODE_1, TEST_GEOCODE_2)); + private static final AlertArea TEST_HAL_ALERT_AREA = createHalAlertArea( + new Polygon[]{TEST_HAL_POLYGON}, new Geocode[]{ + createHalGeocode(TEST_GEOCODE_VALUE_NAME, TEST_GEOCODE_VALUE_1), + createHalGeocode(TEST_GEOCODE_VALUE_NAME, TEST_GEOCODE_VALUE_2)}); + private static final String TEST_LANGUAGE = "en-US"; + + private static final RadioAlert.AlertInfo TEST_ALERT_INFO_1 = new RadioAlert.AlertInfo( + TEST_CATEGORY_ARRAY, TEST_URGENCY, TEST_SEVERITY, TEST_CERTAINTY, + TEST_DESCRIPTION_MESSAGE, List.of(TEST_AREA), TEST_LANGUAGE); + private static final AlertInfo TEST_HAL_ALERT_INFO = createHalAlertInfo( + TEST_HAL_CATEGORY_LIST, TEST_HAL_URGENCY, TEST_HAL_SEVERITY, TEST_HAL_CERTAINTY, + TEST_DESCRIPTION_MESSAGE, new AlertArea[]{TEST_HAL_ALERT_AREA}, TEST_LANGUAGE); + private static final RadioAlert TEST_ALERT = new RadioAlert(TEST_STATUS, TEST_TYPE, + List.of(TEST_ALERT_INFO_1)); + private static final Alert TEST_HAL_ALERT = createHalAlert(TEST_HAL_STATUS, TEST_HAL_TYPE, + new AlertInfo[]{TEST_HAL_ALERT_INFO}); + @Rule public final Expect expect = Expect.create(); @Rule @@ -576,6 +646,42 @@ public final class ConversionUtilsTest extends ExtendedRadioMockitoTestCase { } @Test + @EnableFlags(Flags.FLAG_HD_RADIO_EMERGENCY_ALERT_SYSTEM) + public void programInfoFromHalProgramInfo_withAlertMessageAndFlagEnabled() { + android.hardware.broadcastradio.ProgramSelector halHdSelector = + AidlTestUtils.makeHalSelector(TEST_HAL_HD_STATION_EXT_ID, + new ProgramIdentifier[]{}); + ProgramInfo halHdProgramInfo = AidlTestUtils.makeHalProgramInfo(halHdSelector, + TEST_HAL_HD_STATION_EXT_ID, TEST_HAL_HD_FM_FREQUENCY_ID, TEST_SIGNAL_QUALITY, + new ProgramIdentifier[]{}, new Metadata[]{}); + halHdProgramInfo.emergencyAlert = TEST_HAL_ALERT; + + RadioManager.ProgramInfo programInfo = + ConversionUtils.programInfoFromHalProgramInfo(halHdProgramInfo); + + expect.withMessage("Alert of converted HD program info with alert and enabled flag") + .that(programInfo.getAlert()).isEqualTo(TEST_ALERT); + } + + @Test + @DisableFlags(Flags.FLAG_HD_RADIO_EMERGENCY_ALERT_SYSTEM) + public void programInfoFromHalProgramInfo_withAlertMessageAndFlagDisabled() { + android.hardware.broadcastradio.ProgramSelector halHdSelector = + AidlTestUtils.makeHalSelector(TEST_HAL_HD_STATION_EXT_ID, + new ProgramIdentifier[]{}); + ProgramInfo halHdProgramInfo = AidlTestUtils.makeHalProgramInfo(halHdSelector, + TEST_HAL_HD_STATION_EXT_ID, TEST_HAL_HD_FM_FREQUENCY_ID, TEST_SIGNAL_QUALITY, + new ProgramIdentifier[]{}, new Metadata[]{}); + halHdProgramInfo.emergencyAlert = TEST_HAL_ALERT; + + RadioManager.ProgramInfo programInfo = + ConversionUtils.programInfoFromHalProgramInfo(halHdProgramInfo); + + expect.withMessage("Alert of converted HD program info with alert and disabled flag") + .that(programInfo.getAlert()).isNull(); + } + + @Test public void tunedProgramInfoFromHalProgramInfo_withInvalidDabProgramInfo() { android.hardware.broadcastradio.ProgramSelector invalidHalDabSelector = AidlTestUtils.makeHalSelector(TEST_HAL_DAB_SID_EXT_ID, new ProgramIdentifier[]{ @@ -851,7 +957,7 @@ public final class ConversionUtilsTest extends ExtendedRadioMockitoTestCase { } @Test - public void radioMetadataFromHalMetadata_withHdMedatadataAndFlagEnabled() { + public void radioMetadataFromHalMetadata_withHdMetadataAndFlagEnabled() { mSetFlagsRule.enableFlags(Flags.FLAG_HD_RADIO_IMPROVED); String genreValue = "genreTest"; String commentShortDescriptionValue = "commentShortDescriptionTest"; @@ -973,6 +1079,57 @@ public final class ConversionUtilsTest extends ExtendedRadioMockitoTestCase { .isEmpty(); } + @Test + @EnableFlags(Flags.FLAG_HD_RADIO_EMERGENCY_ALERT_SYSTEM) + public void radioAlertFromHalAlert() { + RadioAlert convertedAlert = ConversionUtils.radioAlertFromHalAlert(TEST_HAL_ALERT); + + expect.withMessage("Converted alert").that(convertedAlert) + .isEqualTo(TEST_ALERT); + } + + @Test + @EnableFlags(Flags.FLAG_HD_RADIO_EMERGENCY_ALERT_SYSTEM) + public void radioAlertFromHalAlert_withLowThanFourCoordinates() { + Polygon invalidPolygon = createHalPolygon(List.of( + TEST_POLYGON_COORDINATE_START, new RadioAlert.Coordinate(38.34, -119.95), + TEST_POLYGON_COORDINATE_START)); + AlertInfo halAlertInfo = createHalAlertInfo(TEST_HAL_CATEGORY_LIST, TEST_HAL_URGENCY, + TEST_HAL_SEVERITY, TEST_HAL_CERTAINTY, TEST_DESCRIPTION_MESSAGE, + new AlertArea[]{createHalAlertArea(new Polygon[]{invalidPolygon}, + new Geocode[]{})}, TEST_LANGUAGE); + Alert halAlert = createHalAlert(TEST_HAL_STATUS, TEST_HAL_TYPE, + new AlertInfo[]{halAlertInfo }); + + RadioAlert convertedAlert = ConversionUtils.radioAlertFromHalAlert(halAlert); + + expect.withMessage("Empty polygon list with less than 4 coordinates") + .that(convertedAlert.getInfoList().get(0).getAreas().get(0).getPolygons()) + .isEmpty(); + } + + @Test + @EnableFlags(Flags.FLAG_HD_RADIO_EMERGENCY_ALERT_SYSTEM) + public void radioAlertFromHalAlert_withDifferentFirstAndLastCoordinate() { + Polygon invalidPolygon = createHalPolygon(List.of( + TEST_POLYGON_COORDINATE_START, new RadioAlert.Coordinate(38.34, -119.95), + new RadioAlert.Coordinate(38.52, -119.74), + new RadioAlert.Coordinate(38.62, -119.89), + new RadioAlert.Coordinate(38.42, -120.14))); + AlertInfo halAlertInfo = createHalAlertInfo(TEST_HAL_CATEGORY_LIST, TEST_HAL_URGENCY, + TEST_HAL_SEVERITY, TEST_HAL_CERTAINTY, TEST_DESCRIPTION_MESSAGE, + new AlertArea[]{createHalAlertArea(new Polygon[]{invalidPolygon}, + new Geocode[]{})}, TEST_LANGUAGE); + Alert halAlert = createHalAlert(TEST_HAL_STATUS, TEST_HAL_TYPE, + new AlertInfo[]{halAlertInfo}); + + RadioAlert convertedAlert = ConversionUtils.radioAlertFromHalAlert(halAlert); + + expect.withMessage("Empty polygon list with different first and last coordinates") + .that(convertedAlert.getInfoList().get(0).getAreas().get(0).getPolygons()) + .isEmpty(); + } + private static RadioManager.ModuleProperties createModuleProperties() { AmFmRegionConfig amFmConfig = createAmFmRegionConfig(); DabTableEntry[] dabTableEntries = new DabTableEntry[]{ @@ -1028,14 +1185,62 @@ public final class ConversionUtilsTest extends ExtendedRadioMockitoTestCase { return halProperties; } - private ProgramSelector.Identifier createHdStationLocationIdWithFlagEnabled() { + private static ProgramSelector.Identifier createHdStationLocationIdWithFlagEnabled() { return new ProgramSelector.Identifier(ProgramSelector.IDENTIFIER_TYPE_HD_STATION_LOCATION, TEST_HD_LOCATION_VALUE); } - private ProgramSelector createHdSelectorWithFlagEnabled() { + private static ProgramSelector createHdSelectorWithFlagEnabled() { return new ProgramSelector(ProgramSelector.PROGRAM_TYPE_FM_HD, TEST_HD_STATION_EXT_ID, new ProgramSelector.Identifier[]{createHdStationLocationIdWithFlagEnabled()}, /* vendorIds= */ null); } + + private static Alert createHalAlert(int status, int messageType, AlertInfo[] alertInfos) { + Alert halAlert = new Alert(); + halAlert.status = status; + halAlert.messageType = messageType; + halAlert.infoArray = alertInfos; + return halAlert; + } + + private static AlertInfo createHalAlertInfo(int[] categoryArray, int urgency, int severity, + int certainty, String description, AlertArea[] areas, @Nullable String language) { + AlertInfo info = new AlertInfo(); + info.categoryArray = categoryArray; + info.urgency = urgency; + info.severity = severity; + info.certainty = certainty; + info.description = description; + info.areas = areas; + info.language = language; + return info; + } + + private static AlertArea createHalAlertArea(Polygon[] polygons, Geocode[] geocodes) { + AlertArea area = new AlertArea(); + area.polygons = polygons; + area.geocodes = geocodes; + return area; + } + + private static Polygon createHalPolygon(List<RadioAlert.Coordinate> coordinates) { + Coordinate[] halCoordinates = new Coordinate[coordinates.size()]; + for (int idx = 0; idx < coordinates.size(); idx++) { + Coordinate halCoordinate = new Coordinate(); + halCoordinate.latitude = coordinates.get(idx).getLatitude(); + halCoordinate.longitude = coordinates.get(idx).getLongitude(); + halCoordinates[idx] = halCoordinate; + } + Polygon polygon = new Polygon(); + polygon.coordinates = halCoordinates; + return polygon; + } + + private static Geocode createHalGeocode(String valueName, String value) { + Geocode halGeocode = new Geocode(); + halGeocode.valueName = valueName; + halGeocode.value = value; + return halGeocode; + } } diff --git a/core/tests/coretests/AppThatUsesAppOps/src/android/app/appops/appthatusesappops/AppOpsUserService.kt b/core/tests/coretests/AppThatUsesAppOps/src/android/app/appops/appthatusesappops/AppOpsUserService.kt index 48053c11f2d1..c5f07ff8f853 100644 --- a/core/tests/coretests/AppThatUsesAppOps/src/android/app/appops/appthatusesappops/AppOpsUserService.kt +++ b/core/tests/coretests/AppThatUsesAppOps/src/android/app/appops/appthatusesappops/AppOpsUserService.kt @@ -22,7 +22,9 @@ import android.app.AsyncNotedAppOp import android.app.Service import android.app.SyncNotedAppOp import android.content.Intent +import android.os.Handler import android.os.IBinder +import android.os.Looper import android.util.Log import com.android.frameworks.coretests.aidl.IAppOpsUserClient import com.android.frameworks.coretests.aidl.IAppOpsUserService @@ -71,6 +73,7 @@ class AppOpsUserService : Service() { override fun onBind(intent: Intent?): IBinder { return object : IAppOpsUserService.Stub() { private val appOpsManager = getSystemService(AppOpsManager::class.java)!! + private val handler = Handler(Looper.getMainLooper()) // Collected note-op calls inside of this process private val noted = mutableListOf<Pair<SyncNotedAppOp, Array<StackTraceElement>>>() @@ -182,6 +185,18 @@ class AppOpsUserService : Service() { } } + override fun callFreezeAndNoteSyncOp(client: IAppOpsUserClient) { + handler.post { + client.freezeAndNoteSyncOp() + } + } + + override fun assertEmptyAsyncNoted() { + forwardThrowableFrom { + assertThat(asyncNoted).isEmpty() + } + } + override fun callApiThatNotesAsyncOpNativelyAndCheckCustomMessage( client: IAppOpsUserClient ) { diff --git a/core/tests/coretests/aidl/com/android/frameworks/coretests/aidl/IAppOpsUserClient.aidl b/core/tests/coretests/aidl/com/android/frameworks/coretests/aidl/IAppOpsUserClient.aidl index 68b393c0ced2..11300b08df5e 100644 --- a/core/tests/coretests/aidl/com/android/frameworks/coretests/aidl/IAppOpsUserClient.aidl +++ b/core/tests/coretests/aidl/com/android/frameworks/coretests/aidl/IAppOpsUserClient.aidl @@ -20,6 +20,7 @@ interface IAppOpsUserClient { void noteSyncOpNative(); void noteNonPermissionSyncOpNative(); oneway void noteSyncOpOnewayNative(); + void freezeAndNoteSyncOp(); void noteSyncOpOtherUidNative(); void noteAsyncOpNative(); void noteAsyncOpNativeWithCustomMessage(); diff --git a/core/tests/coretests/aidl/com/android/frameworks/coretests/aidl/IAppOpsUserService.aidl b/core/tests/coretests/aidl/com/android/frameworks/coretests/aidl/IAppOpsUserService.aidl index f5673c43df09..c6dc7b0621c4 100644 --- a/core/tests/coretests/aidl/com/android/frameworks/coretests/aidl/IAppOpsUserService.aidl +++ b/core/tests/coretests/aidl/com/android/frameworks/coretests/aidl/IAppOpsUserService.aidl @@ -25,4 +25,6 @@ interface IAppOpsUserService { void callApiThatNotesSyncOpOtherUidNativelyAndCheckLog(in IAppOpsUserClient client); void callApiThatNotesAsyncOpNativelyAndCheckCustomMessage(in IAppOpsUserClient client); void callApiThatNotesAsyncOpNativelyAndCheckLog(in IAppOpsUserClient client); + void callFreezeAndNoteSyncOp(in IAppOpsUserClient client); + void assertEmptyAsyncNoted(); } diff --git a/core/tests/coretests/src/android/app/AppOpsLoggingTest.kt b/core/tests/coretests/src/android/app/AppOpsLoggingTest.kt index a10d6a9e0823..ff4e532a272c 100644 --- a/core/tests/coretests/src/android/app/AppOpsLoggingTest.kt +++ b/core/tests/coretests/src/android/app/AppOpsLoggingTest.kt @@ -33,6 +33,9 @@ import android.os.IBinder import android.os.Looper import android.os.Process import android.platform.test.annotations.AppModeFull +import android.platform.test.annotations.RequiresFlagsEnabled +import android.platform.test.flag.junit.CheckFlagsRule +import android.platform.test.flag.junit.DeviceFlagsValueProvider import android.util.Log import androidx.test.platform.app.InstrumentationRegistry import com.android.frameworks.coretests.aidl.IAppOpsUserClient @@ -41,9 +44,15 @@ import com.google.common.truth.Truth.assertThat import org.junit.After import org.junit.Assert.fail import org.junit.Before +import org.junit.Rule import org.junit.Test import java.util.concurrent.CompletableFuture +import java.util.concurrent.LinkedBlockingQueue import java.util.concurrent.TimeUnit.MILLISECONDS +import java.util.concurrent.TimeUnit +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.TimeSource private const val LOG_TAG = "AppOpsLoggingTest" @@ -71,8 +80,11 @@ class AppOpsLoggingTest { private var wasLocationEnabled = false + @get:Rule val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() + private lateinit var testService: IAppOpsUserService private lateinit var serviceConnection: ServiceConnection + private lateinit var freezingTestCompletion: CompletableFuture<Unit> // Collected note-op calls inside of this process private val noted = mutableListOf<Pair<SyncNotedAppOp, Array<StackTraceElement>>>() @@ -123,6 +135,7 @@ class AppOpsLoggingTest { context.bindService(serviceIntent, serviceConnection, BIND_AUTO_CREATE) testService = newService.get(TIMEOUT_MILLIS, MILLISECONDS) + freezingTestCompletion = CompletableFuture<Unit>() } private fun clearCollectedNotedOps() { @@ -253,6 +266,26 @@ class AppOpsLoggingTest { } } + @Test + @RequiresFlagsEnabled(android.os.Flags.FLAG_BINDER_FROZEN_STATE_CHANGE_CALLBACK, + android.permission.flags.Flags.FLAG_USE_FROZEN_AWARE_REMOTE_CALLBACK_LIST) + fun dropAsyncOpNotedWhenFrozen() { + // Here's what the test does: + // 1. AppOpsLoggingTest calls AppOpsUserService + // 2. AppOpsUserService calls freezeAndNoteSyncOp in AppOpsLoggingTest + // 3. freezeAndNoteSyncOp freezes AppOpsUserService + // 4. freezeAndNoteSyncOp calls nativeNoteOp which leads to an async op noted callback + // 5. AppOpsService is expected to drop the callback (via RemoteCallbackList) since + // AppOpsUserService is frozen + // 6. freezeAndNoteSyncOp unfreezes AppOpsUserService + // 7. AppOpsLoggingTest calls AppOpsUserService.assertEmptyAsyncNoted + rethrowThrowableFrom { + testService.callFreezeAndNoteSyncOp(AppOpsUserClient(context)) + freezingTestCompletion.get() + testService.assertEmptyAsyncNoted() + } + } + @After fun removeNotedAppOpsCollector() { appOpsManager.setOnOpNotedCallback(null, null) @@ -263,6 +296,20 @@ class AppOpsLoggingTest { context.unbindService(serviceConnection) } + fun <T> waitForState(queue: LinkedBlockingQueue<T>, state: T, duration: Duration): T? { + val timeSource = TimeSource.Monotonic + val start = timeSource.markNow() + var remaining = duration + while (remaining.inWholeMilliseconds > 0) { + val v = queue.poll(remaining.inWholeMilliseconds, TimeUnit.MILLISECONDS) + if (v == state) { + return v + } + remaining -= timeSource.markNow() - start + } + return null + } + private inner class AppOpsUserClient( context: Context ) : IAppOpsUserClient.Stub() { @@ -285,6 +332,31 @@ class AppOpsLoggingTest { nativeNoteOp(strOpToOp(OPSTR_COARSE_LOCATION), Binder.getCallingUid(), TEST_SERVICE_PKG) } + override fun freezeAndNoteSyncOp() { + handler.post { + var stateChanges = LinkedBlockingQueue<Int>() + // Leave some time for any pending binder transactions to complete. + // + // TODO(327047060) Remove this sleep and instead make am freeze wait for binder + // transactions to complete + Thread.sleep(1000) + testService.asBinder().addFrozenStateChangeCallback { + _, state -> stateChanges.put(state) + } + InstrumentationRegistry.getInstrumentation().uiAutomation + .executeShellCommand("am freeze $TEST_SERVICE_PKG") + waitForState(stateChanges, IBinder.FrozenStateChangeCallback.STATE_FROZEN, + 1000.milliseconds) + nativeNoteOp(strOpToOp(OPSTR_COARSE_LOCATION), Binder.getCallingUid(), + TEST_SERVICE_PKG) + InstrumentationRegistry.getInstrumentation().uiAutomation + .executeShellCommand("am unfreeze $TEST_SERVICE_PKG") + waitForState(stateChanges, IBinder.FrozenStateChangeCallback.STATE_UNFROZEN, + 1000.milliseconds) + freezingTestCompletion.complete(Unit) + } + } + override fun noteSyncOpOtherUidNative() { nativeNoteOp(strOpToOp(OPSTR_COARSE_LOCATION), myUid, myPackage) } diff --git a/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java b/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java index c2d8f9129e2c..da1fffa0fac4 100644 --- a/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java +++ b/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java @@ -17,6 +17,9 @@ package android.app; import static android.app.PropertyInvalidatedCache.NONCE_UNSET; +import static android.app.PropertyInvalidatedCache.MODULE_BLUETOOTH; +import static android.app.PropertyInvalidatedCache.MODULE_SYSTEM; +import static android.app.PropertyInvalidatedCache.MODULE_TEST; import static android.app.PropertyInvalidatedCache.NonceStore.INVALID_NONCE_INDEX; import static com.android.internal.os.Flags.FLAG_APPLICATION_SHARED_MEMORY_ENABLED; @@ -27,6 +30,8 @@ import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; +import android.app.PropertyInvalidatedCache.Args; +import android.annotation.SuppressLint; import com.android.internal.os.ApplicationSharedMemory; import android.platform.test.annotations.IgnoreUnderRavenwood; @@ -57,7 +62,7 @@ public class PropertyInvalidatedCacheTests { DeviceFlagsValueProvider.createCheckFlagsRule(); // Configuration for creating caches - private static final String MODULE = PropertyInvalidatedCache.MODULE_TEST; + private static final String MODULE = MODULE_TEST; private static final String API = "testApi"; // This class is a proxy for binder calls. It contains a counter that increments @@ -245,6 +250,12 @@ public class PropertyInvalidatedCacheTests { mQuery = query; } + // Create a cache from the args. The name of the cache is the api. + TestCache(Args args, TestQuery query) { + super(args, args.mApi(), query); + mQuery = query; + } + public int getRecomputeCount() { return mQuery.getRecomputeCount(); } @@ -374,14 +385,11 @@ public class PropertyInvalidatedCacheTests { @Test public void testPropertyNames() { String n1; - n1 = PropertyInvalidatedCache.createPropertyName( - PropertyInvalidatedCache.MODULE_SYSTEM, "getPackageInfo"); + n1 = PropertyInvalidatedCache.createPropertyName(MODULE_SYSTEM, "getPackageInfo"); assertEquals(n1, "cache_key.system_server.get_package_info"); - n1 = PropertyInvalidatedCache.createPropertyName( - PropertyInvalidatedCache.MODULE_SYSTEM, "get_package_info"); + n1 = PropertyInvalidatedCache.createPropertyName(MODULE_SYSTEM, "get_package_info"); assertEquals(n1, "cache_key.system_server.get_package_info"); - n1 = PropertyInvalidatedCache.createPropertyName( - PropertyInvalidatedCache.MODULE_BLUETOOTH, "getState"); + n1 = PropertyInvalidatedCache.createPropertyName(MODULE_BLUETOOTH, "getState"); assertEquals(n1, "cache_key.bluetooth.get_state"); } @@ -391,7 +399,7 @@ public class PropertyInvalidatedCacheTests { reason = "SystemProperties doesn't have permission check") public void testPermissionFailure() { // Create a cache that will write a system nonce. - TestCache sysCache = new TestCache(PropertyInvalidatedCache.MODULE_SYSTEM, "mode1"); + TestCache sysCache = new TestCache(MODULE_SYSTEM, "mode1"); try { // Invalidate the cache, which writes the system property. There must be a permission // failure. @@ -407,7 +415,7 @@ public class PropertyInvalidatedCacheTests { @Test public void testTestMode() { // Create a cache that will write a system nonce. - TestCache sysCache = new TestCache(PropertyInvalidatedCache.MODULE_SYSTEM, "mode1"); + TestCache sysCache = new TestCache(MODULE_SYSTEM, "mode1"); sysCache.testPropertyName(); // Invalidate the cache. This must succeed because the property has been marked for @@ -416,7 +424,7 @@ public class PropertyInvalidatedCacheTests { // Create a cache that uses MODULE_TEST. Invalidation succeeds whether or not the // property is tagged as being tested. - TestCache testCache = new TestCache(PropertyInvalidatedCache.MODULE_TEST, "mode2"); + TestCache testCache = new TestCache(MODULE_TEST, "mode2"); testCache.invalidateCache(); testCache.testPropertyName(); testCache.invalidateCache(); @@ -432,7 +440,7 @@ public class PropertyInvalidatedCacheTests { // The expected exception. } // Configuring a property for testing must fail if test mode is false. - TestCache cache2 = new TestCache(PropertyInvalidatedCache.MODULE_SYSTEM, "mode3"); + TestCache cache2 = new TestCache(MODULE_SYSTEM, "mode3"); try { cache2.testPropertyName(); fail("expected an IllegalStateException"); @@ -444,6 +452,34 @@ public class PropertyInvalidatedCacheTests { PropertyInvalidatedCache.setTestMode(true); } + // Test the Args-style constructor. + @Test + public void testArgsConstructor() { + // Create a cache with a maximum of four entries. + TestCache cache = new TestCache(new Args(MODULE_TEST).api("init1").maxEntries(4), + new TestQuery()); + + cache.invalidateCache(); + for (int i = 1; i <= 4; i++) { + assertEquals("foo" + i, cache.query(i)); + assertEquals(i, cache.getRecomputeCount()); + } + // Everything is in the cache. The recompute count must not increase. + for (int i = 1; i <= 4; i++) { + assertEquals("foo" + i, cache.query(i)); + assertEquals(4, cache.getRecomputeCount()); + } + // Overflow the max entries. The recompute count increases by one. + assertEquals("foo5", cache.query(5)); + assertEquals(5, cache.getRecomputeCount()); + // The oldest entry (1) has been evicted. Iterating through the first four entries will + // sequentially evict them all because the loop is proceeding oldest to newest. + for (int i = 1; i <= 4; i++) { + assertEquals("foo" + i, cache.query(i)); + assertEquals(5+i, cache.getRecomputeCount()); + } + } + // Verify the behavior of shared memory nonce storage. This does not directly test the cache // storing nonces in shared memory. @RequiresFlagsEnabled(FLAG_APPLICATION_SHARED_MEMORY_ENABLED) @@ -495,4 +531,43 @@ public class PropertyInvalidatedCacheTests { shmem.close(); } + + // Verify that an invalid module causes an exception. + private void testInvalidModule(String module) { + try { + @SuppressLint("UnusedVariable") + Args arg = new Args(module); + fail("expected an invalid module exception: module=" + module); + } catch (IllegalArgumentException e) { + // Expected exception. + } + } + + // Test various instantiation errors. The good path is tested in other methods. + @Test + public void testArgumentErrors() { + // Verify that an illegal module throws an exception. + testInvalidModule(MODULE_SYSTEM.substring(0, MODULE_SYSTEM.length() - 1)); + testInvalidModule(MODULE_SYSTEM + "x"); + testInvalidModule("mymodule"); + + // Verify that a negative max entries throws. + Args arg = new Args(MODULE_SYSTEM); + try { + arg.maxEntries(0); + fail("expected an invalid maxEntries exception"); + } catch (IllegalArgumentException e) { + // Expected exception. + } + + // Verify that creating a cache with an invalid property string throws. + try { + final String badKey = "cache_key.volume_list"; + @SuppressLint("UnusedVariable") + var cache = new PropertyInvalidatedCache<Integer, Void>(4, badKey); + fail("expected bad property exception: prop=" + badKey); + } catch (IllegalArgumentException e) { + // Expected exception. + } + } } diff --git a/core/tests/coretests/src/android/view/ViewFrameRateTest.java b/core/tests/coretests/src/android/view/ViewFrameRateTest.java index 483ebc2c8649..fb1efa86c236 100644 --- a/core/tests/coretests/src/android/view/ViewFrameRateTest.java +++ b/core/tests/coretests/src/android/view/ViewFrameRateTest.java @@ -891,6 +891,65 @@ public class ViewFrameRateTest { }); } + @Test + @RequiresFlagsEnabled({FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY, + FLAG_TOOLKIT_FRAME_RATE_VIEW_ENABLING_READ_ONLY + }) + public void testTouchBoostReset() throws Throwable { + if (!ViewProperties.vrr_enabled().orElse(true)) { + return; + } + mActivityRule.runOnUiThread(() -> { + ViewGroup.LayoutParams layoutParams = mMovingView.getLayoutParams(); + layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT; + layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT; + mMovingView.setLayoutParams(layoutParams); + mMovingView.setOnClickListener((v) -> {}); + }); + waitForFrameRateCategoryToSettle(); + + int[] position = new int[2]; + mActivityRule.runOnUiThread(() -> { + mMovingView.getLocationOnScreen(position); + position[0] += mMovingView.getWidth() / 2; + position[1] += mMovingView.getHeight() / 2; + }); + final Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation(); + + long now = SystemClock.uptimeMillis(); + MotionEvent down = MotionEvent.obtain( + now, // downTime + now, // eventTime + MotionEvent.ACTION_DOWN, // action + position[0], // x + position[1], // y + 0 // metaState + ); + down.setSource(InputDevice.SOURCE_TOUCHSCREEN); + instrumentation.sendPointerSync(down); + assertEquals(FRAME_RATE_CATEGORY_HIGH_HINT, mViewRoot.getLastPreferredFrameRateCategory()); + + MotionEvent up = MotionEvent.obtain( + now, // downTime + now, // eventTime + MotionEvent.ACTION_UP, // action + position[0], // x + position[1], // y + 0 // metaState + ); + up.setSource(InputDevice.SOURCE_TOUCHSCREEN); + instrumentation.sendPointerSync(up); + + // Wait for idle timeout - 100 ms logner to avoid flaky + Thread.sleep(3100); + + // Should not touch boost after the time out + assertEquals(false, mViewRoot.getIsTouchBoosting()); + assertEquals(FRAME_RATE_CATEGORY_NO_PREFERENCE, + mViewRoot.getLastPreferredFrameRateCategory()); + } + + @LargeTest @Test @RequiresFlagsEnabled({FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY, diff --git a/core/tests/coretests/src/android/view/accessibility/AccessibilityNodeInfoTest.java b/core/tests/coretests/src/android/view/accessibility/AccessibilityNodeInfoTest.java index 930e03dfd6db..3b0eab4661ff 100644 --- a/core/tests/coretests/src/android/view/accessibility/AccessibilityNodeInfoTest.java +++ b/core/tests/coretests/src/android/view/accessibility/AccessibilityNodeInfoTest.java @@ -46,7 +46,7 @@ public class AccessibilityNodeInfoTest { // The number of fields tested in the corresponding CTS AccessibilityNodeInfoTest: // See fullyPopulateAccessibilityNodeInfo, assertEqualsAccessibilityNodeInfo, // and assertAccessibilityNodeInfoCleared in that class. - private static final int NUM_MARSHALLED_PROPERTIES = 46; + private static final int NUM_MARSHALLED_PROPERTIES = 47; /** * The number of properties that are purposely not marshalled @@ -58,7 +58,7 @@ public class AccessibilityNodeInfoTest { // The number of flags held in boolean properties. Their values should also be double-checked // in the methods above. - private static final int NUM_BOOLEAN_PROPERTIES = 27; + private static final int NUM_BOOLEAN_PROPERTIES = 28; @Test public void testStandardActions_serializationFlagIsValid() { diff --git a/core/tests/coretests/src/com/android/internal/jank/FrameTrackerTest.java b/core/tests/coretests/src/com/android/internal/jank/FrameTrackerTest.java index 60b5a422ea80..e7a6cb7291c5 100644 --- a/core/tests/coretests/src/com/android/internal/jank/FrameTrackerTest.java +++ b/core/tests/coretests/src/com/android/internal/jank/FrameTrackerTest.java @@ -67,6 +67,7 @@ import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; +import java.util.Arrays; import java.util.concurrent.TimeUnit; @SmallTest @@ -690,10 +691,9 @@ public class FrameTrackerTest { FrameTracker tracker, long durationMillis, long vsyncId, @JankType int jankType) { final ArgumentCaptor<Runnable> captor = ArgumentCaptor.forClass(Runnable.class); doNothing().when(tracker).postCallback(captor.capture()); - mListenerCapture.getValue().onJankDataAvailable(new JankData[] { - new JankData(vsyncId, jankType, FRAME_TIME_60Hz, FRAME_TIME_60Hz, - TimeUnit.MILLISECONDS.toNanos(durationMillis)) - }); + mListenerCapture.getValue().onJankDataAvailable(Arrays.asList(new JankData( + vsyncId, jankType, FRAME_TIME_60Hz, FRAME_TIME_60Hz, + TimeUnit.MILLISECONDS.toNanos(durationMillis)))); captor.getValue().run(); } } diff --git a/core/tests/packagemanagertests/Android.bp b/core/tests/packagemanagertests/Android.bp index 8ff499826866..c2713ade550d 100644 --- a/core/tests/packagemanagertests/Android.bp +++ b/core/tests/packagemanagertests/Android.bp @@ -16,6 +16,7 @@ android_test { "androidx.test.rules", "frameworks-base-testutils", "mockito-target-minus-junit4", + "platform-test-annotations", ], libs: ["android.test.runner.stubs.system"], platform_apis: true, diff --git a/core/tests/packagemanagertests/src/com/android/internal/pm/pkg/component/ParsedMainComponentUtilsTest.java b/core/tests/packagemanagertests/src/com/android/internal/pm/pkg/component/ParsedMainComponentUtilsTest.java new file mode 100644 index 000000000000..23bcb71bcbb0 --- /dev/null +++ b/core/tests/packagemanagertests/src/com/android/internal/pm/pkg/component/ParsedMainComponentUtilsTest.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.pm.pkg.component; + +import static android.security.Flags.FLAG_ENABLE_INTENT_MATCHING_FLAGS; + +import static org.junit.Assert.assertEquals; + +import android.platform.test.annotations.RequiresFlagsEnabled; + +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.HashMap; +import java.util.Map; + +@RunWith(AndroidJUnit4.class) +public class ParsedMainComponentUtilsTest { + + private final Map<String, Integer> mStringToFlagMap = new HashMap<>(); + + { + mStringToFlagMap.put("none", ParsedMainComponentImpl.INTENT_MATCHING_FLAGS_NONE); + mStringToFlagMap.put("enforceIntentFilter", + ParsedMainComponentImpl.INTENT_MATCHING_FLAGS_ENFORCE_INTENT_FILTER); + mStringToFlagMap.put("allowNullAction", + ParsedMainComponentImpl.INTENT_MATCHING_FLAGS_ALLOW_NULL_ACTION); + } + + @Test + @RequiresFlagsEnabled(FLAG_ENABLE_INTENT_MATCHING_FLAGS) + public void testResolveIntentMatchingFlags() { + assertResolution("", "", ""); + assertResolution("none", "none", "none"); + + assertResolution("none", "enforceIntentFilter", "enforceIntentFilter"); + assertResolution("enforceIntentFilter", "none", "none"); + assertResolution("enforceIntentFilter|allowNullAction", "none", + "none"); + assertResolution("enforceIntentFilter|allowNullAction", "enforceIntentFilter", + "enforceIntentFilter"); + + assertResolution("none", "", "none"); + assertResolution("enforceIntentFilter", "", "enforceIntentFilter"); + assertResolution("enforceIntentFilter|allowNullAction", "", + "enforceIntentFilter|allowNullAction"); + + assertResolution("", "none", "none"); + assertResolution("", "enforceIntentFilter", "enforceIntentFilter"); + assertResolution("", "enforceIntentFilter|allowNullAction", + "enforceIntentFilter|allowNullAction"); + } + + private void assertResolution(String applicationStringFlags, String componentStringFlags, + String expectedStringFlags) { + int applicationFlag = stringToFlag(applicationStringFlags); + int componentFlag = stringToFlag(componentStringFlags); + + int expectedFlag = stringToFlag(expectedStringFlags); + int resolvedFlag = ParsedMainComponentUtils.resolveIntentMatchingFlags(applicationFlag, + componentFlag); + + assertEquals(expectedFlag, resolvedFlag); + } + + private int stringToFlag(String flags) { + int result = 0; + String[] flagList = flags.split("\\|"); + for (String flag : flagList) { + String trimmedFlag = flag.trim(); + result |= mStringToFlagMap.getOrDefault(trimmedFlag, 0); + } + return result; + } +} diff --git a/core/tests/vibrator/src/android/os/VibrationEffectTest.java b/core/tests/vibrator/src/android/os/VibrationEffectTest.java index beb69858a85c..1cd1190c4080 100644 --- a/core/tests/vibrator/src/android/os/VibrationEffectTest.java +++ b/core/tests/vibrator/src/android/os/VibrationEffectTest.java @@ -425,6 +425,15 @@ public class VibrationEffectTest { .build(); assertNull(effect.computeCreateWaveformOffOnTimingsOrNull()); + + effect = VibrationEffect.startWaveformEnvelope(/*initialFrequency=*/ 60) + .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 60f, /*timeMillis=*/ 20) + .addControlPoint(/*amplitude=*/ 0.3f, /*frequencyHz=*/ 100f, /*timeMillis=*/ 50) + .addControlPoint(/*amplitude=*/ 0.4f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 80) + .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 40) + .build(); + + assertNull(effect.computeCreateWaveformOffOnTimingsOrNull()); } @Test @@ -643,6 +652,14 @@ public class VibrationEffectTest { .build() .validate(); + VibrationEffect.startWaveformEnvelope(/*initialFrequency=*/ 30) + .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 60f, /*timeMillis=*/ 20) + .addControlPoint(/*amplitude=*/ 0.3f, /*frequencyHz=*/ 100f, /*timeMillis=*/ 50) + .addControlPoint(/*amplitude=*/ 0.4f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 80) + .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 40) + .build() + .validate(); + VibrationEffect.createRepeatingEffect( /*preamble=*/ VibrationEffect.startWaveformEnvelope() .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 60f, @@ -693,6 +710,34 @@ public class VibrationEffectTest { /*timeMillis=*/ 0) .build() .validate()); + + assertThrows(IllegalStateException.class, + () -> VibrationEffect.startWaveformEnvelope(/*initialFrequency=*/30) + .build().validate()); + assertThrows(IllegalArgumentException.class, + () -> VibrationEffect.startWaveformEnvelope(/*initialFrequency=*/30) + .addControlPoint(/*amplitude=*/ -1.0f, /*frequencyHz=*/ 60f, + /*timeMillis=*/ 20) + .build() + .validate()); + assertThrows(IllegalArgumentException.class, + () -> VibrationEffect.startWaveformEnvelope(/*initialFrequency=*/30) + .addControlPoint(/*amplitude=*/ 1.1f, /*frequencyHz=*/ 60f, + /*timeMillis=*/ 20) + .build() + .validate()); + assertThrows(IllegalArgumentException.class, + () -> VibrationEffect.startWaveformEnvelope(/*initialFrequency=*/30) + .addControlPoint(/*amplitude=*/ 0.8f, /*frequencyHz=*/ 0f, + /*timeMillis=*/ 20) + .build() + .validate()); + assertThrows(IllegalArgumentException.class, + () -> VibrationEffect.startWaveformEnvelope(/*initialFrequency=*/30) + .addControlPoint(/*amplitude=*/ 0.8f, /*frequencyHz=*/ 100f, + /*timeMillis=*/ 0) + .build() + .validate()); } @Test @@ -1331,6 +1376,11 @@ public class VibrationEffectTest { .addTransition(Duration.ofMillis(500), targetAmplitude(0)) .build() .isHapticFeedbackCandidate()); + } + + @Test + @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS) + public void testIsHapticFeedbackCandidate_longEnvelopeEffects_notCandidates() { assertFalse(VibrationEffect.startWaveformEnvelope() .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 60f, /*timeMillis=*/ 200) .addControlPoint(/*amplitude=*/ 0.3f, /*frequencyHz=*/ 100f, /*timeMillis=*/ 500) @@ -1338,6 +1388,13 @@ public class VibrationEffectTest { .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 400) .build() .isHapticFeedbackCandidate()); + assertFalse(VibrationEffect.startWaveformEnvelope(/*initialFrequency=*/ 40) + .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 60f, /*timeMillis=*/ 200) + .addControlPoint(/*amplitude=*/ 0.3f, /*frequencyHz=*/ 100f, /*timeMillis=*/ 500) + .addControlPoint(/*amplitude=*/ 0.4f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 800) + .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 400) + .build() + .isHapticFeedbackCandidate()); } @Test @@ -1351,12 +1408,23 @@ public class VibrationEffectTest { .addTransition(Duration.ofMillis(300), targetAmplitude(0)) .build() .isHapticFeedbackCandidate()); + } + + @Test + @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS) + public void testIsHapticFeedbackCandidate_shortEnvelopeEffects_areCandidates() { assertTrue(VibrationEffect.startWaveformEnvelope() .addControlPoint(/*amplitude=*/ 0.3f, /*frequencyHz=*/ 100f, /*timeMillis=*/ 500) .addControlPoint(/*amplitude=*/ 0.4f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 400) .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 100) .build() .isHapticFeedbackCandidate()); + assertTrue(VibrationEffect.startWaveformEnvelope(/*initialFrequency=*/ 30) + .addControlPoint(/*amplitude=*/ 0.3f, /*frequencyHz=*/ 100f, /*timeMillis=*/ 500) + .addControlPoint(/*amplitude=*/ 0.4f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 400) + .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 100) + .build() + .isHapticFeedbackCandidate()); } @Test diff --git a/graphics/java/android/graphics/Bitmap.java b/graphics/java/android/graphics/Bitmap.java index 461a5aec27ec..dfded7321b2c 100644 --- a/graphics/java/android/graphics/Bitmap.java +++ b/graphics/java/android/graphics/Bitmap.java @@ -102,6 +102,8 @@ public final class Bitmap implements Parcelable { private static volatile int sDefaultDensity = -1; + private long mId; + /** * For backwards compatibility, allows the app layer to change the default * density when running old apps. @@ -152,18 +154,19 @@ public final class Bitmap implements Parcelable { Bitmap(long nativeBitmap, int width, int height, int density, boolean requestPremultiplied, byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets) { - this(nativeBitmap, width, height, density, requestPremultiplied, ninePatchChunk, + this(0, nativeBitmap, width, height, density, requestPremultiplied, ninePatchChunk, ninePatchInsets, true); } // called from JNI and Bitmap_Delegate. - Bitmap(long nativeBitmap, int width, int height, int density, + Bitmap(long id, long nativeBitmap, int width, int height, int density, boolean requestPremultiplied, byte[] ninePatchChunk, NinePatch.InsetStruct ninePatchInsets, boolean fromMalloc) { if (nativeBitmap == 0) { throw new RuntimeException("internal error: native bitmap is 0"); } + mId = id; mWidth = width; mHeight = height; mRequestPremultiplied = requestPremultiplied; diff --git a/graphics/java/android/graphics/Paint.java b/graphics/java/android/graphics/Paint.java index 68d8ebbbdabf..b7a1c13c75c7 100644 --- a/graphics/java/android/graphics/Paint.java +++ b/graphics/java/android/graphics/Paint.java @@ -18,6 +18,8 @@ package android.graphics; import static com.android.text.flags.Flags.FLAG_FIX_LINE_HEIGHT_FOR_LOCALE; import static com.android.text.flags.Flags.FLAG_LETTER_SPACING_JUSTIFICATION; +import static com.android.text.flags.Flags.FLAG_DEPRECATE_ELEGANT_TEXT_HEIGHT_API; + import android.annotation.ColorInt; import android.annotation.ColorLong; @@ -39,6 +41,7 @@ import android.text.GraphicsOperations; import android.text.SpannableString; import android.text.SpannedString; import android.text.TextUtils; +import android.util.Log; import com.android.internal.annotations.GuardedBy; import com.android.text.flags.Flags; @@ -61,6 +64,7 @@ import java.util.Objects; * geometries, text and bitmaps. */ public class Paint { + private static final String TAG = "Paint"; @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) private long mNativePaint; @@ -1803,8 +1807,18 @@ public class Paint { /** * Get the elegant metrics flag. * + * Note: + * For applications target API 35 or later, this function returns true by default. + * For applications target API 36 or later, the function call will be ignored and the elegant + * text height is always enabled. + * * @return true if elegant metrics are enabled for text drawing. + * @deprecated The underlying UI fonts are deprecated and will be removed from the system image. + * Applications supporting scripts with large vertical metrics should adapt their UI by using + * fonts designed with corresponding vertical metrics. */ + @Deprecated + @FlaggedApi(FLAG_DEPRECATE_ELEGANT_TEXT_HEIGHT_API) public boolean isElegantTextHeight() { return nGetElegantTextHeight(mNativePaint) != ELEGANT_TEXT_HEIGHT_DISABLED; } @@ -1819,9 +1833,28 @@ public class Paint { * variants that have not been compacted to fit Latin-based vertical * metrics, and also increases top and bottom bounds to provide more space. * + * <p> + * Note: + * For applications target API 35 or later, the default value will be true by default. + * For applications target API 36 or later, the function call will be ignored and the elegant + * text height is always enabled. + * * @param elegant set the paint's elegant metrics flag for drawing text. + * @deprecated This API will be no-op at some point in the future. The underlying UI fonts is + * deprecated and will be removed from the system image. Applications supporting scripts with + * large vertical metrics should adapt their UI by using fonts designed with corresponding + * vertical metrics. */ + @Deprecated + @FlaggedApi(FLAG_DEPRECATE_ELEGANT_TEXT_HEIGHT_API) public void setElegantTextHeight(boolean elegant) { + if (Flags.deprecateElegantTextHeightApi() && !elegant + && CompatChanges.isChangeEnabled(DEPRECATE_UI_FONT_ENFORCE)) { + if (!elegant) { + Log.w(TAG, "The elegant text height cannot be turned off."); + } + return; + } nSetElegantTextHeight(mNativePaint, elegant ? ELEGANT_TEXT_HEIGHT_ENABLED : ELEGANT_TEXT_HEIGHT_DISABLED); } @@ -1839,6 +1872,19 @@ public class Paint { @EnabledSince(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM) public static final long DEPRECATE_UI_FONT = 279646685L; + /** + * A change ID for deprecating UI fonts enforced. + * + * From API 36, the elegant text height will not be able to be overridden and always true if the + * app has a target SDK of API 36 or later. + * + * @hide + */ + @ChangeId + @EnabledSince(targetSdkVersion = 36) + public static final long DEPRECATE_UI_FONT_ENFORCE = 349519475L; + + private void resetElegantTextHeight() { if (CompatChanges.isChangeEnabled(DEPRECATE_UI_FONT)) { nSetElegantTextHeight(mNativePaint, ELEGANT_TEXT_HEIGHT_UNSET); diff --git a/graphics/java/android/graphics/RenderNode.java b/graphics/java/android/graphics/RenderNode.java index 211f74a47bdd..03a8b306f99d 100644 --- a/graphics/java/android/graphics/RenderNode.java +++ b/graphics/java/android/graphics/RenderNode.java @@ -279,7 +279,8 @@ public final class RenderNode { * @hide */ default void positionChanged(long frameNumber, int left, int top, int right, int bottom, - int clipLeft, int clipTop, int clipRight, int clipBottom) { + int clipLeft, int clipTop, int clipRight, int clipBottom, + int nodeWidth, int nodeHeight) { positionChanged(frameNumber, left, top, right, bottom); } @@ -304,11 +305,12 @@ public final class RenderNode { * @hide */ static boolean callPositionChanged2(WeakReference<PositionUpdateListener> weakListener, long frameNumber, int left, int top, int right, int bottom, - int clipLeft, int clipTop, int clipRight, int clipBottom) { + int clipLeft, int clipTop, int clipRight, int clipBottom, + int nodeWidth, int nodeHeight) { final PositionUpdateListener listener = weakListener.get(); if (listener != null) { listener.positionChanged(frameNumber, left, top, right, bottom, clipLeft, - clipTop, clipRight, clipBottom); + clipTop, clipRight, clipBottom, nodeWidth, nodeHeight); return true; } else { return false; @@ -401,10 +403,11 @@ public final class RenderNode { @Override public void positionChanged(long frameNumber, int left, int top, int right, int bottom, - int clipLeft, int clipTop, int clipRight, int clipBottom) { + int clipLeft, int clipTop, int clipRight, int clipBottom, + int nodeWidth, int nodeHeight) { for (PositionUpdateListener pul : mListeners) { pul.positionChanged(frameNumber, left, top, right, bottom, clipLeft, clipTop, - clipRight, clipBottom); + clipRight, clipBottom, nodeWidth, nodeHeight); } } diff --git a/graphics/java/android/graphics/RuntimeColorFilter.java b/graphics/java/android/graphics/RuntimeColorFilter.java new file mode 100644 index 000000000000..52724ceaf301 --- /dev/null +++ b/graphics/java/android/graphics/RuntimeColorFilter.java @@ -0,0 +1,305 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.graphics; + +import android.annotation.ColorInt; +import android.annotation.ColorLong; +import android.annotation.FlaggedApi; +import android.annotation.NonNull; + +import com.android.graphics.hwui.flags.Flags; + + +/** + * <p>A {@link RuntimeColorFilter} calculates a per-pixel color based on the output of a user + * * defined Android Graphics Shading Language (AGSL) function.</p> + * + * <p>This AGSL function takes in an input color to be operated on. This color is in sRGB and the + * * output is also interpreted as sRGB. The AGSL function signature expects a single input + * * of color (packed as a half4 or float4 or vec4).</p> + * + * <pre class="prettyprint"> + * vec4 main(half4 in_color); + * </pre> + */ +@FlaggedApi(Flags.FLAG_RUNTIME_COLOR_FILTERS_BLENDERS) +public class RuntimeColorFilter extends ColorFilter { + + private String mAgsl; + + /** + * Creates a new RuntimeColorFilter. + * + * @param agsl The text of AGSL color filter program to run. + */ + public RuntimeColorFilter(@NonNull String agsl) { + if (agsl == null) { + throw new NullPointerException("RuntimeColorFilter requires a non-null AGSL string"); + } + mAgsl = agsl; + // call to parent class to register native RuntimeColorFilter + // TODO: find way to get super class to create native instance without requiring the storage + // of agsl string + getNativeInstance(); + + } + /** + * Sets the uniform color value corresponding to this color filter. If the effect does not have + * a uniform with that name or if the uniform is declared with a type other than vec3 or vec4 + * and corresponding layout(color) annotation then an IllegalArgumentException is thrown. + * + * @param uniformName name matching the color uniform declared in the AGSL program + * @param color the provided sRGB color + */ + public void setColorUniform(@NonNull String uniformName, @ColorInt int color) { + setUniform(uniformName, Color.valueOf(color).getComponents(), true); + } + + /** + * Sets the uniform color value corresponding to this color filter. If the effect does not have + * a uniform with that name or if the uniform is declared with a type other than vec3 or vec4 + * and corresponding layout(color) annotation then an IllegalArgumentException is thrown. + * + * @param uniformName name matching the color uniform declared in the AGSL program + * @param color the provided sRGB color + */ + public void setColorUniform(@NonNull String uniformName, @ColorLong long color) { + Color exSRGB = Color.valueOf(color).convert(ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB)); + setUniform(uniformName, exSRGB.getComponents(), true); + } + + /** + * Sets the uniform color value corresponding to this color filter. If the effect does not have + * a uniform with that name or if the uniform is declared with a type other than vec3 or vec4 + * and corresponding layout(color) annotation then an IllegalArgumentException is thrown. + * + * @param uniformName name matching the color uniform declared in the AGSL program + * @param color the provided sRGB color + */ + public void setColorUniform(@NonNull String uniformName, @NonNull Color color) { + if (color == null) { + throw new NullPointerException("The color parameter must not be null"); + } + Color exSRGB = color.convert(ColorSpace.get(ColorSpace.Named.EXTENDED_SRGB)); + setUniform(uniformName, exSRGB.getComponents(), true); + } + + /** + * Sets the uniform value corresponding to this color filter. If the effect does not have a + * uniform with that name or if the uniform is declared with a type other than a float or + * float[1] then an IllegalArgumentException is thrown. + * + * @param uniformName name matching the uniform declared in the AGSL program + */ + public void setFloatUniform(@NonNull String uniformName, float value) { + setFloatUniform(uniformName, value, 0.0f, 0.0f, 0.0f, 1); + } + + /** + * Sets the uniform value corresponding to this color filter. If the effect does not have a + * uniform with that name or if the uniform is declared with a type other than a vec2 or + * float[2] then an IllegalArgumentException is thrown. + * + * @param uniformName name matching the uniform declared in the AGSL program + */ + public void setFloatUniform(@NonNull String uniformName, float value1, float value2) { + setFloatUniform(uniformName, value1, value2, 0.0f, 0.0f, 2); + } + + /** + * Sets the uniform value corresponding to this color filter. If the effect does not have a + * uniform with that name or if the uniform is declared with a type other than a vec3 or + * float[3] then an IllegalArgumentException is thrown. + * + * @param uniformName name matching the uniform declared in the AGSL program + */ + public void setFloatUniform(@NonNull String uniformName, float value1, float value2, + float value3) { + setFloatUniform(uniformName, value1, value2, value3, 0.0f, 3); + + } + + /** + * Sets the uniform value corresponding to this color filter. If the effect does not have a + * uniform with that name or if the uniform is declared with a type other than a vec4 or + * float[4] then an IllegalArgumentException is thrown. + * + * @param uniformName name matching the uniform declared in the AGSL program + */ + public void setFloatUniform(@NonNull String uniformName, float value1, float value2, + float value3, float value4) { + setFloatUniform(uniformName, value1, value2, value3, value4, 4); + } + + /** + * Sets the uniform value corresponding to this color filter. If the effect does not have a + * uniform with that name or if the uniform is declared with a type other than a float + * (for N=1), vecN, or float[N] where N is the length of the values param then an + * IllegalArgumentException is thrown. + * + * @param uniformName name matching the uniform declared in the AGSL program + */ + public void setFloatUniform(@NonNull String uniformName, @NonNull float[] values) { + setUniform(uniformName, values, false); + } + + private void setFloatUniform(@NonNull String uniformName, float value1, float value2, + float value3, float value4, int count) { + if (uniformName == null) { + throw new NullPointerException("The uniformName parameter must not be null"); + } + nativeUpdateUniforms(getNativeInstance(), uniformName, value1, value2, value3, value4, + count); + } + + private void setUniform(@NonNull String uniformName, @NonNull float[] values, boolean isColor) { + if (uniformName == null) { + throw new NullPointerException("The uniformName parameter must not be null"); + } + if (values == null) { + throw new NullPointerException("The uniform values parameter must not be null"); + } + nativeUpdateUniforms(getNativeInstance(), uniformName, values, isColor); + } + + /** + * Sets the uniform value corresponding to this color filter. If the effect does not have a + * uniform with that name or if the uniform is declared with a type other than an int or int[1] + * then an IllegalArgumentException is thrown. + * + * @param uniformName name matching the uniform declared in the AGSL program + */ + public void setIntUniform(@NonNull String uniformName, int value) { + setIntUniform(uniformName, value, 0, 0, 0, 1); + } + + /** + * Sets the uniform value corresponding to this color filter. If the effect does not have a + * uniform with that name or if the uniform is declared with a type other than an ivec2 or + * int[2] then an IllegalArgumentException is thrown. + * + * @param uniformName name matching the uniform declared in the AGSL program + */ + public void setIntUniform(@NonNull String uniformName, int value1, int value2) { + setIntUniform(uniformName, value1, value2, 0, 0, 2); + } + + /** + * Sets the uniform value corresponding to this color filter. If the effect does not have a + * uniform with that name or if the uniform is declared with a type other than an ivec3 or + * int[3] then an IllegalArgumentException is thrown. + * + * @param uniformName name matching the uniform declared in the AGSL program + */ + public void setIntUniform(@NonNull String uniformName, int value1, int value2, int value3) { + setIntUniform(uniformName, value1, value2, value3, 0, 3); + } + + /** + * Sets the uniform value corresponding to this color filter. If the effect does not have a + * uniform with that name or if the uniform is declared with a type other than an ivec4 or + * int[4] then an IllegalArgumentException is thrown. + * + * @param uniformName name matching the uniform declared in the AGSL program + */ + public void setIntUniform(@NonNull String uniformName, int value1, int value2, + int value3, int value4) { + setIntUniform(uniformName, value1, value2, value3, value4, 4); + } + + /** + * Sets the uniform value corresponding to this color filter. If the effect does not have a + * uniform with that name or if the uniform is declared with a type other than an int (for N=1), + * ivecN, or int[N] where N is the length of the values param then an IllegalArgumentException + * is thrown. + * + * @param uniformName name matching the uniform declared in the AGSL program + */ + public void setIntUniform(@NonNull String uniformName, @NonNull int[] values) { + if (uniformName == null) { + throw new NullPointerException("The uniformName parameter must not be null"); + } + if (values == null) { + throw new NullPointerException("The uniform values parameter must not be null"); + } + nativeUpdateUniforms(getNativeInstance(), uniformName, values); + } + + private void setIntUniform(@NonNull String uniformName, int value1, int value2, int value3, + int value4, int count) { + if (uniformName == null) { + throw new NullPointerException("The uniformName parameter must not be null"); + } + nativeUpdateUniforms(getNativeInstance(), uniformName, value1, value2, value3, value4, + count); + } + + /** + * Assigns the uniform shader to the provided shader parameter. If the shader program does not + * have a uniform shader with that name then an IllegalArgumentException is thrown. + * + * @param shaderName name matching the uniform declared in the AGSL program + * @param shader shader passed into the AGSL program for sampling + */ + public void setInputShader(@NonNull String shaderName, @NonNull Shader shader) { + if (shaderName == null) { + throw new NullPointerException("The shaderName parameter must not be null"); + } + if (shader == null) { + throw new NullPointerException("The shader parameter must not be null"); + } + nativeUpdateChild(getNativeInstance(), shaderName, shader.getNativeInstance()); + } + + /** + * Assigns the uniform color filter to the provided color filter parameter. If the shader + * program does not have a uniform color filter with that name then an IllegalArgumentException + * is thrown. + * + * @param filterName name matching the uniform declared in the AGSL program + * @param colorFilter filter passed into the AGSL program for sampling + */ + public void setInputColorFilter(@NonNull String filterName, @NonNull ColorFilter colorFilter) { + if (filterName == null) { + throw new NullPointerException("The filterName parameter must not be null"); + } + if (colorFilter == null) { + throw new NullPointerException("The colorFilter parameter must not be null"); + } + nativeUpdateChild(getNativeInstance(), filterName, colorFilter.getNativeInstance()); + } + + /** @hide */ + @Override + protected long createNativeInstance() { + return nativeCreateRuntimeColorFilter(mAgsl); + } + + private static native long nativeCreateRuntimeColorFilter(String agsl); + private static native void nativeUpdateUniforms( + long colorFilter, String uniformName, float[] uniforms, boolean isColor); + private static native void nativeUpdateUniforms( + long colorFilter, String uniformName, float value1, float value2, float value3, + float value4, int count); + private static native void nativeUpdateUniforms( + long colorFilter, String uniformName, int[] uniforms); + private static native void nativeUpdateUniforms( + long colorFilter, String uniformName, int value1, int value2, int value3, + int value4, int count); + private static native void nativeUpdateChild(long colorFilter, String childName, long child); + +} diff --git a/graphics/java/android/graphics/drawable/AnimatedImageDrawable.java b/graphics/java/android/graphics/drawable/AnimatedImageDrawable.java index 82b3f6875d67..71baed8219d5 100644 --- a/graphics/java/android/graphics/drawable/AnimatedImageDrawable.java +++ b/graphics/java/android/graphics/drawable/AnimatedImageDrawable.java @@ -37,6 +37,7 @@ import android.util.DisplayMetrics; import android.util.TypedValue; import android.view.View; +import com.android.graphics.hwui.flags.Flags; import com.android.internal.R; import dalvik.annotation.optimization.FastNative; @@ -525,6 +526,35 @@ public class AnimatedImageDrawable extends Drawable implements Animatable2 { } } + @Override + public void setFilterBitmap(boolean filterBitmap) { + if (!Flags.animatedImageDrawableFilterBitmap()) { + super.setFilterBitmap(filterBitmap); + return; + } + if (mState.mNativePtr == 0) { + throw new IllegalStateException( + "called setFilterBitmap on empty AnimatedImageDrawable" + ); + } + if (nSetFilterBitmap(mState.mNativePtr, filterBitmap)) { + invalidateSelf(); + } + } + + @Override + public boolean isFilterBitmap() { + if (!Flags.animatedImageDrawableFilterBitmap()) { + return super.isFilterBitmap(); + } + if (mState.mNativePtr == 0) { + throw new IllegalStateException( + "called isFilterBitmap on empty AnimatedImageDrawable" + ); + } + return nGetFilterBitmap(mState.mNativePtr); + } + private void postOnAnimationStart() { if (mAnimationCallbacks == null) { return; @@ -618,4 +648,8 @@ public class AnimatedImageDrawable extends Drawable implements Animatable2 { private static native void nSetMirrored(long nativePtr, boolean mirror); @FastNative private static native void nSetBounds(long nativePtr, Rect rect); + @FastNative + private static native boolean nSetFilterBitmap(long nativePtr, boolean filterBitmap); + @FastNative + private static native boolean nGetFilterBitmap(long nativePtr); } diff --git a/graphics/java/android/graphics/text/PositionedGlyphs.java b/graphics/java/android/graphics/text/PositionedGlyphs.java index 792e248ce839..ed17fdefcb53 100644 --- a/graphics/java/android/graphics/text/PositionedGlyphs.java +++ b/graphics/java/android/graphics/text/PositionedGlyphs.java @@ -24,7 +24,6 @@ import android.annotation.NonNull; import android.graphics.Paint; import android.graphics.Typeface; import android.graphics.fonts.Font; -import android.os.Build; import com.android.internal.util.Preconditions; import com.android.text.flags.Flags; @@ -54,8 +53,6 @@ public final class PositionedGlyphs { Typeface.class.getClassLoader(), nReleaseFunc()); } - private static boolean sIsRobolectric = Build.FINGERPRINT.equals("robolectric"); - private final long mLayoutPtr; private final float mXOffset; private final float mYOffset; @@ -255,7 +252,7 @@ public final class PositionedGlyphs { mXOffset = xOffset; mYOffset = yOffset; - if (!sIsRobolectric && Flags.typefaceRedesign()) { + if (Flags.typefaceRedesign()) { int fontCount = nGetFontCount(layoutPtr); mFonts = new ArrayList<>(fontCount); for (int i = 0; i < fontCount; ++i) { diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java index ad194f707cf3..6398c7a2f498 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java @@ -39,6 +39,7 @@ import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSI import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; +import android.annotation.ColorInt; import android.annotation.Nullable; import android.app.Activity; import android.app.ActivityThread; @@ -1394,10 +1395,14 @@ class DividerPresenter implements View.OnTouchListener { } private void showVeils(@NonNull SurfaceControl.Transaction t) { - final Color primaryVeilColor = getContainerBackgroundColor( - mProperties.mPrimaryContainer, DEFAULT_PRIMARY_VEIL_COLOR); - final Color secondaryVeilColor = getContainerBackgroundColor( - mProperties.mSecondaryContainer, DEFAULT_SECONDARY_VEIL_COLOR); + final Color primaryVeilColor = getVeilColor( + mProperties.mDividerAttributes.getPrimaryVeilColor(), + mProperties.mPrimaryContainer, + DEFAULT_PRIMARY_VEIL_COLOR); + final Color secondaryVeilColor = getVeilColor( + mProperties.mDividerAttributes.getSecondaryVeilColor(), + mProperties.mSecondaryContainer, + DEFAULT_SECONDARY_VEIL_COLOR); t.setColor(mPrimaryVeil, colorToFloatArray(primaryVeilColor)) .setColor(mSecondaryVeil, colorToFloatArray(secondaryVeilColor)) .setLayer(mDividerSurface, DIVIDER_LAYER) @@ -1444,6 +1449,21 @@ class DividerPresenter implements View.OnTouchListener { } } + /** + * Returns the veil color. + * + * If the configured color is not transparent, we use the configured color, otherwise we use + * the window background color of the top activity. If the background color of the top + * activity is unavailable, the default color is used. + */ + @NonNull + private static Color getVeilColor(@ColorInt int configuredColor, + @NonNull TaskFragmentContainer container, @NonNull Color defaultColor) { + return configuredColor != Color.TRANSPARENT + ? Color.valueOf(configuredColor) + : getContainerBackgroundColor(container, defaultColor); + } + private static float[] colorToFloatArray(@NonNull Color color) { return new float[]{color.red(), color.green(), color.blue()}; } diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/Android.bp b/libs/WindowManager/Shell/multivalentScreenshotTests/Android.bp index 61c09f2c396b..7f54c75929fb 100644 --- a/libs/WindowManager/Shell/multivalentScreenshotTests/Android.bp +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/Android.bp @@ -22,42 +22,6 @@ package { default_team: "trendy_team_multitasking_windowing", } -android_app { - name: "WMShellRobolectricScreenshotTestApp", - platform_apis: true, - certificate: "platform", - static_libs: [ - "WindowManager-Shell", - "platform-screenshot-diff-core", - "ScreenshotComposeUtilsLib", // ComposableScreenshotTestRule & Theme.PlatformUi.Screenshot - "SystemUI-res", // Theme.SystemUI (dragged in by ScreenshotComposeUtilsLib) - ], - asset_dirs: ["goldens/robolectric"], - manifest: "AndroidManifestRobolectric.xml", - use_resource_processor: true, -} - -android_robolectric_test { - name: "WMShellRobolectricScreenshotTests", - instrumentation_for: "WMShellRobolectricScreenshotTestApp", - upstream: true, - java_resource_dirs: [ - "robolectric/config", - ], - srcs: [ - "src/**/*.kt", - ], - static_libs: [ - "junit", - "androidx.test.runner", - "androidx.test.rules", - "androidx.test.ext.junit", - "truth", - "platform-parametric-runner-lib", - ], - auto_gen_config: true, -} - android_test { name: "WMShellMultivalentScreenshotTestsOnDevice", srcs: [ diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt index 52ce8cb5c0dc..0b515f590f98 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt @@ -23,14 +23,15 @@ import android.content.res.Resources import android.graphics.Color import android.graphics.drawable.Icon import android.os.UserHandle +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.SetFlagsRule -import android.view.IWindowManager import android.view.WindowManager -import android.view.WindowManagerGlobal import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import androidx.test.platform.app.InstrumentationRegistry +import com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn import com.android.internal.logging.testing.UiEventLoggerFake import com.android.internal.protolog.ProtoLog import com.android.launcher3.icons.BubbleIconFactory @@ -41,6 +42,7 @@ import com.android.wm.shell.bubbles.animation.AnimatableScaleMatrix import com.android.wm.shell.common.FloatingContentCoordinator import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils +import com.android.wm.shell.shared.bubbles.BubbleBarLocation import com.android.wm.shell.taskview.TaskView import com.android.wm.shell.taskview.TaskViewTaskController import com.google.common.truth.Truth.assertThat @@ -51,9 +53,7 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.mock -import android.platform.test.annotations.DisableFlags -import android.platform.test.annotations.EnableFlags -import com.android.wm.shell.shared.bubbles.BubbleBarLocation +import org.mockito.kotlin.verify import java.util.concurrent.Semaphore import java.util.concurrent.TimeUnit import java.util.function.Consumer @@ -72,7 +72,7 @@ class BubbleStackViewTest { private lateinit var expandedViewManager: FakeBubbleExpandedViewManager private lateinit var bubbleStackView: BubbleStackView private lateinit var shellExecutor: ShellExecutor - private lateinit var windowManager: IWindowManager + private lateinit var windowManager: WindowManager private lateinit var bubbleTaskViewFactory: BubbleTaskViewFactory private lateinit var bubbleData: BubbleData private lateinit var bubbleStackViewManager: FakeBubbleStackViewManager @@ -83,9 +83,8 @@ class BubbleStackViewTest { PhysicsAnimatorTestUtils.prepareForTest() // Disable protolog tool when running the tests from studio ProtoLog.REQUIRE_PROTOLOGTOOL = false - windowManager = WindowManagerGlobal.getWindowManagerService()!! shellExecutor = TestShellExecutor() - val windowManager = context.getSystemService(WindowManager::class.java) + windowManager = context.getSystemService(WindowManager::class.java) iconFactory = BubbleIconFactory( context, @@ -354,6 +353,16 @@ class BubbleStackViewTest { assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isGreaterThan(-1) } + @Test + fun removeFromWindow_stopMonitoringSwipeUpGesture() { + spyOn(bubbleStackView) + InstrumentationRegistry.getInstrumentation().runOnMainSync { + // No way to add to window in the test environment right now so just pretend + bubbleStackView.onDetachedFromWindow() + } + verify(bubbleStackView).stopMonitoringSwipeUpGesture() + } + private fun createAndInflateChatBubble(key: String): Bubble { val icon = Icon.createWithResource(context.resources, R.drawable.bubble_ic_overflow_button) val shortcutInfo = ShortcutInfo.Builder(context, "fakeId").setIcon(icon).build() diff --git a/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_change_aspect_ratio.xml b/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_change_aspect_ratio.xml new file mode 100644 index 000000000000..4442e9df7688 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/desktop_mode_ic_handle_menu_change_aspect_ratio.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="20dp" + android:height="20dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:fillColor="@color/compat_controls_text" + android:pathData="M19,12h-2v3h-3v2h5v-5zM7,9h3L10,7L5,7v5h2L7,9zM21,3L3,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2L23,5c0,-1.1 -0.9,-2 -2,-2zM21,19.01L3,19.01L3,4.99h18v14.02z"/> +</vector> diff --git a/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_promo_background.xml b/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_promo_background.xml new file mode 100644 index 000000000000..645d24df7c26 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/desktop_windowing_education_promo_background.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<shape android:shape="rectangle" + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"> + <solid android:color="?androidprv:attr/materialColorSurfaceContainerLow" /> + <corners android:radius="@dimen/desktop_windowing_education_promo_corner_radius" /> +</shape> diff --git a/libs/WindowManager/Shell/res/layout/compat_ui_layout.xml b/libs/WindowManager/Shell/res/layout/compat_ui_layout.xml index 62782a784db9..e7ead63a8df6 100644 --- a/libs/WindowManager/Shell/res/layout/compat_ui_layout.xml +++ b/libs/WindowManager/Shell/res/layout/compat_ui_layout.xml @@ -22,14 +22,14 @@ android:gravity="bottom|end"> <include android:id="@+id/size_compat_hint" - android:visibility="gone" + android:visibility="invisible" android:layout_width="@dimen/compat_hint_width" android:layout_height="wrap_content" layout="@layout/compat_mode_hint"/> <ImageButton android:id="@+id/size_compat_restart_button" - android:visibility="gone" + android:visibility="invisible" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="@dimen/compat_button_margin" diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml index 5609663c01a0..f90e165ffc74 100644 --- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml @@ -157,6 +157,14 @@ android:drawableStart="@drawable/desktop_mode_ic_handle_menu_manage_windows" android:drawableTint="?androidprv:attr/materialColorOnSurface" style="@style/DesktopModeHandleMenuActionButton" /> + + <Button + android:id="@+id/change_aspect_ratio_button" + android:contentDescription="@string/change_aspect_ratio_text" + android:text="@string/change_aspect_ratio_text" + android:drawableStart="@drawable/desktop_mode_ic_handle_menu_change_aspect_ratio" + android:drawableTint="?androidprv:attr/materialColorOnSurface" + style="@style/DesktopModeHandleMenuActionButton" /> </LinearLayout> <LinearLayout diff --git a/libs/WindowManager/Shell/res/layout/desktop_windowing_education_promo.xml b/libs/WindowManager/Shell/res/layout/desktop_windowing_education_promo.xml new file mode 100644 index 000000000000..eebfd4bb595e --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/desktop_windowing_education_promo.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/education_container" + android:layout_width="@dimen/desktop_windowing_education_promo_width" + android:layout_height="@dimen/desktop_windowing_education_promo_height" + android:elevation="1dp" + android:orientation="vertical" + android:paddingHorizontal="32dp" + android:paddingVertical="24dp" + android:background="@drawable/desktop_windowing_education_promo_background"> + <TextView + android:id="@+id/education_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textSize="24sp" + android:lineHeight="32dp" + android:textFontWeight="400" + android:fontFamily="google-sans-text" + android:layout_gravity="center_horizontal" + android:gravity="center"/> +</LinearLayout> diff --git a/libs/WindowManager/Shell/res/layout/user_aspect_ratio_settings_layout.xml b/libs/WindowManager/Shell/res/layout/user_aspect_ratio_settings_layout.xml index 433d8546ece0..b5f04c3b815a 100644 --- a/libs/WindowManager/Shell/res/layout/user_aspect_ratio_settings_layout.xml +++ b/libs/WindowManager/Shell/res/layout/user_aspect_ratio_settings_layout.xml @@ -22,14 +22,14 @@ android:gravity="bottom|end"> <include android:id="@+id/user_aspect_ratio_settings_hint" - android:visibility="gone" + android:visibility="invisible" android:layout_width="@dimen/compat_hint_width" android:layout_height="wrap_content" layout="@layout/compat_mode_hint"/> <ImageButton android:id="@+id/user_aspect_ratio_settings_button" - android:visibility="gone" + android:visibility="invisible" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="@dimen/compat_button_margin" diff --git a/libs/WindowManager/Shell/res/values-ar/strings.xml b/libs/WindowManager/Shell/res/values-ar/strings.xml index c36783f8f488..a8febc80ffc1 100644 --- a/libs/WindowManager/Shell/res/values-ar/strings.xml +++ b/libs/WindowManager/Shell/res/values-ar/strings.xml @@ -133,10 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"تكبير الشاشة إلى أقصى حدّ"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"التقاط صورة للشاشة"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"لا يمكن نقل التطبيق إلى هنا"</string> - <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> - <skip /> - <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> - <skip /> + <string name="desktop_mode_maximize_menu_immersive_button_text" msgid="559492223133829481">"مجسَّم"</string> + <string name="desktop_mode_maximize_menu_immersive_restore_button_text" msgid="4900114367354709257">"استعادة"</string> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"تكبير"</string> <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"استعادة"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"المحاذاة إلى اليسار"</string> diff --git a/libs/WindowManager/Shell/res/values-as/strings.xml b/libs/WindowManager/Shell/res/values-as/strings.xml index 71d95edccea6..8c924e342875 100644 --- a/libs/WindowManager/Shell/res/values-as/strings.xml +++ b/libs/WindowManager/Shell/res/values-as/strings.xml @@ -133,10 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"স্ক্ৰীন মেক্সিমাইজ কৰক"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"স্ক্ৰীন স্নেপ কৰক"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"ইয়ালৈ এপ্টো আনিব নোৱাৰি"</string> - <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> - <skip /> - <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> - <skip /> + <string name="desktop_mode_maximize_menu_immersive_button_text" msgid="559492223133829481">"ইমাৰ্ছিভ"</string> + <string name="desktop_mode_maximize_menu_immersive_restore_button_text" msgid="4900114367354709257">"পুনঃস্থাপন কৰক"</string> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"মেক্সিমাইজ কৰক"</string> <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"পুনঃস্থাপন কৰক"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"বাওঁফাললৈ স্নেপ কৰক"</string> diff --git a/libs/WindowManager/Shell/res/values-bn/strings.xml b/libs/WindowManager/Shell/res/values-bn/strings.xml index 4412497c3862..22a445f1754c 100644 --- a/libs/WindowManager/Shell/res/values-bn/strings.xml +++ b/libs/WindowManager/Shell/res/values-bn/strings.xml @@ -133,10 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"স্ক্রিন বড় করুন"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"স্ক্রিনে অ্যাপ মানানসই হিসেবে ছোট বড় করুন"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"অ্যাপটি এখানে সরানো যাবে না"</string> - <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> - <skip /> - <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> - <skip /> + <string name="desktop_mode_maximize_menu_immersive_button_text" msgid="559492223133829481">"ইমারসিভ"</string> + <string name="desktop_mode_maximize_menu_immersive_restore_button_text" msgid="4900114367354709257">"ফিরিয়ে আনুন"</string> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"বড় করুন"</string> <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"ফিরিয়ে আনুন"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"বাঁদিকে স্ন্যাপ করুন"</string> diff --git a/libs/WindowManager/Shell/res/values-bs/strings.xml b/libs/WindowManager/Shell/res/values-bs/strings.xml index 674022be39a2..73f30d797883 100644 --- a/libs/WindowManager/Shell/res/values-bs/strings.xml +++ b/libs/WindowManager/Shell/res/values-bs/strings.xml @@ -133,10 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maksimiziraj ekran"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Snimi ekran"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Ne možete premjestiti aplikaciju ovdje"</string> - <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> - <skip /> - <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> - <skip /> + <string name="desktop_mode_maximize_menu_immersive_button_text" msgid="559492223133829481">"Interaktivno"</string> + <string name="desktop_mode_maximize_menu_immersive_restore_button_text" msgid="4900114367354709257">"Vrati"</string> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maksimiziranje"</string> <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Vraćanje"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Pomicanje ulijevo"</string> diff --git a/libs/WindowManager/Shell/res/values-cs/strings.xml b/libs/WindowManager/Shell/res/values-cs/strings.xml index fc6b679a3a09..6a5780e01822 100644 --- a/libs/WindowManager/Shell/res/values-cs/strings.xml +++ b/libs/WindowManager/Shell/res/values-cs/strings.xml @@ -133,10 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximalizovat obrazovku"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Rozpůlit obrazovku"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Aplikaci sem nelze přesunout"</string> - <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> - <skip /> - <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> - <skip /> + <string name="desktop_mode_maximize_menu_immersive_button_text" msgid="559492223133829481">"Pohlcující"</string> + <string name="desktop_mode_maximize_menu_immersive_restore_button_text" msgid="4900114367354709257">"Obnovit"</string> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maximalizovat"</string> <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Obnovit"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Přichytit vlevo"</string> diff --git a/libs/WindowManager/Shell/res/values-el/strings.xml b/libs/WindowManager/Shell/res/values-el/strings.xml index d06b46fb40d4..d02fae2a986d 100644 --- a/libs/WindowManager/Shell/res/values-el/strings.xml +++ b/libs/WindowManager/Shell/res/values-el/strings.xml @@ -133,10 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Μεγιστοποίηση οθόνης"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Προβολή στο μισό της οθόνης"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Δεν είναι δυνατή η μετακίνηση της εφαρμογής εδώ"</string> - <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> - <skip /> - <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> - <skip /> + <string name="desktop_mode_maximize_menu_immersive_button_text" msgid="559492223133829481">"Καθηλωτικό"</string> + <string name="desktop_mode_maximize_menu_immersive_restore_button_text" msgid="4900114367354709257">"Επαναφορά"</string> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Μεγιστοποίηση"</string> <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Επαναφορά"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Κούμπωμα αριστερά"</string> diff --git a/libs/WindowManager/Shell/res/values-en-rAU/strings.xml b/libs/WindowManager/Shell/res/values-en-rAU/strings.xml index c1a4244896c5..f9911451f4b5 100644 --- a/libs/WindowManager/Shell/res/values-en-rAU/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rAU/strings.xml @@ -133,10 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximise screen"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Snap screen"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"App can\'t be moved here"</string> - <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> - <skip /> - <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> - <skip /> + <string name="desktop_mode_maximize_menu_immersive_button_text" msgid="559492223133829481">"Immersive"</string> + <string name="desktop_mode_maximize_menu_immersive_restore_button_text" msgid="4900114367354709257">"Restore"</string> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maximise"</string> <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Restore"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Snap left"</string> diff --git a/libs/WindowManager/Shell/res/values-en-rGB/strings.xml b/libs/WindowManager/Shell/res/values-en-rGB/strings.xml index c1a4244896c5..f9911451f4b5 100644 --- a/libs/WindowManager/Shell/res/values-en-rGB/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rGB/strings.xml @@ -133,10 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximise screen"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Snap screen"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"App can\'t be moved here"</string> - <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> - <skip /> - <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> - <skip /> + <string name="desktop_mode_maximize_menu_immersive_button_text" msgid="559492223133829481">"Immersive"</string> + <string name="desktop_mode_maximize_menu_immersive_restore_button_text" msgid="4900114367354709257">"Restore"</string> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maximise"</string> <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Restore"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Snap left"</string> diff --git a/libs/WindowManager/Shell/res/values-en-rIN/strings.xml b/libs/WindowManager/Shell/res/values-en-rIN/strings.xml index c1a4244896c5..f9911451f4b5 100644 --- a/libs/WindowManager/Shell/res/values-en-rIN/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rIN/strings.xml @@ -133,10 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximise screen"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Snap screen"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"App can\'t be moved here"</string> - <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> - <skip /> - <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> - <skip /> + <string name="desktop_mode_maximize_menu_immersive_button_text" msgid="559492223133829481">"Immersive"</string> + <string name="desktop_mode_maximize_menu_immersive_restore_button_text" msgid="4900114367354709257">"Restore"</string> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maximise"</string> <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Restore"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Snap left"</string> diff --git a/libs/WindowManager/Shell/res/values-hi/strings.xml b/libs/WindowManager/Shell/res/values-hi/strings.xml index ff675a9f6356..527793eac9c3 100644 --- a/libs/WindowManager/Shell/res/values-hi/strings.xml +++ b/libs/WindowManager/Shell/res/values-hi/strings.xml @@ -133,10 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"स्क्रीन को बड़ा करें"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"स्नैप स्क्रीन"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"ऐप्लिकेशन को यहां मूव नहीं किया जा सकता"</string> - <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> - <skip /> - <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> - <skip /> + <string name="desktop_mode_maximize_menu_immersive_button_text" msgid="559492223133829481">"इमर्सिव"</string> + <string name="desktop_mode_maximize_menu_immersive_restore_button_text" msgid="4900114367354709257">"वापस लाएं"</string> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"बड़ा करें"</string> <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"पहले जैसा करें"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"बाईं ओर स्नैप करें"</string> diff --git a/libs/WindowManager/Shell/res/values-hr/strings.xml b/libs/WindowManager/Shell/res/values-hr/strings.xml index 16fbcd0565fa..659d1ec39b73 100644 --- a/libs/WindowManager/Shell/res/values-hr/strings.xml +++ b/libs/WindowManager/Shell/res/values-hr/strings.xml @@ -133,10 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maksimalno povećaj zaslon"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Izradi snimku zaslona"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Aplikacija se ne može premjestiti ovdje"</string> - <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> - <skip /> - <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> - <skip /> + <string name="desktop_mode_maximize_menu_immersive_button_text" msgid="559492223133829481">"Interaktivno"</string> + <string name="desktop_mode_maximize_menu_immersive_restore_button_text" msgid="4900114367354709257">"Vrati"</string> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maksimiziraj"</string> <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Vrati"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Poravnaj lijevo"</string> diff --git a/libs/WindowManager/Shell/res/values-is/strings.xml b/libs/WindowManager/Shell/res/values-is/strings.xml index f595765a797e..ca1bc15edf33 100644 --- a/libs/WindowManager/Shell/res/values-is/strings.xml +++ b/libs/WindowManager/Shell/res/values-is/strings.xml @@ -133,10 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Stækka skjá"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Smelluskjár"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Ekki er hægt að færa forritið hingað"</string> - <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> - <skip /> - <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> - <skip /> + <string name="desktop_mode_maximize_menu_immersive_button_text" msgid="559492223133829481">"Umlykjandi"</string> + <string name="desktop_mode_maximize_menu_immersive_restore_button_text" msgid="4900114367354709257">"Endurheimta"</string> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Stækka"</string> <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Endurheimta"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Smella til vinstri"</string> diff --git a/libs/WindowManager/Shell/res/values-it/strings.xml b/libs/WindowManager/Shell/res/values-it/strings.xml index 8b7c15ffeb15..87919b5dc1ff 100644 --- a/libs/WindowManager/Shell/res/values-it/strings.xml +++ b/libs/WindowManager/Shell/res/values-it/strings.xml @@ -133,10 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Massimizza schermo"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Aggancia schermo"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Impossibile spostare l\'app qui"</string> - <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> - <skip /> - <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> - <skip /> + <string name="desktop_mode_maximize_menu_immersive_button_text" msgid="559492223133829481">"Immersivo"</string> + <string name="desktop_mode_maximize_menu_immersive_restore_button_text" msgid="4900114367354709257">"Ripristina"</string> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Ingrandisci"</string> <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Ripristina"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Aggancia a sinistra"</string> diff --git a/libs/WindowManager/Shell/res/values-ja/strings.xml b/libs/WindowManager/Shell/res/values-ja/strings.xml index 5464cfa28b3c..c7a77d9b9214 100644 --- a/libs/WindowManager/Shell/res/values-ja/strings.xml +++ b/libs/WindowManager/Shell/res/values-ja/strings.xml @@ -133,10 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"画面の最大化"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"画面のスナップ"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"アプリはここに移動できません"</string> - <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> - <skip /> - <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> - <skip /> + <string name="desktop_mode_maximize_menu_immersive_button_text" msgid="559492223133829481">"没入モード"</string> + <string name="desktop_mode_maximize_menu_immersive_restore_button_text" msgid="4900114367354709257">"復元"</string> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"最大化"</string> <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"復元"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"左にスナップ"</string> diff --git a/libs/WindowManager/Shell/res/values-ka/strings.xml b/libs/WindowManager/Shell/res/values-ka/strings.xml index 7ea9daffca18..39362ef4ca57 100644 --- a/libs/WindowManager/Shell/res/values-ka/strings.xml +++ b/libs/WindowManager/Shell/res/values-ka/strings.xml @@ -133,10 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"აპლიკაციის გაშლა სრულ ეკრანზე"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"აპლიკაციის დაპატარავება ეკრანზე"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"აპის აქ გადატანა შეუძლებელია"</string> - <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> - <skip /> - <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> - <skip /> + <string name="desktop_mode_maximize_menu_immersive_button_text" msgid="559492223133829481">"იმერსიული"</string> + <string name="desktop_mode_maximize_menu_immersive_restore_button_text" msgid="4900114367354709257">"აღდგენა"</string> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"მაქსიმალურად გაშლა"</string> <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"აღდგენა"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"მარცხნივ გადატანა"</string> diff --git a/libs/WindowManager/Shell/res/values-km/strings.xml b/libs/WindowManager/Shell/res/values-km/strings.xml index 83bd3744d27a..9c4ae05f3a36 100644 --- a/libs/WindowManager/Shell/res/values-km/strings.xml +++ b/libs/WindowManager/Shell/res/values-km/strings.xml @@ -133,10 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"ពង្រីកអេក្រង់"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"ថតអេក្រង់"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"មិនអាចផ្លាស់ទីកម្មវិធីមកទីនេះបានទេ"</string> - <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> - <skip /> - <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> - <skip /> + <string name="desktop_mode_maximize_menu_immersive_button_text" msgid="559492223133829481">"ជក់ចិត្ត"</string> + <string name="desktop_mode_maximize_menu_immersive_restore_button_text" msgid="4900114367354709257">"ស្ដារ"</string> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"ពង្រីក"</string> <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"ស្ដារ"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"ផ្លាស់ទីទៅឆ្វេង"</string> diff --git a/libs/WindowManager/Shell/res/values-kn/strings.xml b/libs/WindowManager/Shell/res/values-kn/strings.xml index 502e0c92f844..f365cfb34412 100644 --- a/libs/WindowManager/Shell/res/values-kn/strings.xml +++ b/libs/WindowManager/Shell/res/values-kn/strings.xml @@ -133,10 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"ಸ್ಕ್ರೀನ್ ಅನ್ನು ಮ್ಯಾಕ್ಸಿಮೈಸ್ ಮಾಡಿ"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"ಸ್ನ್ಯಾಪ್ ಸ್ಕ್ರೀನ್"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"ಆ್ಯಪ್ ಅನ್ನು ಇಲ್ಲಿಗೆ ಸರಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ"</string> - <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> - <skip /> - <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> - <skip /> + <string name="desktop_mode_maximize_menu_immersive_button_text" msgid="559492223133829481">"ಇಮ್ಮರ್ಸಿವ್"</string> + <string name="desktop_mode_maximize_menu_immersive_restore_button_text" msgid="4900114367354709257">"ಮರುಸ್ಥಾಪಿಸಿ"</string> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"ಮ್ಯಾಕ್ಸಿಮೈಸ್ ಮಾಡಿ"</string> <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"ಮರುಸ್ಥಾಪಿಸಿ"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"ಎಡಕ್ಕೆ ಸ್ನ್ಯಾಪ್ ಮಾಡಿ"</string> diff --git a/libs/WindowManager/Shell/res/values-lt/strings.xml b/libs/WindowManager/Shell/res/values-lt/strings.xml index 4551f72eb635..5a7f58e5781a 100644 --- a/libs/WindowManager/Shell/res/values-lt/strings.xml +++ b/libs/WindowManager/Shell/res/values-lt/strings.xml @@ -133,10 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Išskleisti ekraną"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Sutraukti ekraną"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Programos negalima perkelti čia"</string> - <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> - <skip /> - <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> - <skip /> + <string name="desktop_mode_maximize_menu_immersive_button_text" msgid="559492223133829481">"Įtraukiantis"</string> + <string name="desktop_mode_maximize_menu_immersive_restore_button_text" msgid="4900114367354709257">"Atkurti"</string> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Padidinti"</string> <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Atkurti"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Pritraukti kairėje"</string> diff --git a/libs/WindowManager/Shell/res/values-mr/strings.xml b/libs/WindowManager/Shell/res/values-mr/strings.xml index a2c610e82823..871bc3fcc8e7 100644 --- a/libs/WindowManager/Shell/res/values-mr/strings.xml +++ b/libs/WindowManager/Shell/res/values-mr/strings.xml @@ -133,10 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"स्क्रीन मोठी करा"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"स्क्रीन स्नॅप करा"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"अॅप इथे हलवू शकत नाही"</string> - <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> - <skip /> - <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> - <skip /> + <string name="desktop_mode_maximize_menu_immersive_button_text" msgid="559492223133829481">"इमर्सिव्ह"</string> + <string name="desktop_mode_maximize_menu_immersive_restore_button_text" msgid="4900114367354709257">"रिस्टोअर करा"</string> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"मोठे करा"</string> <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"रिस्टोअर करा"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"डावीकडे स्नॅप करा"</string> diff --git a/libs/WindowManager/Shell/res/values-ms/strings.xml b/libs/WindowManager/Shell/res/values-ms/strings.xml index 2102a382d232..71666cae93c8 100644 --- a/libs/WindowManager/Shell/res/values-ms/strings.xml +++ b/libs/WindowManager/Shell/res/values-ms/strings.xml @@ -133,10 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maksimumkan Skrin"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Tangkap Skrin"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Apl tidak boleh dialihkan ke sini"</string> - <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> - <skip /> - <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> - <skip /> + <string name="desktop_mode_maximize_menu_immersive_button_text" msgid="559492223133829481">"Mengasyikkan"</string> + <string name="desktop_mode_maximize_menu_immersive_restore_button_text" msgid="4900114367354709257">"Pulihkan"</string> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maksimumkan"</string> <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Pulihkan"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Autojajar ke kiri"</string> diff --git a/libs/WindowManager/Shell/res/values-ne/strings.xml b/libs/WindowManager/Shell/res/values-ne/strings.xml index e9a530621c7e..7015b2c11b32 100644 --- a/libs/WindowManager/Shell/res/values-ne/strings.xml +++ b/libs/WindowManager/Shell/res/values-ne/strings.xml @@ -133,10 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"स्क्रिन ठुलो बनाउनुहोस्"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"स्क्रिन स्न्याप गर्नुहोस्"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"एप सारेर यहाँ ल्याउन सकिएन"</string> - <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> - <skip /> - <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> - <skip /> + <string name="desktop_mode_maximize_menu_immersive_button_text" msgid="559492223133829481">"इमर्सिभ"</string> + <string name="desktop_mode_maximize_menu_immersive_restore_button_text" msgid="4900114367354709257">"रिस्टोर गर्नुहोस्"</string> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"ठुलो बनाउनुहोस्"</string> <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"रिस्टोर गर्नुहोस्"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"बायाँतिर स्न्याप गर्नुहोस्"</string> diff --git a/libs/WindowManager/Shell/res/values-pl/strings.xml b/libs/WindowManager/Shell/res/values-pl/strings.xml index 14b7a09f6143..5f78b134ad22 100644 --- a/libs/WindowManager/Shell/res/values-pl/strings.xml +++ b/libs/WindowManager/Shell/res/values-pl/strings.xml @@ -133,10 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maksymalizuj ekran"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Przyciągnij ekran"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Nie można przenieść aplikacji tutaj"</string> - <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> - <skip /> - <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> - <skip /> + <string name="desktop_mode_maximize_menu_immersive_button_text" msgid="559492223133829481">"Tryb immersyjny"</string> + <string name="desktop_mode_maximize_menu_immersive_restore_button_text" msgid="4900114367354709257">"Przywróć"</string> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maksymalizuj"</string> <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Przywróć"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Przyciągnij do lewej"</string> diff --git a/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml b/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml index 9631c45a8097..cd78ef95d88e 100644 --- a/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml +++ b/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml @@ -133,10 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximizar ecrã"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Encaixar ecrã"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Não é possível mover a app para aqui"</string> - <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> - <skip /> - <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> - <skip /> + <string name="desktop_mode_maximize_menu_immersive_button_text" msgid="559492223133829481">"Envolvente"</string> + <string name="desktop_mode_maximize_menu_immersive_restore_button_text" msgid="4900114367354709257">"Restaurar"</string> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maximizar"</string> <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Restaurar"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Encaixar à esquerda"</string> diff --git a/libs/WindowManager/Shell/res/values-sl/strings.xml b/libs/WindowManager/Shell/res/values-sl/strings.xml index 2012c8a6754e..ae7e524da6cc 100644 --- a/libs/WindowManager/Shell/res/values-sl/strings.xml +++ b/libs/WindowManager/Shell/res/values-sl/strings.xml @@ -133,10 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maksimiraj zaslon"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Pripni zaslon"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Aplikacije ni mogoče premakniti sem"</string> - <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> - <skip /> - <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> - <skip /> + <string name="desktop_mode_maximize_menu_immersive_button_text" msgid="559492223133829481">"Poglobljeno"</string> + <string name="desktop_mode_maximize_menu_immersive_restore_button_text" msgid="4900114367354709257">"Obnovi"</string> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maksimiraj"</string> <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Obnovi"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Pripni levo"</string> diff --git a/libs/WindowManager/Shell/res/values-ta/strings.xml b/libs/WindowManager/Shell/res/values-ta/strings.xml index c4243f7ae90d..2c73d3a14620 100644 --- a/libs/WindowManager/Shell/res/values-ta/strings.xml +++ b/libs/WindowManager/Shell/res/values-ta/strings.xml @@ -133,10 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"திரையைப் பெரிதாக்கு"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"திரையை ஸ்னாப் செய்"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"ஆப்ஸை இங்கே நகர்த்த முடியாது"</string> - <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> - <skip /> - <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> - <skip /> + <string name="desktop_mode_maximize_menu_immersive_button_text" msgid="559492223133829481">"ஈடுபட வைக்கும்"</string> + <string name="desktop_mode_maximize_menu_immersive_restore_button_text" msgid="4900114367354709257">"மீட்டெடுக்கும்"</string> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"பெரிதாக்கும்"</string> <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"மீட்டெடுக்கும்"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"இடதுபுறம் நகர்த்தும்"</string> diff --git a/libs/WindowManager/Shell/res/values-te/strings.xml b/libs/WindowManager/Shell/res/values-te/strings.xml index 47fc48ddc3c3..b17d4d1afaf7 100644 --- a/libs/WindowManager/Shell/res/values-te/strings.xml +++ b/libs/WindowManager/Shell/res/values-te/strings.xml @@ -133,10 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"స్క్రీన్ సైజ్ను పెంచండి"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"స్క్రీన్ను స్నాప్ చేయండి"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"యాప్ను ఇక్కడకి తరలించడం సాధ్యం కాదు"</string> - <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> - <skip /> - <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> - <skip /> + <string name="desktop_mode_maximize_menu_immersive_button_text" msgid="559492223133829481">"లీనమయ్యే"</string> + <string name="desktop_mode_maximize_menu_immersive_restore_button_text" msgid="4900114367354709257">"రీస్టోర్ చేయండి"</string> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"మ్యాగ్జిమైజ్ చేయండి"</string> <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"రీస్టోర్ చేయండి"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"ఎడమ వైపున స్నాప్ చేయండి"</string> diff --git a/libs/WindowManager/Shell/res/values-th/strings.xml b/libs/WindowManager/Shell/res/values-th/strings.xml index 260807537385..43cee41f5a15 100644 --- a/libs/WindowManager/Shell/res/values-th/strings.xml +++ b/libs/WindowManager/Shell/res/values-th/strings.xml @@ -133,10 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"ขยายหน้าจอให้ใหญ่สุด"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"สแนปหน้าจอ"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"ย้ายแอปมาที่นี่ไม่ได้"</string> - <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> - <skip /> - <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> - <skip /> + <string name="desktop_mode_maximize_menu_immersive_button_text" msgid="559492223133829481">"สมจริง"</string> + <string name="desktop_mode_maximize_menu_immersive_restore_button_text" msgid="4900114367354709257">"คืนค่า"</string> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"ขยายใหญ่สุด"</string> <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"คืนค่า"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"จัดพอดีกับทางซ้าย"</string> diff --git a/libs/WindowManager/Shell/res/values-tl/strings.xml b/libs/WindowManager/Shell/res/values-tl/strings.xml index 94166ef5db5e..428499532005 100644 --- a/libs/WindowManager/Shell/res/values-tl/strings.xml +++ b/libs/WindowManager/Shell/res/values-tl/strings.xml @@ -133,10 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"I-maximize ang Screen"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"I-snap ang Screen"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Hindi mailipat dito ang app"</string> - <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> - <skip /> - <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> - <skip /> + <string name="desktop_mode_maximize_menu_immersive_button_text" msgid="559492223133829481">"Immersive"</string> + <string name="desktop_mode_maximize_menu_immersive_restore_button_text" msgid="4900114367354709257">"I-restore"</string> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"I-maximize"</string> <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"I-restore"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"I-snap pakaliwa"</string> diff --git a/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml b/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml index ad50a045c924..2fb3f5ab5ea4 100644 --- a/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml +++ b/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml @@ -133,10 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"最大化屏幕"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"屏幕快照"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"无法将应用移至此处"</string> - <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> - <skip /> - <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> - <skip /> + <string name="desktop_mode_maximize_menu_immersive_button_text" msgid="559492223133829481">"沉浸式"</string> + <string name="desktop_mode_maximize_menu_immersive_restore_button_text" msgid="4900114367354709257">"恢复"</string> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"最大化"</string> <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"恢复"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"贴靠左侧"</string> diff --git a/libs/WindowManager/Shell/res/values/config.xml b/libs/WindowManager/Shell/res/values/config.xml index a14461a57a95..59e6c7786cb8 100644 --- a/libs/WindowManager/Shell/res/values/config.xml +++ b/libs/WindowManager/Shell/res/values/config.xml @@ -186,4 +186,6 @@ <!-- Apps that can trigger Desktop Windowing App handle Education --> <string-array name="desktop_windowing_app_handle_education_allowlist_apps"></string-array> + <!-- Apps that can trigger Desktop Windowing App-To-Web Education --> + <string-array name="desktop_windowing_app_to_web_education_allowlist_apps"></string-array> </resources> diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index fa1aa193e1e3..249e9a26e6a6 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -523,8 +523,9 @@ <dimen name="desktop_mode_handle_menu_width">216dp</dimen> <!-- The maximum height of the handle menu in desktop mode. Three pills at 52dp each, - additional actions pill 156dp, plus 2dp spacing between them plus 4dp top padding. --> - <dimen name="desktop_mode_handle_menu_height">322dp</dimen> + additional actions pill 208dp, plus 2dp spacing between them plus 4dp top padding. + 52*3 + 52*4 + (4-1)*2 + 4 = 374 --> + <dimen name="desktop_mode_handle_menu_height">374dp</dimen> <!-- The elevation set on the handle menu pills. --> <dimen name="desktop_mode_handle_menu_pill_elevation">1dp</dimen> @@ -547,6 +548,9 @@ <!-- The height of the handle menu's "Open in browser" pill in desktop mode. --> <dimen name="desktop_mode_handle_menu_open_in_browser_pill_height">52dp</dimen> + <!-- The height of the handle menu's "Change aspect ratio" pill in desktop mode. --> + <dimen name="desktop_mode_handle_menu_change_aspect_ratio_height">52dp</dimen> + <!-- The margin between pills of the handle menu in desktop mode. --> <dimen name="desktop_mode_handle_menu_pill_spacing_margin">2dp</dimen> @@ -650,4 +654,11 @@ <dimen name="open_by_default_settings_dialog_radio_button_height">64dp</dimen> <!-- The width of radio buttons in the open by default settings dialog. --> <dimen name="open_by_default_settings_dialog_radio_button_width">316dp</dimen> + + <!-- The width of the desktop windowing education promo. --> + <dimen name="desktop_windowing_education_promo_width">412dp</dimen> + <!-- The height of the desktop windowing education promo. --> + <dimen name="desktop_windowing_education_promo_height">352dp</dimen> + <!-- The corner radius of the desktop windowing education promo. --> + <dimen name="desktop_windowing_education_promo_corner_radius">28dp</dimen> </resources> diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml index ef0386a1b4dd..8f1ef6c7e49e 100644 --- a/libs/WindowManager/Shell/res/values/strings.xml +++ b/libs/WindowManager/Shell/res/values/strings.xml @@ -305,6 +305,8 @@ <string name="new_window_text">New Window</string> <!-- Accessibility text for the handle menu new window button [CHAR LIMIT=NONE] --> <string name="manage_windows_text">Manage Windows</string> + <!-- Accessibility text for the handle menu change aspect ratio button [CHAR LIMIT=NONE] --> + <string name="change_aspect_ratio_text">Change aspect ratio</string> <!-- Accessibility text for the handle menu close button [CHAR LIMIT=NONE] --> <string name="close_text">Close</string> <!-- Accessibility text for the handle menu close menu button [CHAR LIMIT=NONE] --> @@ -339,4 +341,7 @@ <string name="open_by_default_dialog_in_browser_text">In your browser</string> <!-- Text for open by default settings dialog dismiss button. --> <string name="open_by_default_dialog_dismiss_button_text">OK</string> + + <!-- Text for the App-to-Web education promo. --> + <string name="desktop_windowing_app_to_web_education_text">Quickly open apps in your browser here</string> </resources> diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/WindowAnimator.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/WindowAnimator.kt index 043508542892..91d66eaeb088 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/WindowAnimator.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/WindowAnimator.kt @@ -16,8 +16,9 @@ package com.android.wm.shell.shared.animation -import android.animation.RectEvaluator +import android.animation.PointFEvaluator import android.animation.ValueAnimator +import android.graphics.PointF import android.graphics.Rect import android.util.DisplayMetrics import android.util.TypedValue @@ -52,46 +53,56 @@ object WindowAnimator { change: TransitionInfo.Change, transaction: SurfaceControl.Transaction, ): ValueAnimator { - val startBounds = - createBounds( + val startPos = + getPosition( displayMetrics, - change.startAbsBounds, + change.endAbsBounds, boundsAnimDef.startScale, boundsAnimDef.startOffsetYDp, ) val leash = change.leash - val endBounds = - createBounds( + val endPos = + getPosition( displayMetrics, - change.startAbsBounds, + change.endAbsBounds, boundsAnimDef.endScale, boundsAnimDef.endOffsetYDp, ) - return ValueAnimator.ofObject(RectEvaluator(), startBounds, endBounds).apply { + return ValueAnimator.ofObject(PointFEvaluator(), startPos, endPos).apply { duration = boundsAnimDef.durationMs interpolator = boundsAnimDef.interpolator addUpdateListener { animation -> - val animBounds = animation.animatedValue as Rect - val animScale = 1 - (1 - boundsAnimDef.endScale) * animation.animatedFraction + val animPos = animation.animatedValue as PointF + val animScale = + interpolate( + boundsAnimDef.startScale, + boundsAnimDef.endScale, + animation.animatedFraction + ) transaction - .setPosition(leash, animBounds.left.toFloat(), animBounds.top.toFloat()) + .setPosition(leash, animPos.x, animPos.y) .setScale(leash, animScale, animScale) .apply() } } } - private fun createBounds( + private fun interpolate(startVal: Float, endVal: Float, fraction: Float): Float { + require(fraction in 0.0f..1.0f) + return startVal + (endVal - startVal) * fraction + } + + private fun getPosition( displayMetrics: DisplayMetrics, - origBounds: Rect, + bounds: Rect, scale: Float, offsetYDp: Float - ) = Rect(origBounds).apply { - check(scale in 0.0..1.0) - // Scale the bounds down with an anchor in the center - inset( - (origBounds.width().toFloat() * (1 - scale) / 2).toInt(), - (origBounds.height().toFloat() * (1 - scale) / 2).toInt(), + ) = PointF(bounds.left.toFloat(), bounds.top.toFloat()).apply { + check(scale in 0.0f..1.0f) + // Scale the bounds down with an anchor in the center + offset( + (bounds.width().toFloat() * (1 - scale) / 2), + (bounds.height().toFloat() * (1 - scale) / 2), ) val offsetYPx = TypedValue.applyDimension( @@ -100,6 +111,6 @@ object WindowAnimator { displayMetrics, ) .toInt() - offset(/* dx= */ 0, offsetYPx) + offset(/* dx= */ 0f, offsetYPx.toFloat()) } } diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java index eb7ef1478a90..62ca5c687a2a 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java @@ -18,6 +18,7 @@ package com.android.wm.shell.shared.pip; import static android.util.TypedValue.COMPLEX_UNIT_DIP; +import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.content.res.TypedArray; @@ -170,26 +171,34 @@ public abstract class PipContentOverlay { private final Context mContext; private final int mAppIconSizePx; - private final Rect mAppBounds; + /** + * The bounds of the application window relative to the task leash. + */ + private final Rect mRelativeAppBounds; private final int mOverlayHalfSize; private final Matrix mTmpTransform = new Matrix(); private final float[] mTmpFloat9 = new float[9]; private Bitmap mBitmap; - public PipAppIconOverlay(Context context, Rect appBounds, Rect destinationBounds, - Drawable appIcon, int appIconSizePx) { + // TODO(b/356277166): add non-match_parent support on PIP2. + /** + * @param context the {@link Context} that contains the icon information + * @param relativeAppBounds the bounds of the app window frame relative to the task leash + * @param destinationBounds the bounds for rhe PIP task + * @param appIcon the app icon {@link Drawable} + * @param appIconSizePx the icon dimension in pixel + */ + public PipAppIconOverlay(@NonNull Context context, @NonNull Rect relativeAppBounds, + @NonNull Rect destinationBounds, @NonNull Drawable appIcon, int appIconSizePx) { mContext = context; final int maxAppIconSizePx = (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, MAX_APP_ICON_SIZE_DP, context.getResources().getDisplayMetrics()); mAppIconSizePx = Math.min(maxAppIconSizePx, appIconSizePx); - final int overlaySize = getOverlaySize(appBounds, destinationBounds); + final int overlaySize = getOverlaySize(relativeAppBounds, destinationBounds); mOverlayHalfSize = overlaySize >> 1; - - // When the activity is in the secondary split, make sure the scaling center is not - // offset. - mAppBounds = new Rect(0, 0, appBounds.width(), appBounds.height()); + mRelativeAppBounds = relativeAppBounds; mBitmap = Bitmap.createBitmap(overlaySize, overlaySize, Bitmap.Config.ARGB_8888); prepareAppIconOverlay(appIcon); @@ -206,9 +215,9 @@ public abstract class PipContentOverlay { * the overlay will be drawn with the max size of the start and end bounds in different * rotation. */ - public static int getOverlaySize(Rect appBounds, Rect destinationBounds) { - final int appWidth = appBounds.width(); - final int appHeight = appBounds.height(); + public static int getOverlaySize(Rect overlayBounds, Rect destinationBounds) { + final int appWidth = overlayBounds.width(); + final int appHeight = overlayBounds.height(); return Math.max(Math.max(appWidth, appHeight), Math.max(destinationBounds.width(), destinationBounds.height())) + 1; @@ -230,15 +239,15 @@ public abstract class PipContentOverlay { mTmpTransform.reset(); // In order for the overlay to always cover the pip window, the overlay may have a // size larger than the pip window. Make sure that app icon is at the center. - final int appBoundsCenterX = mAppBounds.centerX(); - final int appBoundsCenterY = mAppBounds.centerY(); + final int appBoundsCenterX = mRelativeAppBounds.centerX(); + final int appBoundsCenterY = mRelativeAppBounds.centerY(); mTmpTransform.setTranslate( appBoundsCenterX - mOverlayHalfSize, appBoundsCenterY - mOverlayHalfSize); // Scale back the bitmap with the pivot point at center. final float scale = Math.min( - (float) mAppBounds.width() / currentBounds.width(), - (float) mAppBounds.height() / currentBounds.height()); + (float) mRelativeAppBounds.width() / currentBounds.width(), + (float) mRelativeAppBounds.height() / currentBounds.height()); mTmpTransform.postScale(scale, scale, appBoundsCenterX, appBoundsCenterY); atomicTx.setMatrix(mLeash, mTmpTransform, mTmpFloat9) .setAlpha(mLeash, fraction < 0.5f ? 0 : (fraction - 0.5f) * 2); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java index ff3dc33d46e8..b9a305062b46 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java @@ -29,6 +29,7 @@ import static android.window.TransitionInfo.FLAG_SHOW_WALLPAPER; import static com.android.internal.jank.InteractionJankMonitor.CUJ_PREDICTIVE_BACK_HOME; import static com.android.window.flags.Flags.migratePredictiveBackTransition; import static com.android.window.flags.Flags.predictiveBackSystemAnims; +import static com.android.window.flags.Flags.unifyBackNavigationTransition; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW; import static com.android.wm.shell.shared.ShellSharedConstants.KEY_EXTRA_SHELL_BACK_ANIMATION; @@ -191,6 +192,16 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont @Override public void onResult(@Nullable Bundle result) { mShellExecutor.execute(() -> { + if (mBackGestureStarted && result != null && result.getBoolean( + BackNavigationInfo.KEY_TOUCH_GESTURE_TRANSFERRED)) { + // Host app won't able to process motion event anymore, so pilfer + // pointers anyway. + if (mBackNavigationInfo != null) { + mBackNavigationInfo.disableAppProgressGenerationAllowed(); + } + tryPilferPointers(); + return; + } if (!mBackGestureStarted || mPostCommitAnimationInProgress) { // If an uninterruptible animation is already in progress, we should // ignore this due to it may cause focus lost. (alpha = 0) @@ -1269,6 +1280,13 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont if (transition == mClosePrepareTransition && aborted) { mClosePrepareTransition = null; applyFinishOpenTransition(); + } else if (!aborted && unifyBackNavigationTransition()) { + // Since the closing target participates in the predictive back transition, the + // merged transition must be applied with the first transition to ensure a seamless + // animation. + if (mFinishOpenTransaction != null && finishTransaction != null) { + mFinishOpenTransaction.merge(finishTransaction); + } } } @@ -1290,7 +1308,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont final TransitionInfo init = mOpenTransitionInfo; // Find prepare open target boolean openShowWallpaper = false; - final ArrayList<OpenChangeInfo> targets = new ArrayList<>(); + final ArrayList<SurfaceControl> openSurfaces = new ArrayList<>(); int tmpSize; for (int j = init.getChanges().size() - 1; j >= 0; --j) { final TransitionInfo.Change change = init.getChanges().get(j); @@ -1303,13 +1321,13 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont && openToken == null) { continue; } - targets.add(new OpenChangeInfo(openComponent, openTaskId, openToken)); + openSurfaces.add(change.getLeash()); if (change.hasFlags(FLAG_SHOW_WALLPAPER)) { openShowWallpaper = true; } } } - if (targets.isEmpty()) { + if (openSurfaces.isEmpty()) { // This shouldn't happen, but if that happen, consume the initial transition anyway. Log.e(TAG, "Unable to merge following transition, cannot find the gesture " + "animated target from the open transition=" + mOpenTransitionInfo); @@ -1321,7 +1339,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont tmpSize = info.getChanges().size(); for (int j = 0; j < tmpSize; ++j) { final TransitionInfo.Change change = info.getChanges().get(j); - if (isOpenChangeMatched(targets, change)) { + if (isOpenSurfaceMatched(openSurfaces, change)) { // This is original close target, potential be close, but cannot determine // from it. if (change.hasFlags(FLAG_BACK_GESTURE_ANIMATED)) { @@ -1342,7 +1360,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont boolean mergePredictive = false; for (int j = info.getChanges().size() - 1; j >= 0; --j) { final TransitionInfo.Change change = info.getChanges().get(j); - if (isOpenChangeMatched(targets, change)) { + if (isOpenSurfaceMatched(openSurfaces, change)) { if (TransitionUtil.isClosingMode(change.getMode())) { excludeOpenTarget = true; } @@ -1363,7 +1381,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont if (change.hasFlags(FLAG_IS_WALLPAPER)) { continue; } - if (isOpenChangeMatched(targets, change)) { + if (isOpenSurfaceMatched(openSurfaces, change)) { if (excludeOpenTarget) { // App has triggered another change during predictive back // transition, filter out predictive back target. @@ -1398,7 +1416,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont if (nonBackClose && nonBackOpen) { for (int j = info.getChanges().size() - 1; j >= 0; --j) { final TransitionInfo.Change change = info.getChanges().get(j); - if (isOpenChangeMatched(targets, change)) { + if (isOpenSurfaceMatched(openSurfaces, change)) { info.getChanges().remove(j); } else if ((openShowWallpaper && change.hasFlags(FLAG_IS_WALLPAPER))) { info.getChanges().remove(j); @@ -1682,22 +1700,10 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont return INVALID_TASK_ID; } - private static boolean isSameChangeTarget(ComponentName topActivity, int taskId, - WindowContainerToken token, TransitionInfo.Change change) { - final ComponentName openChange = findComponentName(change); - final int firstTaskId = findTaskId(change); - final WindowContainerToken openToken = findToken(change); - return (openChange != null && openChange.equals(topActivity)) - || (firstTaskId != INVALID_TASK_ID && firstTaskId == taskId) - || (openToken != null && openToken.equals(token)); - } - - static boolean isOpenChangeMatched(@NonNull ArrayList<OpenChangeInfo> targets, + static boolean isOpenSurfaceMatched(@NonNull ArrayList<SurfaceControl> openSurfaces, TransitionInfo.Change change) { - for (int i = targets.size() - 1; i >= 0; --i) { - final OpenChangeInfo next = targets.get(i); - if (isSameChangeTarget(next.mOpenComponent, next.mOpenTaskId, next.mOpenToken, - change)) { + for (int i = openSurfaces.size() - 1; i >= 0; --i) { + if (openSurfaces.get(i).isSameSurface(change.getLeash())) { return true; } } @@ -1761,16 +1767,4 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } } } - - static class OpenChangeInfo { - final ComponentName mOpenComponent; - final int mOpenTaskId; - final WindowContainerToken mOpenToken; - OpenChangeInfo(ComponentName openComponent, int openTaskId, - WindowContainerToken openToken) { - mOpenComponent = openComponent; - mOpenTaskId = openTaskId; - mOpenToken = openToken; - } - } } 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 d1246d35e35e..5f0eed9daa1a 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 @@ -1212,7 +1212,7 @@ public class BubbleController implements ConfigurationChangeListener, */ public void startBubbleDrag(String bubbleKey) { if (mBubbleData.getSelectedBubble() != null) { - mBubbleBarViewCallback.expansionChanged(/* isExpanded = */ false); + collapseExpandedViewForBubbleBar(); } if (mBubbleStateListener != null) { boolean overflow = BubbleOverflow.KEY.equals(bubbleKey); @@ -1304,6 +1304,7 @@ public class BubbleController implements ConfigurationChangeListener, if (BubbleOverflow.KEY.equals(key)) { mBubbleData.setSelectedBubbleFromLauncher(mBubbleData.getOverflow()); mLayerView.showExpandedView(mBubbleData.getOverflow()); + mLogger.log(BubbleLogger.Event.BUBBLE_BAR_EXPANDED); return; } @@ -1315,6 +1316,7 @@ public class BubbleController implements ConfigurationChangeListener, // already in the stack mBubbleData.setSelectedBubbleFromLauncher(b); mLayerView.showExpandedView(b); + mLogger.log(b, BubbleLogger.Event.BUBBLE_BAR_EXPANDED); } else if (mBubbleData.hasOverflowBubbleWithKey(b.getKey())) { // TODO: (b/271468319) handle overflow } else { @@ -2024,12 +2026,16 @@ public class BubbleController implements ConfigurationChangeListener, public void expansionChanged(boolean isExpanded) { // in bubble bar mode, let the request to show the expanded view come from launcher. // only collapse here if we're collapsing. - if (mLayerView != null && !isExpanded) { - if (mBubblePositioner.isImeVisible()) { - // If we're collapsing, hide the IME - hideCurrentInputMethod(); - } - mLayerView.collapse(); + if (!isExpanded) { + collapseExpandedViewForBubbleBar(); + } + + BubbleLogger.Event event = isExpanded ? BubbleLogger.Event.BUBBLE_BAR_EXPANDED + : BubbleLogger.Event.BUBBLE_BAR_COLLAPSED; + if (mBubbleData.getSelectedBubble() instanceof Bubble bubble) { + mLogger.log(bubble, event); + } else { + mLogger.log(event); } } @@ -2182,6 +2188,16 @@ public class BubbleController implements ConfigurationChangeListener, } } + private void collapseExpandedViewForBubbleBar() { + if (mLayerView != null && mLayerView.isExpanded()) { + if (mBubblePositioner.isImeVisible()) { + // If we're collapsing, hide the IME + hideCurrentInputMethod(); + } + mLayerView.collapse(); + } + } + private void updateOverflowButtonDot() { BubbleOverflow overflow = mBubbleData.getOverflow(); if (overflow == null) return; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java index 35a0d07a63b2..88f55b8af8f7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java @@ -1704,6 +1704,7 @@ public class BubbleStackView extends FrameLayout getViewTreeObserver().removeOnPreDrawListener(mViewUpdater); getViewTreeObserver().removeOnDrawListener(mSystemGestureExcludeUpdater); getViewTreeObserver().removeOnComputeInternalInsetsListener(this); + stopMonitoringSwipeUpGesture(); } @Override @@ -2313,7 +2314,8 @@ public class BubbleStackView extends FrameLayout /** * Stop monitoring for swipe up gesture */ - void stopMonitoringSwipeUpGesture() { + @VisibleForTesting + public void stopMonitoringSwipeUpGesture() { stopMonitoringSwipeUpGestureInternal(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java index b29e49a48428..813772f20a8a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java @@ -71,6 +71,11 @@ public class DividerSnapAlgorithm { */ private static final int SNAP_MODE_MINIMIZED = 3; + /** + * A mode where apps can be "flexibly offscreen" on smaller displays. + */ + private static final int SNAP_FLEXIBLE_SPLIT = 4; + private final float mMinFlingVelocityPxPerSecond; private final float mMinDismissVelocityPxPerSecond; private final int mDisplayWidth; @@ -78,6 +83,7 @@ public class DividerSnapAlgorithm { private final int mDividerSize; private final ArrayList<SnapTarget> mTargets = new ArrayList<>(); private final Rect mInsets = new Rect(); + private final Rect mPinnedTaskbarInsets = new Rect(); private final int mSnapMode; private final boolean mFreeSnapMode; private final int mMinimalSizeResizableTask; @@ -88,6 +94,8 @@ public class DividerSnapAlgorithm { /** Allows split ratios that go offscreen (a.k.a. "flexible split") */ private final boolean mAllowOffscreenRatios; private final boolean mIsLeftRightSplit; + /** In SNAP_MODE_MINIMIZED, the side of the screen on which an app will "dock" when minimized */ + private final int mDockSide; /** The first target which is still splitting the screen */ private final SnapTarget mFirstSplitTarget; @@ -101,14 +109,14 @@ public class DividerSnapAlgorithm { public DividerSnapAlgorithm(Resources res, int displayWidth, int displayHeight, int dividerSize, - boolean isLeftRightSplit, Rect insets, int dockSide) { + boolean isLeftRightSplit, Rect insets, Rect pinnedTaskbarInsets, int dockSide) { this(res, displayWidth, displayHeight, dividerSize, isLeftRightSplit, insets, - dockSide, false /* minimized */, true /* resizable */); + pinnedTaskbarInsets, dockSide, false /* minimized */, true /* resizable */); } public DividerSnapAlgorithm(Resources res, int displayWidth, int displayHeight, int dividerSize, - boolean isLeftRightSplit, Rect insets, int dockSide, boolean isMinimizedMode, - boolean isHomeResizable) { + boolean isLeftRightSplit, Rect insets, Rect pinnedTaskbarInsets, int dockSide, + boolean isMinimizedMode, boolean isHomeResizable) { mMinFlingVelocityPxPerSecond = MIN_FLING_VELOCITY_DP_PER_SECOND * res.getDisplayMetrics().density; mMinDismissVelocityPxPerSecond = @@ -117,10 +125,11 @@ public class DividerSnapAlgorithm { mDisplayWidth = displayWidth; mDisplayHeight = displayHeight; mIsLeftRightSplit = isLeftRightSplit; + mDockSide = dockSide; mInsets.set(insets); + mPinnedTaskbarInsets.set(pinnedTaskbarInsets); if (Flags.enableFlexibleTwoAppSplit()) { - // In flexible split, we always use a fixed ratio (50%, 66%, or 90%) for splitting - mSnapMode = SNAP_FIXED_RATIO; + mSnapMode = SNAP_FLEXIBLE_SPLIT; } else { // Set SNAP_MODE_MINIMIZED, SNAP_MODE_16_9, or SNAP_FIXED_RATIO depending on config mSnapMode = isMinimizedMode @@ -140,7 +149,7 @@ public class DividerSnapAlgorithm { mAllowOffscreenRatios = SplitScreenUtils.allowOffscreenRatios(res); mTaskHeightInMinimizedMode = isHomeResizable ? res.getDimensionPixelSize( com.android.internal.R.dimen.task_height_of_minimized_mode) : 0; - calculateTargets(isLeftRightSplit, dockSide); + calculateTargets(); mFirstSplitTarget = mTargets.get(1); mLastSplitTarget = mTargets.get(mTargets.size() - 2); mDismissStartTarget = mTargets.get(0); @@ -276,28 +285,31 @@ public class DividerSnapAlgorithm { return mTargets.get(minIndex); } - private void calculateTargets(boolean isLeftRightSplit, int dockedSide) { + private void calculateTargets() { mTargets.clear(); - int dividerMax = isLeftRightSplit + int dividerMax = mIsLeftRightSplit ? mDisplayWidth : mDisplayHeight; int startPos = -mDividerSize; - if (dockedSide == DOCKED_RIGHT) { + if (mDockSide == DOCKED_RIGHT) { startPos += mInsets.left; } mTargets.add(new SnapTarget(startPos, SNAP_TO_START_AND_DISMISS, 0.35f)); switch (mSnapMode) { case SNAP_MODE_16_9: - addRatio16_9Targets(isLeftRightSplit, dividerMax); + addRatio16_9Targets(mIsLeftRightSplit, dividerMax); break; case SNAP_FIXED_RATIO: - addFixedDivisionTargets(isLeftRightSplit, dividerMax); + addFixedDivisionTargets(mIsLeftRightSplit, dividerMax); break; case SNAP_ONLY_1_1: - addMiddleTarget(isLeftRightSplit); + addMiddleTarget(mIsLeftRightSplit); break; case SNAP_MODE_MINIMIZED: - addMinimizedTarget(isLeftRightSplit, dockedSide); + addMinimizedTarget(mIsLeftRightSplit, mDockSide); + break; + case SNAP_FLEXIBLE_SPLIT: + addFlexSplitTargets(mIsLeftRightSplit, dividerMax); break; } mTargets.add(new SnapTarget(dividerMax, SNAP_TO_END_AND_DISMISS, 0.35f)); @@ -321,18 +333,9 @@ public class DividerSnapAlgorithm { ? mDisplayWidth - mInsets.right : mDisplayHeight - mInsets.bottom; - int size; - if (Flags.enableFlexibleTwoAppSplit()) { - float ratio = areOffscreenRatiosSupported() - ? SplitLayout.OFFSCREEN_ASYMMETRIC_RATIO - : SplitLayout.ONSCREEN_ONLY_ASYMMETRIC_RATIO; - size = (int) (ratio * (end - start)) - mDividerSize / 2; - } else { - size = (int) (mFixedRatio * (end - start)) - mDividerSize / 2; - - if (mCalculateRatiosBasedOnAvailableSpace) { - size = Math.max(size, mMinimalSizeResizableTask); - } + int size = (int) (mFixedRatio * (end - start)) - mDividerSize / 2; + if (mCalculateRatiosBasedOnAvailableSpace) { + size = Math.max(size, mMinimalSizeResizableTask); } int topPosition = start + size; @@ -340,6 +343,24 @@ public class DividerSnapAlgorithm { addNonDismissingTargets(isLeftRightSplit, topPosition, bottomPosition, dividerMax); } + private void addFlexSplitTargets(boolean isLeftRightSplit, int dividerMax) { + int start = 0; + int end = isLeftRightSplit ? mDisplayWidth : mDisplayHeight; + int pinnedTaskbarShiftStart = isLeftRightSplit + ? mPinnedTaskbarInsets.left : mPinnedTaskbarInsets.top; + int pinnedTaskbarShiftEnd = isLeftRightSplit + ? mPinnedTaskbarInsets.right : mPinnedTaskbarInsets.bottom; + + float ratio = areOffscreenRatiosSupported() + ? SplitLayout.OFFSCREEN_ASYMMETRIC_RATIO + : SplitLayout.ONSCREEN_ONLY_ASYMMETRIC_RATIO; + int size = (int) (ratio * (end - start)) - mDividerSize / 2; + + int leftTopPosition = start + pinnedTaskbarShiftStart + size; + int rightBottomPosition = end - pinnedTaskbarShiftEnd - size - mDividerSize; + addNonDismissingTargets(isLeftRightSplit, leftTopPosition, rightBottomPosition, dividerMax); + } + private void addRatio16_9Targets(boolean isLeftRightSplit, int dividerMax) { int start = isLeftRightSplit ? mInsets.left : mInsets.top; int end = isLeftRightSplit 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 b1e0e9eab5d0..dab30b0f0b96 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 @@ -49,10 +49,13 @@ import android.app.ActivityManager; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; +import android.graphics.Insets; import android.graphics.Point; import android.graphics.Rect; import android.os.Handler; import android.view.Display; +import android.view.InsetsController; +import android.view.InsetsSource; import android.view.InsetsSourceControl; import android.view.InsetsState; import android.view.RoundedCorner; @@ -153,6 +156,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange private final ResizingEffectPolicy mSurfaceEffectPolicy; private final ShellTaskOrganizer mTaskOrganizer; private final InsetsState mInsetsState = new InsetsState(); + private Insets mPinnedTaskbarInsets = Insets.NONE; private Context mContext; @VisibleForTesting DividerSnapAlgorithm mDividerSnapAlgorithm; @@ -523,6 +527,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange @Override public void insetsChanged(InsetsState insetsState) { mInsetsState.set(insetsState); + if (!mInitialized) { return; } @@ -531,9 +536,51 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange // flicker. return; } + + // Check to see if insets changed in such a way that the divider algorithm needs to be + // recalculated. + Insets pinnedTaskbarInsets = calculatePinnedTaskbarInsets(insetsState); + if (!mPinnedTaskbarInsets.equals(pinnedTaskbarInsets)) { + mPinnedTaskbarInsets = pinnedTaskbarInsets; + // Refresh the DividerSnapAlgorithm. + mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds); + // If the divider is no longer placed on a snap point, animate it to the nearest one. + DividerSnapAlgorithm.SnapTarget snapTarget = + findSnapTarget(mDividerPosition, 0, false /* hardDismiss */); + if (snapTarget.position != mDividerPosition) { + snapToTarget(mDividerPosition, snapTarget, + InsetsController.ANIMATION_DURATION_RESIZE, + InsetsController.RESIZE_INTERPOLATOR); + } + } + mSplitWindowManager.onInsetsChanged(insetsState); } + /** + * Calculates the insets that might trigger a divider algorithm recalculation. Currently, only + * pinned Taskbar does this, and only when the IME is not showing. + */ + private Insets calculatePinnedTaskbarInsets(InsetsState insetsState) { + if (insetsState.isSourceOrDefaultVisible(InsetsSource.ID_IME, WindowInsets.Type.ime())) { + return Insets.NONE; + } + + // If IME is not showing... + for (int i = insetsState.sourceSize() - 1; i >= 0; i--) { + final InsetsSource source = insetsState.sourceAt(i); + // and Taskbar is pinned... + if (source.getType() == WindowInsets.Type.navigationBars() + && source.hasFlags(InsetsSource.FLAG_INSETS_ROUNDED_CORNER)) { + // Return Insets representing the pinned taskbar state. + return source.calculateVisibleInsets(mRootBounds); + } + } + + // Else, divider can calculate based on the full display. + return Insets.NONE; + } + @Override public void insetsControlChanged(InsetsState insetsState, InsetsSourceControl[] activeControls) { @@ -631,8 +678,8 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange } /** - * Same as {@link #snapToTarget(int, SnapTarget)}, with default animation duration and - * interpolator. + * Same as {@link #snapToTarget(int, SnapTarget, int, Interpolator)}, with default animation + * duration and interpolator. */ public void snapToTarget(int currentPosition, SnapTarget snapTarget) { snapToTarget(currentPosition, snapTarget, FLING_RESIZE_DURATION, @@ -683,6 +730,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange mDividerSize, mIsLeftRightSplit, insets, + mPinnedTaskbarInsets.toRect(), mIsLeftRightSplit ? DOCKED_LEFT : DOCKED_TOP /* dockSide */); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java index 6146ecd9ade6..886330f3264a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java @@ -688,6 +688,12 @@ public class CompatUIController implements OnDisplaysChangedListener, private void launchUserAspectRatioSettings( @NonNull TaskInfo taskInfo, @NonNull ShellTaskOrganizer.TaskListener taskListener) { + launchUserAspectRatioSettings(mContext, taskInfo); + } + + /** Launch the user aspect ratio settings for the package of the given task. */ + public static void launchUserAspectRatioSettings( + @NonNull Context context, @NonNull TaskInfo taskInfo) { final Intent intent = new Intent(Settings.ACTION_MANAGE_USER_ASPECT_RATIO_SETTINGS); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); @@ -697,7 +703,7 @@ public class CompatUIController implements OnDisplaysChangedListener, intent.setData(packageUri); } final UserHandle userHandle = UserHandle.of(taskInfo.userId); - mContext.startActivityAsUser(intent, userHandle); + context.startActivityAsUser(intent, userHandle); } @VisibleForTesting diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUILayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUILayout.java index 688f8ca2dc75..49c2785f1ecd 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUILayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUILayout.java @@ -68,7 +68,7 @@ class CompatUILayout extends LinearLayout { private void setViewVisibility(@IdRes int resId, boolean show) { final View view = findViewById(resId); - int visibility = show ? View.VISIBLE : View.GONE; + int visibility = show ? View.VISIBLE : View.INVISIBLE; if (view.getVisibility() == visibility) { return; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayout.java index b141bebbe8b1..fd1bbc477cf4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayout.java @@ -100,7 +100,7 @@ public class UserAspectRatioSettingsLayout extends LinearLayout { private void setViewVisibility(@IdRes int resId, boolean show) { final View view = findViewById(resId); - int visibility = show ? View.VISIBLE : View.GONE; + int visibility = show ? View.VISIBLE : View.INVISIBLE; if (view.getVisibility() == visibility) { return; } @@ -171,7 +171,7 @@ public class UserAspectRatioSettingsLayout extends LinearLayout { fadeOut.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { - view.setVisibility(View.GONE); + view.setVisibility(View.INVISIBLE); } }); fadeOut.start(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index c6cd3204451b..fec4c16992a5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -86,8 +86,13 @@ import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository; import com.android.wm.shell.desktopmode.education.AppHandleEducationController; import com.android.wm.shell.desktopmode.education.AppHandleEducationFilter; +import com.android.wm.shell.desktopmode.education.AppToWebEducationController; +import com.android.wm.shell.desktopmode.education.AppToWebEducationFilter; import com.android.wm.shell.desktopmode.education.data.AppHandleEducationDatastoreRepository; +import com.android.wm.shell.desktopmode.education.data.AppToWebEducationDatastoreRepository; import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository; +import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer; +import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializerImpl; import com.android.wm.shell.draganddrop.DragAndDropController; import com.android.wm.shell.draganddrop.GlobalDragListener; import com.android.wm.shell.freeform.FreeformComponents; @@ -130,6 +135,7 @@ import com.android.wm.shell.windowdecor.CaptionWindowDecorViewModel; import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel; import com.android.wm.shell.windowdecor.WindowDecorViewModel; import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer; +import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationPromoController; import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationTooltipController; import com.android.wm.shell.windowdecor.tiling.DesktopTilingDecorViewModel; @@ -286,6 +292,7 @@ public abstract class WMShellModule { MultiInstanceHelper multiInstanceHelper, Optional<DesktopTasksLimiter> desktopTasksLimiter, AppHandleEducationController appHandleEducationController, + AppToWebEducationController appToWebEducationController, WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository, Optional<DesktopActivityOrientationChangeHandler> desktopActivityOrientationHandler, FocusTransitionObserver focusTransitionObserver, @@ -315,6 +322,7 @@ public abstract class WMShellModule { multiInstanceHelper, desktopTasksLimiter, appHandleEducationController, + appToWebEducationController, windowDecorCaptionHandleRepository, desktopActivityOrientationHandler, focusTransitionObserver, @@ -840,8 +848,8 @@ public abstract class WMShellModule { @WMSingleton @Provides static ReturnToDragStartAnimator provideReturnToDragStartAnimator( - Context context, InteractionJankMonitor interactionJankMonitor) { - return new ReturnToDragStartAnimator(context, interactionJankMonitor); + InteractionJankMonitor interactionJankMonitor) { + return new ReturnToDragStartAnimator(interactionJankMonitor); } @WMSingleton @@ -909,8 +917,12 @@ public abstract class WMShellModule { Context context, ShellInit shellInit, DesktopPersistentRepository desktopPersistentRepository, - @ShellMainThread CoroutineScope mainScope) { - return new DesktopRepository(context, shellInit, desktopPersistentRepository, mainScope); + DesktopRepositoryInitializer desktopRepositoryInitializer, + @ShellMainThread CoroutineScope mainScope + ) { + return new DesktopRepository(context, shellInit, desktopPersistentRepository, + desktopRepositoryInitializer, + mainScope); } @WMSingleton @@ -964,7 +976,10 @@ public abstract class WMShellModule { CloseDesktopTaskTransitionHandler closeDesktopTaskTransitionHandler, Optional<DesktopImmersiveController> desktopImmersiveController, InteractionJankMonitor interactionJankMonitor, - @ShellMainThread Handler handler) { + @ShellMainThread Handler handler, + ShellInit shellInit, + RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer + ) { if (!DesktopModeStatus.canEnterDesktopMode(context)) { return Optional.empty(); } @@ -977,7 +992,9 @@ public abstract class WMShellModule { closeDesktopTaskTransitionHandler, desktopImmersiveController.get(), interactionJankMonitor, - handler)); + handler, + shellInit, + rootTaskDisplayAreaOrganizer)); } @WMSingleton @@ -1029,6 +1046,20 @@ public abstract class WMShellModule { context, additionalSystemViewContainerFactory, displayController); } + @WMSingleton + @Provides + static DesktopWindowingEducationPromoController provideDesktopWindowingEducationPromoController( + Context context, + AdditionalSystemViewContainer.Factory additionalSystemViewContainerFactory, + DisplayController displayController + ) { + return new DesktopWindowingEducationPromoController( + context, + additionalSystemViewContainerFactory, + displayController + ); + } + @OptIn(markerClass = ExperimentalCoroutinesApi.class) @WMSingleton @Provides @@ -1052,11 +1083,53 @@ public abstract class WMShellModule { @WMSingleton @Provides + static AppToWebEducationDatastoreRepository provideAppToWebEducationDatastoreRepository( + Context context) { + return new AppToWebEducationDatastoreRepository(context); + } + + @WMSingleton + @Provides + static AppToWebEducationFilter provideAppToWebEducationFilter( + Context context, + AppToWebEducationDatastoreRepository appToWebEducationDatastoreRepository) { + return new AppToWebEducationFilter(context, appToWebEducationDatastoreRepository); + } + + @OptIn(markerClass = ExperimentalCoroutinesApi.class) + @WMSingleton + @Provides + static AppToWebEducationController provideAppToWebEducationController( + Context context, + AppToWebEducationFilter appToWebEducationFilter, + AppToWebEducationDatastoreRepository appToWebEducationDatastoreRepository, + WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository, + DesktopWindowingEducationPromoController desktopWindowingEducationPromoController, + @ShellMainThread CoroutineScope applicationScope, + @ShellBackgroundThread MainCoroutineDispatcher backgroundDispatcher) { + return new AppToWebEducationController(context, appToWebEducationFilter, + appToWebEducationDatastoreRepository, windowDecorCaptionHandleRepository, + desktopWindowingEducationPromoController, applicationScope, + backgroundDispatcher); + } + + @WMSingleton + @Provides static DesktopPersistentRepository provideDesktopPersistentRepository( Context context, @ShellBackgroundThread CoroutineScope bgScope) { return new DesktopPersistentRepository(context, bgScope); } + @WMSingleton + @Provides + static DesktopRepositoryInitializer provideDesktopRepositoryInitializer( + Context context, + DesktopPersistentRepository desktopPersistentRepository, + @ShellMainThread CoroutineScope mainScope) { + return new DesktopRepositoryInitializerImpl(context, desktopPersistentRepository, + mainScope); + } + // // Drag and drop // diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopImmersiveController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopImmersiveController.kt index e741892bf6fc..f69aa6df6a1d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopImmersiveController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopImmersiveController.kt @@ -224,9 +224,9 @@ class DesktopImmersiveController( finishTransaction: SurfaceControl.Transaction, finishCallback: Transitions.TransitionFinishCallback ): Boolean { - logD("startAnimation transition=%s", transition) val state = requireState() if (transition != state.transition) return false + logD("startAnimation transition=%s", transition) animateResize( targetTaskId = state.taskId, info = info, @@ -334,7 +334,6 @@ class DesktopImmersiveController( startTransaction: SurfaceControl.Transaction, finishTransaction: SurfaceControl.Transaction, ) { - logD("onTransitionReady transition=%s", transition) // Check if this is a pending external exit transition. val pendingExit = pendingExternalExitTransitions .firstOrNull { pendingExit -> pendingExit.transition == transition } @@ -402,7 +401,6 @@ class DesktopImmersiveController( } override fun onTransitionMerged(merged: IBinder, playing: IBinder) { - logD("onTransitionMerged merged=%s playing=%s", merged, playing) val pendingExit = pendingExternalExitTransitions .firstOrNull { pendingExit -> pendingExit.transition == merged } if (pendingExit != null) { @@ -415,7 +413,6 @@ class DesktopImmersiveController( } override fun onTransitionFinished(transition: IBinder, aborted: Boolean) { - logD("onTransitionFinished transition=%s aborted=%b", transition, aborted) val pendingExit = pendingExternalExitTransitions .firstOrNull { pendingExit -> pendingExit.transition == transition } if (pendingExit != null) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt index 0bc571f4782c..48bb2a8b4a74 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt @@ -25,6 +25,7 @@ import android.view.SurfaceControl import android.view.WindowManager import android.window.DesktopModeFlags import android.window.TransitionInfo +import android.window.TransitionInfo.Change import android.window.TransitionRequestInfo import android.window.WindowContainerTransaction import androidx.annotation.VisibleForTesting @@ -32,10 +33,13 @@ import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_C import com.android.internal.jank.InteractionJankMonitor import com.android.internal.protolog.ProtoLog import com.android.window.flags.Flags +import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.freeform.FreeformTaskTransitionHandler import com.android.wm.shell.freeform.FreeformTaskTransitionStarter import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE +import com.android.wm.shell.shared.TransitionUtil import com.android.wm.shell.shared.annotations.ShellMainThread +import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.MixedTransitionHandler import com.android.wm.shell.transition.Transitions import com.android.wm.shell.transition.Transitions.TransitionFinishCallback @@ -50,8 +54,14 @@ class DesktopMixedTransitionHandler( private val desktopImmersiveController: DesktopImmersiveController, private val interactionJankMonitor: InteractionJankMonitor, @ShellMainThread private val handler: Handler, + shellInit: ShellInit, + private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, ) : MixedTransitionHandler, FreeformTaskTransitionStarter { + init { + shellInit.addInitCallback ({ transitions.addHandler(this) }, this) + } + @VisibleForTesting val pendingMixedTransitions = mutableListOf<PendingMixedTransition>() @@ -85,9 +95,11 @@ class DesktopMixedTransitionHandler( @WindowManager.TransitionType transitionType: Int, wct: WindowContainerTransaction, taskId: Int, + minimizingTaskId: Int? = null, exitingImmersiveTask: Int? = null, ): IBinder { - if (!Flags.enableFullyImmersiveInDesktop()) { + if (!Flags.enableFullyImmersiveInDesktop() && + !DesktopModeFlags.ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS.isTrue) { return transitions.startTransition(transitionType, wct, /* handler= */ null) } if (exitingImmersiveTask == null) { @@ -103,11 +115,17 @@ class DesktopMixedTransitionHandler( pendingMixedTransitions.add(PendingMixedTransition.Launch( transition = transition, launchingTask = taskId, - exitingImmersiveTask = exitingImmersiveTask + minimizingTask = minimizingTaskId, + exitingImmersiveTask = exitingImmersiveTask, )) } } + /** Notifies this handler that there is a pending transition for it to handle. */ + fun addPendingMixedTransition(pendingMixedTransition: PendingMixedTransition) { + pendingMixedTransitions.add(pendingMixedTransition) + } + /** Returns null, as it only handles transitions started from Shell. */ override fun handleRequest( transition: IBinder, @@ -123,7 +141,7 @@ class DesktopMixedTransitionHandler( ): Boolean { val pending = pendingMixedTransitions.find { pending -> pending.transition == transition } ?: return false.also { - logW("Should have pending desktop transition") + logV("No pending desktop transition") } pendingMixedTransitions.remove(pending) logV("Animating pending mixed transition: %s", pending) @@ -191,6 +209,9 @@ class DesktopMixedTransitionHandler( val immersiveExitChange = pending.exitingImmersiveTask?.let { exitingTask -> findDesktopTaskChange(info, exitingTask) } + val minimizeChange = pending.minimizingTask?.let { minimizingTask -> + findDesktopTaskChange(info, minimizingTask) + } val launchChange = findDesktopTaskChange(info, pending.launchingTask) ?: error("Should have pending launching task change") @@ -204,9 +225,17 @@ class DesktopMixedTransitionHandler( } logV( - "Animating pending mixed launch transition task#%d immersiveExitTask#%s", - launchChange.taskInfo!!.taskId, immersiveExitChange?.taskInfo?.taskId + "Animating mixed launch transition task#%d, minimizingTask#%s immersiveExitTask#%s", + launchChange.taskInfo!!.taskId, minimizeChange?.taskInfo?.taskId, + immersiveExitChange?.taskInfo?.taskId ) + if (DesktopModeFlags.ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS.isTrue) { + // Only apply minimize change reparenting here if we implement the new app launch + // transitions, otherwise this reparenting is handled in the default handler. + minimizeChange?.let { + applyMinimizeChangeReparenting(info, minimizeChange, startTransaction) + } + } if (immersiveExitChange != null) { subAnimationCount = 2 // Animate the immersive exit change separately. @@ -257,7 +286,7 @@ class DesktopMixedTransitionHandler( change: TransitionInfo.Change, startTransaction: SurfaceControl.Transaction, finishTransaction: SurfaceControl.Transaction, - finishCallback: Transitions.TransitionFinishCallback, + finishCallback: TransitionFinishCallback, ): Boolean { // Starting the jank trace if closing the last window in desktop mode. interactionJankMonitor.begin( @@ -280,6 +309,28 @@ class DesktopMixedTransitionHandler( ) } + /** + * Reparent the minimizing task back to its root display area. + * + * During the launch/minimize animation the all animated tasks will be reparented to a + * transition leash shown in front of other desktop tasks. Reparenting the minimizing task back + * to its root display area ensures that task stays behind other desktop tasks during the + * animation. + */ + private fun applyMinimizeChangeReparenting( + info: TransitionInfo, + minimizeChange: Change, + startTransaction: SurfaceControl.Transaction, + ) { + require(TransitionUtil.isOpeningMode(info.type)) + require(minimizeChange.taskInfo != null) + val taskInfo = minimizeChange.taskInfo!! + require(taskInfo.isFreeform) + logV("Reparenting minimizing task#%d", taskInfo.taskId) + rootTaskDisplayAreaOrganizer.reparentToDisplayArea( + taskInfo.displayId, minimizeChange.leash, startTransaction) + } + private fun dispatchToLeftoverHandler( transition: IBinder, info: TransitionInfo, @@ -341,6 +392,7 @@ class DesktopMixedTransitionHandler( data class Launch( override val transition: IBinder, val launchingTask: Int, + val minimizingTask: Int?, val exitingImmersiveTask: Int?, ) : PendingMixedTransition() } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java index 61de0777ed05..09e77fee7527 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java @@ -19,6 +19,7 @@ package com.android.wm.shell.desktopmode; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; +import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR; @@ -259,6 +260,7 @@ public class DesktopModeVisualIndicator { FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT); lp.setTitle("Desktop Mode Visual Indicator"); lp.setTrustedOverlay(); + lp.inputFeatures |= INPUT_FEATURE_NO_INPUT_CHANNEL; final WindowlessWindowManager windowManager = new WindowlessWindowManager( mTaskInfo.configuration, mLeash, null /* hostInputToken */); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt index 3864f1b08983..fda709a4a2d7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt @@ -23,14 +23,14 @@ import android.util.ArrayMap import android.util.ArraySet import android.util.SparseArray import android.view.Display.INVALID_DISPLAY +import android.window.DesktopModeFlags import android.window.WindowContainerToken import androidx.core.util.forEach import androidx.core.util.keyIterator import androidx.core.util.valueIterator import com.android.internal.protolog.ProtoLog -import com.android.window.flags.Flags import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository -import com.android.wm.shell.desktopmode.persistence.DesktopTaskState +import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE import com.android.wm.shell.shared.annotations.ShellMainThread import com.android.wm.shell.shared.desktopmode.DesktopModeStatus @@ -46,6 +46,7 @@ class DesktopRepository ( private val context: Context, shellInit: ShellInit, private val persistentRepository: DesktopPersistentRepository, + private val repositoryInitializer: DesktopRepositoryInitializer, @ShellMainThread private val mainCoroutineScope: CoroutineScope, ){ @@ -120,32 +121,7 @@ class DesktopRepository ( } private fun initRepoFromPersistentStorage() { - if (!Flags.enableDesktopWindowingPersistence()) return - // TODO: b/365962554 - Handle the case that user moves to desktop before it's initialized - mainCoroutineScope.launch { - val desktop = persistentRepository.readDesktop() ?: return@launch - - val maxTasks = - DesktopModeStatus.getMaxTaskLimit(context).takeIf { it > 0 } - ?: desktop.zOrderedTasksCount - - var visibleTasksCount = 0 - desktop.zOrderedTasksList - // Reverse it so we initialize the repo from bottom to top. - .reversed() - .mapNotNull { taskId -> desktop.tasksByTaskIdMap[taskId] } - .forEach { task -> - if (task.desktopTaskState == DesktopTaskState.VISIBLE - && visibleTasksCount < maxTasks - ) { - visibleTasksCount++ - addTask(desktop.displayId, task.taskId, isVisible = false) - } else { - addTask(desktop.displayId, task.taskId, isVisible = false) - minimizeTask(desktop.displayId, task.taskId) - } - } - } + repositoryInitializer.initialize(this) } /** Adds [activeTasksListener] to be notified of updates to active tasks. */ @@ -318,6 +294,9 @@ class DesktopRepository ( taskId, isVisible, displayId) logD("VisibleTaskCount has changed from %d to %d", prevCount, newCount) notifyVisibleTaskListeners(displayId, newCount) + if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) { + updatePersistentRepository(displayId) + } } } @@ -365,7 +344,7 @@ class DesktopRepository ( desktopTaskDataByDisplayId.getOrCreate(displayId).freeformTasksInZOrder.add(0, taskId) // Unminimize the task if it is minimized. unminimizeTask(displayId, taskId) - if (Flags.enableDesktopWindowingPersistence()) { + if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) { updatePersistentRepository(displayId) } } @@ -383,7 +362,7 @@ class DesktopRepository ( desktopTaskDataByDisplayId.getOrCreate(displayId).minimizedTasks.add(taskId) } updateTask(displayId, taskId, isVisible = false) - if (Flags.enableDesktopWindowingPersistence()) { + if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) { updatePersistentRepository(displayId) } } @@ -431,7 +410,7 @@ class DesktopRepository ( unminimizeTask(displayId, taskId) removeActiveTask(taskId) removeVisibleTask(taskId) - if (Flags.enableDesktopWindowingPersistence()) { + if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) { updatePersistentRepository(displayId) } } 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 577c18cfa5e4..75f8839692a2 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 @@ -54,6 +54,7 @@ import android.view.WindowManager.TRANSIT_CLOSE import android.view.WindowManager.TRANSIT_NONE import android.view.WindowManager.TRANSIT_OPEN import android.view.WindowManager.TRANSIT_TO_FRONT +import android.widget.Toast import android.window.DesktopModeFlags import android.window.DesktopModeFlags.DISABLE_NON_RESIZABLE_APP_SNAP_RESIZE import android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY @@ -697,6 +698,7 @@ class DesktopTasksController( transitionType = transitionType, wct = wct, taskId = taskId, + minimizingTaskId = taskIdToMinimize, exitingImmersiveTask = exitingImmersiveTask, ) taskIdToMinimize?.let { addPendingMinimizeTransition(t, it) } @@ -877,7 +879,6 @@ class DesktopTasksController( taskSurface, startBounds = currentDragBounds, endBounds = containerBounds, - isResizable = taskInfo.isResizeable ) } return @@ -1012,7 +1013,6 @@ class DesktopTasksController( taskSurface, startBounds = currentDragBounds, endBounds = destinationBounds, - isResizable = taskInfo.isResizeable, ) } return @@ -1046,7 +1046,13 @@ class DesktopTasksController( taskSurface, startBounds = currentDragBounds, endBounds = dragStartBounds, - isResizable = taskInfo.isResizeable, + doOnEnd = { + Toast.makeText( + context, + com.android.wm.shell.R.string.desktop_mode_non_resizable_snap_text, + Toast.LENGTH_SHORT + ).show() + }, ) } else { val resizeTrigger = if (position == SnapPosition.LEFT) { @@ -1156,7 +1162,7 @@ class DesktopTasksController( if (runningTaskInfo != null) { // Task is already running, reorder it to the front wct.reorder(runningTaskInfo.token, /* onTop= */ true) - } else if (Flags.enableDesktopWindowingPersistence()) { + } else if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) { // Task is not running, start it wct.startTask( taskId, @@ -1429,6 +1435,7 @@ class DesktopTasksController( ) val transition = transitions.startTransition(TRANSIT_OPEN, wct, null) taskIdToMinimize?.let { addPendingMinimizeTransition(transition, it) } + addPendingAppLaunchTransition(transition, requestedTaskId, taskIdToMinimize) exitResult.asExit()?.runOnTransitionStart?.invoke(transition) } else { val splitPosition = splitScreenController.determineNewInstancePosition(callingTask) @@ -1569,6 +1576,7 @@ class DesktopTasksController( desktopImmersiveController.exitImmersiveIfApplicable(transition, wct, task.displayId) // 2) minimize a Task if needed. val taskIdToMinimize = addAndGetMinimizeChanges(task.displayId, wct, task.taskId) + addPendingAppLaunchTransition(transition, task.taskId, taskIdToMinimize) if (taskIdToMinimize != null) { addPendingMinimizeTransition(transition, taskIdToMinimize) return wct @@ -1600,6 +1608,7 @@ class DesktopTasksController( // minimize another Task. val taskIdToMinimize = addAndGetMinimizeChanges(task.displayId, wct, task.taskId) taskIdToMinimize?.let { addPendingMinimizeTransition(transition, it) } + addPendingAppLaunchTransition(transition, task.taskId, taskIdToMinimize) desktopImmersiveController.exitImmersiveIfApplicable( transition, wct, task.displayId ) @@ -1780,6 +1789,20 @@ class DesktopTasksController( } } + private fun addPendingAppLaunchTransition( + transition: IBinder, + launchTaskId: Int, + minimizeTaskId: Int?, + ) { + if (!DesktopModeFlags.ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS.isTrue) { + return + } + // TODO b/359523924: pass immersive task here? + desktopMixedTransitionHandler.addPendingMixedTransition( + DesktopMixedTransitionHandler.PendingMixedTransition.Launch( + transition, launchTaskId, minimizeTaskId, /* exitingImmersiveTask= */ null)) + } + fun removeDesktop(displayId: Int) { if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) return @@ -1984,17 +2007,32 @@ class DesktopTasksController( ) } IndicatorType.NO_INDICATOR -> { + // Create a copy so that we can animate from the current bounds if we end up having + // to snap the surface back without a WCT change. + val destinationBounds = Rect(currentDragBounds) // If task bounds are outside valid drag area, snap them inward DragPositioningCallbackUtility.snapTaskBoundsIfNecessary( - currentDragBounds, + destinationBounds, validDragArea ) - if (currentDragBounds == dragStartBounds) return + if (destinationBounds == dragStartBounds) { + // There's no actual difference between the start and end bounds, so while a + // WCT change isn't needed, the dragged surface still needs to be snapped back + // to its original location. + releaseVisualIndicator() + returnToDragStartAnimator.start( + taskInfo.taskId, + taskSurface, + startBounds = currentDragBounds, + endBounds = dragStartBounds, + ) + return + } // Update task bounds so that the task position will match the position of its leash val wct = WindowContainerTransaction() - wct.setBounds(taskInfo.token, currentDragBounds) + wct.setBounds(taskInfo.token, destinationBounds) transitions.startTransition(TRANSIT_CHANGE, wct, null) releaseVisualIndicator() diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt index 7bcc5d1691aa..f0e3a2bd8ffc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt @@ -133,7 +133,6 @@ class DesktopTasksLimiter ( } override fun onTransitionFinished(transition: IBinder, aborted: Boolean) { - logV("transition %s finished", transition) if (activeTransitionTokensAndTasks.remove(transition) != null) { if (aborted) { interactionJankMonitor.cancel(CUJ_DESKTOP_MODE_MINIMIZE_WINDOW) @@ -252,10 +251,6 @@ class DesktopTasksLimiter ( ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) } - private fun logE(msg: String, vararg arguments: Any?) { - ProtoLog.e(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) - } - private companion object { const val TAG = "DesktopTasksLimiter" } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java index dedd44f3950a..d537da802efd 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java @@ -60,7 +60,8 @@ import java.util.function.Supplier; * entering and exiting freeform. */ public class ExitDesktopTaskTransitionHandler implements Transitions.TransitionHandler { - private static final int FULLSCREEN_ANIMATION_DURATION = 336; + @VisibleForTesting + static final int FULLSCREEN_ANIMATION_DURATION = 336; private final Context mContext; private final Transitions mTransitions; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ReturnToDragStartAnimator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ReturnToDragStartAnimator.kt index f4df42cde10f..4e08d106052a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ReturnToDragStartAnimator.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ReturnToDragStartAnimator.kt @@ -19,28 +19,24 @@ package com.android.wm.shell.desktopmode import android.animation.Animator import android.animation.RectEvaluator import android.animation.ValueAnimator -import android.content.Context import android.graphics.Rect import android.view.SurfaceControl -import android.widget.Toast import androidx.core.animation.addListener import com.android.internal.jank.Cuj import com.android.internal.jank.InteractionJankMonitor -import com.android.wm.shell.R import com.android.wm.shell.windowdecor.OnTaskRepositionAnimationListener import java.util.function.Supplier /** Animates the task surface moving from its current drag position to its pre-drag position. */ class ReturnToDragStartAnimator( - private val context: Context, private val transactionSupplier: Supplier<SurfaceControl.Transaction>, private val interactionJankMonitor: InteractionJankMonitor ) { private var boundsAnimator: Animator? = null private lateinit var taskRepositionAnimationListener: OnTaskRepositionAnimationListener - constructor(context: Context, interactionJankMonitor: InteractionJankMonitor) : - this(context, Supplier { SurfaceControl.Transaction() }, interactionJankMonitor) + constructor(interactionJankMonitor: InteractionJankMonitor) : + this(Supplier { SurfaceControl.Transaction() }, interactionJankMonitor) /** Sets a listener for the start and end of the reposition animation. */ fun setTaskRepositionAnimationListener(listener: OnTaskRepositionAnimationListener) { @@ -53,7 +49,7 @@ class ReturnToDragStartAnimator( taskSurface: SurfaceControl, startBounds: Rect, endBounds: Rect, - isResizable: Boolean + doOnEnd: (() -> Unit)? = null, ) { val tx = transactionSupplier.get() @@ -87,13 +83,7 @@ class ReturnToDragStartAnimator( .apply() taskRepositionAnimationListener.onAnimationEnd(taskId) boundsAnimator = null - if (!isResizable) { - Toast.makeText( - context, - R.string.desktop_mode_non_resizable_snap_text, - Toast.LENGTH_SHORT - ).show() - } + doOnEnd?.invoke() interactionJankMonitor.end(Cuj.CUJ_DESKTOP_MODE_SNAP_RESIZE) } ) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepository.kt index 7ae537088832..8bfcca093855 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepository.kt @@ -18,6 +18,10 @@ package com.android.wm.shell.desktopmode import android.app.ActivityManager.RunningTaskInfo import android.graphics.Rect +import com.android.wm.shell.desktopmode.CaptionState.AppHandle +import com.android.wm.shell.desktopmode.CaptionState.AppHeader +import com.android.wm.shell.desktopmode.CaptionState.NoCaption +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -26,11 +30,20 @@ class WindowDecorCaptionHandleRepository { private val _captionStateFlow = MutableStateFlow<CaptionState>(CaptionState.NoCaption) /** Observer for app handle state changes. */ val captionStateFlow: StateFlow<CaptionState> = _captionStateFlow + private val _appToWebUsageFlow = MutableSharedFlow<Unit>() + /** Observer for App-to-Web usage. */ + val appToWebUsageFlow = _appToWebUsageFlow + /** Notifies [captionStateFlow] if there is a change to caption state. */ fun notifyCaptionChanged(captionState: CaptionState) { _captionStateFlow.value = captionState } + + /** Notifies [appToWebUsageFlow] if App-to-Web feature is used. */ + fun onAppToWebUsage() { + _appToWebUsageFlow.tryEmit(Unit) + } } /** @@ -41,17 +54,19 @@ class WindowDecorCaptionHandleRepository { * * [AppHeader]: Indicating that there is at least one visible app chip on the screen. * * [NoCaption]: Signifying that no caption handle is currently visible on the device. */ -sealed class CaptionState { +sealed class CaptionState{ data class AppHandle( - val runningTaskInfo: RunningTaskInfo, - val isHandleMenuExpanded: Boolean, - val globalAppHandleBounds: Rect + val runningTaskInfo: RunningTaskInfo, + val isHandleMenuExpanded: Boolean, + val globalAppHandleBounds: Rect, + val isCapturedLinkAvailable: Boolean ) : CaptionState() data class AppHeader( - val runningTaskInfo: RunningTaskInfo, - val isHeaderMenuExpanded: Boolean, - val globalAppChipBounds: Rect + val runningTaskInfo: RunningTaskInfo, + val isHeaderMenuExpanded: Boolean, + val globalAppChipBounds: Rect, + val isCapturedLinkAvailable: Boolean ) : CaptionState() data object NoCaption : CaptionState() diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt index f21a124f0b8b..e01c448be8e5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt @@ -36,7 +36,7 @@ import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource import com.android.wm.shell.windowdecor.common.DecorThemeUtil import com.android.wm.shell.windowdecor.common.Theme import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationTooltipController -import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationTooltipController.EducationViewConfig +import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationTooltipController.TooltipEducationViewConfig import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationTooltipController.TooltipColorScheme import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.CoroutineScope @@ -137,7 +137,7 @@ class AppHandleEducationController( // TODO: b/370546801 - Differentiate between user dismissing the tooltip vs following the cue. // Populate information important to inflate app handle education tooltip. val appHandleTooltipConfig = - EducationViewConfig( + TooltipEducationViewConfig( tooltipViewLayout = R.layout.desktop_windowing_education_top_arrow_tooltip, tooltipColorScheme = tooltipColorScheme, tooltipViewGlobalCoordinates = tooltipGlobalCoordinates, @@ -196,7 +196,7 @@ class AppHandleEducationController( windowingOptionPillHeight / 2) // Populate information important to inflate windowing image button education tooltip. val windowingImageButtonTooltipConfig = - EducationViewConfig( + TooltipEducationViewConfig( tooltipViewLayout = R.layout.desktop_windowing_education_left_arrow_tooltip, tooltipColorScheme = tooltipColorScheme, tooltipViewGlobalCoordinates = tooltipGlobalCoordinates, @@ -249,7 +249,7 @@ class AppHandleEducationController( globalAppChipBounds.top + globalAppChipBounds.height() / 2) // Populate information important to inflate exit desktop mode education tooltip. val exitWindowingTooltipConfig = - EducationViewConfig( + TooltipEducationViewConfig( tooltipViewLayout = R.layout.desktop_windowing_education_left_arrow_tooltip, tooltipColorScheme = tooltipColorScheme, tooltipViewGlobalCoordinates = tooltipGlobalCoordinates, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppToWebEducationController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppToWebEducationController.kt new file mode 100644 index 000000000000..693da81ec4a0 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppToWebEducationController.kt @@ -0,0 +1,207 @@ +/* + * 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.desktopmode.education + +import android.annotation.DimenRes +import android.annotation.StringRes +import android.app.ActivityManager.RunningTaskInfo +import android.content.Context +import android.content.res.Resources +import android.graphics.Point +import android.os.SystemProperties +import androidx.compose.ui.graphics.toArgb +import com.android.window.flags.Flags +import com.android.wm.shell.R +import com.android.wm.shell.desktopmode.CaptionState +import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository +import com.android.wm.shell.desktopmode.education.data.AppToWebEducationDatastoreRepository +import com.android.wm.shell.shared.annotations.ShellBackgroundThread +import com.android.wm.shell.shared.annotations.ShellMainThread +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus.canEnterDesktopMode +import com.android.wm.shell.windowdecor.common.DecorThemeUtil +import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationPromoController +import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationPromoController.EducationColorScheme +import com.android.wm.shell.windowdecor.education.DesktopWindowingEducationPromoController.EducationViewConfig +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.MainCoroutineDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch + +/** + * Controls App-to-Web education end to end. + * + * Listen to usages of App-to-Web, calls an api to check if the education + * should be shown and controls education UI. + */ +@OptIn(kotlinx.coroutines.FlowPreview::class) +@kotlinx.coroutines.ExperimentalCoroutinesApi +class AppToWebEducationController( + private val context: Context, + private val appToWebEducationFilter: AppToWebEducationFilter, + private val appToWebEducationDatastoreRepository: AppToWebEducationDatastoreRepository, + private val windowDecorCaptionHandleRepository: WindowDecorCaptionHandleRepository, + private val windowingEducationViewController: DesktopWindowingEducationPromoController, + @ShellMainThread private val applicationCoroutineScope: CoroutineScope, + @ShellBackgroundThread private val backgroundDispatcher: MainCoroutineDispatcher, +) { + private val decorThemeUtil = DecorThemeUtil(context) + + init { + runIfEducationFeatureEnabled { + applicationCoroutineScope.launch { + // Central block handling the App-to-Web's educational flow end-to-end. + isEducationViewLimitReachedFlow() + .flatMapLatest { countExceedsMaximum -> + if (countExceedsMaximum) { + // If the education has been viewed the maximum amount of times then + // return emptyFlow() that completes immediately. This will help us to + // not listen to [captionHandleStateFlow] after the education should + // not be shown. + emptyFlow() + } else { + // Listen for changes to window decor's caption. + windowDecorCaptionHandleRepository.captionStateFlow + // Wait for few seconds before emitting the latest state. + .debounce(APP_TO_WEB_EDUCATION_DELAY_MILLIS) + .filter { captionState -> + captionState !is CaptionState.NoCaption && + appToWebEducationFilter + .shouldShowAppToWebEducation(captionState) + } + } + } + .flowOn(backgroundDispatcher) + .collectLatest { captionState -> + val educationColorScheme = educationColorScheme(captionState) + showEducation(captionState, educationColorScheme!!) + // After showing first tooltip, increase count of education views + appToWebEducationDatastoreRepository.updateEducationShownCount() + } + } + + applicationCoroutineScope.launch { + if (isFeatureUsed()) return@launch + windowDecorCaptionHandleRepository.appToWebUsageFlow + .collect { + // If user utilizes App-to-Web, mark user has used the feature + appToWebEducationDatastoreRepository + .updateFeatureUsedTimestampMillis(isViewed = true) + } + } + } + } + + private inline fun runIfEducationFeatureEnabled(block: () -> Unit) { + if (canEnterDesktopMode(context) && Flags.enableDesktopWindowingAppToWebEducation()) block() + } + + private fun showEducation(captionState: CaptionState, colorScheme: EducationColorScheme) { + val educationGlobalCoordinates: Point + val taskId: Int + when (captionState) { + is CaptionState.AppHandle -> { + val appHandleBounds = captionState.globalAppHandleBounds + val educationWidth = + loadDimensionPixelSize(R.dimen.desktop_windowing_education_promo_width) + educationGlobalCoordinates = Point( + appHandleBounds.centerX() - educationWidth / 2, + appHandleBounds.bottom + ) + taskId = captionState.runningTaskInfo.taskId + } + + is CaptionState.AppHeader -> { + val taskBounds = + captionState.runningTaskInfo.configuration.windowConfiguration.bounds + educationGlobalCoordinates = + Point(taskBounds.left, captionState.globalAppChipBounds.bottom) + taskId = captionState.runningTaskInfo.taskId + } + + else -> return + } + + // Populate information important to inflate education promo. + val educationConfig = + EducationViewConfig( + viewLayout = R.layout.desktop_windowing_education_promo, + educationColorScheme = colorScheme, + viewGlobalCoordinates = educationGlobalCoordinates, + educationText = getString(R.string.desktop_windowing_app_to_web_education_text), + widthId = R.dimen.desktop_windowing_education_promo_width, + heightId = R.dimen.desktop_windowing_education_promo_height + ) + + windowingEducationViewController.showEducation( + viewConfig = educationConfig, taskId = taskId) + } + + private fun educationColorScheme(captionState: CaptionState): EducationColorScheme? { + val taskInfo: RunningTaskInfo = when (captionState) { + is CaptionState.AppHandle -> captionState.runningTaskInfo + is CaptionState.AppHeader -> captionState.runningTaskInfo + else -> return null + } + + val colorScheme = decorThemeUtil.getColorScheme(taskInfo) + val tooltipContainerColor = colorScheme.surfaceBright.toArgb() + val tooltipTextColor = colorScheme.onSurface.toArgb() + return EducationColorScheme(tooltipContainerColor, tooltipTextColor) + } + + /** + * Listens to changes in the number of times the education has been viewed, mapping the count to + * true if the education has been viewed the maximum amount of times. + */ + private fun isEducationViewLimitReachedFlow(): Flow<Boolean> = + appToWebEducationDatastoreRepository.dataStoreFlow + .map { preferences -> + appToWebEducationFilter.isEducationViewLimitReached(preferences)} + .distinctUntilChanged() + + /** + * Listens to the changes to [WindowingEducationProto#hasFeatureUsedTimestampMillis()] in + * datastore proto object. + */ + private suspend fun isFeatureUsed(): Boolean = + appToWebEducationDatastoreRepository.dataStoreFlow.first().hasFeatureUsedTimestampMillis() + + private fun loadDimensionPixelSize(@DimenRes resourceId: Int): Int { + if (resourceId == Resources.ID_NULL) return 0 + return context.resources.getDimensionPixelSize(resourceId) + } + + private fun getString(@StringRes resId: Int): String = context.resources.getString(resId) + + companion object { + const val TAG = "AppToWebEducationController" + val APP_TO_WEB_EDUCATION_DELAY_MILLIS: Long + get() = SystemProperties.getLong( + "persist.windowing_app_handle_education_delay", + 3000L + ) + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppToWebEducationFilter.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppToWebEducationFilter.kt new file mode 100644 index 000000000000..feee6ed86da1 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppToWebEducationFilter.kt @@ -0,0 +1,92 @@ +/* + * 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.desktopmode.education + +import android.annotation.IntegerRes +import android.app.ActivityManager.RunningTaskInfo +import android.content.Context +import android.os.SystemClock +import android.provider.Settings.Secure +import com.android.wm.shell.R +import com.android.wm.shell.desktopmode.CaptionState +import com.android.wm.shell.desktopmode.education.data.AppToWebEducationDatastoreRepository +import com.android.wm.shell.desktopmode.education.data.WindowingEducationProto +import java.time.Duration + +/** Filters incoming App-to-Web education triggers based on set conditions. */ +class AppToWebEducationFilter( + private val context: Context, + private val appToWebEducationDatastoreRepository: AppToWebEducationDatastoreRepository +) { + + /** Returns true if conditions to show App-to-web education are met, returns false otherwise. */ + suspend fun shouldShowAppToWebEducation(captionState: CaptionState): Boolean { + val (taskInfo: RunningTaskInfo, isCapturedLinkAvailable: Boolean) = when (captionState) { + is CaptionState.AppHandle -> + Pair(captionState.runningTaskInfo, captionState.isCapturedLinkAvailable) + is CaptionState.AppHeader -> + Pair(captionState.runningTaskInfo, captionState.isCapturedLinkAvailable) + else -> return false + } + + val focusAppPackageName = taskInfo.topActivityInfo?.packageName ?: return false + val windowingEducationProto = appToWebEducationDatastoreRepository.windowingEducationProto() + + return !isOtherEducationShowing() && + !isEducationViewLimitReached(windowingEducationProto) && + hasSufficientTimeSinceSetup() && + !isFeatureUsedBefore(windowingEducationProto) && + isCapturedLinkAvailable && + isFocusAppInAllowlist(focusAppPackageName) + } + + private fun isFocusAppInAllowlist(focusAppPackageName: String): Boolean = + focusAppPackageName in + context.resources.getStringArray( + R.array.desktop_windowing_app_to_web_education_allowlist_apps) + + // TODO: b/350953004 - Add checks based on App compat + // TODO: b/350951797 - Add checks based on PKT tips education + private fun isOtherEducationShowing(): Boolean = isTaskbarEducationShowing() || + isCompatUiEducationShowing() + + private fun isTaskbarEducationShowing(): Boolean = + Secure.getInt(context.contentResolver, Secure.LAUNCHER_TASKBAR_EDUCATION_SHOWING, 0) == 1 + + private fun isCompatUiEducationShowing(): Boolean = + Secure.getInt(context.contentResolver, Secure.COMPAT_UI_EDUCATION_SHOWING, 0) == 1 + + private fun hasSufficientTimeSinceSetup(): Boolean = + Duration.ofMillis(SystemClock.elapsedRealtime()) > + convertIntegerResourceToDuration( + R.integer.desktop_windowing_education_required_time_since_setup_seconds) + + /** Returns true if education is viewed maximum amount of times it should be shown. */ + fun isEducationViewLimitReached(windowingEducationProto: WindowingEducationProto): Boolean = + windowingEducationProto.getAppToWebEducation().getEducationShownCount() >= + MAXIMUM_TIMES_EDUCATION_SHOWN + + private fun isFeatureUsedBefore(windowingEducationProto: WindowingEducationProto): Boolean = + windowingEducationProto.hasFeatureUsedTimestampMillis() + + private fun convertIntegerResourceToDuration(@IntegerRes resourceId: Int): Duration = + Duration.ofSeconds(context.resources.getInteger(resourceId).toLong()) + + companion object { + private const val MAXIMUM_TIMES_EDUCATION_SHOWN = 100 + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppToWebEducationDatastoreRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppToWebEducationDatastoreRepository.kt new file mode 100644 index 000000000000..8be6e6dff6fe --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppToWebEducationDatastoreRepository.kt @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.desktopmode.education.data + +import android.content.Context +import android.util.Slog +import androidx.datastore.core.CorruptionException +import androidx.datastore.core.DataStore +import androidx.datastore.core.DataStoreFactory +import androidx.datastore.core.Serializer +import androidx.datastore.dataStoreFile +import com.android.framework.protobuf.InvalidProtocolBufferException +import com.android.internal.annotations.VisibleForTesting +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.first +import java.io.IOException +import java.io.InputStream +import java.io.OutputStream + +/** Updates data in App-to-Web's education datastore. */ +class AppToWebEducationDatastoreRepository +@VisibleForTesting +constructor(private val dataStore: DataStore<WindowingEducationProto>) { + constructor( + context: Context + ) : this( + DataStoreFactory.create( + serializer = WindowingEducationProtoSerializer, + produceFile = { context.dataStoreFile(APP_TO_WEB_EDUCATION_DATASTORE_FILEPATH) })) + + /** Provides dataStore.data flow and handles exceptions thrown during collection */ + val dataStoreFlow: Flow<WindowingEducationProto> = + dataStore.data.catch { exception -> + // dataStore.data throws an IOException when an error is encountered when reading data + if (exception is IOException) { + Slog.e( + TAG, + "Error in reading App-to-Web education related data from datastore," + + "data is stored in a file named" + + "$APP_TO_WEB_EDUCATION_DATASTORE_FILEPATH", exception) + } else { + throw exception + } + } + + /** + * Reads and returns the [WindowingEducationProto] Proto object from the DataStore. If the + * DataStore is empty or there's an error reading, it returns the default value of Proto. + */ + suspend fun windowingEducationProto(): WindowingEducationProto = dataStoreFlow.first() + + /** + * Updates [WindowingEducationProto.featureUsedTimestampMillis_] field in datastore with current + * timestamp if [isViewed] is true, if not then clears the field. + */ + suspend fun updateFeatureUsedTimestampMillis(isViewed: Boolean) { + dataStore.updateData { preferences -> + if (isViewed) { + preferences + .toBuilder().setFeatureUsedTimestampMillis(System.currentTimeMillis()).build() + } else { + preferences.toBuilder().clearFeatureUsedTimestampMillis().build() + } + } + } + + /** + * Increases [AppToWebEducation.educationShownCount] field by one. + */ + suspend fun updateEducationShownCount() { + val currentAppHandleProto = windowingEducationProto().appToWebEducation.toBuilder() + currentAppHandleProto + .setEducationShownCount(currentAppHandleProto.getEducationShownCount() + 1) + dataStore.updateData { preferences -> + preferences.toBuilder().setAppToWebEducation(currentAppHandleProto).build() + } + } + + + companion object { + private const val TAG = "AppToWebEducationDatastoreRepository" + private const val APP_TO_WEB_EDUCATION_DATASTORE_FILEPATH = "app_to_web_education.pb" + + object WindowingEducationProtoSerializer : Serializer<WindowingEducationProto> { + + override val defaultValue: WindowingEducationProto = + WindowingEducationProto.getDefaultInstance() + + override suspend fun readFrom(input: InputStream): WindowingEducationProto = + try { + WindowingEducationProto.parseFrom(input) + } catch (exception: InvalidProtocolBufferException) { + throw CorruptionException("Cannot read proto.", exception) + } + + override suspend fun writeTo( + windowingProto: WindowingEducationProto, + output: OutputStream + ) = windowingProto.writeTo(output) + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/proto/windowing_education.proto b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/proto/windowing_education.proto index d29ec53d9c61..4cddd01ee96b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/proto/windowing_education.proto +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/proto/windowing_education.proto @@ -28,6 +28,8 @@ message WindowingEducationProto { oneof education_data { // Fields specific to app handle education AppHandleEducation app_handle_education = 3; + // Fields specific to App-to-Web education + AppToWebEducation app_to_web_education = 4; } message AppHandleEducation { @@ -36,4 +38,9 @@ message WindowingEducationProto { // Timestamp of when app_usage_stats was last cached optional int64 app_usage_stats_last_update_timestamp_millis = 2; } + + message AppToWebEducation { + // Number of times education is shown + optional int64 education_shown_count = 1; + } }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt index 2d11e02bd3c6..9e646f430c98 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt @@ -50,7 +50,9 @@ class DesktopPersistentRepository( DataStoreFactory.create( serializer = DesktopPersistentRepositoriesSerializer, produceFile = { context.dataStoreFile(DESKTOP_REPOSITORIES_DATASTORE_FILE) }, - scope = bgCoroutineScope)) + scope = bgCoroutineScope, + ), + ) /** Provides `dataStore.data` flow and handles exceptions thrown during collection */ private val dataStoreFlow: Flow<DesktopPersistentRepositories> = @@ -116,7 +118,11 @@ class DesktopPersistentRepository( val desktop = getDesktop(currentRepository, desktopId) .toBuilder() - .updateTaskStates(visibleTasks, minimizedTasks) + .updateTaskStates( + visibleTasks, + minimizedTasks, + freeformTasksInZOrder, + ) .updateZOrder(freeformTasksInZOrder) desktopPersistentRepositories @@ -169,9 +175,21 @@ class DesktopPersistentRepository( private fun Desktop.Builder.updateTaskStates( visibleTasks: ArraySet<Int>, - minimizedTasks: ArraySet<Int> + minimizedTasks: ArraySet<Int>, + freeformTasksInZOrder: ArrayList<Int>, ): Desktop.Builder { clearTasksByTaskId() + + // Handle the case where tasks are not marked as visible but are meant to be visible + // after reboot. E.g. User moves out of desktop when there are multiple tasks are + // visible, they will be marked as not visible afterwards. This ensures that they are + // still persisted as visible. + // TODO - b/350476823: Remove this logic once repository holds expanded tasks + if (freeformTasksInZOrder.size > visibleTasks.size + minimizedTasks.size && + visibleTasks.isEmpty() + ) { + visibleTasks.addAll(freeformTasksInZOrder.filterNot { it in minimizedTasks }) + } putAllTasksByTaskId( visibleTasks.associateWith { createDesktopTask(it, state = DesktopTaskState.VISIBLE) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializer.kt new file mode 100644 index 000000000000..771c3d1cb9a1 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializer.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.desktopmode.persistence + +import com.android.wm.shell.desktopmode.DesktopRepository + +/** Interface for initializing the [DesktopRepository]. */ +fun interface DesktopRepositoryInitializer { + fun initialize(repository: DesktopRepository) +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt new file mode 100644 index 000000000000..d8156561ff19 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.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.wm.shell.desktopmode.persistence + +import android.content.Context +import android.window.DesktopModeFlags +import com.android.wm.shell.desktopmode.DesktopRepository +import com.android.wm.shell.shared.annotations.ShellMainThread +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +/** + * Initializes the [DesktopRepository] from the [DesktopPersistentRepository]. + * + * This class is responsible for reading the [DesktopPersistentRepository] and initializing the + * [DesktopRepository] with the tasks that previously existed in desktop. + */ +class DesktopRepositoryInitializerImpl( + private val context: Context, + private val persistentRepository: DesktopPersistentRepository, + @ShellMainThread private val mainCoroutineScope: CoroutineScope, +) : DesktopRepositoryInitializer { + override fun initialize(repository: DesktopRepository) { + if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) return + // TODO: b/365962554 - Handle the case that user moves to desktop before it's initialized + mainCoroutineScope.launch { + val desktop = persistentRepository.readDesktop() ?: return@launch + + val maxTasks = + DesktopModeStatus.getMaxTaskLimit(context).takeIf { it > 0 } + ?: desktop.zOrderedTasksCount + + var visibleTasksCount = 0 + desktop.zOrderedTasksList + // Reverse it so we initialize the repo from bottom to top. + .reversed() + .mapNotNull { taskId -> desktop.tasksByTaskIdMap[taskId] } + .forEach { task -> + if (task.desktopTaskState == DesktopTaskState.VISIBLE + && visibleTasksCount < maxTasks + ) { + visibleTasksCount++ + repository.addTask(desktop.displayId, task.taskId, isVisible = false) + } else { + repository.addTask(desktop.displayId, task.taskId, isVisible = false) + repository.minimizeTask(desktop.displayId, task.taskId) + } + } + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java index f060158766fe..4aeecbec7dfb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java @@ -30,6 +30,7 @@ import android.annotation.NonNull; import android.app.TaskInfo; import android.content.Context; import android.content.pm.ActivityInfo; +import android.graphics.Point; import android.graphics.Rect; import android.os.SystemClock; import android.view.Surface; @@ -152,7 +153,6 @@ public class PipAnimationController { return mCurrentAnimator; } - @SuppressWarnings("unchecked") /** * Construct and return an animator that animates from the {@param startBounds} to the * {@param endBounds} with the given {@param direction}. If {@param direction} is type @@ -171,6 +171,7 @@ public class PipAnimationController { * leaving PiP to fullscreen, and the {@param endBounds} is the fullscreen bounds before the * rotation change. */ + @SuppressWarnings("unchecked") @VisibleForTesting public PipTransitionAnimator getAnimator(TaskInfo taskInfo, SurfaceControl leash, Rect baseBounds, Rect startBounds, Rect endBounds, Rect sourceHintRect, @@ -566,7 +567,7 @@ public class PipAnimationController { } getSurfaceTransactionHelper() .resetScale(tx, leash, getDestinationBounds()) - .crop(tx, leash, getDestinationBounds()) + .cropAndPosition(tx, leash, getDestinationBounds()) .round(tx, leash, true /* applyCornerRadius */) .shadow(tx, leash, shouldApplyShadowRadius()); tx.show(leash); @@ -590,18 +591,50 @@ public class PipAnimationController { // Just for simplicity we'll interpolate between the source rect hint insets and empty // insets to calculate the window crop final Rect initialSourceValue; + final Rect mainWindowFrame = taskInfo.topActivityMainWindowFrame; + final boolean hasNonMatchFrame = mainWindowFrame != null; + final boolean changeOrientation = + rotationDelta == ROTATION_90 || rotationDelta == ROTATION_270; + final Rect baseBounds = new Rect(baseValue); + final Rect startBounds = new Rect(startValue); + final Rect endBounds = new Rect(endValue); if (isOutPipDirection) { - initialSourceValue = new Rect(endValue); + // TODO(b/356277166): handle rotation change with activity that provides main window + // frame. + if (hasNonMatchFrame && !changeOrientation) { + endBounds.set(mainWindowFrame); + } + initialSourceValue = new Rect(endBounds); + } else if (isInPipDirection) { + if (hasNonMatchFrame) { + baseBounds.set(mainWindowFrame); + if (startValue.equals(baseValue)) { + // If the start value is at initial state as in PIP animation, also override + // the start bounds with nonMatchParentBounds. + startBounds.set(mainWindowFrame); + } + } + initialSourceValue = new Rect(baseBounds); } else { - initialSourceValue = new Rect(baseValue); + // Note that we assume the window bounds always match task bounds in PIP mode. + initialSourceValue = new Rect(baseBounds); + } + + final Point leashOffset; + if (isInPipDirection) { + leashOffset = new Point(baseValue.left, baseValue.top); + } else if (isOutPipDirection) { + leashOffset = new Point(endValue.left, endValue.top); + } else { + leashOffset = new Point(baseValue.left, baseValue.top); } final Rect rotatedEndRect; final Rect lastEndRect; final Rect initialContainerRect; - if (rotationDelta == ROTATION_90 || rotationDelta == ROTATION_270) { - lastEndRect = new Rect(endValue); - rotatedEndRect = new Rect(endValue); + if (changeOrientation) { + lastEndRect = new Rect(endBounds); + rotatedEndRect = new Rect(endBounds); // Rotate the end bounds according to the rotation delta because the display will // be rotated to the same orientation. rotateBounds(rotatedEndRect, initialSourceValue, rotationDelta); @@ -617,9 +650,9 @@ public class PipAnimationController { // Crop a Rect matches the aspect ratio and pivots at the center point. // This is done for entering case only. if (isInPipDirection(direction)) { - final float aspectRatio = endValue.width() / (float) endValue.height(); + final float aspectRatio = endBounds.width() / (float) endBounds.height(); adjustedSourceRectHint.set(PipUtils.getEnterPipWithOverlaySrcRectHint( - startValue, aspectRatio)); + startBounds, aspectRatio)); } } else { adjustedSourceRectHint.set(sourceRectHint); @@ -644,7 +677,7 @@ public class PipAnimationController { // construct new Rect instances in case they are recycled return new PipTransitionAnimator<Rect>(taskInfo, leash, ANIM_TYPE_BOUNDS, - endValue, new Rect(baseValue), new Rect(startValue), new Rect(endValue)) { + endBounds, new Rect(baseBounds), new Rect(startBounds), new Rect(endBounds)) { private final RectEvaluator mRectEvaluator = new RectEvaluator(new Rect()); private final RectEvaluator mInsetsEvaluator = new RectEvaluator(new Rect()); @@ -668,11 +701,22 @@ public class PipAnimationController { setCurrentValue(bounds); if (inScaleTransition() || adjustedSourceRectHint.isEmpty()) { if (isOutPipDirection) { - getSurfaceTransactionHelper().crop(tx, leash, end) - .scale(tx, leash, end, bounds); + // Use the bounds relative to the task leash in case the leash does not + // start from (0, 0). + final Rect relativeEndBounds = new Rect(end); + relativeEndBounds.offset(-leashOffset.x, -leashOffset.y); + getSurfaceTransactionHelper() + .crop(tx, leash, relativeEndBounds) + .scale(tx, leash, relativeEndBounds, bounds, + false /* shouldOffset */); } else { - getSurfaceTransactionHelper().crop(tx, leash, base) - .scale(tx, leash, base, bounds, angle) + // TODO(b/356277166): add support to specify sourceRectHint with + // non-match parent activity. + // If there's a PIP resize animation, we should offset the bounds to + // (0, 0) since the window bounds should match the leash bounds in PIP + // mode. + getSurfaceTransactionHelper().cropAndPosition(tx, leash, base) + .scale(tx, leash, base, bounds, angle, inScaleTransition()) .round(tx, leash, base, bounds) .shadow(tx, leash, shouldApplyShadowRadius()); } @@ -680,7 +724,7 @@ public class PipAnimationController { final Rect insets = computeInsets(fraction); getSurfaceTransactionHelper().scaleAndCrop(tx, leash, adjustedSourceRectHint, initialSourceValue, bounds, insets, - isInPipDirection, fraction); + isInPipDirection, fraction, leashOffset); final Rect sourceBounds = new Rect(initialContainerRect); sourceBounds.inset(insets); getSurfaceTransactionHelper() @@ -733,8 +777,7 @@ public class PipAnimationController { getSurfaceTransactionHelper() .rotateAndScaleWithCrop(tx, leash, initialContainerRect, bounds, insets, degree, x, y, isOutPipDirection, - rotationDelta == ROTATION_270 /* clockwise */); - getSurfaceTransactionHelper() + rotationDelta == ROTATION_270 /* clockwise */) .round(tx, leash, sourceBounds, bounds) .shadow(tx, leash, shouldApplyShadowRadius()); if (!handlePipTransaction(leash, tx, bounds, 1f /* alpha */)) { @@ -772,7 +815,7 @@ public class PipAnimationController { tx.setPosition(leash, 0, 0); tx.setWindowCrop(leash, 0, 0); } else { - getSurfaceTransactionHelper().crop(tx, leash, destBounds); + getSurfaceTransactionHelper().cropAndPosition(tx, leash, destBounds); } if (mContentOverlay != null) { clearContentOverlay(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java index 3d1994cac534..b02bd0ffec6c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java @@ -16,8 +16,10 @@ package com.android.wm.shell.pip; +import android.annotation.NonNull; import android.content.Context; import android.graphics.Matrix; +import android.graphics.Point; import android.graphics.Rect; import android.graphics.RectF; import android.view.Choreographer; @@ -68,52 +70,102 @@ public class PipSurfaceTransactionHelper { * Operates the crop (and position) on a given transaction and leash * @return same {@link PipSurfaceTransactionHelper} instance for method chaining */ - public PipSurfaceTransactionHelper crop(SurfaceControl.Transaction tx, SurfaceControl leash, - Rect destinationBounds) { + public PipSurfaceTransactionHelper cropAndPosition(@NonNull SurfaceControl.Transaction tx, + @NonNull SurfaceControl leash, @NonNull Rect destinationBounds) { tx.setWindowCrop(leash, destinationBounds.width(), destinationBounds.height()) .setPosition(leash, destinationBounds.left, destinationBounds.top); return this; } /** + * Operates {@link SurfaceControl.Transaction#setCrop} on a given transaction and leash. + * + * @param tx the transaction to apply + * @param leash the leash to crop + * @param relativeDestinationBounds the bounds to crop, which is relative to the leash + * coordinate + * @return same {@link PipSurfaceTransactionHelper} instance for method chaining + */ + public PipSurfaceTransactionHelper crop(@NonNull SurfaceControl.Transaction tx, + @NonNull SurfaceControl leash, @NonNull Rect relativeDestinationBounds) { + tx.setCrop(leash, relativeDestinationBounds); + return this; + } + + /** * Operates the scale (setMatrix) on a given transaction and leash * @return same {@link PipSurfaceTransactionHelper} instance for method chaining */ public PipSurfaceTransactionHelper scale(SurfaceControl.Transaction tx, SurfaceControl leash, Rect sourceBounds, Rect destinationBounds) { mTmpDestinationRectF.set(destinationBounds); - return scale(tx, leash, sourceBounds, mTmpDestinationRectF, 0 /* degrees */); + return scale(tx, leash, sourceBounds, mTmpDestinationRectF, 0 /* degrees */, + true /* shouldOffset */); } /** * Operates the scale (setMatrix) on a given transaction and leash * @return same {@link PipSurfaceTransactionHelper} instance for method chaining */ - public PipSurfaceTransactionHelper scale(SurfaceControl.Transaction tx, SurfaceControl leash, - Rect sourceBounds, RectF destinationBounds) { - return scale(tx, leash, sourceBounds, destinationBounds, 0 /* degrees */); + public PipSurfaceTransactionHelper scale(@NonNull SurfaceControl.Transaction tx, + @NonNull SurfaceControl leash, @NonNull Rect sourceBounds, + @NonNull RectF destinationBounds) { + return scale(tx, leash, sourceBounds, destinationBounds, 0 /* degrees */, + true /* shouldOffset */); } /** - * Operates the scale (setMatrix) on a given transaction and leash + * Operates the scale (setMatrix) on a given transaction and leash. + * + * @param shouldOffset {@code true} to offset the leash to (0, 0) * @return same {@link PipSurfaceTransactionHelper} instance for method chaining */ - public PipSurfaceTransactionHelper scale(SurfaceControl.Transaction tx, SurfaceControl leash, - Rect sourceBounds, Rect destinationBounds, float degrees) { + public PipSurfaceTransactionHelper scale(@NonNull SurfaceControl.Transaction tx, + @NonNull SurfaceControl leash, @NonNull Rect sourceBounds, + @NonNull Rect destinationBounds, boolean shouldOffset) { + mTmpDestinationRectF.set(destinationBounds); + return scale(tx, leash, sourceBounds, mTmpDestinationRectF, 0 /* degrees */, shouldOffset); + } + + /** + * Operates the scale (setMatrix) on a given transaction and leash. + * + * @return same {@link PipSurfaceTransactionHelper} instance for method chaining + */ + public PipSurfaceTransactionHelper scale(@NonNull SurfaceControl.Transaction tx, + @NonNull SurfaceControl leash, @NonNull Rect sourceBounds, + @NonNull Rect destinationBounds, float degrees) { + return scale(tx, leash, sourceBounds, destinationBounds, degrees, true /* shouldOffset */); + } + + /** + * Operates the scale (setMatrix) on a given transaction and leash. + * + * @param shouldOffset {@code true} to offset the leash to (0, 0) + * @return same {@link PipSurfaceTransactionHelper} instance for method chaining + */ + public PipSurfaceTransactionHelper scale(@NonNull SurfaceControl.Transaction tx, + @NonNull SurfaceControl leash, @NonNull Rect sourceBounds, + @NonNull Rect destinationBounds, float degrees, boolean shouldOffset) { mTmpDestinationRectF.set(destinationBounds); - return scale(tx, leash, sourceBounds, mTmpDestinationRectF, degrees); + return scale(tx, leash, sourceBounds, mTmpDestinationRectF, degrees, shouldOffset); } /** * Operates the scale (setMatrix) on a given transaction and leash, along with a rotation. + * + * @param shouldOffset {@code true} to offset the leash to (0, 0) * @return same {@link PipSurfaceTransactionHelper} instance for method chaining */ - public PipSurfaceTransactionHelper scale(SurfaceControl.Transaction tx, SurfaceControl leash, - Rect sourceBounds, RectF destinationBounds, float degrees) { + public PipSurfaceTransactionHelper scale(@NonNull SurfaceControl.Transaction tx, + @NonNull SurfaceControl leash, @NonNull Rect sourceBounds, + @NonNull RectF destinationBounds, float degrees, boolean shouldOffset) { mTmpSourceRectF.set(sourceBounds); // We want the matrix to position the surface relative to the screen coordinates so offset - // the source to 0,0 - mTmpSourceRectF.offsetTo(0, 0); + // the source to (0, 0) if {@code shouldOffset} is true. + if (shouldOffset) { + mTmpSourceRectF.offsetTo(0, 0); + } mTmpDestinationRectF.set(destinationBounds); mTmpTransform.setRectToRect(mTmpSourceRectF, mTmpDestinationRectF, Matrix.ScaleToFit.FILL); mTmpTransform.postRotate(degrees, @@ -123,17 +175,19 @@ public class PipSurfaceTransactionHelper { } /** - * Operates the scale (setMatrix) on a given transaction and leash + * Operates the scale (setMatrix) on a given transaction and leash. + * + * @param leashOffset the offset of the leash bounds relative to the screen coordinate * @return same {@link PipSurfaceTransactionHelper} instance for method chaining */ - public PipSurfaceTransactionHelper scaleAndCrop(SurfaceControl.Transaction tx, - SurfaceControl leash, Rect sourceRectHint, - Rect sourceBounds, Rect destinationBounds, Rect insets, - boolean isInPipDirection, float fraction) { + public PipSurfaceTransactionHelper scaleAndCrop(@NonNull SurfaceControl.Transaction tx, + @NonNull SurfaceControl leash, @NonNull Rect sourceRectHint, @NonNull Rect sourceBounds, + @NonNull Rect destinationBounds, @NonNull Rect insets, boolean isInPipDirection, + float fraction, @NonNull Point leashOffset) { mTmpDestinationRect.set(sourceBounds); // Similar to {@link #scale}, we want to position the surface relative to the screen - // coordinates so offset the bounds to 0,0 - mTmpDestinationRect.offsetTo(0, 0); + // coordinates so offset the bounds relative to the leash. + mTmpDestinationRect.offset(-leashOffset.x, -leashOffset.y); mTmpDestinationRect.inset(insets); // Scale to the bounds no smaller than the destination and offset such that the top/left // of the scaled inset source rect aligns with the top/left of the destination bounds @@ -152,13 +206,13 @@ public class PipSurfaceTransactionHelper { scale = Math.max((float) destinationBounds.width() / sourceBounds.width(), (float) destinationBounds.height() / sourceBounds.height()); } - float left = destinationBounds.left - insets.left * scale; - float top = destinationBounds.top - insets.top * scale; + float left = destinationBounds.left - mTmpDestinationRect.left * scale; + float top = destinationBounds.top - mTmpDestinationRect.top * scale; if (scale == 1) { // Work around the 1 pixel off error by rounding the position down at very beginning. // We noticed such error from flicker tests, not visually. - left = sourceBounds.left; - top = sourceBounds.top; + left = leashOffset.x; + top = leashOffset.y; } mTmpTransform.setScale(scale, scale); tx.setMatrix(leash, mTmpTransform, mTmpFloat9) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java index b4e03299f14c..c4e63dfdade9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java @@ -960,7 +960,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, final SurfaceControl.Transaction boundsChangeTx = mSurfaceControlTransactionFactory.getTransaction(); mSurfaceTransactionHelper - .crop(boundsChangeTx, mLeash, destinationBounds) + .cropAndPosition(boundsChangeTx, mLeash, destinationBounds) .round(boundsChangeTx, mLeash, true /* applyCornerRadius */); mPipTransitionState.setTransitionState(PipTransitionState.ENTRY_SCHEDULED); @@ -988,7 +988,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); mSurfaceTransactionHelper .resetScale(tx, mLeash, destinationBounds) - .crop(tx, mLeash, destinationBounds) + .cropAndPosition(tx, mLeash, destinationBounds) .round(tx, mLeash, isInPip()); // The animation is finished in the Launcher and here we directly apply the final touch. applyEnterPipSyncTransaction(destinationBounds, () -> { @@ -1525,7 +1525,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mPipBoundsState.setBounds(toBounds); final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); mSurfaceTransactionHelper - .crop(tx, mLeash, toBounds) + .cropAndPosition(tx, mLeash, toBounds) .round(tx, mLeash, mPipTransitionState.isInPip()); if (shouldSyncPipTransactionWithMenu()) { mPipMenuController.resizePipMenu(mLeash, tx, toBounds); @@ -1628,7 +1628,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, Rect destinationBounds) { final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); mSurfaceTransactionHelper - .crop(tx, mLeash, destinationBounds) + .cropAndPosition(tx, mLeash, destinationBounds) .resetScale(tx, mLeash, destinationBounds) .round(tx, mLeash, mPipTransitionState.isInPip()); return tx; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java index 6da39951efbe..28b91c6cb812 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java @@ -30,7 +30,6 @@ import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_PIP; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; -import static android.view.WindowManager.transitTypeToString; import static android.window.TransitionInfo.FLAG_IS_DISPLAY; import static com.android.wm.shell.pip.PipAnimationController.ANIM_TYPE_ALPHA; @@ -45,6 +44,7 @@ import static com.android.wm.shell.transition.Transitions.TRANSIT_CLEANUP_PIP_EX import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP; import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP_TO_SPLIT; import static com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP; +import static com.android.wm.shell.transition.Transitions.transitTypeToString; import android.annotation.IntDef; import android.app.ActivityManager; @@ -551,7 +551,7 @@ public class PipTransition extends PipTransitionController { } // Reset the scale with bounds change synchronously. if (hasValidLeash) { - mSurfaceTransactionHelper.crop(tx, leash, destinationBounds) + mSurfaceTransactionHelper.cropAndPosition(tx, leash, destinationBounds) .resetScale(tx, leash, destinationBounds) .round(tx, leash, true /* applyCornerRadius */); final Rect appBounds = mPipOrganizer.mAppBounds; @@ -588,7 +588,8 @@ public class PipTransition extends PipTransitionController { ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: Destination bounds were changed during animation", TAG); rotateBounds(finishBounds, displayBounds, mEndFixedRotation, displayRotation); - mSurfaceTransactionHelper.crop(mFinishTransaction, leash, finishBounds); + mSurfaceTransactionHelper.cropAndPosition(mFinishTransaction, leash, + finishBounds); } } mFinishTransaction = null; @@ -1068,6 +1069,11 @@ public class PipTransition extends PipTransitionController { mPipBoundsState.mayUseCachedLauncherShelfHeight(); final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); final Rect currentBounds = pipChange.getStartAbsBounds(); + // The app bounds should offset relative to the task leash to make the center calculation + // correctly. + final Rect relativeAppBounds = new Rect(taskInfo.topActivityMainWindowFrame != null + ? taskInfo.topActivityMainWindowFrame : currentBounds); + relativeAppBounds.offset(-currentBounds.left, -currentBounds.top); int rotationDelta = deltaRotation(startRotation, endRotation); Rect sourceHintRect = mPipOrganizer.takeSwipeSourceRectHint(); @@ -1089,6 +1095,8 @@ public class PipTransition extends PipTransitionController { if (taskInfo.pictureInPictureParams != null && taskInfo.pictureInPictureParams.isAutoEnterEnabled() && mPipTransitionState.getInSwipePipToHomeTransition()) { + // TODO(b/356277166): add support to swipe PIP to home with + // non-match parent activity. handleSwipePipToHomeTransition(startTransaction, finishTransaction, leash, sourceHintRect, destinationBounds, taskInfo); return; @@ -1119,8 +1127,8 @@ public class PipTransition extends PipTransitionController { // TODO(b/272819817): cleanup the null-check and extra logging. final boolean hasTopActivityInfo = taskInfo.topActivityInfo != null; if (hasTopActivityInfo && mFixedRotationState != FIXED_ROTATION_TRANSITION) { - animator.setAppIconContentOverlay( - mContext, currentBounds, destinationBounds, taskInfo.topActivityInfo, + animator.setAppIconContentOverlay(mContext, relativeAppBounds, + destinationBounds, taskInfo.topActivityInfo, mPipBoundsState.getLauncherState().getAppIconSizePx()); } else { ProtoLog.w(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, @@ -1149,14 +1157,14 @@ public class PipTransition extends PipTransitionController { animationDuration = 0; } mSurfaceTransactionHelper - .crop(finishTransaction, leash, destinationBounds) + .cropAndPosition(finishTransaction, leash, destinationBounds) .round(finishTransaction, leash, true /* applyCornerRadius */); // Always reset to bounds animation type afterwards. setEnterAnimationType(ANIM_TYPE_BOUNDS); } else { throw new RuntimeException("Unrecognized animation type: " + enterAnimationType); } - mPipOrganizer.setContentOverlay(animator.getContentOverlayLeash(), currentBounds); + mPipOrganizer.setContentOverlay(animator.getContentOverlayLeash(), relativeAppBounds); animator.setTransitionDirection(TRANSITION_DIRECTION_TO_PIP) .setPipAnimationCallback(mPipAnimationCallback) .setDuration(animationDuration); @@ -1340,11 +1348,11 @@ public class PipTransition extends PipTransitionController { "%s: Update pip for unhandled transition, change=%s, destBounds=%s, isInPip=%b", TAG, pipChange, destBounds, isInPip); mSurfaceTransactionHelper - .crop(startTransaction, leash, destBounds) + .cropAndPosition(startTransaction, leash, destBounds) .round(startTransaction, leash, isInPip) .shadow(startTransaction, leash, isInPip); mSurfaceTransactionHelper - .crop(finishTransaction, leash, destBounds) + .cropAndPosition(finishTransaction, leash, destBounds) .round(finishTransaction, leash, isInPip) .shadow(finishTransaction, leash, isInPip); // Make sure the PiP keeps invisible if it was faded out. If it needs to fade in, that will diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java index 7a0e6694cb51..d3ae411469cc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java @@ -23,7 +23,6 @@ import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_PIP; import static android.view.WindowManager.TRANSIT_TO_BACK; -import static android.view.WindowManager.transitTypeToString; import static com.android.wm.shell.common.pip.PipMenuController.ALPHA_NO_CHANGE; import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP; @@ -35,6 +34,7 @@ import static com.android.wm.shell.pip.PipTransitionState.EXITING_PIP; import static com.android.wm.shell.pip.PipTransitionState.UNDEFINED; import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP; import static com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP; +import static com.android.wm.shell.transition.Transitions.transitTypeToString; import android.animation.AnimationHandler; import android.animation.Animator; @@ -338,7 +338,7 @@ public class TvPipTransition extends PipTransitionController { final Rect pipBounds = mPipBoundsState.getBounds(); mSurfaceTransactionHelper .resetScale(startTransaction, pipLeash, pipBounds) - .crop(startTransaction, pipLeash, pipBounds) + .cropAndPosition(startTransaction, pipLeash, pipBounds) .shadow(startTransaction, pipLeash, false); final SurfaceControl.Transaction transaction = mTransactionFactory.getTransaction(); @@ -420,7 +420,7 @@ public class TvPipTransition extends PipTransitionController { mSurfaceTransactionHelper .resetScale(finishTransaction, leash, pipBounds) - .crop(finishTransaction, leash, pipBounds) + .cropAndPosition(finishTransaction, leash, pipBounds) .shadow(finishTransaction, leash, false); final Rect currentBounds = pipChange.getStartAbsBounds(); @@ -443,7 +443,7 @@ public class TvPipTransition extends PipTransitionController { SurfaceControl.Transaction tx = mTransactionFactory.getTransaction(); mSurfaceTransactionHelper .resetScale(tx, leash, pipBounds) - .crop(tx, leash, pipBounds) + .cropAndPosition(tx, leash, pipBounds) .shadow(tx, leash, false); mShellTaskOrganizer.applyTransaction(resizePipWct); tx.apply(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipExpandAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipExpandAnimator.java index a93ef12cb7fa..3f9b0c30e314 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipExpandAnimator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipExpandAnimator.java @@ -17,6 +17,7 @@ package com.android.wm.shell.pip2.animation; import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; import android.animation.RectEvaluator; import android.animation.ValueAnimator; import android.content.Context; @@ -27,6 +28,7 @@ import android.view.SurfaceControl; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.internal.annotations.VisibleForTesting; import com.android.wm.shell.R; import com.android.wm.shell.pip2.PipSurfaceTransactionHelper; import com.android.wm.shell.shared.animation.Interpolators; @@ -34,8 +36,7 @@ import com.android.wm.shell.shared.animation.Interpolators; /** * Animator that handles bounds animations for exit-via-expanding PIP. */ -public class PipExpandAnimator extends ValueAnimator - implements ValueAnimator.AnimatorUpdateListener, ValueAnimator.AnimatorListener { +public class PipExpandAnimator extends ValueAnimator { @NonNull private final SurfaceControl mLeash; private final SurfaceControl.Transaction mStartTransaction; @@ -58,12 +59,61 @@ public class PipExpandAnimator extends ValueAnimator // Bounds updated by the evaluator as animator is running. private final Rect mAnimatedRect = new Rect(); - private final PipSurfaceTransactionHelper.SurfaceControlTransactionFactory + private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory mSurfaceControlTransactionFactory; private final RectEvaluator mRectEvaluator; private final RectEvaluator mInsetEvaluator; private final PipSurfaceTransactionHelper mPipSurfaceTransactionHelper; + private final Animator.AnimatorListener mAnimatorListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + super.onAnimationStart(animation); + if (mAnimationStartCallback != null) { + mAnimationStartCallback.run(); + } + if (mStartTransaction != null) { + mStartTransaction.apply(); + } + } + + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + if (mFinishTransaction != null) { + // finishTransaction might override some state (eg. corner radii) so we want to + // manually set the state to the end of the animation + mPipSurfaceTransactionHelper.scaleAndCrop(mFinishTransaction, mLeash, + mSourceRectHint, mBaseBounds, mAnimatedRect, getInsets(1f), + false /* isInPipDirection */, 1f) + .round(mFinishTransaction, mLeash, false /* applyCornerRadius */) + .shadow(mFinishTransaction, mLeash, false /* applyCornerRadius */); + } + if (mAnimationEndCallback != null) { + mAnimationEndCallback.run(); + } + } + }; + + private final ValueAnimator.AnimatorUpdateListener mAnimatorUpdateListener = + new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(@NonNull ValueAnimator animation) { + final SurfaceControl.Transaction tx = + mSurfaceControlTransactionFactory.getTransaction(); + final float fraction = getAnimatedFraction(); + + // TODO (b/350801661): implement fixed rotation + Rect insets = getInsets(fraction); + mPipSurfaceTransactionHelper.scaleAndCrop(tx, mLeash, mSourceRectHint, + mBaseBounds, mAnimatedRect, + insets, false /* isInPipDirection */, fraction) + .round(tx, mLeash, false /* applyCornerRadius */) + .shadow(tx, mLeash, false /* applyCornerRadius */); + tx.apply(); + } + }; + public PipExpandAnimator(Context context, @NonNull SurfaceControl leash, SurfaceControl.Transaction startTransaction, @@ -105,8 +155,8 @@ public class PipExpandAnimator extends ValueAnimator setObjectValues(startBounds, endBounds); setEvaluator(mRectEvaluator); setInterpolator(Interpolators.FAST_OUT_SLOW_IN); - addListener(this); - addUpdateListener(this); + addListener(mAnimatorListener); + addUpdateListener(mAnimatorUpdateListener); } public void setAnimationStartCallback(@NonNull Runnable runnable) { @@ -117,58 +167,15 @@ public class PipExpandAnimator extends ValueAnimator mAnimationEndCallback = runnable; } - @Override - public void onAnimationStart(@NonNull Animator animation) { - if (mAnimationStartCallback != null) { - mAnimationStartCallback.run(); - } - if (mStartTransaction != null) { - mStartTransaction.apply(); - } - } - - @Override - public void onAnimationEnd(@NonNull Animator animation) { - if (mFinishTransaction != null) { - // finishTransaction might override some state (eg. corner radii) so we want to - // manually set the state to the end of the animation - mPipSurfaceTransactionHelper.scaleAndCrop(mFinishTransaction, mLeash, mSourceRectHint, - mBaseBounds, mAnimatedRect, getInsets(1f), - false /* isInPipDirection */, 1f) - .round(mFinishTransaction, mLeash, false /* applyCornerRadius */) - .shadow(mFinishTransaction, mLeash, false /* applyCornerRadius */); - } - if (mAnimationEndCallback != null) { - mAnimationEndCallback.run(); - } - } - - @Override - public void onAnimationUpdate(@NonNull ValueAnimator animation) { - final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); - final float fraction = getAnimatedFraction(); - - // TODO (b/350801661): implement fixed rotation - - Rect insets = getInsets(fraction); - mPipSurfaceTransactionHelper.scaleAndCrop(tx, mLeash, mSourceRectHint, - mBaseBounds, mAnimatedRect, insets, false /* isInPipDirection */, fraction) - .round(tx, mLeash, false /* applyCornerRadius */) - .shadow(tx, mLeash, false /* applyCornerRadius */); - tx.apply(); - } - private Rect getInsets(float fraction) { final Rect startInsets = mSourceRectHintInsets; final Rect endInsets = mZeroInsets; return mInsetEvaluator.evaluate(fraction, startInsets, endInsets); } - // no-ops - - @Override - public void onAnimationCancel(@NonNull Animator animation) {} - - @Override - public void onAnimationRepeat(@NonNull Animator animation) {} + @VisibleForTesting + void setSurfaceControlTransactionFactory( + @NonNull PipSurfaceTransactionHelper.SurfaceControlTransactionFactory factory) { + mSurfaceControlTransactionFactory = factory; + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipResizeAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipResizeAnimator.java index d565776c9917..012dabbbb9f8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipResizeAnimator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipResizeAnimator.java @@ -17,6 +17,7 @@ package com.android.wm.shell.pip2.animation; import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; import android.animation.RectEvaluator; import android.animation.ValueAnimator; import android.content.Context; @@ -27,13 +28,13 @@ import android.view.SurfaceControl; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.internal.annotations.VisibleForTesting; import com.android.wm.shell.pip2.PipSurfaceTransactionHelper; /** * Animator that handles any resize related animation for PIP. */ -public class PipResizeAnimator extends ValueAnimator - implements ValueAnimator.AnimatorUpdateListener, Animator.AnimatorListener{ +public class PipResizeAnimator extends ValueAnimator { @NonNull private final Context mContext; @NonNull @@ -61,9 +62,47 @@ public class PipResizeAnimator extends ValueAnimator private final Rect mAnimatedRect = new Rect(); private final float mDelta; - private final PipSurfaceTransactionHelper.SurfaceControlTransactionFactory + private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory mSurfaceControlTransactionFactory; + private final Animator.AnimatorListener mAnimatorListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + super.onAnimationStart(animation); + if (mAnimationStartCallback != null) { + mAnimationStartCallback.run(); + } + if (mStartTx != null) { + setBoundsAndRotation(mStartTx, mLeash, mBaseBounds, mStartBounds, mDelta); + mStartTx.apply(); + } + } + + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + if (mFinishTx != null) { + setBoundsAndRotation(mFinishTx, mLeash, mBaseBounds, mEndBounds, 0f); + } + if (mAnimationEndCallback != null) { + mAnimationEndCallback.run(); + } + } + }; + + private final ValueAnimator.AnimatorUpdateListener mAnimatorUpdateListener = + new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(@NonNull ValueAnimator animation) { + final SurfaceControl.Transaction tx = + mSurfaceControlTransactionFactory.getTransaction(); + final float fraction = getAnimatedFraction(); + final float degrees = (1.0f - fraction) * mDelta; + setBoundsAndRotation(tx, mLeash, mBaseBounds, mAnimatedRect, degrees); + tx.apply(); + } + }; + public PipResizeAnimator(@NonNull Context context, @NonNull SurfaceControl leash, @Nullable SurfaceControl.Transaction startTransaction, @@ -89,8 +128,8 @@ public class PipResizeAnimator extends ValueAnimator mRectEvaluator = new RectEvaluator(mAnimatedRect); setObjectValues(startBounds, endBounds); - addListener(this); - addUpdateListener(this); + addListener(mAnimatorListener); + addUpdateListener(mAnimatorUpdateListener); setEvaluator(mRectEvaluator); setDuration(duration); } @@ -103,26 +142,6 @@ public class PipResizeAnimator extends ValueAnimator mAnimationEndCallback = runnable; } - @Override - public void onAnimationStart(@NonNull Animator animation) { - if (mAnimationStartCallback != null) { - mAnimationStartCallback.run(); - } - if (mStartTx != null) { - setBoundsAndRotation(mStartTx, mLeash, mBaseBounds, mStartBounds, mDelta); - mStartTx.apply(); - } - } - - @Override - public void onAnimationUpdate(@NonNull ValueAnimator animation) { - final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); - final float fraction = getAnimatedFraction(); - final float degrees = (1.0f - fraction) * mDelta; - setBoundsAndRotation(tx, mLeash, mBaseBounds, mAnimatedRect, degrees); - tx.apply(); - } - /** * Set a proper transform matrix for a leash to move it to given bounds with a certain rotation. * @@ -130,7 +149,7 @@ public class PipResizeAnimator extends ValueAnimator * @param targetBounds bounds to which we are scaling the leash. * @param degrees degrees of rotation - counter-clockwise is positive by convention. */ - public static void setBoundsAndRotation(SurfaceControl.Transaction tx, SurfaceControl leash, + private static void setBoundsAndRotation(SurfaceControl.Transaction tx, SurfaceControl leash, Rect baseBounds, Rect targetBounds, float degrees) { Matrix transformTensor = new Matrix(); final float[] mMatrixTmp = new float[9]; @@ -144,19 +163,9 @@ public class PipResizeAnimator extends ValueAnimator tx.setMatrix(leash, transformTensor, mMatrixTmp); } - @Override - public void onAnimationEnd(@NonNull Animator animation) { - if (mFinishTx != null) { - setBoundsAndRotation(mFinishTx, mLeash, mBaseBounds, mEndBounds, 0f); - } - if (mAnimationEndCallback != null) { - mAnimationEndCallback.run(); - } + @VisibleForTesting + void setSurfaceControlTransactionFactory(@NonNull + PipSurfaceTransactionHelper.SurfaceControlTransactionFactory factory) { + mSurfaceControlTransactionFactory = factory; } - - @Override - public void onAnimationCancel(@NonNull Animator animation) {} - - @Override - public void onAnimationRepeat(@NonNull Animator animation) {} } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl index 245829ecafb3..371bdd5c6469 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl @@ -45,4 +45,7 @@ oneway interface IRecentTasksListener { /** A task has moved to front. */ oneway void onTaskMovedToFront(in RunningTaskInfo taskInfo); + + /** A task info has changed. */ + oneway void onTaskInfoChanged(in RunningTaskInfo taskInfo); }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java index 6086801491e2..faa20159f64a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java @@ -47,7 +47,6 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.android.internal.protolog.ProtoLog; -import com.android.window.flags.Flags; import com.android.wm.shell.common.ExternalInterfaceBinder; import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; @@ -289,6 +288,11 @@ public class RecentTasksController implements TaskStackListenerCallback, } @Override + public void onTaskChangedThroughTransition(@NonNull ActivityManager.RunningTaskInfo taskInfo) { + notifyTaskInfoChanged(taskInfo); + } + + @Override public void onTaskMovedToFrontThroughTransition( ActivityManager.RunningTaskInfo runningTaskInfo) { notifyTaskMovedToFront(runningTaskInfo); @@ -355,6 +359,19 @@ public class RecentTasksController implements TaskStackListenerCallback, } } + private void notifyTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { + if (mListener == null + || !DesktopModeFlags.ENABLE_TASK_STACK_OBSERVER_IN_SHELL.isTrue() + || taskInfo.realActivity == null) { + return; + } + try { + mListener.onTaskInfoChanged(taskInfo); + } catch (RemoteException e) { + Slog.w(TAG, "Failed call onTaskInfoChanged", e); + } + } + private void notifyTaskMovedToFront(ActivityManager.RunningTaskInfo taskInfo) { if (mListener == null || !DesktopModeFlags.ENABLE_TASK_STACK_OBSERVER_IN_SHELL.isTrue() @@ -426,7 +443,7 @@ public class RecentTasksController implements TaskStackListenerCallback, // If task has their app bounds set to null which happens after reboot, set the // app bounds to persisted lastFullscreenBounds. Also set the position in parent // to the top left of the bounds. - if (Flags.enableDesktopWindowingPersistence() + if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue() && taskInfo.configuration.windowConfiguration.getAppBounds() == null) { taskInfo.configuration.windowConfiguration.setAppBounds( taskInfo.lastNonFullscreenBounds); @@ -636,6 +653,11 @@ public class RecentTasksController implements TaskStackListenerCallback, public void onTaskMovedToFront(ActivityManager.RunningTaskInfo taskInfo) { mListener.call(l -> l.onTaskMovedToFront(taskInfo)); } + + @Override + public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { + mListener.call(l -> l.onTaskInfoChanged(taskInfo)); + } }; public IRecentTasksImpl(RecentTasksController controller) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt index 1af99f974a28..d28a462546f9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt @@ -20,8 +20,9 @@ import android.app.ActivityManager.RunningTaskInfo import android.os.IBinder import android.util.ArrayMap import android.view.SurfaceControl -import android.window.TransitionInfo +import android.view.WindowManager.TRANSIT_CHANGE import android.window.DesktopModeFlags +import android.window.TransitionInfo import com.android.wm.shell.shared.TransitionUtil import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.Transitions @@ -69,8 +70,10 @@ class TaskStackTransitionObserver( // Find the first task that is opening, this should be the one at the front after // the transition if (TransitionUtil.isOpeningType(change.mode)) { - notifyTaskStackTransitionObserverListeners(taskInfo) + notifyOnTaskMovedToFront(taskInfo) break + } else if (change.mode == TRANSIT_CHANGE) { + notifyOnTaskChanged(taskInfo) } } } @@ -95,15 +98,23 @@ class TaskStackTransitionObserver( taskStackTransitionObserverListeners.remove(taskStackTransitionObserverListener) } - private fun notifyTaskStackTransitionObserverListeners(taskInfo: RunningTaskInfo) { + private fun notifyOnTaskMovedToFront(taskInfo: RunningTaskInfo) { taskStackTransitionObserverListeners.forEach { (listener, executor) -> executor.execute { listener.onTaskMovedToFrontThroughTransition(taskInfo) } } } + private fun notifyOnTaskChanged(taskInfo: RunningTaskInfo) { + taskStackTransitionObserverListeners.forEach { (listener, executor) -> + executor.execute { listener.onTaskChangedThroughTransition(taskInfo) } + } + } + /** Listener to use to get updates regarding task stack from this observer */ interface TaskStackTransitionObserverListener { /** Called when a task is moved to front. */ fun onTaskMovedToFrontThroughTransition(taskInfo: RunningTaskInfo) {} + /** Called when a task info has changed. */ + fun onTaskChangedThroughTransition(taskInfo: RunningTaskInfo) {} } } 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 839bb4e7a766..cc0e1df115c2 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 @@ -30,7 +30,6 @@ import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_KEYGUARD_OCCLUDE; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; -import static android.view.WindowManager.transitTypeToString; import static android.window.TransitionInfo.FLAG_IS_DISPLAY; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER; @@ -73,6 +72,7 @@ import static com.android.wm.shell.splitscreen.SplitScreenController.exitReasonT import static com.android.wm.shell.transition.MixedTransitionHelper.getPipReplacingChange; import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE; import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_SCREEN_PAIR_OPEN; +import static com.android.wm.shell.transition.Transitions.transitTypeToString; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultSurfaceAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultSurfaceAnimator.java index a1a9ca9fd2bd..4ea4613185e1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultSurfaceAnimator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultSurfaceAnimator.java @@ -136,6 +136,7 @@ public class DefaultSurfaceAnimator { @NonNull final Animation mAnim; @Nullable final Point mPosition; @Nullable final Rect mClipRect; + @Nullable private final Rect mAnimClipRect; final float mCornerRadius; final boolean mIsActivity; @@ -147,6 +148,7 @@ public class DefaultSurfaceAnimator { mPosition = (position != null && (position.x != 0 || position.y != 0)) ? position : null; mClipRect = (clipRect != null && !clipRect.isEmpty()) ? clipRect : null; + mAnimClipRect = mClipRect != null ? new Rect() : null; mCornerRadius = cornerRadius; mIsActivity = isActivity; } @@ -169,18 +171,26 @@ public class DefaultSurfaceAnimator { t.setAlpha(leash, transformation.getAlpha()); if (mClipRect != null) { - Rect clipRect = mClipRect; + boolean needCrop = false; + mAnimClipRect.set(mClipRect); + if (transformation.hasClipRect() + && com.android.window.flags.Flags.respectAnimationClip()) { + mAnimClipRect.intersectUnchecked(transformation.getClipRect()); + needCrop = true; + } final Insets extensionInsets = Insets.min(transformation.getInsets(), Insets.NONE); if (!extensionInsets.equals(Insets.NONE)) { // Clip out any overflowing edge extension. - clipRect = new Rect(mClipRect); - clipRect.inset(extensionInsets); - t.setCrop(leash, clipRect); + mAnimClipRect.inset(extensionInsets); + needCrop = true; } if (mCornerRadius > 0 && mAnim.hasRoundedCorners()) { // Rounded corner can only be applied if a crop is set. - t.setCrop(leash, clipRect); t.setCornerRadius(leash, mCornerRadius); + needCrop = true; + } + if (needCrop) { + t.setCrop(leash, mAnimClipRect); } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java index a27c14bda15a..4feb4753096e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java @@ -24,7 +24,6 @@ import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; -import static android.view.WindowManager.transitTypeToString; import static android.window.TransitionInfo.FLAGS_IS_NON_APP_WINDOW; import static android.window.TransitionInfo.FLAG_IS_DISPLAY; import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT; @@ -34,6 +33,7 @@ import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITI import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_INTRA_CLOSE; import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_INTRA_OPEN; import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_OPEN; +import static com.android.wm.shell.transition.Transitions.transitTypeToString; import android.annotation.ColorInt; import android.annotation.NonNull; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java index 7c9cd0862b69..1d456aed5f4d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java @@ -29,7 +29,6 @@ import static android.view.WindowManager.TRANSIT_SLEEP; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; import static android.view.WindowManager.fixScale; -import static android.view.WindowManager.transitTypeToString; import static android.window.TransitionInfo.FLAGS_IS_NON_APP_WINDOW; import static android.window.TransitionInfo.FLAG_BACK_GESTURE_ANIMATED; import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY; @@ -725,7 +724,7 @@ public class Transitions implements RemoteCallable<Transitions>, @NonNull SurfaceControl.Transaction t, @NonNull SurfaceControl.Transaction finishT) { info.setUnreleasedWarningCallSiteForAllSurfaces("Transitions.onTransitionReady"); ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "onTransitionReady (#%d) %s: %s", - info.getDebugId(), transitionToken, info); + info.getDebugId(), transitionToken, info.toString(" " /* prefix */)); int activeIdx = findByToken(mPendingTransitions, transitionToken); if (activeIdx < 0) { final ActiveTransition existing = mKnownTransitions.get(transitionToken); @@ -1847,6 +1846,40 @@ public class Transitions implements RemoteCallable<Transitions>, } } + /** + * Like WindowManager#transitTypeToString(), but also covers known custom transition types as + * well. + */ + public static String transitTypeToString(int transitType) { + if (transitType < TRANSIT_FIRST_CUSTOM) { + return WindowManager.transitTypeToString(transitType); + } + + String typeStr = switch (transitType) { + case TRANSIT_EXIT_PIP -> "EXIT_PIP"; + case TRANSIT_EXIT_PIP_TO_SPLIT -> "EXIT_PIP_TO_SPLIT"; + case TRANSIT_REMOVE_PIP -> "REMOVE_PIP"; + case TRANSIT_SPLIT_SCREEN_PAIR_OPEN -> "SPLIT_SCREEN_PAIR_OPEN"; + case TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE -> "SPLIT_SCREEN_OPEN_TO_SIDE"; + case TRANSIT_SPLIT_DISMISS_SNAP -> "SPLIT_DISMISS_SNAP"; + case TRANSIT_SPLIT_DISMISS -> "SPLIT_DISMISS"; + case TRANSIT_MAXIMIZE -> "MAXIMIZE"; + case TRANSIT_RESTORE_FROM_MAXIMIZE -> "RESTORE_FROM_MAXIMIZE"; + case TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP -> "DESKTOP_MODE_START_DRAG_TO_DESKTOP"; + case TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP -> "DESKTOP_MODE_END_DRAG_TO_DESKTOP"; + case TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP -> + "DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP"; + case TRANSIT_DESKTOP_MODE_TOGGLE_RESIZE -> "DESKTOP_MODE_TOGGLE_RESIZE"; + case TRANSIT_RESIZE_PIP -> "RESIZE_PIP"; + case TRANSIT_TASK_FRAGMENT_DRAG_RESIZE -> "TASK_FRAGMENT_DRAG_RESIZE"; + case TRANSIT_SPLIT_PASSTHROUGH -> "SPLIT_PASSTHROUGH"; + case TRANSIT_CLEANUP_PIP_EXIT -> "CLEANUP_PIP_EXIT"; + case TRANSIT_MINIMIZE -> "MINIMIZE"; + default -> ""; + }; + return typeStr + "(FIRST_CUSTOM+" + (transitType - TRANSIT_FIRST_CUSTOM) + ")"; + } + private static boolean getShellTransitEnabled() { try { if (AppGlobals.getPackageManager().hasSystemFeature( diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java index 3946b6173b0b..c9546731a193 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java @@ -180,12 +180,13 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL // all other cases, it is expected that the transition handler positions and crops the task // in order to allow the handler time to animate before the task before the final // position and crop are set. - final boolean shouldSetTaskPositionAndCrop = mTaskDragResizer.isResizingOrAnimating(); + final boolean shouldSetTaskVisibilityPositionAndCrop = + mTaskDragResizer.isResizingOrAnimating(); // Use |applyStartTransactionOnDraw| so that the transaction (that applies task crop) is // synced with the buffer transaction (that draws the View). Both will be shown on screen // at the same, whereas applying them independently causes flickering. See b/270202228. relayout(taskInfo, t, t, true /* applyStartTransactionOnDraw */, - shouldSetTaskPositionAndCrop, hasGlobalFocus); + shouldSetTaskVisibilityPositionAndCrop, hasGlobalFocus); } @VisibleForTesting @@ -193,7 +194,7 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL RelayoutParams relayoutParams, ActivityManager.RunningTaskInfo taskInfo, boolean applyStartTransactionOnDraw, - boolean setTaskCropAndPosition, + boolean shouldSetTaskVisibilityPositionAndCrop, boolean isStatusBarVisible, boolean isKeyguardVisibleAndOccluded, InsetsState displayInsetsState, @@ -206,7 +207,7 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL ? R.dimen.freeform_decor_shadow_focused_thickness : R.dimen.freeform_decor_shadow_unfocused_thickness; relayoutParams.mApplyStartTransactionOnDraw = applyStartTransactionOnDraw; - relayoutParams.mSetTaskPositionAndCrop = setTaskCropAndPosition; + relayoutParams.mSetTaskVisibilityPositionAndCrop = shouldSetTaskVisibilityPositionAndCrop; relayoutParams.mIsCaptionVisible = taskInfo.isFreeform() || (isStatusBarVisible && !isKeyguardVisibleAndOccluded); @@ -234,7 +235,7 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL @SuppressLint("MissingPermission") void relayout(RunningTaskInfo taskInfo, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, - boolean applyStartTransactionOnDraw, boolean setTaskCropAndPosition, + boolean applyStartTransactionOnDraw, boolean shouldSetTaskVisibilityPositionAndCrop, boolean hasGlobalFocus) { final boolean isFreeform = taskInfo.getWindowingMode() == WindowConfiguration.WINDOWING_MODE_FREEFORM; @@ -246,7 +247,8 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL final WindowContainerTransaction wct = new WindowContainerTransaction(); updateRelayoutParams(mRelayoutParams, taskInfo, applyStartTransactionOnDraw, - setTaskCropAndPosition, mIsStatusBarVisible, mIsKeyguardVisibleAndOccluded, + shouldSetTaskVisibilityPositionAndCrop, mIsStatusBarVisible, + mIsKeyguardVisibleAndOccluded, mDisplayController.getInsetsState(taskInfo.displayId), hasGlobalFocus); relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index a3324cc6f286..e1683f3a903c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -103,6 +103,7 @@ import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.MultiInstanceHelper; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.compatui.CompatUIController; import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler; import com.android.wm.shell.desktopmode.DesktopModeEventLogger; import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator; @@ -113,6 +114,7 @@ import com.android.wm.shell.desktopmode.DesktopTasksLimiter; import com.android.wm.shell.desktopmode.DesktopWallpaperActivity; import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository; import com.android.wm.shell.desktopmode.education.AppHandleEducationController; +import com.android.wm.shell.desktopmode.education.AppToWebEducationController; import com.android.wm.shell.freeform.FreeformTaskTransitionStarter; import com.android.wm.shell.shared.FocusTransitionListener; import com.android.wm.shell.shared.annotations.ShellBackgroundThread; @@ -130,7 +132,6 @@ import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.FocusTransitionObserver; import com.android.wm.shell.transition.Transitions; import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration.ExclusionRegionListener; -import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer; import com.android.wm.shell.windowdecor.extension.InsetsStateKt; import com.android.wm.shell.windowdecor.extension.TaskInfoKt; import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder; @@ -177,11 +178,11 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, private final WindowDecorCaptionHandleRepository mWindowDecorCaptionHandleRepository; private final Optional<DesktopTasksLimiter> mDesktopTasksLimiter; private final AppHandleEducationController mAppHandleEducationController; + private final AppToWebEducationController mAppToWebEducationController; private final AppHeaderViewHolder.Factory mAppHeaderViewHolderFactory; private boolean mTransitionDragActive; private SparseArray<EventReceiver> mEventReceiversByDisplay = new SparseArray<>(); - private DesktopStatusBarInputLayerSupplier mStatusBarInputLayerSupplier; private final ExclusionRegionListener mExclusionRegionListener = new ExclusionRegionListenerImpl(); @@ -251,6 +252,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, MultiInstanceHelper multiInstanceHelper, Optional<DesktopTasksLimiter> desktopTasksLimiter, AppHandleEducationController appHandleEducationController, + AppToWebEducationController appToWebEducationController, WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository, Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler, FocusTransitionObserver focusTransitionObserver, @@ -284,6 +286,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, interactionJankMonitor, desktopTasksLimiter, appHandleEducationController, + appToWebEducationController, windowDecorCaptionHandleRepository, activityOrientationChangeHandler, new TaskPositionerFactory(), @@ -321,6 +324,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, InteractionJankMonitor interactionJankMonitor, Optional<DesktopTasksLimiter> desktopTasksLimiter, AppHandleEducationController appHandleEducationController, + AppToWebEducationController appToWebEducationController, WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository, Optional<DesktopActivityOrientationChangeHandler> activityOrientationChangeHandler, TaskPositionerFactory taskPositionerFactory, @@ -356,6 +360,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, mInteractionJankMonitor = interactionJankMonitor; mDesktopTasksLimiter = desktopTasksLimiter; mAppHandleEducationController = appHandleEducationController; + mAppToWebEducationController = appToWebEducationController; mWindowDecorCaptionHandleRepository = windowDecorCaptionHandleRepository; mActivityOrientationChangeHandler = activityOrientationChangeHandler; mAssistContentRequester = assistContentRequester; @@ -420,11 +425,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, return Unit.INSTANCE; }); } - if (Flags.enableHandleInputFix()) { - mStatusBarInputLayerSupplier = - new DesktopStatusBarInputLayerSupplier(mContext, mMainHandler); - mFocusTransitionObserver.setLocalFocusTransitionListener(this, mMainExecutor); - } + mFocusTransitionObserver.setLocalFocusTransitionListener(this, mMainExecutor); } @Override @@ -480,7 +481,6 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, removeTaskFromEventReceiver(oldTaskInfo.displayId); incrementEventReceiverTasks(taskInfo.displayId); } - decoration.setStatusBarInputLayer(getStatusBarInputLayer(taskInfo)); decoration.relayout(taskInfo, decoration.mHasGlobalFocus); mActivityOrientationChangeHandler.ifPresent(handler -> handler.handleActivityOrientationChange(oldTaskInfo, taskInfo)); @@ -519,7 +519,6 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, if (decoration == null) { createWindowDecoration(taskInfo, taskSurface, startT, finishT); } else { - decoration.setStatusBarInputLayer(getStatusBarInputLayer(taskInfo)); decoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */, false /* shouldSetTaskPositionAndCrop */, mFocusTransitionObserver.hasGlobalFocus(taskInfo)); @@ -673,7 +672,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, decoration.closeHandleMenu(); // When the app enters split-select, the handle will no longer be visible, meaning // we shouldn't receive input for it any longer. - decoration.detachStatusBarInputLayer(); + decoration.disposeStatusBarInputLayer(); mDesktopTasksController.requestSplit(decoration.mTaskInfo, false /* leftOrTop */); } @@ -1314,8 +1313,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, // should not be receiving any input. if (resultType == TO_SPLIT_LEFT_INDICATOR || resultType == TO_SPLIT_RIGHT_INDICATOR) { - relevantDecor.detachStatusBarInputLayer(); - // We should also detach the other split task's input layer if + relevantDecor.disposeStatusBarInputLayer(); + // We should also dispose the other split task's input layer if // applicable. final int splitPosition = mSplitScreenController .getSplitPosition(relevantDecor.mTaskInfo.taskId); @@ -1328,7 +1327,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, mSplitScreenController.getTaskInfo(oppositePosition); if (oppositeTaskInfo != null) { mWindowDecorByTaskId.get(oppositeTaskInfo.taskId) - .detachStatusBarInputLayer(); + .disposeStatusBarInputLayer(); } } } @@ -1574,11 +1573,14 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, onManageWindows(windowDecoration); return Unit.INSTANCE; }); + windowDecoration.setOnChangeAspectRatioClickListener(() -> { + CompatUIController.launchUserAspectRatioSettings(mContext, taskInfo); + return Unit.INSTANCE; + }); windowDecoration.setCaptionListeners( touchEventListener, touchEventListener, touchEventListener, touchEventListener); windowDecoration.setExclusionRegionListener(mExclusionRegionListener); windowDecoration.setDragPositioningCallback(taskPositioner); - windowDecoration.setStatusBarInputLayer(getStatusBarInputLayer(taskInfo)); windowDecoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */, false /* shouldSetTaskPositionAndCrop */, mFocusTransitionObserver.hasGlobalFocus(taskInfo)); @@ -1587,18 +1589,6 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, } } - /** Decide which cached status bar input layer should be used for a decoration. */ - private AdditionalSystemViewContainer getStatusBarInputLayer( - RunningTaskInfo taskInfo - ) { - if (mStatusBarInputLayerSupplier == null) return null; - return mStatusBarInputLayerSupplier.getStatusBarInputLayer( - taskInfo, - mSplitScreenController.getSplitPosition(taskInfo.taskId), - mSplitScreenController.isLeftRightSplit() - ); - } - private RunningTaskInfo getOtherSplitTask(int taskId) { @SplitPosition int remainingTaskPosition = mSplitScreenController .getSplitPosition(taskId) == SPLIT_POSITION_BOTTOM_OR_RIGHT diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java index 9f37358b6044..d97632a9428c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java @@ -106,7 +106,6 @@ import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource; import com.android.wm.shell.shared.desktopmode.ManageWindowsViewContainer; import com.android.wm.shell.splitscreen.SplitScreenController; -import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer; import com.android.wm.shell.windowdecor.extension.TaskInfoKt; import com.android.wm.shell.windowdecor.viewholder.AppHandleViewHolder; import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder; @@ -155,6 +154,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin private Function0<Unit> mOnToSplitscreenClickListener; private Function0<Unit> mOnNewWindowClickListener; private Function0<Unit> mOnManageWindowsClickListener; + private Function0<Unit> mOnChangeAspectRatioClickListener; private DragPositioningCallback mDragPositioningCallback; private DragResizeInputListener mDragResizeListener; private Runnable mCurrentViewHostRunnable = null; @@ -206,7 +206,6 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin private final MultiInstanceHelper mMultiInstanceHelper; private final WindowDecorCaptionHandleRepository mWindowDecorCaptionHandleRepository; private final DesktopRepository mDesktopRepository; - private AdditionalSystemViewContainer mStatusBarInputLayer; DesktopModeWindowDecoration( Context context, @@ -366,6 +365,11 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mOnManageWindowsClickListener = listener; } + /** Registers a listener to be called when the aspect ratio action is triggered. */ + void setOnChangeAspectRatioClickListener(Function0<Unit> listener) { + mOnChangeAspectRatioClickListener = listener; + } + void setCaptionListeners( View.OnClickListener onCaptionButtonClickListener, View.OnTouchListener onCaptionTouchListener, @@ -392,18 +396,25 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin @Override void relayout(ActivityManager.RunningTaskInfo taskInfo, boolean hasGlobalFocus) { final SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get(); - // The crop and position of the task should only be set when a task is fluid resizing. In - // all other cases, it is expected that the transition handler positions and crops the task - // in order to allow the handler time to animate before the task before the final - // position and crop are set. - final boolean shouldSetTaskPositionAndCrop = !DesktopModeStatus.isVeiledResizeEnabled() - && mTaskDragResizer.isResizingOrAnimating(); + // The visibility, crop and position of the task should only be set when a task is + // fluid resizing. In all other cases, it is expected that the transition handler sets + // those task properties to allow the handler time to animate with full control of the task + // leash. In general, allowing the window decoration to set any of these is likely to cause + // incorrect frames and flickering because relayouts from TaskListener#onTaskInfoChanged + // aren't synchronized with shell transition callbacks, so if they come too early it + // might show/hide or crop the task at a bad time. + // Fluid resizing is exempt from this because it intentionally doesn't use shell + // transitions to resize the task, so onTaskInfoChanged relayouts is the only way to make + // sure the crop is set correctly. + final boolean shouldSetTaskVisibilityPositionAndCrop = + !DesktopModeStatus.isVeiledResizeEnabled() + && mTaskDragResizer.isResizingOrAnimating(); // For headers only (i.e. in freeform): use |applyStartTransactionOnDraw| so that the // transaction (that applies task crop) is synced with the buffer transaction (that draws // the View). Both will be shown on screen at the same, whereas applying them independently // causes flickering. See b/270202228. final boolean applyTransactionOnDraw = taskInfo.isFreeform(); - relayout(taskInfo, t, t, applyTransactionOnDraw, shouldSetTaskPositionAndCrop, + relayout(taskInfo, t, t, applyTransactionOnDraw, shouldSetTaskVisibilityPositionAndCrop, hasGlobalFocus); if (!applyTransactionOnDraw) { t.apply(); @@ -430,19 +441,19 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin void relayout(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, - boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop, + boolean applyStartTransactionOnDraw, boolean shouldSetTaskVisibilityPositionAndCrop, boolean hasGlobalFocus) { Trace.beginSection("DesktopModeWindowDecoration#relayout"); if (taskInfo.isFreeform()) { // The Task is in Freeform mode -> show its header in sync since it's an integral part // of the window itself - a delayed header might cause bad UX. relayoutInSync(taskInfo, startT, finishT, applyStartTransactionOnDraw, - shouldSetTaskPositionAndCrop, hasGlobalFocus); + shouldSetTaskVisibilityPositionAndCrop, hasGlobalFocus); } else { // The Task is outside Freeform mode -> allow the handle view to be delayed since the // handle is just a small addition to the window. relayoutWithDelayedViewHost(taskInfo, startT, finishT, applyStartTransactionOnDraw, - shouldSetTaskPositionAndCrop, hasGlobalFocus); + shouldSetTaskVisibilityPositionAndCrop, hasGlobalFocus); } Trace.endSection(); } @@ -450,12 +461,12 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin /** Run the whole relayout phase immediately without delay. */ private void relayoutInSync(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, - boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop, + boolean applyStartTransactionOnDraw, boolean shouldSetTaskVisibilityPositionAndCrop, boolean hasGlobalFocus) { // Clear the current ViewHost runnable as we will update the ViewHost here clearCurrentViewHostRunnable(); updateRelayoutParamsAndSurfaces(taskInfo, startT, finishT, applyStartTransactionOnDraw, - shouldSetTaskPositionAndCrop, hasGlobalFocus); + shouldSetTaskVisibilityPositionAndCrop, hasGlobalFocus); if (mResult.mRootView != null) { updateViewHost(mRelayoutParams, startT, mResult); } @@ -477,7 +488,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin */ private void relayoutWithDelayedViewHost(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, - boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop, + boolean applyStartTransactionOnDraw, boolean shouldSetTaskVisibilityPositionAndCrop, boolean hasGlobalFocus) { if (applyStartTransactionOnDraw) { throw new IllegalArgumentException( @@ -486,7 +497,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin // Clear the current ViewHost runnable as we will update the ViewHost here clearCurrentViewHostRunnable(); updateRelayoutParamsAndSurfaces(taskInfo, startT, finishT, - false /* applyStartTransactionOnDraw */, shouldSetTaskPositionAndCrop, + false /* applyStartTransactionOnDraw */, shouldSetTaskVisibilityPositionAndCrop, hasGlobalFocus); if (mResult.mRootView == null) { // This means something blocks the window decor from showing, e.g. the task is hidden. @@ -501,10 +512,9 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin @SuppressLint("MissingPermission") private void updateRelayoutParamsAndSurfaces(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, - boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop, + boolean applyStartTransactionOnDraw, boolean shouldSetTaskVisibilityPositionAndCrop, boolean hasGlobalFocus) { Trace.beginSection("DesktopModeWindowDecoration#updateRelayoutParamsAndSurfaces"); - if (Flags.enableDesktopWindowingAppToWeb()) { setCapturedLink(taskInfo.capturedLink, taskInfo.capturedLinkTimestamp); } @@ -526,9 +536,9 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin final boolean inFullImmersive = mDesktopRepository .isTaskInFullImmersiveState(taskInfo.taskId); updateRelayoutParams(mRelayoutParams, mContext, taskInfo, applyStartTransactionOnDraw, - shouldSetTaskPositionAndCrop, mIsStatusBarVisible, mIsKeyguardVisibleAndOccluded, - inFullImmersive, mDisplayController.getInsetsState(taskInfo.displayId), - hasGlobalFocus); + shouldSetTaskVisibilityPositionAndCrop, mIsStatusBarVisible, + mIsKeyguardVisibleAndOccluded, inFullImmersive, + mDisplayController.getInsetsState(taskInfo.displayId), hasGlobalFocus); final WindowDecorLinearLayout oldRootView = mResult.mRootView; final SurfaceControl oldDecorationSurface = mDecorationContainerSurface; @@ -546,17 +556,17 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin if (mResult.mRootView == null) { // This means something blocks the window decor from showing, e.g. the task is hidden. // Nothing is set up in this case including the decoration surface. - if (canEnterDesktopMode(mContext) && Flags.enableDesktopWindowingAppHandleEducation()) { + if (canEnterDesktopMode(mContext) && isEducationEnabled()) { notifyNoCaptionHandle(); } mExclusionRegionListener.onExclusionRegionDismissed(mTaskInfo.taskId); - detachStatusBarInputLayer(); + disposeStatusBarInputLayer(); Trace.endSection(); // DesktopModeWindowDecoration#updateRelayoutParamsAndSurfaces return; } if (oldRootView != mResult.mRootView) { - detachStatusBarInputLayer(); + disposeStatusBarInputLayer(); mWindowDecorViewHolder = createViewHolder(); } Trace.beginSection("DesktopModeWindowDecoration#relayout-binding"); @@ -565,7 +575,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin if (isAppHandle(mWindowDecorViewHolder)) { position.set(determineHandlePosition()); } - if (canEnterDesktopMode(mContext) && Flags.enableDesktopWindowingAppHandleEducation()) { + if (canEnterDesktopMode(mContext) && isEducationEnabled()) { notifyCaptionStateChanged(); } @@ -574,9 +584,6 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mTaskInfo, position, mResult.mCaptionWidth, mResult.mCaptionHeight, isCaptionVisible() )); - if (mStatusBarInputLayer != null) { - asAppHandle(mWindowDecorViewHolder).bindStatusBarInputLayer(mStatusBarInputLayer); - } } else { mWindowDecorViewHolder.bindData(new AppHeaderViewHolder.HeaderData( mTaskInfo, @@ -708,7 +715,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin private void notifyCaptionStateChanged() { // TODO: b/366159408 - Ensure bounds sent with notification account for RTL mode. - if (!canEnterDesktopMode(mContext) || !Flags.enableDesktopWindowingAppHandleEducation()) { + if (!canEnterDesktopMode(mContext) || !isEducationEnabled()) { return; } if (!isCaptionVisible()) { @@ -717,7 +724,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin // App handle is visible since `mWindowDecorViewHolder` is of type // [AppHandleViewHolder]. final CaptionState captionState = new CaptionState.AppHandle(mTaskInfo, - isHandleMenuActive(), getCurrentAppHandleBounds()); + isHandleMenuActive(), getCurrentAppHandleBounds(), isCapturedLinkAvailable()); mWindowDecorCaptionHandleRepository.notifyCaptionChanged(captionState); } else { // App header is visible since `mWindowDecorViewHolder` is of type @@ -730,8 +737,12 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin } } + private boolean isCapturedLinkAvailable() { + return mCapturedLink != null && !mCapturedLink.mExpired; + } + private void notifyNoCaptionHandle() { - if (!canEnterDesktopMode(mContext) || !Flags.enableDesktopWindowingAppHandleEducation()) { + if (!canEnterDesktopMode(mContext) || !isEducationEnabled()) { return; } mWindowDecorCaptionHandleRepository.notifyCaptionChanged( @@ -758,7 +769,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin final CaptionState captionState = new CaptionState.AppHeader( mTaskInfo, isHandleMenuActive(), - appChipGlobalPosition); + appChipGlobalPosition, + isCapturedLinkAvailable()); mWindowDecorCaptionHandleRepository.notifyCaptionChanged(captionState); } @@ -790,15 +802,15 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin } /** - * Detach the status bar input layer from this decoration. Intended to be + * Dispose of the view used to forward inputs in status bar region. Intended to be * used any time handle is no longer visible. */ - void detachStatusBarInputLayer() { + void disposeStatusBarInputLayer() { if (!isAppHandle(mWindowDecorViewHolder) || !Flags.enableHandleInputFix()) { return; } - asAppHandle(mWindowDecorViewHolder).detachStatusBarInputLayer(); + asAppHandle(mWindowDecorViewHolder).disposeStatusBarInputLayer(); } private WindowDecorationViewHolder createViewHolder() { @@ -857,7 +869,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin Context context, ActivityManager.RunningTaskInfo taskInfo, boolean applyStartTransactionOnDraw, - boolean shouldSetTaskPositionAndCrop, + boolean shouldSetTaskVisibilityPositionAndCrop, boolean isStatusBarVisible, boolean isKeyguardVisibleAndOccluded, boolean inFullImmersiveMode, @@ -953,7 +965,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin : R.dimen.freeform_decor_shadow_unfocused_thickness; } relayoutParams.mApplyStartTransactionOnDraw = applyStartTransactionOnDraw; - relayoutParams.mSetTaskPositionAndCrop = shouldSetTaskPositionAndCrop; + relayoutParams.mSetTaskVisibilityPositionAndCrop = shouldSetTaskVisibilityPositionAndCrop; // The configuration used to layout the window decoration. A copy is made instead of using // the original reference so that the configuration isn't mutated on config changes and @@ -1094,13 +1106,13 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin if (mAppIconBitmap != null && mAppName != null) { return; } - final ComponentName baseActivity = mTaskInfo.baseActivity; - if (baseActivity == null) { - Slog.e(TAG, "Base activity component not found in task"); + if (mTaskInfo.baseIntent == null) { + Slog.e(TAG, "Base intent not found in task"); return; } final PackageManager pm = mUserContext.getPackageManager(); - final ActivityInfo activityInfo = pm.getActivityInfo(baseActivity, 0 /* flags */); + final ActivityInfo activityInfo = + pm.getActivityInfo(mTaskInfo.baseIntent.getComponent(), 0 /* flags */); final IconProvider provider = new IconProvider(mContext); final Drawable appIconDrawable = provider.getIcon(activityInfo); final Drawable badgedAppIconDrawable = pm.getUserBadgedIcon(appIconDrawable, @@ -1352,6 +1364,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin && Flags.enableDesktopWindowingMultiInstanceFeatures(); final boolean shouldShowManageWindowsButton = supportsMultiInstance && mMinimumInstancesFound; + final boolean shouldShowChangeAspectRatioButton = HandleMenu.Companion + .shouldShowChangeAspectRatioButton(mTaskInfo); final boolean inDesktopImmersive = mDesktopRepository .isTaskInFullImmersiveState(mTaskInfo.taskId); mHandleMenu = mHandleMenuFactory.create( @@ -1364,6 +1378,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin canEnterDesktopMode(mContext), supportsMultiInstance, shouldShowManageWindowsButton, + shouldShowChangeAspectRatioButton, getBrowserLink(), mResult.mCaptionWidth, mResult.mCaptionHeight, @@ -1384,9 +1399,13 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin /* onToSplitScreenClickListener= */ mOnToSplitscreenClickListener, /* onNewWindowClickListener= */ mOnNewWindowClickListener, /* onManageWindowsClickListener= */ mOnManageWindowsClickListener, + /* onAspectRatioSettingsClickListener= */ mOnChangeAspectRatioClickListener, /* openInBrowserClickListener= */ (intent) -> { mOpenInBrowserClickListener.accept(intent); onCapturedLinkExpired(); + if (Flags.enableDesktopWindowingAppToWebEducation()) { + mWindowDecorCaptionHandleRepository.onAppToWebUsage(); + } return Unit.INSTANCE; }, /* onOpenByDefaultClickListener= */ () -> { @@ -1405,7 +1424,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin }, /* forceShowSystemBars= */ inDesktopImmersive ); - if (canEnterDesktopMode(mContext) && Flags.enableDesktopWindowingAppHandleEducation()) { + if (canEnterDesktopMode(mContext) && isEducationEnabled()) { notifyCaptionStateChanged(); } mMinimumInstancesFound = false; @@ -1476,7 +1495,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mWindowDecorViewHolder.onHandleMenuClosed(); mHandleMenu.close(); mHandleMenu = null; - if (canEnterDesktopMode(mContext) && Flags.enableDesktopWindowingAppHandleEducation()) { + if (canEnterDesktopMode(mContext) && isEducationEnabled()) { notifyCaptionStateChanged(); } } @@ -1631,6 +1650,12 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin && v.getTop() <= y && v.getBottom() >= y; } + /** Returns true if at least one education flag is enabled. */ + private boolean isEducationEnabled() { + return Flags.enableDesktopWindowingAppHandleEducation() + || Flags.enableDesktopWindowingAppToWebEducation(); + } + @Override public void close() { closeDragResizeListener(); @@ -1638,9 +1663,9 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin closeManageWindowsMenu(); mExclusionRegionListener.onExclusionRegionDismissed(mTaskInfo.taskId); disposeResizeVeil(); - detachStatusBarInputLayer(); + disposeStatusBarInputLayer(); clearCurrentViewHostRunnable(); - if (canEnterDesktopMode(mContext) && Flags.enableDesktopWindowingAppHandleEducation()) { + if (canEnterDesktopMode(mContext) && isEducationEnabled()) { notifyNoCaptionHandle(); } super.close(); @@ -1755,16 +1780,6 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin + "}"; } - /** - * Set the view container to be used to forward input through status bar. Null in cases - * where input forwarding isn't needed. - */ - public void setStatusBarInputLayer( - @Nullable AdditionalSystemViewContainer additionalSystemViewContainer - ) { - mStatusBarInputLayer = additionalSystemViewContainer; - } - static class Factory { DesktopModeWindowDecoration create( diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopStatusBarInputLayerSupplier.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopStatusBarInputLayerSupplier.kt deleted file mode 100644 index 9c5215d1249c..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopStatusBarInputLayerSupplier.kt +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.wm.shell.windowdecor - -import android.app.ActivityManager.RunningTaskInfo -import android.app.WindowConfiguration -import android.content.Context -import android.graphics.PixelFormat -import android.os.Handler -import android.view.Gravity -import android.view.View -import android.view.WindowManager -import com.android.wm.shell.shared.annotations.ShellMainThread -import com.android.wm.shell.shared.split.SplitScreenConstants -import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer - -/** - * Supplier for [AdditionalSystemViewContainer] objects to be used for forwarding input - * events through status bar to an app handle. Currently supports two simultaneous input layers. - * - * The supplier will pick one of two input layer view containers to use: one for tasks in - * fullscreen or top/left split stage, and one for tasks in right split stage. - */ -class DesktopStatusBarInputLayerSupplier( - private val context: Context, - @ShellMainThread handler: Handler -) { - private val inputLayers: MutableList<AdditionalSystemViewContainer> = mutableListOf() - - init { - // Post this as creation of the input layer views is a relatively expensive operation. - handler.post { - repeat(TOTAL_INPUT_LAYERS) { - inputLayers.add(createInputLayer()) - } - } - } - - private fun createInputLayer(): AdditionalSystemViewContainer { - val lp = WindowManager.LayoutParams( - WindowManager.LayoutParams.TYPE_STATUS_BAR_ADDITIONAL, - WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, - PixelFormat.TRANSPARENT - ) - lp.title = "Desktop status bar input layer" - lp.gravity = Gravity.LEFT or Gravity.TOP - lp.setTrustedOverlay() - - // Make this window a spy window to enable it to pilfer pointers from the system-wide - // gesture listener that receives events before window. This is to prevent notification - // shade gesture when we swipe down to enter desktop. - lp.inputFeatures = WindowManager.LayoutParams.INPUT_FEATURE_SPY - lp.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE - val view = View(context) - view.visibility = View.GONE - return AdditionalSystemViewContainer( - WindowManagerWrapper( - context.getSystemService<WindowManager>(WindowManager::class.java) - ), - view, - lp - ) - } - - /** - * Decide which cached status bar input layer should be used for a decoration, if any. - * - * [splitPosition] and [isLeftRightSplit] are used to determine which input layer we use. - * The first one is reserved for fullscreen tasks or tasks in top/left split, - * while the second one is exclusively used for tasks in right split stage. Note we care about - * left-right vs top-bottom split as the bottom stage should not use an input layer. - */ - fun getStatusBarInputLayer( - taskInfo: RunningTaskInfo, - @SplitScreenConstants.SplitPosition splitPosition: Int, - isLeftRightSplit: Boolean - ): AdditionalSystemViewContainer? { - if (!taskInfo.isVisibleRequested) return null - // Fullscreen and top/left split tasks will use the first input layer. - if (taskInfo.windowingMode == WindowConfiguration.WINDOWING_MODE_FULLSCREEN - || splitPosition == SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT - ) { - return inputLayers[LEFT_TOP_INPUT_LAYER] - } - // Right split tasks will use the second one. - if (isLeftRightSplit && splitPosition == SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT - ) { - return inputLayers[RIGHT_SPLIT_INPUT_LAYER] - } - // Which leaves bottom split and freeform tasks, which do not need an input layer - // as the status bar is not blocking them. - return null - } - - companion object { - private const val TOTAL_INPUT_LAYERS = 2 - // Input layer index for fullscreen tasks and tasks in top-left split - private const val LEFT_TOP_INPUT_LAYER = 0 - // Input layer index for tasks in right split stage. Does not include bottom split as that - // stage is not blocked by status bar. - private const val RIGHT_SPLIT_INPUT_LAYER = 1 - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt index 93bd9290dfeb..2edc380756ac 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt @@ -19,6 +19,7 @@ import android.annotation.ColorInt import android.annotation.DimenRes import android.annotation.SuppressLint import android.app.ActivityManager.RunningTaskInfo +import android.app.WindowConfiguration import android.content.Context import android.content.Intent import android.content.res.ColorStateList @@ -71,6 +72,7 @@ class HandleMenu( private val shouldShowWindowingPill: Boolean, private val shouldShowNewWindowButton: Boolean, private val shouldShowManageWindowsButton: Boolean, + private val shouldShowChangeAspectRatioButton: Boolean, private val openInBrowserIntent: Intent?, private val captionWidth: Int, private val captionHeight: Int, @@ -111,6 +113,10 @@ class HandleMenu( private val shouldShowBrowserPill: Boolean get() = openInBrowserIntent != null + private val shouldShowMoreActionsPill: Boolean + get() = SHOULD_SHOW_SCREENSHOT_BUTTON || shouldShowNewWindowButton || + shouldShowManageWindowsButton || shouldShowChangeAspectRatioButton + init { updateHandleMenuPillPositions(captionX, captionY) } @@ -121,6 +127,7 @@ class HandleMenu( onToSplitScreenClickListener: () -> Unit, onNewWindowClickListener: () -> Unit, onManageWindowsClickListener: () -> Unit, + onChangeAspectRatioClickListener: () -> Unit, openInBrowserClickListener: (Intent) -> Unit, onOpenByDefaultClickListener: () -> Unit, onCloseMenuClickListener: () -> Unit, @@ -138,6 +145,7 @@ class HandleMenu( onToSplitScreenClickListener = onToSplitScreenClickListener, onNewWindowClickListener = onNewWindowClickListener, onManageWindowsClickListener = onManageWindowsClickListener, + onChangeAspectRatioClickListener = onChangeAspectRatioClickListener, openInBrowserClickListener = openInBrowserClickListener, onOpenByDefaultClickListener = onOpenByDefaultClickListener, onCloseMenuClickListener = onCloseMenuClickListener, @@ -158,6 +166,7 @@ class HandleMenu( onToSplitScreenClickListener: () -> Unit, onNewWindowClickListener: () -> Unit, onManageWindowsClickListener: () -> Unit, + onChangeAspectRatioClickListener: () -> Unit, openInBrowserClickListener: (Intent) -> Unit, onOpenByDefaultClickListener: () -> Unit, onCloseMenuClickListener: () -> Unit, @@ -171,14 +180,16 @@ class HandleMenu( shouldShowWindowingPill = shouldShowWindowingPill, shouldShowBrowserPill = shouldShowBrowserPill, shouldShowNewWindowButton = shouldShowNewWindowButton, - shouldShowManageWindowsButton = shouldShowManageWindowsButton + shouldShowManageWindowsButton = shouldShowManageWindowsButton, + shouldShowChangeAspectRatioButton = shouldShowChangeAspectRatioButton ).apply { - bind(taskInfo, appIconBitmap, appName) + bind(taskInfo, appIconBitmap, appName, shouldShowMoreActionsPill) this.onToDesktopClickListener = onToDesktopClickListener this.onToFullscreenClickListener = onToFullscreenClickListener this.onToSplitScreenClickListener = onToSplitScreenClickListener this.onNewWindowClickListener = onNewWindowClickListener this.onManageWindowsClickListener = onManageWindowsClickListener + this.onChangeAspectRatioClickListener = onChangeAspectRatioClickListener this.onOpenInBrowserClickListener = { openInBrowserClickListener.invoke(openInBrowserIntent!!) } @@ -392,8 +403,11 @@ class HandleMenu( R.dimen.desktop_mode_handle_menu_manage_windows_height ) } - if (!SHOULD_SHOW_SCREENSHOT_BUTTON && !shouldShowNewWindowButton - && !shouldShowManageWindowsButton) { + if (!shouldShowChangeAspectRatioButton) { + menuHeight -= loadDimensionPixelSize( + R.dimen.desktop_mode_handle_menu_change_aspect_ratio_height) + } + if (!shouldShowMoreActionsPill) { menuHeight -= pillTopMargin } if (!shouldShowBrowserPill) { @@ -427,7 +441,8 @@ class HandleMenu( private val shouldShowWindowingPill: Boolean, private val shouldShowBrowserPill: Boolean, private val shouldShowNewWindowButton: Boolean, - private val shouldShowManageWindowsButton: Boolean + private val shouldShowManageWindowsButton: Boolean, + private val shouldShowChangeAspectRatioButton: Boolean ) { val rootView = LayoutInflater.from(context) .inflate(R.layout.desktop_mode_window_decor_handle_menu, null /* root */) as View @@ -454,6 +469,8 @@ class HandleMenu( private val newWindowBtn = moreActionsPill.requireViewById<Button>(R.id.new_window_button) private val manageWindowBtn = moreActionsPill .requireViewById<Button>(R.id.manage_windows_button) + private val changeAspectRatioBtn = moreActionsPill + .requireViewById<Button>(R.id.change_aspect_ratio_button) // Open in Browser Pill. private val openInBrowserPill = rootView.requireViewById<View>(R.id.open_in_browser_pill) @@ -472,6 +489,7 @@ class HandleMenu( var onToSplitScreenClickListener: (() -> Unit)? = null var onNewWindowClickListener: (() -> Unit)? = null var onManageWindowsClickListener: (() -> Unit)? = null + var onChangeAspectRatioClickListener: (() -> Unit)? = null var onOpenInBrowserClickListener: (() -> Unit)? = null var onOpenByDefaultClickListener: (() -> Unit)? = null var onCloseMenuClickListener: (() -> Unit)? = null @@ -488,6 +506,7 @@ class HandleMenu( collapseMenuButton.setOnClickListener { onCloseMenuClickListener?.invoke() } newWindowBtn.setOnClickListener { onNewWindowClickListener?.invoke() } manageWindowBtn.setOnClickListener { onManageWindowsClickListener?.invoke() } + changeAspectRatioBtn.setOnClickListener { onChangeAspectRatioClickListener?.invoke() } rootView.setOnTouchListener { _, event -> if (event.actionMasked == ACTION_OUTSIDE) { @@ -499,7 +518,12 @@ class HandleMenu( } /** Binds the menu views to the new data. */ - fun bind(taskInfo: RunningTaskInfo, appIconBitmap: Bitmap?, appName: CharSequence?) { + fun bind( + taskInfo: RunningTaskInfo, + appIconBitmap: Bitmap?, + appName: CharSequence?, + shouldShowMoreActionsPill: Boolean + ) { this.taskInfo = taskInfo this.style = calculateMenuStyle(taskInfo) @@ -507,7 +531,10 @@ class HandleMenu( if (shouldShowWindowingPill) { bindWindowingPill(style) } - bindMoreActionsPill(style) + moreActionsPill.isGone = !shouldShowMoreActionsPill + if (shouldShowMoreActionsPill) { + bindMoreActionsPill(style) + } bindOpenInBrowserPill(style) } @@ -616,27 +643,20 @@ class HandleMenu( } private fun bindMoreActionsPill(style: MenuStyle) { - moreActionsPill.apply { - isGone = !shouldShowNewWindowButton && !SHOULD_SHOW_SCREENSHOT_BUTTON - && !shouldShowManageWindowsButton - } - screenshotBtn.apply { - isGone = !SHOULD_SHOW_SCREENSHOT_BUTTON - background.setTint(style.backgroundColor) - setTextColor(style.textColor) - compoundDrawableTintList = ColorStateList.valueOf(style.textColor) - } - newWindowBtn.apply { - isGone = !shouldShowNewWindowButton - background.setTint(style.backgroundColor) - setTextColor(style.textColor) - compoundDrawableTintList = ColorStateList.valueOf(style.textColor) - } - manageWindowBtn.apply { - isGone = !shouldShowManageWindowsButton - background.setTint(style.backgroundColor) - setTextColor(style.textColor) - compoundDrawableTintList = ColorStateList.valueOf(style.textColor) + arrayOf( + screenshotBtn to SHOULD_SHOW_SCREENSHOT_BUTTON, + newWindowBtn to shouldShowNewWindowButton, + manageWindowBtn to shouldShowManageWindowsButton, + changeAspectRatioBtn to shouldShowChangeAspectRatioButton, + ).forEach { + val button = it.first + val shouldShow = it.second + button.apply { + isGone = !shouldShow + background.setTint(style.backgroundColor) + setTextColor(style.textColor) + compoundDrawableTintList = ColorStateList.valueOf(style.textColor) + } } } @@ -664,6 +684,14 @@ class HandleMenu( companion object { private const val TAG = "HandleMenu" private const val SHOULD_SHOW_SCREENSHOT_BUTTON = false + + /** + * Returns whether the aspect ratio button should be shown for the task. It usually means + * that the task is on a large screen with ignore-orientation-request. + */ + fun shouldShowChangeAspectRatioButton(taskInfo: RunningTaskInfo): Boolean = + taskInfo.appCompatTaskInfo.eligibleForUserAspectRatioButton() && + taskInfo.windowingMode == WindowConfiguration.WINDOWING_MODE_FULLSCREEN } } @@ -679,6 +707,7 @@ interface HandleMenuFactory { shouldShowWindowingPill: Boolean, shouldShowNewWindowButton: Boolean, shouldShowManageWindowsButton: Boolean, + shouldShowChangeAspectRatioButton: Boolean, openInBrowserIntent: Intent?, captionWidth: Int, captionHeight: Int, @@ -699,6 +728,7 @@ object DefaultHandleMenuFactory : HandleMenuFactory { shouldShowWindowingPill: Boolean, shouldShowNewWindowButton: Boolean, shouldShowManageWindowsButton: Boolean, + shouldShowChangeAspectRatioButton: Boolean, openInBrowserIntent: Intent?, captionWidth: Int, captionHeight: Int, @@ -715,6 +745,7 @@ object DefaultHandleMenuFactory : HandleMenuFactory { shouldShowWindowingPill, shouldShowNewWindowButton, shouldShowManageWindowsButton, + shouldShowChangeAspectRatioButton, openInBrowserIntent, captionWidth, captionHeight, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java index f97dfb89bc0d..b016c755e323 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java @@ -249,7 +249,9 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> if (!mTaskInfo.isVisible) { releaseViews(wct); - finishT.hide(mTaskSurface); + if (params.mSetTaskVisibilityPositionAndCrop) { + finishT.hide(mTaskSurface); + } return; } @@ -422,7 +424,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> private void updateTaskSurface(RelayoutParams params, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, RelayoutResult<T> outResult) { - if (params.mSetTaskPositionAndCrop) { + if (params.mSetTaskVisibilityPositionAndCrop) { final Point taskPosition = mTaskInfo.positionInParent; startT.setWindowCrop(mTaskSurface, outResult.mWidth, outResult.mHeight); finishT.setWindowCrop(mTaskSurface, outResult.mWidth, outResult.mHeight) @@ -437,9 +439,13 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> shadowRadius = loadDimension(mDecorWindowContext.getResources(), params.mShadowRadiusId); } - startT.setShadowRadius(mTaskSurface, shadowRadius).show(mTaskSurface); + startT.setShadowRadius(mTaskSurface, shadowRadius); finishT.setShadowRadius(mTaskSurface, shadowRadius); + if (params.mSetTaskVisibilityPositionAndCrop) { + startT.show(mTaskSurface); + } + if (mTaskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) { if (!DesktopModeStatus.isVeiledResizeEnabled()) { // When fluid resize is enabled, add a background to freeform tasks @@ -758,7 +764,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> Configuration mWindowDecorConfig; boolean mApplyStartTransactionOnDraw; - boolean mSetTaskPositionAndCrop; + boolean mSetTaskVisibilityPositionAndCrop; boolean mHasGlobalFocus; void reset() { @@ -777,7 +783,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> mIsCaptionVisible = false; mApplyStartTransactionOnDraw = false; - mSetTaskPositionAndCrop = false; + mSetTaskVisibilityPositionAndCrop = false; mWindowDecorConfig = null; mHasGlobalFocus = false; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt index 1451f363ec73..8b6aaaf619e0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt @@ -23,8 +23,8 @@ import android.view.Gravity import android.view.LayoutInflater import android.view.SurfaceControl import android.view.View +import android.view.WindowInsets import android.view.WindowManager -import android.view.WindowManager.LayoutParams import com.android.wm.shell.windowdecor.WindowManagerWrapper /** @@ -33,11 +33,27 @@ import com.android.wm.shell.windowdecor.WindowManagerWrapper */ class AdditionalSystemViewContainer( private val windowManagerWrapper: WindowManagerWrapper, - override val view: View, - val lp: LayoutParams + taskId: Int, + x: Int, + y: Int, + width: Int, + height: Int, + flags: Int, + @WindowInsets.Type.InsetsType forciblyShownTypes: Int = 0, + override val view: View ) : AdditionalViewContainer() { + val lp: WindowManager.LayoutParams = WindowManager.LayoutParams( + width, height, x, y, + WindowManager.LayoutParams.TYPE_STATUS_BAR_ADDITIONAL, + flags, + PixelFormat.TRANSPARENT + ).apply { + title = "Additional view container of Task=$taskId" + gravity = Gravity.LEFT or Gravity.TOP + setTrustedOverlay() + this.forciblyShownTypes = forciblyShownTypes + } - /** Provide a layout id of a view to inflate for this view container. */ constructor( context: Context, windowManagerWrapper: WindowManagerWrapper, @@ -50,30 +66,15 @@ class AdditionalSystemViewContainer( @LayoutRes layoutId: Int ) : this( windowManagerWrapper = windowManagerWrapper, - view = LayoutInflater.from(context).inflate(layoutId, null /* parent */), - lp = createLayoutParams(x, y, width, height, flags, taskId) + taskId = taskId, + x = x, + y = y, + width = width, + height = height, + flags = flags, + view = LayoutInflater.from(context).inflate(layoutId, null /* parent */) ) - /** Provide a view directly for this view container */ - constructor( - windowManagerWrapper: WindowManagerWrapper, - taskId: Int, - x: Int, - y: Int, - width: Int, - height: Int, - flags: Int, - view: View, - forciblyShownTypes: Int = 0 - ) : this( - windowManagerWrapper = windowManagerWrapper, - view = view, - lp = createLayoutParams(x, y, width, height, flags, taskId).apply { - this.forciblyShownTypes = forciblyShownTypes - } - ) - - /** Do not supply a view at all, instead creating the view container with a basic view. */ constructor( context: Context, windowManagerWrapper: WindowManagerWrapper, @@ -85,7 +86,12 @@ class AdditionalSystemViewContainer( flags: Int ) : this( windowManagerWrapper = windowManagerWrapper, - lp = createLayoutParams(x, y, width, height, flags, taskId), + taskId = taskId, + x = x, + y = y, + width = width, + height = height, + flags = flags, view = View(context) ) @@ -98,7 +104,7 @@ class AdditionalSystemViewContainer( } override fun setPosition(t: SurfaceControl.Transaction, x: Float, y: Float) { - lp.apply { + val lp = (view.layoutParams as WindowManager.LayoutParams).apply { this.x = x.toInt() this.y = y.toInt() } @@ -118,29 +124,13 @@ class AdditionalSystemViewContainer( ): AdditionalSystemViewContainer = AdditionalSystemViewContainer( windowManagerWrapper = windowManagerWrapper, - view = view, - lp = createLayoutParams(x, y, width, height, flags, taskId) + taskId = taskId, + x = x, + y = y, + width = width, + height = height, + flags = flags, + view = view ) } - companion object { - fun createLayoutParams( - x: Int, - y: Int, - width: Int, - height: Int, - flags: Int, - taskId: Int - ): LayoutParams { - return LayoutParams( - width, height, x, y, - LayoutParams.TYPE_STATUS_BAR_ADDITIONAL, - flags, - PixelFormat.TRANSPARENT - ).apply { - title = "Additional view container of Task=$taskId" - gravity = Gravity.LEFT or Gravity.TOP - setTrustedOverlay() - } - } - } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationPromoController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationPromoController.kt new file mode 100644 index 000000000000..b3489a4b4348 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationPromoController.kt @@ -0,0 +1,231 @@ +/* + * 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.windowdecor.education + +import android.annotation.ColorInt +import android.annotation.DimenRes +import android.annotation.LayoutRes +import android.content.Context +import android.content.res.Resources +import android.graphics.Point +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View +import android.view.WindowManager +import android.widget.LinearLayout +import android.widget.TextView +import android.window.DisplayAreaInfo +import android.window.WindowContainerTransaction +import androidx.dynamicanimation.animation.DynamicAnimation +import androidx.dynamicanimation.animation.SpringForce +import com.android.wm.shell.R +import com.android.wm.shell.common.DisplayChangeController.OnDisplayChangingListener +import com.android.wm.shell.common.DisplayController +import com.android.wm.shell.shared.animation.PhysicsAnimator +import com.android.wm.shell.windowdecor.WindowManagerWrapper +import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer + +/** + * Controls the lifecycle of an education promo, including showing and hiding it. + */ +class DesktopWindowingEducationPromoController( + private val context: Context, + private val additionalSystemViewContainerFactory: AdditionalSystemViewContainer.Factory, + private val displayController: DisplayController, +) : OnDisplayChangingListener { + private var educationView: View? = null + private var animator: PhysicsAnimator<View>? = null + private val springConfig by lazy { + PhysicsAnimator.SpringConfig( + SpringForce.STIFFNESS_MEDIUM, + SpringForce.DAMPING_RATIO_LOW_BOUNCY + ) + } + private var popupWindow: AdditionalSystemViewContainer? = null + + override fun onDisplayChange( + displayId: Int, + fromRotation: Int, + toRotation: Int, + newDisplayAreaInfo: DisplayAreaInfo?, + t: WindowContainerTransaction? + ) { + // Exit if the rotation hasn't changed or is changed by 180 degrees. [fromRotation] and + // [toRotation] can be one of the [@Surface.Rotation] values. + if ((fromRotation % 2 == toRotation % 2)) return + hideEducation() + } + + /** + * Shows education promo. + * + * @param viewConfig features of the education. + * @param taskId is used in the title of popup window created for the education view. + */ + fun showEducation( + viewConfig: EducationViewConfig, + taskId: Int + ) { + hideEducation() + educationView = createEducationView(viewConfig, taskId) + animator = createAnimator() + animateShowEducationTransition() + displayController.addDisplayChangingController(this) + } + + /** Hide the current education view if visible */ + private fun hideEducation() = animateHideEducationTransition { cleanUp() } + + /** Create education view by inflating layout provided. */ + private fun createEducationView( + viewConfig: EducationViewConfig, + taskId: Int + ): View { + val educationView = + LayoutInflater.from(context) + .inflate( + viewConfig.viewLayout, /* root= */ null, /* attachToRoot= */ false) + .apply { + alpha = 0f + scaleX = 0f + scaleY = 0f + + requireViewById<TextView>(R.id.education_text).apply { + text = viewConfig.educationText + } + setOnTouchListener { _, motionEvent -> + if (motionEvent.action == MotionEvent.ACTION_OUTSIDE) { + hideEducation() + true + } else { + false + } + } + setOnClickListener { + hideEducation() + } + setEducationColorScheme(viewConfig.educationColorScheme) + } + + createEducationPopupWindow( + taskId, + viewConfig.viewGlobalCoordinates, + loadDimensionPixelSize(viewConfig.widthId), + loadDimensionPixelSize(viewConfig.heightId), + educationView = educationView) + + return educationView + } + + /** Create animator for education transitions */ + private fun createAnimator(): PhysicsAnimator<View>? = + educationView?.let { + PhysicsAnimator.getInstance(it).apply { setDefaultSpringConfig(springConfig) } + } + + /** Animate show transition for the education view */ + private fun animateShowEducationTransition() { + animator + ?.spring(DynamicAnimation.ALPHA, 1f) + ?.spring(DynamicAnimation.SCALE_X, 1f) + ?.spring(DynamicAnimation.SCALE_Y, 1f) + ?.start() + } + + /** Animate hide transition for the education view */ + private fun animateHideEducationTransition(endActions: () -> Unit) { + animator + ?.spring(DynamicAnimation.ALPHA, 0f) + ?.spring(DynamicAnimation.SCALE_X, 0f) + ?.spring(DynamicAnimation.SCALE_Y, 0f) + ?.start() + endActions() + } + + /** Remove education promo and clean up all relative properties */ + private fun cleanUp() { + educationView = null + animator = null + popupWindow?.releaseView() + popupWindow = null + displayController.removeDisplayChangingController(this) + } + + private fun createEducationPopupWindow( + taskId: Int, + educationViewGlobalCoordinates: Point, + width: Int, + height: Int, + educationView: View, + ) { + popupWindow = + additionalSystemViewContainerFactory.create( + windowManagerWrapper = + WindowManagerWrapper(context.getSystemService(WindowManager::class.java)), + taskId = taskId, + x = educationViewGlobalCoordinates.x, + y = educationViewGlobalCoordinates.y, + width = width, + height = height, + flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or + WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH, + view = educationView) + } + + private fun View.setEducationColorScheme(educationColorScheme: EducationColorScheme) { + requireViewById<LinearLayout>(R.id.education_container).apply { + background.setTint(educationColorScheme.container) + } + requireViewById<TextView>(R.id.education_text).apply { + setTextColor(educationColorScheme.text) + } + } + + private fun loadDimensionPixelSize(@DimenRes resourceId: Int): Int { + if (resourceId == Resources.ID_NULL) return 0 + return context.resources.getDimensionPixelSize(resourceId) + } + + /** + * The configuration for education view features: + * + * @property viewLayout Layout resource ID of the view to be used for education promo. + * @property viewGlobalCoordinates Global (screen) coordinates of the education. + * @property educationText Text to be added to the TextView of the promo. + * @property widthId res Id for education width + * @property heightId res Id for education height + */ + data class EducationViewConfig( + @LayoutRes val viewLayout: Int, + val educationColorScheme: EducationColorScheme, + val viewGlobalCoordinates: Point, + val educationText: String, + @DimenRes val widthId: Int, + @DimenRes val heightId: Int + ) + + /** + * Color scheme of education view: + * + * @property container Color of the container of the education. + * @property text Text color of the [TextView] of education promo. + */ + data class EducationColorScheme( + @ColorInt val container: Int, + @ColorInt val text: Int, + ) +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipController.kt index c61b31e7ba01..4fa2744b4c12 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipController.kt @@ -80,7 +80,7 @@ class DesktopWindowingEducationTooltipController( * @param tooltipViewConfig features of tooltip. * @param taskId is used in the title of popup window created for the tooltip view. */ - fun showEducationTooltip(tooltipViewConfig: EducationViewConfig, taskId: Int) { + fun showEducationTooltip(tooltipViewConfig: TooltipEducationViewConfig, taskId: Int) { hideEducationTooltip() tooltipView = createEducationTooltipView(tooltipViewConfig, taskId) animator = createAnimator() @@ -93,7 +93,7 @@ class DesktopWindowingEducationTooltipController( /** Create education view by inflating layout provided. */ private fun createEducationTooltipView( - tooltipViewConfig: EducationViewConfig, + tooltipViewConfig: TooltipEducationViewConfig, taskId: Int, ): View { val tooltipView = @@ -271,7 +271,7 @@ class DesktopWindowingEducationTooltipController( * @property onEducationClickAction Lambda to be executed when the tooltip is clicked. * @property onDismissAction Lambda to be executed when the tooltip is dismissed. */ - data class EducationViewConfig( + data class TooltipEducationViewConfig( @LayoutRes val tooltipViewLayout: Int, val tooltipColorScheme: TooltipColorScheme, val tooltipViewGlobalCoordinates: Point, @@ -299,4 +299,4 @@ class DesktopWindowingEducationTooltipController( UP, LEFT, } -} +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModel.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModel.kt index ff418c6daa02..e43c3a613157 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModel.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModel.kt @@ -21,10 +21,13 @@ import android.app.ActivityManager.RunningTaskInfo import android.content.Context import android.graphics.Rect import android.util.SparseArray +import android.window.DisplayAreaInfo +import android.window.WindowContainerTransaction import androidx.core.util.valueIterator import com.android.internal.annotations.VisibleForTesting import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.common.DisplayChangeController import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.SyncTransactionQueue import com.android.wm.shell.desktopmode.DesktopRepository @@ -45,10 +48,16 @@ class DesktopTilingDecorViewModel( private val toggleResizeDesktopTaskTransitionHandler: ToggleResizeDesktopTaskTransitionHandler, private val returnToDragStartAnimator: ReturnToDragStartAnimator, private val taskRepository: DesktopRepository, -) { +) : DisplayChangeController.OnDisplayChangingListener { @VisibleForTesting var tilingTransitionHandlerByDisplayId = SparseArray<DesktopTilingWindowDecoration>() + init { + // TODO(b/374309287): Move this interface implementation to + // [DesktopModeWindowDecorViewModel] when the migration is done. + displayController.addDisplayChangingController(this) + } + fun snapToHalfScreen( taskInfo: ActivityManager.RunningTaskInfo, desktopModeWindowDecoration: DesktopModeWindowDecoration, @@ -102,7 +111,20 @@ class DesktopTilingDecorViewModel( fun onUserChange() { for (tilingHandler in tilingTransitionHandlerByDisplayId.valueIterator()) { - tilingHandler.onUserChange() + tilingHandler.resetTilingSession() } } + + override fun onDisplayChange( + displayId: Int, + fromRotation: Int, + toRotation: Int, + newDisplayAreaInfo: DisplayAreaInfo?, + t: WindowContainerTransaction?, + ) { + // Exit if the rotation hasn't changed or is changed by 180 degrees. [fromRotation] and + // [toRotation] can be one of the [@Surface.Rotation] values. + if ((fromRotation % 2 == toRotation % 2)) return + tilingTransitionHandlerByDisplayId.get(displayId)?.resetTilingSession() + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManager.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManager.kt index 9bf1304f2b39..209eb5e501b2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManager.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManager.kt @@ -23,6 +23,7 @@ import android.graphics.Rect import android.graphics.Region import android.os.Binder import android.view.LayoutInflater +import android.view.RoundedCorner import android.view.SurfaceControl import android.view.SurfaceControlViewHost import android.view.View @@ -53,12 +54,14 @@ class DesktopTilingDividerWindowManager( private val transitionHandler: DesktopTilingWindowDecoration, private val transactionSupplier: Supplier<SurfaceControl.Transaction>, private var dividerBounds: Rect, + private val displayContext: Context, ) : WindowlessWindowManager(config, leash, null), DividerMoveCallback, View.OnLayoutChangeListener { private lateinit var viewHost: SurfaceControlViewHost private var tilingDividerView: TilingDividerView? = null private var dividerShown = false private var handleRegionWidth: Int = -1 private var setTouchRegion = true + private val maxRoundedCornerRadius = getMaxRoundedCornerRadius() /** * Gets bounds of divider window with screen based coordinate on the param Rect. @@ -93,7 +96,11 @@ class DesktopTilingDividerWindowManager( getDividerBounds(tmpDividerBounds) dividerView.setup(this, tmpDividerBounds) t.setRelativeLayer(leash, relativeLeash, 1) - .setPosition(leash, dividerBounds.left.toFloat(), dividerBounds.top.toFloat()) + .setPosition( + leash, + dividerBounds.left.toFloat() - maxRoundedCornerRadius, + dividerBounds.top.toFloat(), + ) .show(leash) syncQueue.runInSync { transaction -> transaction.merge(t) @@ -144,7 +151,7 @@ class DesktopTilingDividerWindowManager( */ override fun onDividerMove(pos: Int): Boolean { val t = transactionSupplier.get() - t.setPosition(leash, pos.toFloat(), dividerBounds.top.toFloat()) + t.setPosition(leash, pos.toFloat() - maxRoundedCornerRadius, dividerBounds.top.toFloat()) val dividerWidth = dividerBounds.width() dividerBounds.set(pos, dividerBounds.top, pos + dividerWidth, dividerBounds.bottom) return transitionHandler.onDividerHandleMoved(dividerBounds, t) @@ -157,7 +164,7 @@ class DesktopTilingDividerWindowManager( override fun onDividerMovedEnd(pos: Int) { setSlippery(true) val t = transactionSupplier.get() - t.setPosition(leash, pos.toFloat(), dividerBounds.top.toFloat()) + t.setPosition(leash, pos.toFloat() - maxRoundedCornerRadius, dividerBounds.top.toFloat()) val dividerWidth = dividerBounds.width() dividerBounds.set(pos, dividerBounds.top, pos + dividerWidth, dividerBounds.bottom) transitionHandler.onDividerHandleDragEnd(dividerBounds, t) @@ -166,7 +173,7 @@ class DesktopTilingDividerWindowManager( private fun getWindowManagerParams(): WindowManager.LayoutParams { val lp = WindowManager.LayoutParams( - dividerBounds.width(), + dividerBounds.width() + 2 * maxRoundedCornerRadius, dividerBounds.height(), TYPE_DOCK_DIVIDER, FLAG_NOT_FOCUSABLE or @@ -225,4 +232,15 @@ class DesktopTilingDividerWindowManager( } viewHost.relayout(lp) } + + private fun getMaxRoundedCornerRadius(): Int { + val display = displayContext.display + return listOf( + RoundedCorner.POSITION_TOP_LEFT, + RoundedCorner.POSITION_TOP_RIGHT, + RoundedCorner.POSITION_BOTTOM_RIGHT, + RoundedCorner.POSITION_BOTTOM_LEFT, + ) + .maxOf { position -> display.getRoundedCorner(position)?.getRadius() ?: 0 } + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt index 8f089dcf62d8..c46767c3a51d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt @@ -23,6 +23,7 @@ import android.content.res.Resources import android.graphics.Bitmap import android.graphics.Rect import android.os.IBinder +import android.os.UserHandle import android.util.Slog import android.view.SurfaceControl import android.view.SurfaceControl.Transaction @@ -126,7 +127,6 @@ class DesktopTilingWindowDecoration( resizeMetadata.getLeash(), startBounds = currentBounds, endBounds = destinationBounds, - isResizable = taskInfo.isResizeable, ) } } @@ -195,6 +195,7 @@ class DesktopTilingWindowDecoration( val builder = SurfaceControl.Builder() rootTdaOrganizer.attachToDisplayArea(displayId, builder) val leash = builder.setName(TILING_DIVIDER_TAG).setContainerLayer().build() + val displayContext = displayController.getDisplayContext(displayId) ?: return null val tilingManager = displayLayout?.let { dividerBounds = inflateDividerBounds(it) @@ -207,6 +208,7 @@ class DesktopTilingWindowDecoration( this, transactionSupplier, dividerBounds, + displayContext, ) } // a leash to present the divider on top of, without re-parenting. @@ -361,6 +363,8 @@ class DesktopTilingWindowDecoration( private lateinit var resizeVeilBitmap: Bitmap private lateinit var resizeVeil: ResizeVeil private val displayContext = displayController.getDisplayContext(taskInfo.displayId) + private val userContext = + context.createContextAsUser(UserHandle.of(taskInfo.userId), /* flags= */ 0) fun initIfNeeded() { if (!isInitialised) { @@ -379,7 +383,7 @@ class DesktopTilingWindowDecoration( displayContext?.let { createIconFactory(displayContext, R.dimen.desktop_mode_resize_veil_icon_size) } ?: return - val pm = context.getApplicationContext().getPackageManager() + val pm = userContext.getPackageManager() val activityInfo = pm.getActivityInfo(baseActivity, 0 /* flags */) val provider = IconProvider(displayContext) val appIconDrawable = provider.getIcon(activityInfo) @@ -481,7 +485,7 @@ class DesktopTilingWindowDecoration( } } - fun onUserChange() { + fun resetTilingSession() { if (leftTaskResizingHelper != null) { removeTask(leftTaskResizingHelper, taskVanished = false, shouldDelayUpdate = true) leftTaskResizingHelper = null diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/TilingDividerView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/TilingDividerView.kt index f8113c219bd1..89229051941c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/TilingDividerView.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/TilingDividerView.kt @@ -118,7 +118,7 @@ class TilingDividerView : FrameLayout, View.OnTouchListener, DragDetector.Motion val dividerSize = resources.getDimensionPixelSize(R.dimen.split_divider_bar_width) val backgroundLeft = (width - dividerSize) / 2 val backgroundTop = 0 - val backgroundRight = left + dividerSize + val backgroundRight = backgroundLeft + dividerSize val backgroundBottom = height backgroundRect.set(backgroundLeft, backgroundTop, backgroundRight, backgroundBottom) } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt index c4e49466343e..b5700ffb046b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt @@ -36,10 +36,13 @@ import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction import android.widget.ImageButton import androidx.core.view.ViewCompat import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat +import com.android.internal.policy.SystemBarUtils +import com.android.window.flags.Flags import com.android.wm.shell.R import com.android.wm.shell.shared.animation.Interpolators import com.android.wm.shell.windowdecor.WindowManagerWrapper import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer +import com.android.wm.shell.windowdecor.viewholder.WindowDecorationViewHolder.Data /** * A desktop mode window decoration used when the window is in full "focus" (i.e. fullscreen/split). @@ -66,12 +69,10 @@ internal class AppHandleViewHolder( ) : Data() private lateinit var taskInfo: RunningTaskInfo - private val position: Point = Point() - private var width: Int = 0 - private var height: Int = 0 private val captionView: View = rootView.requireViewById(R.id.desktop_mode_caption) private val captionHandle: ImageButton = rootView.requireViewById(R.id.caption_handle) private val inputManager = context.getSystemService(InputManager::class.java) + private var statusBarInputLayerExists = false // An invisible View that takes up the same coordinates as captionHandle but is layered // above the status bar. The purpose of this View is to receive input intended for @@ -111,54 +112,21 @@ internal class AppHandleViewHolder( ) { captionHandle.imageTintList = ColorStateList.valueOf(getCaptionHandleBarColor(taskInfo)) this.taskInfo = taskInfo - this.position.set(position) - this.width = width - this.height = height - if (!isCaptionVisible && statusBarInputLayer != null) { - detachStatusBarInputLayer() + // If handle is not in status bar region(i.e., bottom stage in vertical split), + // do not create an input layer + if (position.y >= SystemBarUtils.getStatusBarHeight(context)) return + if (!isCaptionVisible && statusBarInputLayerExists) { + disposeStatusBarInputLayer() return } - } - - fun bindStatusBarInputLayer( - statusBarLayer: AdditionalSystemViewContainer - ) { - // Input layer view modification takes a significant amount of time; + // Input layer view creation / modification takes a significant amount of time; // post them so we don't hold up DesktopModeWindowDecoration#relayout. - if (statusBarLayer == statusBarInputLayer) { + if (statusBarInputLayerExists) { handler.post { updateStatusBarInputLayer(position) } - return - } - // Remove the old input layer when changing to a new one. - if (statusBarInputLayer != null) detachStatusBarInputLayer() - if (statusBarLayer.view.visibility == View.GONE) { - statusBarLayer.view.visibility = View.VISIBLE - } - statusBarInputLayer = statusBarLayer - statusBarInputLayer?.let { - inputLayer -> setupAppHandleA11y(inputLayer.view) - } - handler.post { - val view = statusBarInputLayer?.view - ?: error("Unable to find statusBarInputLayer View") - // Caption handle is located within the status bar region, meaning the - // DisplayPolicy will attempt to transfer this input to status bar if it's - // a swipe down. Pilfer here to keep the gesture in handle alone. - view.setOnTouchListener { v, event -> - if (event.actionMasked == ACTION_DOWN) { - inputManager.pilferPointers(v.viewRootImpl.inputToken) - } - captionHandle.dispatchTouchEvent(event) - return@setOnTouchListener true - } - view.setOnHoverListener { _, event -> - captionHandle.onHoverEvent(event) - } - val lp = statusBarInputLayer?.view?.layoutParams as WindowManager.LayoutParams - lp.x = position.x - lp.y = position.y - lp.width = width - lp.height = height + } else { + // Input layer is created on a delay; prevent multiple from being created. + statusBarInputLayerExists = true + handler.post { createStatusBarInputLayer(position, width, height) } } } @@ -170,6 +138,40 @@ internal class AppHandleViewHolder( animateCaptionHandleAlpha(startValue = 0f, endValue = 1f) } + private fun createStatusBarInputLayer(handlePosition: Point, + handleWidth: Int, + handleHeight: Int) { + if (!Flags.enableHandleInputFix()) return + statusBarInputLayer = AdditionalSystemViewContainer(context, windowManagerWrapper, + taskInfo.taskId, handlePosition.x, handlePosition.y, handleWidth, handleHeight, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + ) + val view = statusBarInputLayer?.view ?: error("Unable to find statusBarInputLayer View") + val lp = statusBarInputLayer?.lp ?: error("Unable to find statusBarInputLayer " + + "LayoutParams") + lp.title = "Handle Input Layer of task " + taskInfo.taskId + lp.setTrustedOverlay() + // Make this window a spy window to enable it to pilfer pointers from the system-wide + // gesture listener that receives events before window. This is to prevent notification + // shade gesture when we swipe down to enter desktop. + lp.inputFeatures = WindowManager.LayoutParams.INPUT_FEATURE_SPY + view.setOnHoverListener { _, event -> + captionHandle.onHoverEvent(event) + } + // Caption handle is located within the status bar region, meaning the + // DisplayPolicy will attempt to transfer this input to status bar if it's + // a swipe down. Pilfer here to keep the gesture in handle alone. + view.setOnTouchListener { v, event -> + if (event.actionMasked == ACTION_DOWN) { + inputManager.pilferPointers(v.viewRootImpl.inputToken) + } + captionHandle.dispatchTouchEvent(event) + return@setOnTouchListener true + } + setupAppHandleA11y(view) + windowManagerWrapper.updateViewLayout(view, lp) + } + private fun setupAppHandleA11y(view: View) { view.accessibilityDelegate = object : View.AccessibilityDelegate() { override fun onInitializeAccessibilityNodeInfo( @@ -222,12 +224,15 @@ internal class AppHandleViewHolder( } /** - * Remove the input listeners from the input layer and remove it from this view holder. + * Remove the input layer from [WindowManager]. Should be used when caption handle + * is not visible. */ - fun detachStatusBarInputLayer() { - statusBarInputLayer?.view?.setOnTouchListener(null) - statusBarInputLayer?.view?.setOnHoverListener(null) - statusBarInputLayer = null + fun disposeStatusBarInputLayer() { + statusBarInputLayerExists = false + handler.post { + statusBarInputLayer?.releaseView() + statusBarInputLayer = null + } } private fun getCaptionHandleBarColor(taskInfo: RunningTaskInfo): Int { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java index 72d4dc6ffac9..13a8518ae8ed 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java @@ -700,7 +700,7 @@ public class BackAnimationControllerTest extends ShellTestCase { eq(tInfo), eq(st), eq(ft), eq(callback)); mBackTransitionHandler.onAnimationFinished(); - final TransitionInfo.Change openToClose = createAppChange(openTaskId, TRANSIT_CLOSE, + final TransitionInfo.Change openToClose = createAppChangeFromChange(open, TRANSIT_CLOSE, FLAG_BACK_GESTURE_ANIMATED); tInfo2 = createTransitionInfo(TRANSIT_CLOSE_PREPARE_BACK_NAVIGATION, openToClose); mBackTransitionHandler.mClosePrepareTransition = mock(IBinder.class); @@ -830,6 +830,16 @@ public class BackAnimationControllerTest extends ShellTestCase { return change; } + private TransitionInfo.Change createAppChangeFromChange( + TransitionInfo.Change originalChange, @TransitionInfo.TransitionMode int mode, + @TransitionInfo.ChangeFlags int flags) { + final TransitionInfo.Change change = new TransitionInfo.Change( + originalChange.getTaskInfo().token, originalChange.getLeash()); + change.setMode(mode); + change.setFlags(flags); + return change; + } + private static TransitionInfo createTransitionInfo( @WindowManager.TransitionType int type, TransitionInfo.Change ... changes) { final TransitionInfo info = new TransitionInfo(type, 0); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt index 9747b191a925..aabd973fce90 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt @@ -43,6 +43,7 @@ import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreef import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFullscreenTask import com.android.wm.shell.desktopmode.persistence.Desktop import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository +import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.Transitions @@ -50,10 +51,10 @@ import junit.framework.Assert.assertEquals import junit.framework.Assert.assertTrue import kotlin.test.assertNotNull import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.cancel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.setMain @@ -94,6 +95,7 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() { @Mock lateinit var resizeTransitionHandler: ToggleResizeDesktopTaskTransitionHandler @Mock lateinit var taskStackListener: TaskStackListenerImpl @Mock lateinit var persistentRepository: DesktopPersistentRepository + @Mock lateinit var repositoryInitializer: DesktopRepositoryInitializer private lateinit var mockitoSession: StaticMockitoSession private lateinit var handler: DesktopActivityOrientationChangeHandler @@ -116,7 +118,13 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() { testScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) shellInit = spy(ShellInit(testExecutor)) taskRepository = - DesktopRepository(context, shellInit, persistentRepository, testScope) + DesktopRepository( + context, + shellInit, + persistentRepository, + repositoryInitializer, + testScope + ) whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenAnswer { runningTasks } whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() } whenever(runBlocking { persistentRepository.readDesktop(any(), any()) }).thenReturn( diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt index 1c4b9bfe2fd7..e05a0b54fcf4 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt @@ -81,7 +81,7 @@ class DesktopImmersiveControllerTest : ShellTestCase() { @Before fun setUp() { desktopRepository = DesktopRepository( - context, ShellInit(TestShellExecutor()), mock(), mock() + context, ShellInit(TestShellExecutor()), mock(), mock(), mock() ) whenever(mockDisplayController.getDisplayLayout(DEFAULT_DISPLAY)) .thenReturn(mockDisplayLayout) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt index 868883d12ac0..df061e368071 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt @@ -39,9 +39,12 @@ import androidx.test.filters.SmallTest import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE import com.android.internal.jank.InteractionJankMonitor import com.android.window.flags.Flags +import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTestCase import com.android.wm.shell.TestRunningTaskInfoBuilder +import com.android.wm.shell.desktopmode.DesktopMixedTransitionHandler.PendingMixedTransition import com.android.wm.shell.freeform.FreeformTaskTransitionHandler +import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.Transitions import com.google.common.truth.Truth.assertThat import org.junit.Assert.assertFalse @@ -51,7 +54,9 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock +import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull @@ -80,6 +85,8 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { @Mock lateinit var interactionJankMonitor: InteractionJankMonitor @Mock lateinit var mockHandler: Handler @Mock lateinit var closingTaskLeash: SurfaceControl + @Mock lateinit var shellInit: ShellInit + @Mock lateinit var rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer private lateinit var mixedHandler: DesktopMixedTransitionHandler @@ -94,7 +101,9 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { closeDesktopTaskTransitionHandler, desktopImmersiveController, interactionJankMonitor, - mockHandler + mockHandler, + shellInit, + rootTaskDisplayAreaOrganizer, ) } @@ -238,8 +247,10 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { } @Test - @DisableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) - fun startLaunchTransition_immersiveMixDisabled_doesNotUseMixedHandler() { + @DisableFlags( + Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP, + Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS) + fun startLaunchTransition_immersiveAndAppLaunchFlagsDisabled_doesNotUseMixedHandler() { val wct = WindowContainerTransaction() val task = createTask(WINDOWING_MODE_FREEFORM) whenever(transitions.startTransition(eq(TRANSIT_OPEN), eq(wct), anyOrNull())) @@ -274,6 +285,24 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS) + fun startLaunchTransition_desktopAppLaunchEnabled_usesMixedHandler() { + val wct = WindowContainerTransaction() + val task = createTask(WINDOWING_MODE_FREEFORM) + whenever(transitions.startTransition(eq(TRANSIT_OPEN), eq(wct), anyOrNull())) + .thenReturn(Binder()) + + mixedHandler.startLaunchTransition( + transitionType = TRANSIT_OPEN, + wct = wct, + taskId = task.taskId, + exitingImmersiveTask = null + ) + + verify(transitions).startTransition(TRANSIT_OPEN, wct, mixedHandler) + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) fun startAndAnimateLaunchTransition_withoutImmersiveChange_dispatchesAllChangesToLeftOver() { val wct = WindowContainerTransaction() @@ -355,6 +384,134 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS) + fun startAndAnimateLaunchTransition_noMinimizeChange_doesNotReparentMinimizeChange() { + val wct = WindowContainerTransaction() + val launchingTask = createTask(WINDOWING_MODE_FREEFORM) + val launchTaskChange = createChange(launchingTask) + val transition = Binder() + whenever(transitions.startTransition(eq(TRANSIT_OPEN), eq(wct), anyOrNull())) + .thenReturn(transition) + + mixedHandler.startLaunchTransition( + transitionType = TRANSIT_OPEN, + wct = wct, + taskId = launchingTask.taskId, + minimizingTaskId = null, + ) + mixedHandler.startAnimation( + transition, + createTransitionInfo( + TRANSIT_OPEN, + listOf(launchTaskChange) + ), + SurfaceControl.Transaction(), + SurfaceControl.Transaction(), + ) { } + + verify(rootTaskDisplayAreaOrganizer, times(0)) + .reparentToDisplayArea(anyInt(), any(), any()) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS) + fun startAndAnimateLaunchTransition_withMinimizeChange_reparentsMinimizeChange() { + val wct = WindowContainerTransaction() + val launchingTask = createTask(WINDOWING_MODE_FREEFORM) + val minimizingTask = createTask(WINDOWING_MODE_FREEFORM) + val launchTaskChange = createChange(launchingTask) + val minimizeChange = createChange(minimizingTask) + val transition = Binder() + whenever(transitions.startTransition(eq(TRANSIT_OPEN), eq(wct), anyOrNull())) + .thenReturn(transition) + + mixedHandler.startLaunchTransition( + transitionType = TRANSIT_OPEN, + wct = wct, + taskId = launchingTask.taskId, + minimizingTaskId = minimizingTask.taskId, + ) + mixedHandler.startAnimation( + transition, + createTransitionInfo( + TRANSIT_OPEN, + listOf(launchTaskChange, minimizeChange) + ), + SurfaceControl.Transaction(), + SurfaceControl.Transaction(), + ) { } + + verify(rootTaskDisplayAreaOrganizer).reparentToDisplayArea( + anyInt(), eq(minimizeChange.leash), any()) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS) + fun addPendingAndAnimateLaunchTransition_noMinimizeChange_doesNotReparentMinimizeChange() { + val wct = WindowContainerTransaction() + val launchingTask = createTask(WINDOWING_MODE_FREEFORM) + val launchTaskChange = createChange(launchingTask) + val transition = Binder() + whenever(transitions.startTransition(eq(TRANSIT_OPEN), eq(wct), anyOrNull())) + .thenReturn(transition) + + mixedHandler.addPendingMixedTransition( + PendingMixedTransition.Launch( + transition = transition, + launchingTask = launchingTask.taskId, + minimizingTask = null, + exitingImmersiveTask = null, + ) + ) + mixedHandler.startAnimation( + transition, + createTransitionInfo( + TRANSIT_OPEN, + listOf(launchTaskChange) + ), + SurfaceControl.Transaction(), + SurfaceControl.Transaction(), + ) { } + + verify(rootTaskDisplayAreaOrganizer, times(0)) + .reparentToDisplayArea(anyInt(), any(), any()) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS) + fun addPendingAndAnimateLaunchTransition_withMinimizeChange_reparentsMinimizeChange() { + val wct = WindowContainerTransaction() + val launchingTask = createTask(WINDOWING_MODE_FREEFORM) + val minimizingTask = createTask(WINDOWING_MODE_FREEFORM) + val launchTaskChange = createChange(launchingTask) + val minimizeChange = createChange(minimizingTask) + val transition = Binder() + whenever(transitions.startTransition(eq(TRANSIT_OPEN), eq(wct), anyOrNull())) + .thenReturn(transition) + + mixedHandler.addPendingMixedTransition( + PendingMixedTransition.Launch( + transition = transition, + launchingTask = launchingTask.taskId, + minimizingTask = minimizingTask.taskId, + exitingImmersiveTask = null, + ) + ) + mixedHandler.startAnimation( + transition, + createTransitionInfo( + TRANSIT_OPEN, + listOf(launchTaskChange, minimizeChange) + ), + SurfaceControl.Transaction(), + SurfaceControl.Transaction(), + ) { } + + verify(rootTaskDisplayAreaOrganizer).reparentToDisplayArea( + anyInt(), eq(minimizeChange.leash), any()) + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) fun startAndAnimateLaunchTransition_removesPendingMixedTransition() { val wct = WindowContainerTransaction() diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt index 715b045fcbca..414c1a658b95 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt @@ -30,6 +30,7 @@ import com.android.wm.shell.TestShellExecutor import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.desktopmode.persistence.Desktop import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository +import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer import com.android.wm.shell.sysui.ShellInit import com.google.common.truth.Truth.assertThat import junit.framework.Assert.fail @@ -52,6 +53,7 @@ import org.mockito.Mockito.inOrder import org.mockito.Mockito.spy import org.mockito.kotlin.any import org.mockito.kotlin.never +import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @@ -68,6 +70,7 @@ class DesktopRepositoryTest : ShellTestCase() { @Mock private lateinit var testExecutor: ShellExecutor @Mock private lateinit var persistentRepository: DesktopPersistentRepository + @Mock lateinit var repositoryInitializer: DesktopRepositoryInitializer @Before fun setUp() { @@ -75,7 +78,14 @@ class DesktopRepositoryTest : ShellTestCase() { datastoreScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) shellInit = spy(ShellInit(testExecutor)) - repo = DesktopRepository(context, shellInit, persistentRepository, datastoreScope) + repo = + DesktopRepository( + context, + shellInit, + persistentRepository, + repositoryInitializer, + datastoreScope + ) whenever(runBlocking { persistentRepository.readDesktop(any(), any()) }).thenReturn( Desktop.getDefaultInstance() ) @@ -218,6 +228,33 @@ class DesktopRepositoryTest : ShellTestCase() { } @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE) + fun updateTaskVisibility_multipleTasks_persistsVisibleTasks() = + runTest(StandardTestDispatcher()) { + repo.updateTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true) + repo.updateTask(DEFAULT_DISPLAY, taskId = 2, isVisible = true) + + inOrder(persistentRepository).run { + verify(persistentRepository) + .addOrUpdateDesktop( + DEFAULT_USER_ID, + DEFAULT_DESKTOP_ID, + visibleTasks = ArraySet(arrayOf(1)), + minimizedTasks = ArraySet(), + freeformTasksInZOrder = arrayListOf() + ) + verify(persistentRepository) + .addOrUpdateDesktop( + DEFAULT_USER_ID, + DEFAULT_DESKTOP_ID, + visibleTasks = ArraySet(arrayOf(1, 2)), + minimizedTasks = ArraySet(), + freeformTasksInZOrder = arrayListOf() + ) + } + } + + @Test fun isOnlyVisibleNonClosingTask_singleVisibleClosingTask() { repo.updateTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true) repo.addClosingTask(DEFAULT_DISPLAY, 1) @@ -605,7 +642,7 @@ class DesktopRepositoryTest : ShellTestCase() { minimizedTasks = ArraySet(), freeformTasksInZOrder = arrayListOf(7, 6, 5) ) - verify(persistentRepository) + verify(persistentRepository, times(2)) .addOrUpdateDesktop( DEFAULT_USER_ID, DEFAULT_DESKTOP_ID, 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 051079c9d519..b157d557c1d8 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 @@ -110,6 +110,7 @@ import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createSplit import com.android.wm.shell.desktopmode.minimize.DesktopWindowLimitRemoteHandler import com.android.wm.shell.desktopmode.persistence.Desktop import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository +import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer import com.android.wm.shell.draganddrop.DragAndDropController import com.android.wm.shell.freeform.FreeformTaskTransitionStarter import com.android.wm.shell.recents.RecentTasksController @@ -224,6 +225,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Mock private lateinit var mockInputManager: InputManager @Mock private lateinit var mockFocusTransitionObserver: FocusTransitionObserver @Mock lateinit var motionEvent: MotionEvent + @Mock lateinit var repositoryInitializer: DesktopRepositoryInitializer private lateinit var mockitoSession: StaticMockitoSession @Mock @@ -264,7 +266,8 @@ class DesktopTasksControllerTest : ShellTestCase() { testScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) shellInit = spy(ShellInit(testExecutor)) - taskRepository = DesktopRepository(context, shellInit, persistentRepository, testScope) + taskRepository = + DesktopRepository(context, shellInit, persistentRepository, repositoryInitializer, testScope) desktopTasksLimiter = DesktopTasksLimiter( transitions, @@ -1410,7 +1413,8 @@ class DesktopTasksControllerTest : ShellTestCase() { eq(TRANSIT_TO_FRONT), any(), eq(freeformTasks[0].taskId), - anyOrNull() + anyOrNull(), + anyOrNull(), )).thenReturn(Binder()) controller.moveTaskToFront(freeformTasks[0], remoteTransition = null) @@ -1468,7 +1472,7 @@ class DesktopTasksControllerTest : ShellTestCase() { val task = createTaskInfo(1001) whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(null) whenever(desktopMixedTransitionHandler - .startLaunchTransition(eq(TRANSIT_OPEN), any(), eq(task.taskId), anyOrNull())) + .startLaunchTransition(eq(TRANSIT_OPEN), any(), eq(task.taskId), anyOrNull(), anyOrNull())) .thenReturn(Binder()) controller.moveTaskToFront(task.taskId, remoteTransition = null) @@ -3026,7 +3030,7 @@ class DesktopTasksControllerTest : ShellTestCase() { eq(mockSurface), eq(currentDragBounds), eq(STABLE_BOUNDS), - eq(true) + anyOrNull(), ) } @@ -3341,7 +3345,7 @@ class DesktopTasksControllerTest : ShellTestCase() { eq(mockSurface), eq(currentDragBounds), eq(bounds), - eq(true) + anyOrNull(), ) verify(desktopModeEventLogger, times(1)).logTaskResizingEnded( ResizeTrigger.SNAP_LEFT_MENU, @@ -3398,7 +3402,7 @@ class DesktopTasksControllerTest : ShellTestCase() { eq(mockSurface), eq(currentDragBounds), eq(preDragBounds), - eq(false) + any(), ) verify(desktopModeEventLogger, never()).logTaskResizingStarted( any(), @@ -3689,7 +3693,8 @@ class DesktopTasksControllerTest : ShellTestCase() { runOnTransitionStart = runOnStartTransit, )) whenever(desktopMixedTransitionHandler - .startLaunchTransition(any(), any(), anyInt(), anyOrNull())).thenReturn(transition) + .startLaunchTransition(any(), any(), anyInt(), anyOrNull(), anyOrNull())) + .thenReturn(transition) controller.moveTaskToFront(task.taskId, remoteTransition = null) @@ -3710,7 +3715,7 @@ class DesktopTasksControllerTest : ShellTestCase() { runOnTransitionStart = runOnStartTransit, )) whenever(desktopMixedTransitionHandler - .startLaunchTransition(any(), any(), eq(task.taskId), anyOrNull())) + .startLaunchTransition(any(), any(), eq(task.taskId), anyOrNull(), anyOrNull())) .thenReturn(transition) controller.moveTaskToFront(task.taskId, remoteTransition = null) @@ -4099,7 +4104,7 @@ class DesktopTasksControllerTest : ShellTestCase() { val arg: ArgumentCaptor<WindowContainerTransaction> = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) verify(desktopMixedTransitionHandler) - .startLaunchTransition(eq(type), capture(arg), anyInt(), anyOrNull()) + .startLaunchTransition(eq(type), capture(arg), anyInt(), anyOrNull(), anyOrNull()) return arg.value } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt index d44728de3462..01b69aed8465 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt @@ -41,6 +41,7 @@ import com.android.wm.shell.ShellTestCase import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository +import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.TransitionInfoBuilder @@ -89,6 +90,7 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Mock lateinit var handler: Handler @Mock lateinit var testExecutor: ShellExecutor @Mock lateinit var persistentRepository: DesktopPersistentRepository + @Mock lateinit var repositoryInitializer: DesktopRepositoryInitializer private lateinit var mockitoSession: StaticMockitoSession private lateinit var desktopTasksLimiter: DesktopTasksLimiter @@ -106,7 +108,13 @@ class DesktopTasksLimiterTest : ShellTestCase() { testScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) desktopTaskRepo = - DesktopRepository(context, shellInit, persistentRepository, testScope) + DesktopRepository( + context, + shellInit, + persistentRepository, + repositoryInitializer, + testScope + ) desktopTasksLimiter = DesktopTasksLimiter(transitions, desktopTaskRepo, shellTaskOrganizer, MAX_TASK_LIMIT, interactionJankMonitor, mContext, handler) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java index fefa933c5208..a82e5e8434b2 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java @@ -19,8 +19,6 @@ package com.android.wm.shell.desktopmode; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; -import static androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread; - import static com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN; import static org.junit.Assert.assertTrue; @@ -28,6 +26,7 @@ import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import android.animation.AnimatorTestRule; import android.annotation.NonNull; import android.app.ActivityManager; import android.app.WindowConfiguration; @@ -36,6 +35,8 @@ import android.content.res.Resources; import android.graphics.Point; import android.os.Handler; import android.os.IBinder; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; import android.util.DisplayMetrics; import android.view.SurfaceControl; import android.view.WindowManager; @@ -53,17 +54,23 @@ import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource; import com.android.wm.shell.transition.Transitions; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; +import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -import java.util.ArrayList; import java.util.function.Supplier; /** Tests of {@link com.android.wm.shell.desktopmode.ExitDesktopTaskTransitionHandler} */ @SmallTest +@TestableLooper.RunWithLooper +@RunWith(AndroidTestingRunner.class) public class ExitDesktopTaskTransitionHandlerTest extends ShellTestCase { + @Rule + public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(this); + @Mock private Transitions mTransitions; @Mock @@ -105,7 +112,7 @@ public class ExitDesktopTaskTransitionHandlerTest extends ShellTestCase { } @Test - public void testTransitExitDesktopModeAnimation() throws Throwable { + public void testTransitExitDesktopModeAnimation() { final int transitionType = TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN; final int taskId = 1; WindowContainerTransaction wct = new WindowContainerTransaction(); @@ -118,21 +125,16 @@ public class ExitDesktopTaskTransitionHandlerTest extends ShellTestCase { TransitionInfo.Change change = createChange(WindowManager.TRANSIT_CHANGE, taskId, WINDOWING_MODE_FULLSCREEN); TransitionInfo info = createTransitionInfo(TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN, change); - ArrayList<Exception> exceptions = new ArrayList<>(); - runOnUiThread(() -> { - try { - assertTrue(mExitDesktopTaskTransitionHandler - .startAnimation(mToken, info, - new SurfaceControl.Transaction(), - new SurfaceControl.Transaction(), - mTransitionFinishCallback)); - } catch (Exception e) { - exceptions.add(e); - } - }); - if (!exceptions.isEmpty()) { - throw exceptions.get(0); - } + + final boolean animated = mExitDesktopTaskTransitionHandler + .startAnimation(mToken, info, + new SurfaceControl.Transaction(), + new SurfaceControl.Transaction(), + mTransitionFinishCallback); + mAnimatorTestRule.advanceTimeBy( + ExitDesktopTaskTransitionHandler.FULLSCREEN_ANIMATION_DURATION); + + assertTrue(animated); } private TransitionInfo.Change createChange(@WindowManager.TransitionType int type, int taskId, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepositoryTest.kt index e3caf2ede99d..38c6ed90241c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepositoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepositoryTest.kt @@ -49,7 +49,10 @@ class WindowDecorCaptionHandleRepositoryTest { val taskInfo = createTaskInfo(WINDOWING_MODE_FULLSCREEN, GMAIL_PACKAGE_NAME) val appHandleCaptionState = CaptionState.AppHandle( - taskInfo, false, Rect(/* left= */ 0, /* top= */ 1, /* right= */ 2, /* bottom= */ 3)) + runningTaskInfo = taskInfo, + isHandleMenuExpanded = false, + globalAppHandleBounds = Rect(/* left= */ 0, /* top= */ 1, /* right= */ 2, /* bottom= */ 3), + isCapturedLinkAvailable = false) captionHandleRepository.notifyCaptionChanged(appHandleCaptionState) @@ -61,7 +64,10 @@ class WindowDecorCaptionHandleRepositoryTest { val taskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM, GMAIL_PACKAGE_NAME) val appHeaderCaptionState = CaptionState.AppHeader( - taskInfo, true, Rect(/* left= */ 0, /* top= */ 1, /* right= */ 2, /* bottom= */ 3)) + runningTaskInfo = taskInfo, + isHeaderMenuExpanded = true, + globalAppChipBounds = Rect(/* left= */ 0, /* top= */ 1, /* right= */ 2, /* bottom= */ 3), + isCapturedLinkAvailable = false) captionHandleRepository.notifyCaptionChanged(appHeaderCaptionState) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt index 7dbadc9d9083..d94186c8284e 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt @@ -84,7 +84,7 @@ class AppHandleEducationControllerTest : ShellTestCase() { private val testDataStoreFlow = MutableStateFlow(createWindowingEducationProto()) private val testCaptionStateFlow = MutableStateFlow<CaptionState>(CaptionState.NoCaption) private val educationConfigCaptor = - argumentCaptor<DesktopWindowingEducationTooltipController.EducationViewConfig>() + argumentCaptor<DesktopWindowingEducationTooltipController.TooltipEducationViewConfig>() @Mock private lateinit var mockEducationFilter: AppHandleEducationFilter @Mock private lateinit var mockDataStoreRepository: AppHandleEducationDatastoreRepository @Mock private lateinit var mockCaptionHandleRepository: WindowDecorCaptionHandleRepository diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerTest.kt new file mode 100644 index 000000000000..975342902814 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerTest.kt @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.desktopmode.persistence + +import android.platform.test.annotations.EnableFlags +import android.testing.AndroidTestingRunner +import android.view.Display.DEFAULT_DISPLAY +import androidx.test.filters.SmallTest +import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.common.ShellExecutor +import com.android.wm.shell.desktopmode.DesktopRepository +import com.android.wm.shell.sysui.ShellInit +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.inOrder +import org.mockito.Mockito.spy +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@ExperimentalCoroutinesApi +class DesktopRepositoryInitializerTest : ShellTestCase() { + + private lateinit var repositoryInitializer: DesktopRepositoryInitializer + private lateinit var shellInit: ShellInit + private lateinit var datastoreScope: CoroutineScope + + private lateinit var desktopRepository: DesktopRepository + private val persistentRepository = mock<DesktopPersistentRepository>() + private val testExecutor = mock<ShellExecutor>() + + @Before + fun setUp() { + Dispatchers.setMain(StandardTestDispatcher()) + shellInit = spy(ShellInit(testExecutor)) + datastoreScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) + repositoryInitializer = + DesktopRepositoryInitializerImpl(context, persistentRepository, datastoreScope) + desktopRepository = + DesktopRepository( + context, shellInit, persistentRepository, repositoryInitializer, datastoreScope) + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE) + fun initWithPersistence_multipleTasks_addedCorrectly() = + runTest(StandardTestDispatcher()) { + val freeformTasksInZOrder = listOf(1, 2, 3) + whenever(persistentRepository.readDesktop(any(), any())) + .thenReturn( + Desktop.newBuilder() + .setDesktopId(1) + .addAllZOrderedTasks(freeformTasksInZOrder) + .putTasksByTaskId( + 1, + DesktopTask.newBuilder() + .setTaskId(1) + .setDesktopTaskState(DesktopTaskState.VISIBLE) + .build()) + .putTasksByTaskId( + 2, + DesktopTask.newBuilder() + .setTaskId(2) + .setDesktopTaskState(DesktopTaskState.VISIBLE) + .build()) + .putTasksByTaskId( + 3, + DesktopTask.newBuilder() + .setTaskId(3) + .setDesktopTaskState(DesktopTaskState.MINIMIZED) + .build()) + .build()) + + repositoryInitializer.initialize(desktopRepository) + + verify(persistentRepository).readDesktop(any(), any()) + assertThat(desktopRepository.getActiveTasks(DEFAULT_DISPLAY)) + .containsExactly(1, 2, 3) + .inOrder() + assertThat(desktopRepository.getExpandedTasksOrdered(DEFAULT_DISPLAY)) + .containsExactly(1, 2) + .inOrder() + assertThat(desktopRepository.getMinimizedTasks(DEFAULT_DISPLAY)).containsExactly(3) + } + + @After + fun tearDown() { + datastoreScope.cancel() + } +} 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 e74c804d4f40..bcb7461bfae7 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 @@ -280,7 +280,7 @@ public class PipTaskOrganizerTest extends ShellTestCase { private void preparePipSurfaceTransactionHelper() { doReturn(mMockPipSurfaceTransactionHelper).when(mMockPipSurfaceTransactionHelper) - .crop(any(), any(), any()); + .cropAndPosition(any(), any(), any()); doReturn(mMockPipSurfaceTransactionHelper).when(mMockPipSurfaceTransactionHelper) .resetScale(any(), any(), any()); doReturn(mMockPipSurfaceTransactionHelper).when(mMockPipSurfaceTransactionHelper) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/animation/PipExpandAnimatorTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/animation/PipExpandAnimatorTest.java new file mode 100644 index 000000000000..e19a10a78417 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/animation/PipExpandAnimatorTest.java @@ -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.pip2.animation; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyFloat; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.view.Surface; +import android.view.SurfaceControl; + +import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.wm.shell.pip2.PipSurfaceTransactionHelper; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Unit test against {@link PipExpandAnimator}. + */ +@SmallTest +@TestableLooper.RunWithLooper +@RunWith(AndroidTestingRunner.class) +public class PipExpandAnimatorTest { + + @Mock private Context mMockContext; + + @Mock private Resources mMockResources; + + @Mock private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory mMockFactory; + + @Mock private SurfaceControl.Transaction mMockTransaction; + + @Mock private SurfaceControl.Transaction mMockStartTransaction; + + @Mock private SurfaceControl.Transaction mMockFinishTransaction; + + @Mock private Runnable mMockStartCallback; + + @Mock private Runnable mMockEndCallback; + + private PipExpandAnimator mPipExpandAnimator; + private Rect mBaseBounds; + private Rect mStartBounds; + private Rect mEndBounds; + private Rect mSourceRectHint; + @Surface.Rotation private int mRotation; + private SurfaceControl mTestLeash; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mMockContext.getResources()).thenReturn(mMockResources); + when(mMockResources.getInteger(anyInt())).thenReturn(0); + when(mMockFactory.getTransaction()).thenReturn(mMockTransaction); + // No-op on the mMockTransaction + when(mMockTransaction.setAlpha(any(SurfaceControl.class), anyFloat())) + .thenReturn(mMockTransaction); + when(mMockTransaction.setCrop(any(SurfaceControl.class), any(Rect.class))) + .thenReturn(mMockTransaction); + when(mMockTransaction.setMatrix(any(SurfaceControl.class), any(Matrix.class), any())) + .thenReturn(mMockTransaction); + when(mMockTransaction.setCornerRadius(any(SurfaceControl.class), anyFloat())) + .thenReturn(mMockTransaction); + when(mMockTransaction.setShadowRadius(any(SurfaceControl.class), anyFloat())) + .thenReturn(mMockTransaction); + // Do the same for mMockFinishTransaction + when(mMockFinishTransaction.setAlpha(any(SurfaceControl.class), anyFloat())) + .thenReturn(mMockFinishTransaction); + when(mMockFinishTransaction.setCrop(any(SurfaceControl.class), any(Rect.class))) + .thenReturn(mMockFinishTransaction); + when(mMockFinishTransaction.setMatrix(any(SurfaceControl.class), any(Matrix.class), any())) + .thenReturn(mMockFinishTransaction); + when(mMockFinishTransaction.setCornerRadius(any(SurfaceControl.class), anyFloat())) + .thenReturn(mMockFinishTransaction); + when(mMockFinishTransaction.setShadowRadius(any(SurfaceControl.class), anyFloat())) + .thenReturn(mMockFinishTransaction); + + mTestLeash = new SurfaceControl.Builder() + .setContainerLayer() + .setName("PipExpandAnimatorTest") + .setCallsite("PipExpandAnimatorTest") + .build(); + } + + @Test + public void setAnimationStartCallback_expand_callbackStartCallback() { + mRotation = Surface.ROTATION_0; + mBaseBounds = new Rect(0, 0, 1_000, 2_000); + mStartBounds = new Rect(500, 1_000, 1_000, 2_000); + mEndBounds = new Rect(mBaseBounds); + mPipExpandAnimator = new PipExpandAnimator(mMockContext, mTestLeash, + mMockStartTransaction, mMockFinishTransaction, + mBaseBounds, mStartBounds, mEndBounds, mSourceRectHint, + mRotation); + mPipExpandAnimator.setSurfaceControlTransactionFactory(mMockFactory); + + mPipExpandAnimator.setAnimationStartCallback(mMockStartCallback); + mPipExpandAnimator.setAnimationEndCallback(mMockEndCallback); + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + mPipExpandAnimator.start(); + mPipExpandAnimator.pause(); + }); + + verify(mMockStartCallback).run(); + verifyZeroInteractions(mMockEndCallback); + } + + @Test + public void setAnimationEndCallback_expand_callbackStartAndEndCallback() { + mRotation = Surface.ROTATION_0; + mBaseBounds = new Rect(0, 0, 1_000, 2_000); + mStartBounds = new Rect(500, 1_000, 1_000, 2_000); + mEndBounds = new Rect(mBaseBounds); + mPipExpandAnimator = new PipExpandAnimator(mMockContext, mTestLeash, + mMockStartTransaction, mMockFinishTransaction, + mBaseBounds, mStartBounds, mEndBounds, mSourceRectHint, + mRotation); + mPipExpandAnimator.setSurfaceControlTransactionFactory(mMockFactory); + + mPipExpandAnimator.setAnimationStartCallback(mMockStartCallback); + mPipExpandAnimator.setAnimationEndCallback(mMockEndCallback); + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + mPipExpandAnimator.start(); + mPipExpandAnimator.end(); + }); + + verify(mMockStartCallback).run(); + verify(mMockEndCallback).run(); + } + + @Test + public void onAnimationEnd_expand_leashIsFullscreen() { + mRotation = Surface.ROTATION_0; + mBaseBounds = new Rect(0, 0, 1_000, 2_000); + mStartBounds = new Rect(500, 1_000, 1_000, 2_000); + mEndBounds = new Rect(mBaseBounds); + mPipExpandAnimator = new PipExpandAnimator(mMockContext, mTestLeash, + mMockStartTransaction, mMockFinishTransaction, + mBaseBounds, mStartBounds, mEndBounds, mSourceRectHint, + mRotation); + mPipExpandAnimator.setSurfaceControlTransactionFactory(mMockFactory); + + mPipExpandAnimator.setAnimationStartCallback(mMockStartCallback); + mPipExpandAnimator.setAnimationEndCallback(mMockEndCallback); + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + mPipExpandAnimator.start(); + clearInvocations(mMockTransaction); + mPipExpandAnimator.end(); + }); + + verify(mMockTransaction).setCrop(mTestLeash, mEndBounds); + verify(mMockTransaction).setCornerRadius(mTestLeash, 0f); + verify(mMockTransaction).setShadowRadius(mTestLeash, 0f); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/animation/PipResizeAnimatorTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/animation/PipResizeAnimatorTest.java new file mode 100644 index 000000000000..0adb50b81896 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/animation/PipResizeAnimatorTest.java @@ -0,0 +1,276 @@ +/* + * 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.pip2.animation; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; +import static org.mockito.kotlin.MatchersKt.eq; +import static org.junit.Assert.assertEquals; + +import android.content.Context; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.view.SurfaceControl; + +import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.wm.shell.pip2.PipSurfaceTransactionHelper; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Unit test against {@link PipResizeAnimator}. + */ +@SmallTest +@TestableLooper.RunWithLooper +@RunWith(AndroidTestingRunner.class) +public class PipResizeAnimatorTest { + + private static final float FLOAT_COMPARISON_DELTA = 0.001f; + + @Mock private Context mMockContext; + + @Mock private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory mMockFactory; + + @Mock private SurfaceControl.Transaction mMockTransaction; + + @Mock private SurfaceControl.Transaction mMockStartTransaction; + + @Mock private SurfaceControl.Transaction mMockFinishTransaction; + + @Mock private Runnable mMockStartCallback; + + @Mock private Runnable mMockEndCallback; + + private PipResizeAnimator mPipResizeAnimator; + private Rect mBaseBounds; + private Rect mStartBounds; + private Rect mEndBounds; + private SurfaceControl mTestLeash; + private ArgumentCaptor<Matrix> mArgumentCaptor; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mMockFactory.getTransaction()).thenReturn(mMockTransaction); + when(mMockTransaction.setMatrix(any(SurfaceControl.class), any(Matrix.class), any())) + .thenReturn(mMockTransaction); + when(mMockStartTransaction.setMatrix(any(SurfaceControl.class), any(Matrix.class), any())) + .thenReturn(mMockStartTransaction); + when(mMockFinishTransaction.setMatrix(any(SurfaceControl.class), any(Matrix.class), any())) + .thenReturn(mMockFinishTransaction); + + mArgumentCaptor = ArgumentCaptor.forClass(Matrix.class); + mTestLeash = new SurfaceControl.Builder() + .setContainerLayer() + .setName("PipResizeAnimatorTest") + .setCallsite("PipResizeAnimatorTest") + .build(); + } + + @Test + public void setAnimationStartCallback_resize_callbackStartCallback() { + mBaseBounds = new Rect(100, 100, 500, 500); + mStartBounds = new Rect(200, 200, 1_000, 1_000); + mEndBounds = new Rect(mBaseBounds); + final int duration = 10; + final float delta = 0; + mPipResizeAnimator = new PipResizeAnimator(mMockContext, mTestLeash, + mMockStartTransaction, mMockFinishTransaction, + mBaseBounds, mStartBounds, mEndBounds, + duration, delta); + + mPipResizeAnimator.setAnimationStartCallback(mMockStartCallback); + mPipResizeAnimator.setAnimationEndCallback(mMockEndCallback); + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + mPipResizeAnimator.start(); + mPipResizeAnimator.pause(); + }); + + verify(mMockStartCallback).run(); + verifyZeroInteractions(mMockEndCallback); + } + + @Test + public void setAnimationEndCallback_resize_callbackStartAndEndCallback() { + mBaseBounds = new Rect(100, 100, 500, 500); + mStartBounds = new Rect(200, 200, 1_000, 1_000); + mEndBounds = new Rect(mBaseBounds); + final int duration = 10; + final float delta = 0; + mPipResizeAnimator = new PipResizeAnimator(mMockContext, mTestLeash, + mMockStartTransaction, mMockFinishTransaction, + mBaseBounds, mStartBounds, mEndBounds, + duration, delta); + + mPipResizeAnimator.setAnimationStartCallback(mMockStartCallback); + mPipResizeAnimator.setAnimationEndCallback(mMockEndCallback); + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + mPipResizeAnimator.start(); + mPipResizeAnimator.end(); + }); + + verify(mMockStartCallback).run(); + verify(mMockEndCallback).run(); + } + + @Test + public void onAnimationEnd_resizeDown_sizeChanged() { + // Resize from 800x800 to 400x400, eg. resize down + mBaseBounds = new Rect(100, 100, 500, 500); + mStartBounds = new Rect(200, 200, 1_000, 1_000); + mEndBounds = new Rect(mBaseBounds); + final int duration = 10; + final float delta = 0; + final float[] matrix = new float[9]; + mPipResizeAnimator = new PipResizeAnimator(mMockContext, mTestLeash, + mMockStartTransaction, mMockFinishTransaction, + mBaseBounds, mStartBounds, mEndBounds, + duration, delta); + mPipResizeAnimator.setSurfaceControlTransactionFactory(mMockFactory); + + mPipResizeAnimator.setAnimationStartCallback(mMockStartCallback); + mPipResizeAnimator.setAnimationEndCallback(mMockEndCallback); + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + mPipResizeAnimator.start(); + clearInvocations(mMockTransaction); + mPipResizeAnimator.end(); + }); + + // Start transaction scales down from its final state + verify(mMockStartTransaction).setMatrix(eq(mTestLeash), mArgumentCaptor.capture(), any()); + mArgumentCaptor.getValue().getValues(matrix); + assertEquals(matrix[Matrix.MSCALE_X], + mStartBounds.width() / (float) mEndBounds.width(), FLOAT_COMPARISON_DELTA); + assertEquals(matrix[Matrix.MSCALE_Y], + mStartBounds.height() / (float) mEndBounds.height(), FLOAT_COMPARISON_DELTA); + assertEquals(matrix[Matrix.MTRANS_X], mStartBounds.left, FLOAT_COMPARISON_DELTA); + assertEquals(matrix[Matrix.MTRANS_Y], mStartBounds.top, FLOAT_COMPARISON_DELTA); + + // Final animation transaction scales to 1 and puts the leash at final position + verify(mMockTransaction).setMatrix(eq(mTestLeash), mArgumentCaptor.capture(), any()); + mArgumentCaptor.getValue().getValues(matrix); + assertEquals(matrix[Matrix.MSCALE_X], 1f, FLOAT_COMPARISON_DELTA); + assertEquals(matrix[Matrix.MSCALE_Y], 1f, FLOAT_COMPARISON_DELTA); + assertEquals(matrix[Matrix.MTRANS_X], mEndBounds.left, FLOAT_COMPARISON_DELTA); + assertEquals(matrix[Matrix.MTRANS_Y], mEndBounds.top, FLOAT_COMPARISON_DELTA); + + // Finish transaction resets scale and puts the leash at final position + verify(mMockFinishTransaction).setMatrix(eq(mTestLeash), mArgumentCaptor.capture(), any()); + mArgumentCaptor.getValue().getValues(matrix); + assertEquals(matrix[Matrix.MSCALE_X], 1f, FLOAT_COMPARISON_DELTA); + assertEquals(matrix[Matrix.MSCALE_Y], 1f, FLOAT_COMPARISON_DELTA); + assertEquals(matrix[Matrix.MTRANS_X], mEndBounds.left, FLOAT_COMPARISON_DELTA); + assertEquals(matrix[Matrix.MTRANS_Y], mEndBounds.top, FLOAT_COMPARISON_DELTA); + } + + @Test + public void onAnimationEnd_resizeUp_sizeChanged() { + // Resize from 400x400 to 800x800, eg. resize up + mBaseBounds = new Rect(200, 200, 1_000, 1_000); + mStartBounds = new Rect(100, 100, 500, 500); + mEndBounds = new Rect(mBaseBounds); + final int duration = 10; + final float delta = 0; + final float[] matrix = new float[9]; + mPipResizeAnimator = new PipResizeAnimator(mMockContext, mTestLeash, + mMockStartTransaction, mMockFinishTransaction, + mBaseBounds, mStartBounds, mEndBounds, + duration, delta); + mPipResizeAnimator.setSurfaceControlTransactionFactory(mMockFactory); + + mPipResizeAnimator.setAnimationStartCallback(mMockStartCallback); + mPipResizeAnimator.setAnimationEndCallback(mMockEndCallback); + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + mPipResizeAnimator.start(); + clearInvocations(mMockTransaction); + mPipResizeAnimator.end(); + }); + + // Start transaction scales up from its final state + verify(mMockStartTransaction).setMatrix(eq(mTestLeash), mArgumentCaptor.capture(), any()); + mArgumentCaptor.getValue().getValues(matrix); + assertEquals(matrix[Matrix.MSCALE_X], + mStartBounds.width() / (float) mEndBounds.width(), FLOAT_COMPARISON_DELTA); + assertEquals(matrix[Matrix.MSCALE_Y], + mStartBounds.height() / (float) mEndBounds.height(), FLOAT_COMPARISON_DELTA); + assertEquals(matrix[Matrix.MTRANS_X], mStartBounds.left, FLOAT_COMPARISON_DELTA); + assertEquals(matrix[Matrix.MTRANS_Y], mStartBounds.top, FLOAT_COMPARISON_DELTA); + + // Final animation transaction scales to 1 and puts the leash at final position + verify(mMockTransaction).setMatrix(eq(mTestLeash), mArgumentCaptor.capture(), any()); + mArgumentCaptor.getValue().getValues(matrix); + assertEquals(matrix[Matrix.MSCALE_X], 1f, FLOAT_COMPARISON_DELTA); + assertEquals(matrix[Matrix.MSCALE_Y], 1f, FLOAT_COMPARISON_DELTA); + assertEquals(matrix[Matrix.MTRANS_X], mEndBounds.left, FLOAT_COMPARISON_DELTA); + assertEquals(matrix[Matrix.MTRANS_Y], mEndBounds.top, FLOAT_COMPARISON_DELTA); + + // Finish transaction resets scale and puts the leash at final position + verify(mMockFinishTransaction).setMatrix(eq(mTestLeash), mArgumentCaptor.capture(), any()); + mArgumentCaptor.getValue().getValues(matrix); + assertEquals(matrix[Matrix.MSCALE_X], 1f, FLOAT_COMPARISON_DELTA); + assertEquals(matrix[Matrix.MSCALE_Y], 1f, FLOAT_COMPARISON_DELTA); + assertEquals(matrix[Matrix.MTRANS_X], mEndBounds.left, FLOAT_COMPARISON_DELTA); + assertEquals(matrix[Matrix.MTRANS_Y], mEndBounds.top, FLOAT_COMPARISON_DELTA); + } + + @Test + public void onAnimationEnd_withInitialDelta_rotateToZeroDegree() { + mBaseBounds = new Rect(200, 200, 1_000, 1_000); + mStartBounds = new Rect(100, 100, 500, 500); + mEndBounds = new Rect(mBaseBounds); + final int duration = 10; + final float delta = 45; + final float[] matrix = new float[9]; + mPipResizeAnimator = new PipResizeAnimator(mMockContext, mTestLeash, + mMockStartTransaction, mMockFinishTransaction, + mBaseBounds, mStartBounds, mEndBounds, + duration, delta); + mPipResizeAnimator.setSurfaceControlTransactionFactory(mMockFactory); + + mPipResizeAnimator.setAnimationStartCallback(mMockStartCallback); + mPipResizeAnimator.setAnimationEndCallback(mMockEndCallback); + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + mPipResizeAnimator.start(); + clearInvocations(mMockTransaction); + mPipResizeAnimator.end(); + }); + + // Final animation transaction sets skew to zero + verify(mMockTransaction).setMatrix(eq(mTestLeash), mArgumentCaptor.capture(), any()); + mArgumentCaptor.getValue().getValues(matrix); + assertEquals(matrix[Matrix.MSKEW_X], 0f, FLOAT_COMPARISON_DELTA); + assertEquals(matrix[Matrix.MSKEW_Y], 0f, FLOAT_COMPARISON_DELTA); + + // Finish transaction sets skew to zero + verify(mMockFinishTransaction).setMatrix(eq(mTestLeash), mArgumentCaptor.capture(), any()); + mArgumentCaptor.getValue().getValues(matrix); + assertEquals(matrix[Matrix.MSKEW_X], 0f, FLOAT_COMPARISON_DELTA); + assertEquals(matrix[Matrix.MSKEW_Y], 0f, FLOAT_COMPARISON_DELTA); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt index afdb68776d04..efe4fb18f273 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt @@ -34,6 +34,7 @@ import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.TransitionInfoBuilder import com.android.wm.shell.transition.Transitions +import com.android.wm.shell.windowdecor.extension.isFullscreen import com.google.common.truth.Truth.assertThat import dagger.Lazy import org.junit.Before @@ -107,8 +108,8 @@ class TaskStackTransitionObserverTest { callOnTransitionFinished() executor.flushAll() - assertThat(listener.taskInfoToBeNotified.taskId).isEqualTo(change.taskInfo?.taskId) - assertThat(listener.taskInfoToBeNotified.windowingMode) + assertThat(listener.taskInfoOnTaskMovedToFront.taskId).isEqualTo(change.taskInfo?.taskId) + assertThat(listener.taskInfoOnTaskMovedToFront.windowingMode) .isEqualTo(change.taskInfo?.windowingMode) } @@ -130,8 +131,8 @@ class TaskStackTransitionObserverTest { callOnTransitionFinished() executor.flushAll() - assertThat(listener.taskInfoToBeNotified.taskId).isEqualTo(1) - assertThat(listener.taskInfoToBeNotified.windowingMode) + assertThat(listener.taskInfoOnTaskMovedToFront.taskId).isEqualTo(1) + assertThat(listener.taskInfoOnTaskMovedToFront.windowingMode) .isEqualTo(WindowConfiguration.WINDOWING_MODE_FULLSCREEN) } @@ -161,9 +162,9 @@ class TaskStackTransitionObserverTest { callOnTransitionFinished() executor.flushAll() - assertThat(listener.taskInfoToBeNotified.taskId) + assertThat(listener.taskInfoOnTaskMovedToFront.taskId) .isEqualTo(freeformOpenChange.taskInfo?.taskId) - assertThat(listener.taskInfoToBeNotified.windowingMode) + assertThat(listener.taskInfoOnTaskMovedToFront.windowingMode) .isEqualTo(freeformOpenChange.taskInfo?.windowingMode) } @@ -199,9 +200,15 @@ class TaskStackTransitionObserverTest { callOnTransitionFinished() executor.flushAll() - assertThat(listener.taskInfoToBeNotified.taskId).isEqualTo(change.taskInfo?.taskId) - assertThat(listener.taskInfoToBeNotified.windowingMode) + assertThat(listener.taskInfoOnTaskMovedToFront.taskId).isEqualTo(change.taskInfo?.taskId) + assertThat(listener.taskInfoOnTaskMovedToFront.windowingMode) .isEqualTo(change.taskInfo?.windowingMode) + + assertThat(listener.taskInfoOnTaskChanged.size).isEqualTo(1) + with(listener.taskInfoOnTaskChanged.last()) { + assertThat(taskId).isEqualTo(mergedChange.taskInfo?.taskId) + assertThat(windowingMode).isEqualTo(mergedChange.taskInfo?.windowingMode) + } } @Test @@ -236,18 +243,151 @@ class TaskStackTransitionObserverTest { callOnTransitionFinished() executor.flushAll() - assertThat(listener.taskInfoToBeNotified.taskId).isEqualTo(mergedChange.taskInfo?.taskId) - assertThat(listener.taskInfoToBeNotified.windowingMode) - .isEqualTo(mergedChange.taskInfo?.windowingMode) + assertThat(listener.taskInfoOnTaskMovedToFront.taskId) + .isEqualTo(mergedChange.taskInfo?.taskId) + assertThat(listener.taskInfoOnTaskMovedToFront.windowingMode) + .isEqualTo(mergedChange.taskInfo?.windowingMode) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL) + fun taskChange_freeformWindowToFullscreenWindow_listenerNotified() { + val listener = TestListener() + val executor = TestShellExecutor() + transitionObserver.addTaskStackTransitionObserverListener(listener, executor) + val freeformState = + createChange( + WindowManager.TRANSIT_OPEN, + createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FREEFORM) + ) + val transitionInfoOpen = + TransitionInfoBuilder(WindowManager.TRANSIT_OPEN, 0).addChange(freeformState).build() + callOnTransitionReady(transitionInfoOpen) + callOnTransitionFinished() + executor.flushAll() + + assertThat(listener.taskInfoOnTaskMovedToFront.taskId) + .isEqualTo(freeformState.taskInfo?.taskId) + assertThat(listener.taskInfoOnTaskMovedToFront.windowingMode) + .isEqualTo(freeformState.taskInfo?.windowingMode) + assertThat(listener.taskInfoOnTaskMovedToFront.isFullscreen).isEqualTo(false) + + // create change transition to update the windowing mode to full screen. + val fullscreenState = + createChange( + WindowManager.TRANSIT_CHANGE, + createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FULLSCREEN) + ) + val transitionInfoChange = + TransitionInfoBuilder(WindowManager.TRANSIT_CHANGE, 0) + .addChange(fullscreenState) + .build() + + callOnTransitionReady(transitionInfoChange) + callOnTransitionFinished() + executor.flushAll() + + // Asserting whether freeformState remains the same as before the change + assertThat(listener.taskInfoOnTaskMovedToFront.taskId) + .isEqualTo(freeformState.taskInfo?.taskId) + assertThat(listener.taskInfoOnTaskMovedToFront.isFullscreen).isEqualTo(false) + + // Asserting changes + assertThat(listener.taskInfoOnTaskChanged.size).isEqualTo(1) + with(listener.taskInfoOnTaskChanged.last()) { + assertThat(taskId).isEqualTo(fullscreenState.taskInfo?.taskId) + assertThat(isFullscreen).isEqualTo(true) + } + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL) + fun singleTransition_withOpenAndChange_onlyOpenIsNotified() { + val listener = TestListener() + val executor = TestShellExecutor() + transitionObserver.addTaskStackTransitionObserverListener(listener, executor) + + // Creating multiple changes to be fired in a single transition + val freeformState = + createChange( + mode = WindowManager.TRANSIT_OPEN, + taskInfo = createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FREEFORM) + ) + val fullscreenState = + createChange( + mode = WindowManager.TRANSIT_CHANGE, + taskInfo = createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FULLSCREEN) + ) + + val transitionInfoWithChanges = + TransitionInfoBuilder(WindowManager.TRANSIT_CHANGE, 0) + .addChange(freeformState) + .addChange(fullscreenState) + .build() + + callOnTransitionReady(transitionInfoWithChanges) + callOnTransitionFinished() + executor.flushAll() + + assertThat(listener.taskInfoOnTaskMovedToFront.taskId) + .isEqualTo(freeformState.taskInfo?.taskId) + assertThat(listener.taskInfoOnTaskMovedToFront.isFullscreen).isEqualTo(false) + assertThat(listener.taskInfoOnTaskChanged.size).isEqualTo(0) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL) + fun singleTransition_withMultipleChanges_listenerNotified_forEachChange() { + val listener = TestListener() + val executor = TestShellExecutor() + transitionObserver.addTaskStackTransitionObserverListener(listener, executor) + + val taskId = 1 + + // Creating multiple changes to be fired in a single transition + val changes = + listOf( + WindowConfiguration.WINDOWING_MODE_FREEFORM, + WindowConfiguration.WINDOW_CONFIG_DISPLAY_ROTATION, + WindowConfiguration.WINDOWING_MODE_FULLSCREEN + ) + .map { change -> + createChange( + mode = WindowManager.TRANSIT_CHANGE, + taskInfo = createTaskInfo(taskId, change) + ) + } + + val transitionInfoWithChanges = + TransitionInfoBuilder(WindowManager.TRANSIT_CHANGE, 0) + .apply { changes.forEach { c -> this@apply.addChange(c) } } + .build() + + callOnTransitionReady(transitionInfoWithChanges) + callOnTransitionFinished() + executor.flushAll() + + assertThat(listener.taskInfoOnTaskChanged.size).isEqualTo(changes.size) + changes.forEachIndexed { index, change -> + assertThat(listener.taskInfoOnTaskChanged[index].taskId) + .isEqualTo(change.taskInfo?.taskId) + assertThat(listener.taskInfoOnTaskChanged[index].windowingMode) + .isEqualTo(change.taskInfo?.windowingMode) + } } class TestListener : TaskStackTransitionObserver.TaskStackTransitionObserverListener { - var taskInfoToBeNotified = ActivityManager.RunningTaskInfo() + var taskInfoOnTaskMovedToFront = ActivityManager.RunningTaskInfo() + var taskInfoOnTaskChanged = mutableListOf<ActivityManager.RunningTaskInfo>() override fun onTaskMovedToFrontThroughTransition( taskInfo: ActivityManager.RunningTaskInfo ) { - taskInfoToBeNotified = taskInfo + taskInfoOnTaskMovedToFront = taskInfo + } + + override fun onTaskChangedThroughTransition(taskInfo: ActivityManager.RunningTaskInfo) { + taskInfoOnTaskChanged += taskInfo } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/animation/WindowAnimatorTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/animation/WindowAnimatorTest.kt index 841ffcc60c88..c19232b6f787 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/animation/WindowAnimatorTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/animation/WindowAnimatorTest.kt @@ -16,7 +16,7 @@ package com.android.wm.shell.shared.animation -import android.animation.ValueAnimator +import android.graphics.PointF import android.graphics.Rect import android.util.DisplayMetrics import android.view.SurfaceControl @@ -31,6 +31,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.anyFloat +import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.mock import org.mockito.kotlin.whenever @@ -44,12 +45,33 @@ class WindowAnimatorTest { private val displayMetrics = DisplayMetrics().apply { density = 1f } + private val positionXArgumentCaptor = argumentCaptor<Float>() + private val positionYArgumentCaptor = argumentCaptor<Float>() + private val scaleXArgumentCaptor = argumentCaptor<Float>() + private val scaleYArgumentCaptor = argumentCaptor<Float>() + @Before fun setup() { whenever(change.leash).thenReturn(leash) - whenever(change.startAbsBounds).thenReturn(START_BOUNDS) + whenever(change.endAbsBounds).thenReturn(END_BOUNDS) whenever(transaction.setPosition(any(), anyFloat(), anyFloat())).thenReturn(transaction) whenever(transaction.setScale(any(), anyFloat(), anyFloat())).thenReturn(transaction) + whenever( + transaction.setPosition( + any(), + positionXArgumentCaptor.capture(), + positionYArgumentCaptor.capture(), + ) + ) + .thenReturn(transaction) + whenever( + transaction.setScale( + any(), + scaleXArgumentCaptor.capture(), + scaleYArgumentCaptor.capture(), + ) + ) + .thenReturn(transaction) } @Test @@ -67,16 +89,18 @@ class WindowAnimatorTest { change, transaction ) + valueAnimator.start() assertThat(valueAnimator.duration).isEqualTo(100L) assertThat(valueAnimator.interpolator).isEqualTo(Interpolators.STANDARD_ACCELERATE) - assertStartAndEndBounds(valueAnimator, startBounds = START_BOUNDS, endBounds = START_BOUNDS) + val expectedPosition = PointF(END_BOUNDS.left.toFloat(), END_BOUNDS.top.toFloat()) + assertTransactionParams(expectedPosition, expectedScale = PointF(1f, 1f)) } @Test - fun createBoundsAnimator_startScaleAndOffset_returnsCorrectBounds() = runOnUiThread { + fun createBoundsAnimator_startScaleAndOffset_correctPosAndScale() = runOnUiThread { val bounds = Rect(/* left= */ 100, /* top= */ 200, /* right= */ 300, /* bottom= */ 400) - whenever(change.startAbsBounds).thenReturn(bounds) + whenever(change.endAbsBounds).thenReturn(bounds) val boundsAnimParams = WindowAnimator.BoundsAnimationParams( durationMs = 100L, @@ -92,19 +116,18 @@ class WindowAnimatorTest { change, transaction ) + valueAnimator.start() - assertStartAndEndBounds( - valueAnimator, - startBounds = - Rect(/* left= */ 150, /* top= */ 260, /* right= */ 250, /* bottom= */ 360), - endBounds = bounds, + assertTransactionParams( + expectedPosition = PointF(150f, 260f), + expectedScale = PointF(0.5f, 0.5f), ) } @Test - fun createBoundsAnimator_endScaleAndOffset_returnsCorrectBounds() = runOnUiThread { + fun createBoundsAnimator_endScaleAndOffset_correctPosAndScale() = runOnUiThread { val bounds = Rect(/* left= */ 100, /* top= */ 200, /* right= */ 300, /* bottom= */ 400) - whenever(change.startAbsBounds).thenReturn(bounds) + whenever(change.endAbsBounds).thenReturn(bounds) val boundsAnimParams = WindowAnimator.BoundsAnimationParams( durationMs = 100L, @@ -120,28 +143,56 @@ class WindowAnimatorTest { change, transaction ) + valueAnimator.start() + valueAnimator.end() - assertStartAndEndBounds( - valueAnimator, - startBounds = bounds, - endBounds = Rect(/* left= */ 150, /* top= */ 260, /* right= */ 250, /* bottom= */ 360), + assertTransactionParams( + expectedPosition = PointF(150f, 260f), + expectedScale = PointF(0.5f, 0.5f), ) } - private fun assertStartAndEndBounds( - valueAnimator: ValueAnimator, - startBounds: Rect, - endBounds: Rect, - ) { - valueAnimator.start() - valueAnimator.animatedValue - assertThat(valueAnimator.animatedValue).isEqualTo(startBounds) - valueAnimator.end() - assertThat(valueAnimator.animatedValue).isEqualTo(endBounds) + @Test + fun createBoundsAnimator_middleOfAnimation_correctPosAndScale() = runOnUiThread { + val bounds = Rect(/* left= */ 100, /* top= */ 200, /* right= */ 300, /* bottom= */ 400) + whenever(change.endAbsBounds).thenReturn(bounds) + val boundsAnimParams = + WindowAnimator.BoundsAnimationParams( + durationMs = 100L, + endOffsetYDp = 10f, + startScale = 0.5f, + endScale = 0.9f, + interpolator = Interpolators.LINEAR, + ) + + val valueAnimator = + WindowAnimator.createBoundsAnimator( + displayMetrics, + boundsAnimParams, + change, + transaction + ) + valueAnimator.currentPlayTime = 50 + + assertTransactionParams( + // We should have a window of size 140x140, which we centre by placing at pos 130, 230. + // Then add 10*0.5 as y-offset + expectedPosition = PointF(130f, 235f), + expectedScale = PointF(0.7f, 0.7f), + ) + } + + private fun assertTransactionParams(expectedPosition: PointF, expectedScale: PointF) { + assertThat(positionXArgumentCaptor.lastValue).isWithin(TOLERANCE).of(expectedPosition.x) + assertThat(positionYArgumentCaptor.lastValue).isWithin(TOLERANCE).of(expectedPosition.y) + assertThat(scaleXArgumentCaptor.lastValue).isWithin(TOLERANCE).of(expectedScale.x) + assertThat(scaleYArgumentCaptor.lastValue).isWithin(TOLERANCE).of(expectedScale.y) } companion object { - private val START_BOUNDS = + private val END_BOUNDS = Rect(/* left= */ 10, /* top= */ 20, /* right= */ 30, /* bottom= */ 40) + + private const val TOLERANCE = 1e-3f } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/util/WindowingEducationTestUtils.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/util/WindowingEducationTestUtils.kt index 708fadb7fbe2..99e82959fcd6 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/util/WindowingEducationTestUtils.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/util/WindowingEducationTestUtils.kt @@ -32,11 +32,13 @@ fun createAppHandleState( runningTaskInfo: RunningTaskInfo = createTaskInfo(), isHandleMenuExpanded: Boolean = false, globalAppHandleBounds: Rect = Rect(), + isCapturedLinkAvailable: Boolean = false ): CaptionState.AppHandle = CaptionState.AppHandle( runningTaskInfo = runningTaskInfo, isHandleMenuExpanded = isHandleMenuExpanded, - globalAppHandleBounds = globalAppHandleBounds) + globalAppHandleBounds = globalAppHandleBounds, + isCapturedLinkAvailable = isCapturedLinkAvailable) /** * Create an instance of [CaptionState.AppHeader] with parameters as properties. @@ -47,11 +49,13 @@ fun createAppHeaderState( runningTaskInfo: RunningTaskInfo = createTaskInfo(), isHeaderMenuExpanded: Boolean = false, globalAppChipBounds: Rect = Rect(), + isCapturedLinkAvailable: Boolean = false ): CaptionState.AppHeader = CaptionState.AppHeader( runningTaskInfo = runningTaskInfo, isHeaderMenuExpanded = isHeaderMenuExpanded, - globalAppChipBounds = globalAppChipBounds) + globalAppChipBounds = globalAppChipBounds, + isCapturedLinkAvailable = isCapturedLinkAvailable) /** * Create an instance of [RunningTaskInfo] with parameters as properties. diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenuTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenuTest.kt index cb8c74307449..1215c52209a5 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenuTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenuTest.kt @@ -64,6 +64,7 @@ class DesktopHeaderManageWindowsMenuTest : ShellTestCase() { context = context, shellInit = ShellInit(TestShellExecutor()), persistentRepository = mock(), + repositoryInitializer = mock(), mainCoroutineScope = mock() ) } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt index c42be7fef2a6..03aab18d8d87 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt @@ -95,6 +95,7 @@ import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition import com.android.wm.shell.desktopmode.DesktopTasksLimiter import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository import com.android.wm.shell.desktopmode.education.AppHandleEducationController +import com.android.wm.shell.desktopmode.education.AppToWebEducationController import com.android.wm.shell.freeform.FreeformTaskTransitionStarter import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource @@ -195,6 +196,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { DesktopModeWindowDecorViewModel.TaskPositionerFactory @Mock private lateinit var mockTaskPositioner: TaskPositioner @Mock private lateinit var mockAppHandleEducationController: AppHandleEducationController + @Mock private lateinit var mockAppToWebEducationController: AppToWebEducationController @Mock private lateinit var mockFocusTransitionObserver: FocusTransitionObserver @Mock private lateinit var mockCaptionHandleRepository: WindowDecorCaptionHandleRepository @Mock private lateinit var motionEvent: MotionEvent @@ -261,6 +263,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { mockInteractionJankMonitor, Optional.of(mockTasksLimiter), mockAppHandleEducationController, + mockAppToWebEducationController, mockCaptionHandleRepository, Optional.of(mockActivityOrientationChangeHandler), mockTaskPositionerFactory, @@ -946,7 +949,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { } @Test - fun testDecor_onClickToSplitScreen_detachesStatusBarInputLayer() { + fun testDecor_onClickToSplitScreen_disposesStatusBarInputLayer() { val toSplitScreenListenerCaptor = forClass(Function0::class.java) as ArgumentCaptor<Function0<Unit>> val decor = createOpenTaskDecoration( @@ -956,7 +959,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { toSplitScreenListenerCaptor.value.invoke() - verify(decor).detachStatusBarInputLayer() + verify(decor).disposeStatusBarInputLayer() } @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java index 8a2c7782906d..41f57ae0fd97 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java @@ -49,6 +49,7 @@ 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.mockito.kotlin.VerificationKt.times; import android.app.ActivityManager; import android.app.assist.AssistContent; @@ -261,8 +262,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { doReturn(defaultDisplay).when(mMockDisplayController).getDisplay(Display.DEFAULT_DISPLAY); doReturn(mInsetsState).when(mMockDisplayController).getInsetsState(anyInt()); when(mMockHandleMenuFactory.create(any(), any(), anyInt(), any(), any(), any(), - anyBoolean(), anyBoolean(), anyBoolean(), any(), anyInt(), anyInt(), anyInt(), - anyInt())) + anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(), any(), anyInt(), anyInt(), + anyInt(), anyInt())) .thenReturn(mMockHandleMenu); when(mMockMultiInstanceHelper.supportsMultiInstanceSplit(any())).thenReturn(false); when(mMockAppHeaderViewHolderFactory.create(any(), any(), any(), any(), any(), any(), any(), @@ -849,7 +850,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class); spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */); - verify(mMockHandler).post(runnableArgument.capture()); + // Once for view host, the other for the AppHandle input layer. + verify(mMockHandler, times(2)).post(runnableArgument.capture()); runnableArgument.getValue().run(); verify(mMockSurfaceControlViewHostFactory).create(any(), any(), any()); } @@ -876,7 +878,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class); spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */); - verify(mMockHandler).post(runnableArgument.capture()); + // Once for view host, the other for the AppHandle input layer. + verify(mMockHandler, times(2)).post(runnableArgument.capture()); spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */); @@ -890,7 +893,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class); spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */); - verify(mMockHandler).post(runnableArgument.capture()); + // Once for view host, the other for the AppHandle input layer. + verify(mMockHandler, times(2)).post(runnableArgument.capture()); spyWindowDecor.close(); @@ -1174,6 +1178,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { any(), any(), any(), + any(), openInBrowserCaptor.capture(), any(), any(), @@ -1204,6 +1209,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { any(), any(), any(), + any(), openInBrowserCaptor.capture(), any(), any(), @@ -1259,6 +1265,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { any(), any(), any(), + any(), closeClickListener.capture(), any(), anyBoolean() @@ -1290,12 +1297,14 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { any(), any(), any(), + any(), /* forceShowSystemBars= */ eq(true) ); } @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) + @DisableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_TO_WEB_EDUCATION}) public void notifyCaptionStateChanged_flagDisabled_doNoNotify() { when(DesktopModeStatus.canEnterDesktopMode(mContext)).thenReturn(true); final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); @@ -1433,7 +1442,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { private void verifyHandleMenuCreated(@Nullable Uri uri) { verify(mMockHandleMenuFactory).create(any(), any(), anyInt(), any(), any(), - any(), anyBoolean(), anyBoolean(), anyBoolean(), + any(), anyBoolean(), anyBoolean(), anyBoolean(), anyBoolean(), argThat(intent -> (uri == null && intent == null) || intent.getData().equals(uri)), anyInt(), anyInt(), anyInt(), anyInt()); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt index 9544fa823b5a..ade17c61eda1 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt @@ -266,6 +266,7 @@ class HandleMenuTest : ShellTestCase() { WindowManagerWrapper(mockWindowManager), layoutId, appIcon, appName, splitScreenController, shouldShowWindowingPill = true, shouldShowNewWindowButton = true, shouldShowManageWindowsButton = false, + shouldShowChangeAspectRatioButton = false, null /* openInBrowserLink */, captionWidth = HANDLE_WIDTH, captionHeight = 50, captionX = captionX, captionY = 0, @@ -276,6 +277,7 @@ class HandleMenuTest : ShellTestCase() { onToSplitScreenClickListener = mock(), onNewWindowClickListener = mock(), onManageWindowsClickListener = mock(), + onChangeAspectRatioClickListener = mock(), openInBrowserClickListener = mock(), onOpenByDefaultClickListener = mock(), onCloseMenuClickListener = mock(), diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java index cb7fadee9822..8e0434cb28f7 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java @@ -218,8 +218,6 @@ public class WindowDecorationTests extends ShellTestCase { verify(captionContainerSurfaceBuilder, never()).build(); verify(mMockSurfaceControlViewHostFactory, never()).create(any(), any(), any()); - verify(mMockSurfaceControlFinishT).hide(mMockTaskSurface); - assertNull(mRelayoutResult.mRootView); } @@ -281,8 +279,6 @@ public class WindowDecorationTests extends ShellTestCase { verify(mMockSurfaceControlStartT).setCornerRadius(mMockTaskSurface, CORNER_RADIUS); verify(mMockSurfaceControlFinishT).setCornerRadius(mMockTaskSurface, CORNER_RADIUS); - verify(mMockSurfaceControlStartT) - .show(mMockTaskSurface); verify(mMockSurfaceControlStartT).setShadowRadius(mMockTaskSurface, 10); assertEquals(300, mRelayoutResult.mWidth); @@ -863,7 +859,7 @@ public class WindowDecorationTests extends ShellTestCase { final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo); - mRelayoutParams.mSetTaskPositionAndCrop = false; + mRelayoutParams.mSetTaskVisibilityPositionAndCrop = false; windowDecor.relayout(taskInfo, true /* hasGlobalFocus */); verify(mMockSurfaceControlStartT, never()).setWindowCrop( @@ -891,7 +887,7 @@ public class WindowDecorationTests extends ShellTestCase { taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2; final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo); - mRelayoutParams.mSetTaskPositionAndCrop = true; + mRelayoutParams.mSetTaskVisibilityPositionAndCrop = true; windowDecor.relayout(taskInfo, true /* hasGlobalFocus */); verify(mMockSurfaceControlStartT).setWindowCrop( diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipControllerTest.kt index 741dfb8dd885..15f2c7be38b7 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/education/DesktopWindowingEducationTooltipControllerTest.kt @@ -285,7 +285,7 @@ class DesktopWindowingEducationTooltipControllerTest : ShellTestCase() { onEducationClickAction: () -> Unit = {}, onDismissAction: () -> Unit = {} ) = - DesktopWindowingEducationTooltipController.EducationViewConfig( + DesktopWindowingEducationTooltipController.TooltipEducationViewConfig( tooltipViewLayout = tooltipViewLayout, tooltipColorScheme = tooltipColorScheme, tooltipViewGlobalCoordinates = tooltipViewGlobalCoordinates, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModelTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModelTest.kt index 0ccd424c2b30..80ad1df44a1b 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModelTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModelTest.kt @@ -36,9 +36,11 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.any +import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.times import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidTestingRunner::class) @@ -73,6 +75,7 @@ class DesktopTilingDecorViewModelTest : ShellTestCase() { returnToDragStartAnimatorMock, desktopRepository, ) + whenever(contextMock.createContextAsUser(any(), any())).thenReturn(contextMock) } @Test @@ -143,7 +146,7 @@ class DesktopTilingDecorViewModelTest : ShellTestCase() { } @Test - fun userChange_starting_allTilingSessionsShouldBeDestroyed() { + fun onUserChange_allTilingSessionsShouldBeDestroyed() { desktopTilingDecorViewModel.tilingTransitionHandlerByDisplayId.put( 1, desktopTilingDecoration, @@ -155,7 +158,29 @@ class DesktopTilingDecorViewModelTest : ShellTestCase() { desktopTilingDecorViewModel.onUserChange() - verify(desktopTilingDecoration, times(2)).onUserChange() + verify(desktopTilingDecoration, times(2)).resetTilingSession() + } + + @Test + fun displayOrientationChange_tilingForDisplayShouldBeDestroyed() { + desktopTilingDecorViewModel.tilingTransitionHandlerByDisplayId.put( + 1, + desktopTilingDecoration, + ) + desktopTilingDecorViewModel.tilingTransitionHandlerByDisplayId.put( + 2, + desktopTilingDecoration, + ) + + desktopTilingDecorViewModel.onDisplayChange(1, 1, 2, null, null) + + verify(desktopTilingDecoration, times(1)).resetTilingSession() + verify(displayControllerMock, times(1)) + .addDisplayChangingController(eq(desktopTilingDecorViewModel)) + + desktopTilingDecorViewModel.onDisplayChange(1, 1, 3, null, null) + // No extra calls after 180 degree change. + verify(desktopTilingDecoration, times(1)).resetTilingSession() } companion object { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManagerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManagerTest.kt index 0ee3f4695e85..3143946fa828 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManagerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManagerTest.kt @@ -16,9 +16,12 @@ package com.android.wm.shell.windowdecor.tiling +import android.content.Context import android.content.res.Configuration import android.graphics.Rect import android.testing.AndroidTestingRunner +import android.view.Display +import android.view.RoundedCorner import android.view.SurfaceControl import androidx.test.annotation.UiThreadTest import androidx.test.filters.SmallTest @@ -29,6 +32,7 @@ import kotlin.test.Test import org.junit.Before import org.junit.runner.RunWith import org.mockito.kotlin.any +import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.times import org.mockito.kotlin.verify @@ -55,10 +59,17 @@ class DesktopTilingDividerWindowManagerTest : ShellTestCase() { private lateinit var desktopTilingWindowManager: DesktopTilingDividerWindowManager + private val context = mock<Context>() + private val display = mock<Display>() + private val roundedCorner = mock<RoundedCorner>() + @Before fun setup() { config = Configuration() config.setToDefaults() + whenever(context.display).thenReturn(display) + whenever(display.getRoundedCorner(any())).thenReturn(roundedCorner) + whenever(roundedCorner.radius).thenReturn(CORNER_RADIUS) desktopTilingWindowManager = DesktopTilingDividerWindowManager( config, @@ -69,6 +80,7 @@ class DesktopTilingDividerWindowManagerTest : ShellTestCase() { transitionHandlerMock, transactionSupplierMock, BOUNDS, + context, ) } @@ -85,7 +97,6 @@ class DesktopTilingDividerWindowManagerTest : ShellTestCase() { // Ensure a surfaceControl transaction runs to show the divider. verify(transactionSupplierMock, times(1)).get() - verify(syncQueueMock, times(1)).runInSync(any()) desktopTilingWindowManager.release() verify(transaction, times(1)).hide(any()) @@ -93,7 +104,24 @@ class DesktopTilingDividerWindowManagerTest : ShellTestCase() { verify(transaction, times(1)).apply() } + @Test + @UiThreadTest + fun testWindowManager_accountsForRoundedCornerDimensions() { + whenever(transactionSupplierMock.get()).thenReturn(transaction) + whenever(transaction.setRelativeLayer(any(), any(), any())).thenReturn(transaction) + whenever(transaction.setRelativeLayer(any(), any(), any())).thenReturn(transaction) + whenever(transaction.setPosition(any(), any(), any())).thenReturn(transaction) + whenever(transaction.show(any())).thenReturn(transaction) + + desktopTilingWindowManager.generateViewHost(surfaceControl) + + // Ensure a surfaceControl transaction runs to show the divider. + verify(transaction, times(1)) + .setPosition(any(), eq(BOUNDS.left.toFloat() - CORNER_RADIUS), any()) + } + companion object { private val BOUNDS = Rect(1, 2, 3, 4) + private val CORNER_RADIUS = 28 } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt index 0b04a211f717..f371f5223419 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt @@ -49,6 +49,7 @@ import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.eq import org.mockito.Captor import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.capture import org.mockito.kotlin.mock import org.mockito.kotlin.never @@ -112,6 +113,7 @@ class DesktopTilingWindowDecorationTest : ShellTestCase() { returnToDragStartAnimator, desktopRepository, ) + whenever(context.createContextAsUser(any(), any())).thenReturn(context) } @Test @@ -195,7 +197,7 @@ class DesktopTilingWindowDecorationTest : ShellTestCase() { verify(toggleResizeDesktopTaskTransitionHandler, times(1)) .startTransition(capture(wctCaptor), any()) - verify(returnToDragStartAnimator, times(1)).start(any(), any(), any(), any(), any()) + verify(returnToDragStartAnimator, times(1)).start(any(), any(), any(), any(), anyOrNull()) for (change in wctCaptor.value.changes) { val bounds = change.value.configuration.windowConfiguration.bounds val leftBounds = getLeftTaskBounds() @@ -473,12 +475,13 @@ class DesktopTilingWindowDecorationTest : ShellTestCase() { tilingDecoration.rightTaskResizingHelper = tiledTaskHelper tilingDecoration.desktopTilingDividerWindowManager = desktopTilingDividerWindowManager - tilingDecoration.onUserChange() + tilingDecoration.resetTilingSession() assertThat(tilingDecoration.leftTaskResizingHelper).isNull() assertThat(tilingDecoration.rightTaskResizingHelper).isNull() verify(desktopWindowDecoration, times(2)).removeDragResizeListener(any()) verify(tiledTaskHelper, times(2)).dispose() + verify(context, never()).getApplicationContext() } private fun initTiledTaskHelperMock(taskInfo: ActivityManager.RunningTaskInfo) { diff --git a/libs/androidfw/PngCrunch.cpp b/libs/androidfw/PngCrunch.cpp index cf3c0eeff402..e94540544cf5 100644 --- a/libs/androidfw/PngCrunch.cpp +++ b/libs/androidfw/PngCrunch.cpp @@ -506,8 +506,7 @@ bool WritePng(const Image* image, const NinePatch* nine_patch, OutputStream* out // Set up the write functions which write to our custom data sources. png_set_write_fn(write_ptr, (png_voidp)out, WriteDataToStream, nullptr); - // We want small files and can take the performance hit to achieve this goal. - png_set_compression_level(write_ptr, Z_BEST_COMPRESSION); + png_set_compression_level(write_ptr, options.compression_level); // Begin analysis of the image data. // Scan the entire image and determine if: diff --git a/libs/androidfw/include/androidfw/Png.h b/libs/androidfw/include/androidfw/Png.h index 2ece43e08110..72be59b8f313 100644 --- a/libs/androidfw/include/androidfw/Png.h +++ b/libs/androidfw/include/androidfw/Png.h @@ -31,6 +31,8 @@ constexpr size_t kPngSignatureSize = 8u; struct PngOptions { int grayscale_tolerance = 0; + // By default we want small files and can take the performance hit to achieve this goal. + int compression_level = 9; }; /** diff --git a/libs/appfunctions/api/current.txt b/libs/appfunctions/api/current.txt index 27817e9eb984..faf84a8ab5ac 100644 --- a/libs/appfunctions/api/current.txt +++ b/libs/appfunctions/api/current.txt @@ -3,8 +3,9 @@ package com.google.android.appfunctions.sidecar { public final class AppFunctionManager { ctor public AppFunctionManager(android.content.Context); - method public void executeAppFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>); - method public void isAppFunctionEnabled(@NonNull String, @NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,java.lang.Exception>); + method @RequiresPermission(anyOf={android.Manifest.permission.EXECUTE_APP_FUNCTIONS_TRUSTED, android.Manifest.permission.EXECUTE_APP_FUNCTIONS}, conditional=true) public void executeAppFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>); + method @RequiresPermission(anyOf={android.Manifest.permission.EXECUTE_APP_FUNCTIONS_TRUSTED, android.Manifest.permission.EXECUTE_APP_FUNCTIONS}, conditional=true) public void isAppFunctionEnabled(@NonNull String, @NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,java.lang.Exception>); + method public void isAppFunctionEnabled(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,java.lang.Exception>); method public void setAppFunctionEnabled(@NonNull String, int, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,java.lang.Exception>); field public static final int APP_FUNCTION_STATE_DEFAULT = 0; // 0x0 field public static final int APP_FUNCTION_STATE_DISABLED = 2; // 0x2 @@ -34,6 +35,7 @@ package com.google.android.appfunctions.sidecar { } public final class ExecuteAppFunctionResponse { + method public int getErrorCategory(); method @Nullable public String getErrorMessage(); method @NonNull public android.os.Bundle getExtras(); method public int getResultCode(); @@ -41,14 +43,19 @@ package com.google.android.appfunctions.sidecar { method public boolean isSuccess(); method @NonNull public static com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse newFailure(int, @Nullable String, @Nullable android.os.Bundle); method @NonNull public static com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse newSuccess(@NonNull android.app.appsearch.GenericDocument, @Nullable android.os.Bundle); + field public static final int ERROR_CATEGORY_APP = 3; // 0x3 + field public static final int ERROR_CATEGORY_REQUEST_ERROR = 1; // 0x1 + field public static final int ERROR_CATEGORY_SYSTEM = 2; // 0x2 + field public static final int ERROR_CATEGORY_UNKNOWN = 0; // 0x0 field public static final String PROPERTY_RETURN_VALUE = "returnValue"; - field public static final int RESULT_APP_UNKNOWN_ERROR = 2; // 0x2 - field public static final int RESULT_CANCELLED = 6; // 0x6 - field public static final int RESULT_DENIED = 1; // 0x1 - field public static final int RESULT_DISABLED = 5; // 0x5 - field public static final int RESULT_INTERNAL_ERROR = 3; // 0x3 - field public static final int RESULT_INVALID_ARGUMENT = 4; // 0x4 + field public static final int RESULT_APP_UNKNOWN_ERROR = 3000; // 0xbb8 + field public static final int RESULT_CANCELLED = 2001; // 0x7d1 + field public static final int RESULT_DENIED = 1000; // 0x3e8 + field public static final int RESULT_DISABLED = 1002; // 0x3ea + field public static final int RESULT_FUNCTION_NOT_FOUND = 1003; // 0x3eb + field public static final int RESULT_INVALID_ARGUMENT = 1001; // 0x3e9 field public static final int RESULT_OK = 0; // 0x0 + field public static final int RESULT_SYSTEM_ERROR = 2000; // 0x7d0 } } diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java index 6870446fc3a0..2075104ff868 100644 --- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java +++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java @@ -16,9 +16,11 @@ package com.google.android.appfunctions.sidecar; +import android.Manifest; import android.annotation.CallbackExecutor; import android.annotation.IntDef; import android.annotation.NonNull; +import android.annotation.RequiresPermission; import android.annotation.SuppressLint; import android.annotation.UserHandleAware; import android.content.Context; @@ -103,6 +105,12 @@ public final class AppFunctionManager { * <p>See {@link android.app.appfunctions.AppFunctionManager#executeAppFunction} for the * documented behaviour of this method. */ + @RequiresPermission( + anyOf = { + Manifest.permission.EXECUTE_APP_FUNCTIONS_TRUSTED, + Manifest.permission.EXECUTE_APP_FUNCTIONS + }, + conditional = true) public void executeAppFunction( @NonNull ExecuteAppFunctionRequest sidecarRequest, @NonNull @CallbackExecutor Executor executor, @@ -131,6 +139,12 @@ public final class AppFunctionManager { * <p>See {@link android.app.appfunctions.AppFunctionManager#isAppFunctionEnabled} for the * documented behaviour of this method. */ + @RequiresPermission( + anyOf = { + Manifest.permission.EXECUTE_APP_FUNCTIONS_TRUSTED, + Manifest.permission.EXECUTE_APP_FUNCTIONS + }, + conditional = true) public void isAppFunctionEnabled( @NonNull String functionIdentifier, @NonNull String targetPackage, @@ -140,6 +154,19 @@ public final class AppFunctionManager { } /** + * Returns a boolean through a callback, indicating whether the app function is enabled. + * + * <p>See {@link android.app.appfunctions.AppFunctionManager#isAppFunctionEnabled} for the + * documented behaviour of this method. + */ + public void isAppFunctionEnabled( + @NonNull String functionIdentifier, + @NonNull Executor executor, + @NonNull OutcomeReceiver<Boolean, Exception> callback) { + mManager.isAppFunctionEnabled(functionIdentifier, executor, callback); + } + + /** * Sets the enabled state of the app function owned by the calling package. * * <p>See {@link android.app.appfunctions.AppFunctionManager#setAppFunctionEnabled} for the diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java index d5dfaeb77140..4e88fb025a9d 100644 --- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java +++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java @@ -50,38 +50,102 @@ public final class ExecuteAppFunctionResponse { */ public static final String PROPERTY_RETURN_VALUE = "returnValue"; - /** The call was successful. */ + /** + * The call was successful. + * + * <p>This result code does not belong in an error category. + */ public static final int RESULT_OK = 0; - /** The caller does not have the permission to execute an app function. */ - public static final int RESULT_DENIED = 1; + /** + * The caller does not have the permission to execute an app function. + * + * <p>This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category. + */ + public static final int RESULT_DENIED = 1000; + + /** + * The caller supplied invalid arguments to the execution request. + * + * <p>This error may be considered similar to {@link IllegalArgumentException}. + * + * <p>This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category. + */ + public static final int RESULT_INVALID_ARGUMENT = 1001; + + /** + * The caller tried to execute a disabled app function. + * + * <p>This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category. + */ + public static final int RESULT_DISABLED = 1002; + + /** + * The caller tried to execute a function that does not exist. + * + * <p>This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category. + */ + public static final int RESULT_FUNCTION_NOT_FOUND = 1003; + + /** + * An internal unexpected error coming from the system. + * + * <p>This error is in the {@link #ERROR_CATEGORY_SYSTEM} category. + */ + public static final int RESULT_SYSTEM_ERROR = 2000; + + /** + * The operation was cancelled. Use this error code to report that a cancellation is done after + * receiving a cancellation signal. + * + * <p>This error is in the {@link #ERROR_CATEGORY_SYSTEM} category. + */ + public static final int RESULT_CANCELLED = 2001; /** * An unknown error occurred while processing the call in the AppFunctionService. * * <p>This error is thrown when the service is connected in the remote application but an * unexpected error is thrown from the bound application. + * + * <p>This error is in the {@link #ERROR_CATEGORY_APP} category. */ - public static final int RESULT_APP_UNKNOWN_ERROR = 2; + public static final int RESULT_APP_UNKNOWN_ERROR = 3000; - /** An internal unexpected error coming from the system. */ - public static final int RESULT_INTERNAL_ERROR = 3; + /** + * The error category is unknown. + * + * <p>This is the default value for {@link #getErrorCategory}. + */ + public static final int ERROR_CATEGORY_UNKNOWN = 0; /** - * The caller supplied invalid arguments to the call. + * The error is caused by the app requesting a function execution. * - * <p>This error may be considered similar to {@link IllegalArgumentException}. + * <p>For example, the caller provided invalid parameters in the execution request e.g. an + * invalid function ID. + * + * <p>Errors in the category fall in the range 1000-1999 inclusive. */ - public static final int RESULT_INVALID_ARGUMENT = 4; + public static final int ERROR_CATEGORY_REQUEST_ERROR = 1; - /** The caller tried to execute a disabled app function. */ - public static final int RESULT_DISABLED = 5; + /** + * The error is caused by an issue in the system. + * + * <p>For example, the AppFunctionService implementation is not found by the system. + * + * <p>Errors in the category fall in the range 2000-2999 inclusive. + */ + public static final int ERROR_CATEGORY_SYSTEM = 2; /** - * The operation was cancelled. Use this error code to report that a cancellation is done after - * receiving a cancellation signal. + * The error is caused by the app providing the function. + * + * <p>For example, the app crashed when the system is executing the request. + * + * <p>Errors in the category fall in the range 3000-3999 inclusive. */ - public static final int RESULT_CANCELLED = 6; + public static final int ERROR_CATEGORY_APP = 3; /** The result code of the app function execution. */ @ResultCode private final int mResultCode; @@ -171,6 +235,36 @@ public final class ExecuteAppFunctionResponse { } /** + * Returns the error category of the {@link ExecuteAppFunctionResponse}. + * + * <p>This method categorizes errors based on their underlying cause, allowing developers to + * implement targeted error handling and provide more informative error messages to users. It + * maps ranges of result codes to specific error categories. + * + * <p>When constructing a {@link #newFailure} response, use the appropriate result code value to + * ensure correct categorization of the failed response. + * + * <p>This method returns {@code ERROR_CATEGORY_UNKNOWN} if the result code does not belong to + * any error category, for example, in the case of a successful result with {@link #RESULT_OK}. + * + * <p>See {@link ErrorCategory} for a complete list of error categories and their corresponding + * result code ranges. + */ + @ErrorCategory + public int getErrorCategory() { + if (mResultCode >= 1000 && mResultCode < 2000) { + return ERROR_CATEGORY_REQUEST_ERROR; + } + if (mResultCode >= 2000 && mResultCode < 3000) { + return ERROR_CATEGORY_SYSTEM; + } + if (mResultCode >= 3000 && mResultCode < 4000) { + return ERROR_CATEGORY_APP; + } + return ERROR_CATEGORY_UNKNOWN; + } + + /** * Returns a generic document containing the return value of the executed function. * * <p>The {@link #PROPERTY_RETURN_VALUE} key can be used to obtain the return value. @@ -238,11 +332,28 @@ public final class ExecuteAppFunctionResponse { RESULT_OK, RESULT_DENIED, RESULT_APP_UNKNOWN_ERROR, - RESULT_INTERNAL_ERROR, + RESULT_SYSTEM_ERROR, + RESULT_FUNCTION_NOT_FOUND, RESULT_INVALID_ARGUMENT, RESULT_DISABLED, RESULT_CANCELLED }) @Retention(RetentionPolicy.SOURCE) public @interface ResultCode {} + + /** + * Error categories. + * + * @hide + */ + @IntDef( + prefix = {"ERROR_CATEGORY_"}, + value = { + ERROR_CATEGORY_UNKNOWN, + ERROR_CATEGORY_REQUEST_ERROR, + ERROR_CATEGORY_APP, + ERROR_CATEGORY_SYSTEM + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ErrorCategory {} } diff --git a/libs/appfunctions/tests/src/com/google/android/appfunctions/sidecar/tests/SidecarConverterTest.kt b/libs/appfunctions/tests/src/com/google/android/appfunctions/sidecar/tests/SidecarConverterTest.kt index 1f9fddd3c1ec..264f84209caf 100644 --- a/libs/appfunctions/tests/src/com/google/android/appfunctions/sidecar/tests/SidecarConverterTest.kt +++ b/libs/appfunctions/tests/src/com/google/android/appfunctions/sidecar/tests/SidecarConverterTest.kt @@ -105,7 +105,7 @@ class SidecarConverterTest { val emptyGd = GenericDocument.Builder<GenericDocument.Builder<*>>("", "", "").build() val platformResponse = ExecuteAppFunctionResponse.newFailure( - ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR, + ExecuteAppFunctionResponse.RESULT_SYSTEM_ERROR, null, null ) @@ -119,7 +119,7 @@ class SidecarConverterTest { assertThat(sidecarResponse.resultDocument.id).isEqualTo(emptyGd.id) assertThat(sidecarResponse.resultDocument.schemaType).isEqualTo(emptyGd.schemaType) assertThat(sidecarResponse.resultCode) - .isEqualTo(ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR) + .isEqualTo(ExecuteAppFunctionResponse.RESULT_SYSTEM_ERROR) assertThat(sidecarResponse.errorMessage).isNull() } @@ -152,7 +152,7 @@ class SidecarConverterTest { val emptyGd = GenericDocument.Builder<GenericDocument.Builder<*>>("", "", "").build() val sidecarResponse = com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse.newFailure( - ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR, + ExecuteAppFunctionResponse.RESULT_SYSTEM_ERROR, null, null ) @@ -166,7 +166,7 @@ class SidecarConverterTest { assertThat(platformResponse.resultDocument.id).isEqualTo(emptyGd.id) assertThat(platformResponse.resultDocument.schemaType).isEqualTo(emptyGd.schemaType) assertThat(platformResponse.resultCode) - .isEqualTo(ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR) + .isEqualTo(ExecuteAppFunctionResponse.RESULT_SYSTEM_ERROR) assertThat(platformResponse.errorMessage).isNull() } } diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp index 266c23631800..fcb7efc35c94 100644 --- a/libs/hwui/Android.bp +++ b/libs/hwui/Android.bp @@ -384,6 +384,7 @@ cc_defaults { "jni/ScopedParcel.cpp", "jni/Shader.cpp", "jni/RenderEffect.cpp", + "jni/RuntimeEffectUtils.cpp", "jni/Typeface.cpp", "jni/Utils.cpp", "jni/YuvToJpegEncoder.cpp", @@ -579,6 +580,7 @@ cc_defaults { "utils/Color.cpp", "utils/LinearAllocator.cpp", "utils/StringUtils.cpp", + "utils/StatsUtils.cpp", "utils/TypefaceUtils.cpp", "utils/VectorDrawableUtils.cpp", "AnimationContext.cpp", diff --git a/libs/hwui/ColorFilter.h b/libs/hwui/ColorFilter.h index 3a3bfb47dfdd..f6b6be08997a 100644 --- a/libs/hwui/ColorFilter.h +++ b/libs/hwui/ColorFilter.h @@ -22,6 +22,7 @@ #include <memory> #include "GraphicsJNI.h" +#include "RuntimeEffectUtils.h" #include "SkColorFilter.h" namespace android { @@ -113,6 +114,36 @@ private: std::vector<float> mMatrix; }; +class RuntimeColorFilter : public ColorFilter { +public: + RuntimeColorFilter(SkRuntimeEffectBuilder* builder) : mBuilder(builder) {} + + void updateUniforms(JNIEnv* env, const char* name, const float vals[], int count, + bool isColor) { + UpdateFloatUniforms(env, mBuilder, name, vals, count, isColor); + discardInstance(); + } + + void updateUniforms(JNIEnv* env, const char* name, const int vals[], int count) { + UpdateIntUniforms(env, mBuilder, name, vals, count); + discardInstance(); + } + + void updateChild(JNIEnv* env, const char* name, SkFlattenable* childEffect) { + UpdateChild(env, mBuilder, name, childEffect); + discardInstance(); + } + +private: + sk_sp<SkColorFilter> createInstance() override { + // TODO: throw error if null + return mBuilder->makeColorFilter(); + } + +private: + SkRuntimeEffectBuilder* mBuilder; +}; + } // namespace uirenderer } // namespace android diff --git a/libs/hwui/aconfig/hwui_flags.aconfig b/libs/hwui/aconfig/hwui_flags.aconfig index 93df47853769..5ad788c67816 100644 --- a/libs/hwui/aconfig/hwui_flags.aconfig +++ b/libs/hwui/aconfig/hwui_flags.aconfig @@ -139,3 +139,18 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "bitmap_ashmem_long_name" + namespace: "core_graphics" + description: "Whether to have more information in ashmem filenames for bitmaps" + bug: "369619160" +} + +flag { + name: "animated_image_drawable_filter_bitmap" + is_exported: true + namespace: "core_graphics" + description: "API's that enable animated image drawables to use nearest sampling when scaling." + bug: "370523334" +} diff --git a/libs/hwui/hwui/AnimatedImageDrawable.cpp b/libs/hwui/hwui/AnimatedImageDrawable.cpp index 69613c7d17cb..5e379aad9326 100644 --- a/libs/hwui/hwui/AnimatedImageDrawable.cpp +++ b/libs/hwui/hwui/AnimatedImageDrawable.cpp @@ -347,4 +347,26 @@ int AnimatedImageDrawable::currentFrameDuration() { return adjustFrameDuration(mSkAnimatedImage->currentFrameDuration()); } +bool AnimatedImageDrawable::getFilterBitmap() const { + const SkFilterMode kFilterBitmap = mSkAnimatedImage->getFilterMode(); + if (kFilterBitmap == SkFilterMode::kLinear) { + return true; + } + return false; +} + +bool AnimatedImageDrawable::setFilterBitmap(bool filterBitmap) { + if (filterBitmap) { + if (mSkAnimatedImage->getFilterMode() == SkFilterMode::kLinear) { + return false; + } + mSkAnimatedImage->setFilterMode(SkFilterMode::kLinear); + } else { + if (mSkAnimatedImage->getFilterMode() == SkFilterMode::kNearest) { + return false; + } + mSkAnimatedImage->setFilterMode(SkFilterMode::kNearest); + } + return true; +} } // namespace android diff --git a/libs/hwui/hwui/AnimatedImageDrawable.h b/libs/hwui/hwui/AnimatedImageDrawable.h index 1e965abc82b5..22123249b7d6 100644 --- a/libs/hwui/hwui/AnimatedImageDrawable.h +++ b/libs/hwui/hwui/AnimatedImageDrawable.h @@ -87,6 +87,11 @@ public: bool isRunning(); int getRepetitionCount() const { return mSkAnimatedImage->getRepetitionCount(); } void setRepetitionCount(int count) { mSkAnimatedImage->setRepetitionCount(count); } + // Returns true if the filter mode is set to linear sampling; false if it is + // set to nearest neighbor sampling. + bool getFilterBitmap() const; + // Returns true if the filter mode was changed; false otherwise. + bool setFilterBitmap(bool filterBitmap); void setOnAnimationEndListener(std::unique_ptr<OnAnimationEndListener> listener) { mEndListener = std::move(listener); diff --git a/libs/hwui/hwui/Bitmap.cpp b/libs/hwui/hwui/Bitmap.cpp index b73380e38fd1..cc292d9de7b2 100644 --- a/libs/hwui/hwui/Bitmap.cpp +++ b/libs/hwui/hwui/Bitmap.cpp @@ -15,6 +15,7 @@ */ #include "Bitmap.h" +#include <android-base/file.h> #include "HardwareBitmapUploader.h" #include "Properties.h" #ifdef __ANDROID__ // Layoutlib does not support render thread @@ -46,16 +47,27 @@ #include <SkImage.h> #include <SkImageAndroid.h> #include <SkImagePriv.h> +#include <SkJpegEncoder.h> #include <SkJpegGainmapEncoder.h> #include <SkPixmap.h> +#include <SkPngEncoder.h> #include <SkRect.h> #include <SkStream.h> -#include <SkJpegEncoder.h> -#include <SkPngEncoder.h> #include <SkWebpEncoder.h> +#include <atomic> +#include <format> #include <limits> +#ifdef __ANDROID__ +#include <com_android_graphics_hwui_flags.h> +namespace hwui_flags = com::android::graphics::hwui::flags; +#else +namespace hwui_flags { +constexpr bool bitmap_ashmem_long_name() { return false; } +} +#endif + namespace android { #ifdef __ANDROID__ @@ -86,6 +98,28 @@ static uint64_t AHardwareBuffer_getAllocationSize(AHardwareBuffer* aHardwareBuff } #endif +// generate an ID for this Bitmap, id is a 64-bit integer of 3 parts: +// 0000xxxxxx - the lower 6 decimal digits is a monotonically increasing number +// 000x000000 - the 7th decimal digit is the storage type (see PixelStorageType) +// xxx0000000 - the 8th decimal digit and above is the current pid +// +// e.g. 43231000076 - means this bitmap is the 76th bitmap created, has the +// storage type of 'Heap', and is created in a process with pid 4323. +// +// NOTE: +// 1) the monotonic number could increase beyond 1000,000 and wrap around, which +// only happens when more than 1,000,000 bitmaps have been created over time. +// This could result in two IDs being the same despite being really rare. +// 2) the IDs are intentionally represented in decimal to make it easier to +// reason and associate with numbers shown in heap dump (mostly in decimal) +// and PIDs shown in different tools (mostly in decimal as well). +uint64_t Bitmap::getId(PixelStorageType type) { + static std::atomic<uint64_t> idCounter{0}; + return (idCounter.fetch_add(1) % 1000000) + + static_cast<uint64_t>(type) * 1000000 + + static_cast<uint64_t>(getpid()) * 10000000; +} + bool Bitmap::computeAllocationSize(size_t rowBytes, int height, size_t* size) { return 0 <= height && height <= std::numeric_limits<size_t>::max() && !__builtin_mul_overflow(rowBytes, (size_t)height, size) && @@ -117,6 +151,20 @@ static sk_sp<Bitmap> allocateBitmap(SkBitmap* bitmap, AllocPixelRef alloc) { return wrapper; } +std::string Bitmap::getAshmemId(const char* tag, uint64_t bitmapId, + int width, int height, size_t size) { + if (!hwui_flags::bitmap_ashmem_long_name()) { + return "bitmap"; + } + static std::string sCmdline = [] { + std::string temp; + android::base::ReadFileToString("/proc/self/cmdline", &temp); + return temp; + }(); + return std::format("bitmap/{}-id_{}-{}x{}-size_{}-{}", + tag, bitmapId, width, height, size, sCmdline); +} + sk_sp<Bitmap> Bitmap::allocateAshmemBitmap(SkBitmap* bitmap) { return allocateBitmap(bitmap, &Bitmap::allocateAshmemBitmap); } @@ -124,7 +172,9 @@ sk_sp<Bitmap> Bitmap::allocateAshmemBitmap(SkBitmap* bitmap) { sk_sp<Bitmap> Bitmap::allocateAshmemBitmap(size_t size, const SkImageInfo& info, size_t rowBytes) { #ifdef __ANDROID__ // Create new ashmem region with read/write priv - int fd = ashmem_create_region("bitmap", size); + uint64_t id = getId(PixelStorageType::Ashmem); + auto ashmemId = getAshmemId("allocate", id, info.width(), info.height(), size); + int fd = ashmem_create_region(ashmemId.c_str(), size); if (fd < 0) { return nullptr; } @@ -140,7 +190,7 @@ sk_sp<Bitmap> Bitmap::allocateAshmemBitmap(size_t size, const SkImageInfo& info, close(fd); return nullptr; } - return sk_sp<Bitmap>(new Bitmap(addr, fd, size, info, rowBytes)); + return sk_sp<Bitmap>(new Bitmap(addr, fd, size, info, rowBytes, id)); #else return Bitmap::allocateHeapBitmap(size, info, rowBytes); #endif @@ -261,7 +311,8 @@ void Bitmap::reconfigure(const SkImageInfo& newInfo, size_t rowBytes) { Bitmap::Bitmap(void* address, size_t size, const SkImageInfo& info, size_t rowBytes) : SkPixelRef(info.width(), info.height(), address, rowBytes) , mInfo(validateAlpha(info)) - , mPixelStorageType(PixelStorageType::Heap) { + , mPixelStorageType(PixelStorageType::Heap) + , mId(getId(mPixelStorageType)) { mPixelStorage.heap.address = address; mPixelStorage.heap.size = size; traceBitmapCreate(); @@ -270,16 +321,19 @@ Bitmap::Bitmap(void* address, size_t size, const SkImageInfo& info, size_t rowBy Bitmap::Bitmap(SkPixelRef& pixelRef, const SkImageInfo& info) : SkPixelRef(info.width(), info.height(), pixelRef.pixels(), pixelRef.rowBytes()) , mInfo(validateAlpha(info)) - , mPixelStorageType(PixelStorageType::WrappedPixelRef) { + , mPixelStorageType(PixelStorageType::WrappedPixelRef) + , mId(getId(mPixelStorageType)) { pixelRef.ref(); mPixelStorage.wrapped.pixelRef = &pixelRef; traceBitmapCreate(); } -Bitmap::Bitmap(void* address, int fd, size_t mappedSize, const SkImageInfo& info, size_t rowBytes) +Bitmap::Bitmap(void* address, int fd, size_t mappedSize, const SkImageInfo& info, + size_t rowBytes, uint64_t id) : SkPixelRef(info.width(), info.height(), address, rowBytes) , mInfo(validateAlpha(info)) - , mPixelStorageType(PixelStorageType::Ashmem) { + , mPixelStorageType(PixelStorageType::Ashmem) + , mId(id != INVALID_BITMAP_ID ? id : getId(mPixelStorageType)) { mPixelStorage.ashmem.address = address; mPixelStorage.ashmem.fd = fd; mPixelStorage.ashmem.size = mappedSize; @@ -293,7 +347,8 @@ Bitmap::Bitmap(AHardwareBuffer* buffer, const SkImageInfo& info, size_t rowBytes , mInfo(validateAlpha(info)) , mPixelStorageType(PixelStorageType::Hardware) , mPalette(palette) - , mPaletteGenerationId(getGenerationID()) { + , mPaletteGenerationId(getGenerationID()) + , mId(getId(mPixelStorageType)) { mPixelStorage.hardware.buffer = buffer; mPixelStorage.hardware.size = AHardwareBuffer_getAllocationSize(buffer); AHardwareBuffer_acquire(buffer); @@ -578,6 +633,7 @@ void Bitmap::setGainmap(sp<uirenderer::Gainmap>&& gainmap) { } std::mutex Bitmap::mLock{}; + size_t Bitmap::mTotalBitmapBytes = 0; size_t Bitmap::mTotalBitmapCount = 0; diff --git a/libs/hwui/hwui/Bitmap.h b/libs/hwui/hwui/Bitmap.h index 3d55d859ed5f..8abe6a8c445a 100644 --- a/libs/hwui/hwui/Bitmap.h +++ b/libs/hwui/hwui/Bitmap.h @@ -37,10 +37,10 @@ class SkWStream; namespace android { enum class PixelStorageType { - WrappedPixelRef, - Heap, - Ashmem, - Hardware, + WrappedPixelRef = 0, + Heap = 1, + Ashmem = 2, + Hardware = 3, }; // TODO: Find a better home for this. It's here because hwui/Bitmap is exported and CanvasTransform @@ -79,6 +79,9 @@ public: static sk_sp<Bitmap> allocateHeapBitmap(const SkImageInfo& info); static sk_sp<Bitmap> allocateHeapBitmap(size_t size, const SkImageInfo& i, size_t rowBytes); + static std::string getAshmemId(const char* tag, uint64_t bitmapId, + int width, int height, size_t size); + /* The createFrom factories construct a new Bitmap object by wrapping the already allocated * memory that is provided as an input param. */ @@ -104,6 +107,10 @@ public: void setColorSpace(sk_sp<SkColorSpace> colorSpace); void setAlphaType(SkAlphaType alphaType); + uint64_t getId() const { + return mId; + } + void getSkBitmap(SkBitmap* outBitmap); SkBitmap getSkBitmap() { @@ -177,11 +184,14 @@ public: static bool compress(const SkBitmap& bitmap, JavaCompressFormat format, int32_t quality, SkWStream* stream); private: + static constexpr uint64_t INVALID_BITMAP_ID = 0u; + static sk_sp<Bitmap> allocateAshmemBitmap(size_t size, const SkImageInfo& i, size_t rowBytes); Bitmap(void* address, size_t allocSize, const SkImageInfo& info, size_t rowBytes); Bitmap(SkPixelRef& pixelRef, const SkImageInfo& info); - Bitmap(void* address, int fd, size_t mappedSize, const SkImageInfo& info, size_t rowBytes); + Bitmap(void* address, int fd, size_t mappedSize, const SkImageInfo& info, size_t rowBytes, + uint64_t id = INVALID_BITMAP_ID); #ifdef __ANDROID__ // Layoutlib does not support hardware acceleration Bitmap(AHardwareBuffer* buffer, const SkImageInfo& info, size_t rowBytes, BitmapPalette palette); @@ -229,6 +239,9 @@ private: sk_sp<SkImage> mImage; // Cache is used only for HW Bitmaps with Skia pipeline. + uint64_t mId; // unique ID for this bitmap + static uint64_t getId(PixelStorageType type); + // for tracing total number and memory usage of bitmaps static std::mutex mLock; static size_t mTotalBitmapBytes; diff --git a/libs/hwui/hwui/ImageDecoder.cpp b/libs/hwui/hwui/ImageDecoder.cpp index e074a27db38f..a9a5db8181ba 100644 --- a/libs/hwui/hwui/ImageDecoder.cpp +++ b/libs/hwui/hwui/ImageDecoder.cpp @@ -27,8 +27,8 @@ #include <SkColorSpace.h> #include <SkColorType.h> #include <SkEncodedOrigin.h> -#include <SkImageInfo.h> #include <SkGainmapInfo.h> +#include <SkImageInfo.h> #include <SkMatrix.h> #include <SkPaint.h> #include <SkPngChunkReader.h> @@ -43,6 +43,8 @@ #include <memory> +#include "modules/skcms/src/skcms_public.h" + using namespace android; sk_sp<SkColorSpace> ImageDecoder::getDefaultColorSpace() const { diff --git a/libs/hwui/jni/AnimatedImageDrawable.cpp b/libs/hwui/jni/AnimatedImageDrawable.cpp index b01e38d014a9..2c8530d4daeb 100644 --- a/libs/hwui/jni/AnimatedImageDrawable.cpp +++ b/libs/hwui/jni/AnimatedImageDrawable.cpp @@ -276,6 +276,18 @@ static void AnimatedImageDrawable_nSetBounds(JNIEnv* env, jobject /*clazz*/, jlo drawable->setStagingBounds(rect); } +static jboolean AnimatedImageDrawable_nSetFilterBitmap(JNIEnv* env, jobject /*clazz*/, + jlong nativePtr, jboolean filterBitmap) { + auto* drawable = reinterpret_cast<AnimatedImageDrawable*>(nativePtr); + return drawable->setFilterBitmap(filterBitmap); +} + +static jboolean AnimatedImageDrawable_nGetFilterBitmap(JNIEnv* env, jobject /*clazz*/, + jlong nativePtr) { + auto* drawable = reinterpret_cast<AnimatedImageDrawable*>(nativePtr); + return drawable->getFilterBitmap(); +} + static const JNINativeMethod gAnimatedImageDrawableMethods[] = { {"nCreate", "(JLandroid/graphics/ImageDecoder;IIJZLandroid/graphics/Rect;)J", (void*)AnimatedImageDrawable_nCreate}, @@ -294,6 +306,8 @@ static const JNINativeMethod gAnimatedImageDrawableMethods[] = { {"nNativeByteSize", "(J)J", (void*)AnimatedImageDrawable_nNativeByteSize}, {"nSetMirrored", "(JZ)V", (void*)AnimatedImageDrawable_nSetMirrored}, {"nSetBounds", "(JLandroid/graphics/Rect;)V", (void*)AnimatedImageDrawable_nSetBounds}, + {"nSetFilterBitmap", "(JZ)Z", (void*)AnimatedImageDrawable_nSetFilterBitmap}, + {"nGetFilterBitmap", "(J)Z", (void*)AnimatedImageDrawable_nGetFilterBitmap}, }; int register_android_graphics_drawable_AnimatedImageDrawable(JNIEnv* env) { diff --git a/libs/hwui/jni/Bitmap.cpp b/libs/hwui/jni/Bitmap.cpp index 010c4e8dfb3a..29efd98b41d0 100644 --- a/libs/hwui/jni/Bitmap.cpp +++ b/libs/hwui/jni/Bitmap.cpp @@ -196,7 +196,7 @@ jobject createBitmap(JNIEnv* env, Bitmap* bitmap, int density) { static jmethodID gBitmap_constructorMethodID = GetMethodIDOrDie(env, gBitmap_class, - "<init>", "(JIIIZ[BLandroid/graphics/NinePatch$InsetStruct;Z)V"); + "<init>", "(JJIIIZ[BLandroid/graphics/NinePatch$InsetStruct;Z)V"); bool isMutable = bitmapCreateFlags & kBitmapCreateFlag_Mutable; bool isPremultiplied = bitmapCreateFlags & kBitmapCreateFlag_Premultiplied; @@ -209,7 +209,8 @@ jobject createBitmap(JNIEnv* env, Bitmap* bitmap, bitmapWrapper->bitmap().setImmutable(); } jobject obj = env->NewObject(gBitmap_class, gBitmap_constructorMethodID, - reinterpret_cast<jlong>(bitmapWrapper), bitmap->width(), bitmap->height(), density, + static_cast<jlong>(bitmap->getId()), reinterpret_cast<jlong>(bitmapWrapper), + bitmap->width(), bitmap->height(), density, isPremultiplied, ninePatchChunk, ninePatchInsets, fromMalloc); if (env->ExceptionCheck() != 0) { @@ -668,14 +669,20 @@ static binder_status_t writeBlobFromFd(AParcel* parcel, int32_t size, int fd) { return STATUS_OK; } -static binder_status_t writeBlob(AParcel* parcel, const int32_t size, const void* data, bool immutable) { +static binder_status_t writeBlob(AParcel* parcel, uint64_t bitmapId, const SkBitmap& bitmap) { + const size_t size = bitmap.computeByteSize(); + const void* data = bitmap.getPixels(); + const bool immutable = bitmap.isImmutable(); + if (size <= 0 || data == nullptr) { return STATUS_NOT_ENOUGH_DATA; } binder_status_t error = STATUS_OK; if (shouldUseAshmem(parcel, size)) { // Create new ashmem region with read/write priv - base::unique_fd fd(ashmem_create_region("bitmap", size)); + auto ashmemId = Bitmap::getAshmemId("writeblob", bitmapId, + bitmap.width(), bitmap.height(), size); + base::unique_fd fd(ashmem_create_region(ashmemId.c_str(), size)); if (fd.get() < 0) { return STATUS_NO_MEMORY; } @@ -883,8 +890,7 @@ static jboolean Bitmap_writeToParcel(JNIEnv* env, jobject, p.allowFds() ? "allowed" : "forbidden"); #endif - size_t size = bitmap.computeByteSize(); - status = writeBlob(p.get(), size, bitmap.getPixels(), bitmap.isImmutable()); + status = writeBlob(p.get(), bitmapWrapper->bitmap().getId(), bitmap); if (status) { doThrowRE(env, "Could not copy bitmap to parcel blob."); return JNI_FALSE; diff --git a/libs/hwui/jni/BitmapFactory.cpp b/libs/hwui/jni/BitmapFactory.cpp index 49a7f73fb3a3..8b43f1db84af 100644 --- a/libs/hwui/jni/BitmapFactory.cpp +++ b/libs/hwui/jni/BitmapFactory.cpp @@ -10,6 +10,7 @@ #include <stdint.h> #include <stdio.h> #include <sys/stat.h> +#include <utils/StatsUtils.h> #include <memory> @@ -630,6 +631,7 @@ static jobject doDecode(JNIEnv* env, std::unique_ptr<SkStreamRewindable> stream, } bitmap::reinitBitmap(env, javaBitmap, outputBitmap.info(), isPremultiplied); outputBitmap.notifyPixelsChanged(); + uirenderer::logBitmapDecode(*reuseBitmap); // If a java bitmap was passed in for reuse, pass it back return javaBitmap; } @@ -650,6 +652,7 @@ static jobject doDecode(JNIEnv* env, std::unique_ptr<SkStreamRewindable> stream, } } + uirenderer::logBitmapDecode(*hardwareBitmap); return bitmap::createBitmap(env, hardwareBitmap.release(), bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1); } @@ -659,6 +662,7 @@ static jobject doDecode(JNIEnv* env, std::unique_ptr<SkStreamRewindable> stream, heapBitmap->setGainmap(std::move(gainmap)); } + uirenderer::logBitmapDecode(*heapBitmap); // now create the java bitmap return bitmap::createBitmap(env, heapBitmap, bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1); diff --git a/libs/hwui/jni/BitmapRegionDecoder.cpp b/libs/hwui/jni/BitmapRegionDecoder.cpp index f7e8e073a272..5ffd5b9016d8 100644 --- a/libs/hwui/jni/BitmapRegionDecoder.cpp +++ b/libs/hwui/jni/BitmapRegionDecoder.cpp @@ -19,6 +19,7 @@ #include <HardwareBitmapUploader.h> #include <androidfw/Asset.h> #include <sys/stat.h> +#include <utils/StatsUtils.h> #include <memory> @@ -376,6 +377,7 @@ static jobject nativeDecodeRegion(JNIEnv* env, jobject, jlong brdHandle, jint in recycledBitmap->setGainmap(std::move(gainmap)); } bitmap::reinitBitmap(env, javaBitmap, recycledBitmap->info(), !requireUnpremul); + uirenderer::logBitmapDecode(*recycledBitmap); return javaBitmap; } @@ -392,12 +394,14 @@ static jobject nativeDecodeRegion(JNIEnv* env, jobject, jlong brdHandle, jint in hardwareBitmap->setGainmap(std::move(gm)); } } + uirenderer::logBitmapDecode(*hardwareBitmap); return bitmap::createBitmap(env, hardwareBitmap.release(), bitmapCreateFlags); } Bitmap* heapBitmap = heapAlloc.getStorageObjAndReset(); if (hasGainmap && heapBitmap != nullptr) { heapBitmap->setGainmap(std::move(gainmap)); } + uirenderer::logBitmapDecode(*heapBitmap); return android::bitmap::createBitmap(env, heapBitmap, bitmapCreateFlags); } diff --git a/libs/hwui/jni/ColorFilter.cpp b/libs/hwui/jni/ColorFilter.cpp index 0b95148d3c82..20301d2c76ec 100644 --- a/libs/hwui/jni/ColorFilter.cpp +++ b/libs/hwui/jni/ColorFilter.cpp @@ -18,7 +18,9 @@ #include "ColorFilter.h" #include "GraphicsJNI.h" +#include "RuntimeEffectUtils.h" #include "SkBlendMode.h" +#include "include/effects/SkRuntimeEffect.h" namespace android { @@ -89,6 +91,78 @@ public: filter->setMatrix(getMatrixFromJFloatArray(env, jarray)); } } + + static jlong RuntimeColorFilter_createColorFilter(JNIEnv* env, jobject, jstring agsl) { + ScopedUtfChars strSksl(env, agsl); + auto result = SkRuntimeEffect::MakeForColorFilter(SkString(strSksl.c_str()), + SkRuntimeEffect::Options{}); + if (result.effect.get() == nullptr) { + doThrowIAE(env, result.errorText.c_str()); + return 0; + } + auto builder = new SkRuntimeEffectBuilder(std::move(result.effect)); + auto* runtimeColorFilter = new RuntimeColorFilter(builder); + runtimeColorFilter->incStrong(nullptr); + return static_cast<jlong>(reinterpret_cast<uintptr_t>(runtimeColorFilter)); + } + + static void RuntimeColorFilter_updateUniformsFloatArray(JNIEnv* env, jobject, + jlong colorFilterPtr, + jstring uniformName, + jfloatArray uniforms, + jboolean isColor) { + auto* filter = reinterpret_cast<RuntimeColorFilter*>(colorFilterPtr); + ScopedUtfChars name(env, uniformName); + AutoJavaFloatArray autoValues(env, uniforms, 0, kRO_JNIAccess); + if (filter) { + filter->updateUniforms(env, name.c_str(), autoValues.ptr(), autoValues.length(), + isColor); + } + } + + static void RuntimeColorFilter_updateUniformsFloats(JNIEnv* env, jobject, jlong colorFilterPtr, + jstring uniformName, jfloat value1, + jfloat value2, jfloat value3, jfloat value4, + jint count) { + auto* filter = reinterpret_cast<RuntimeColorFilter*>(colorFilterPtr); + ScopedUtfChars name(env, uniformName); + const float values[4] = {value1, value2, value3, value4}; + if (filter) { + filter->updateUniforms(env, name.c_str(), values, count, false); + } + } + + static void RuntimeColorFilter_updateUniformsIntArray(JNIEnv* env, jobject, + jlong colorFilterPtr, jstring uniformName, + jintArray uniforms) { + auto* filter = reinterpret_cast<RuntimeColorFilter*>(colorFilterPtr); + ScopedUtfChars name(env, uniformName); + AutoJavaIntArray autoValues(env, uniforms, 0); + if (filter) { + filter->updateUniforms(env, name.c_str(), autoValues.ptr(), autoValues.length()); + } + } + + static void RuntimeColorFilter_updateUniformsInts(JNIEnv* env, jobject, jlong colorFilterPtr, + jstring uniformName, jint value1, jint value2, + jint value3, jint value4, jint count) { + auto* filter = reinterpret_cast<RuntimeColorFilter*>(colorFilterPtr); + ScopedUtfChars name(env, uniformName); + const int values[4] = {value1, value2, value3, value4}; + if (filter) { + filter->updateUniforms(env, name.c_str(), values, count); + } + } + + static void RuntimeColorFilter_updateChild(JNIEnv* env, jobject, jlong colorFilterPtr, + jstring childName, jlong childPtr) { + auto* filter = reinterpret_cast<RuntimeColorFilter*>(colorFilterPtr); + ScopedUtfChars name(env, childName); + auto* child = reinterpret_cast<SkFlattenable*>(childPtr); + if (filter && child) { + filter->updateChild(env, name.c_str(), child); + } + } }; static const JNINativeMethod colorfilter_methods[] = { @@ -107,6 +181,20 @@ static const JNINativeMethod colormatrix_methods[] = { {"nativeColorMatrixFilter", "([F)J", (void*)ColorFilterGlue::CreateColorMatrixFilter}, {"nativeSetColorMatrix", "(J[F)V", (void*)ColorFilterGlue::SetColorMatrix}}; +static const JNINativeMethod runtime_color_filter_methods[] = { + {"nativeCreateRuntimeColorFilter", "(Ljava/lang/String;)J", + (void*)ColorFilterGlue::RuntimeColorFilter_createColorFilter}, + {"nativeUpdateUniforms", "(JLjava/lang/String;[FZ)V", + (void*)ColorFilterGlue::RuntimeColorFilter_updateUniformsFloatArray}, + {"nativeUpdateUniforms", "(JLjava/lang/String;FFFFI)V", + (void*)ColorFilterGlue::RuntimeColorFilter_updateUniformsFloats}, + {"nativeUpdateUniforms", "(JLjava/lang/String;[I)V", + (void*)ColorFilterGlue::RuntimeColorFilter_updateUniformsIntArray}, + {"nativeUpdateUniforms", "(JLjava/lang/String;IIIII)V", + (void*)ColorFilterGlue::RuntimeColorFilter_updateUniformsInts}, + {"nativeUpdateChild", "(JLjava/lang/String;J)V", + (void*)ColorFilterGlue::RuntimeColorFilter_updateChild}}; + int register_android_graphics_ColorFilter(JNIEnv* env) { android::RegisterMethodsOrDie(env, "android/graphics/ColorFilter", colorfilter_methods, NELEM(colorfilter_methods)); @@ -118,7 +206,10 @@ int register_android_graphics_ColorFilter(JNIEnv* env) { NELEM(lighting_methods)); android::RegisterMethodsOrDie(env, "android/graphics/ColorMatrixColorFilter", colormatrix_methods, NELEM(colormatrix_methods)); - + android::RegisterMethodsOrDie(env, "android/graphics/RuntimeColorFilter", + runtime_color_filter_methods, + NELEM(runtime_color_filter_methods)); + return 0; } diff --git a/libs/hwui/jni/ImageDecoder.cpp b/libs/hwui/jni/ImageDecoder.cpp index aebc4db37898..90fd3d87cba7 100644 --- a/libs/hwui/jni/ImageDecoder.cpp +++ b/libs/hwui/jni/ImageDecoder.cpp @@ -37,6 +37,7 @@ #include <hwui/Bitmap.h> #include <hwui/ImageDecoder.h> #include <sys/stat.h> +#include <utils/StatsUtils.h> #include "Bitmap.h" #include "BitmapFactory.h" @@ -485,6 +486,7 @@ static jobject ImageDecoder_nDecodeBitmap(JNIEnv* env, jobject /*clazz*/, jlong hwBitmap->setGainmap(std::move(gm)); } } + uirenderer::logBitmapDecode(*hwBitmap); return bitmap::createBitmap(env, hwBitmap.release(), bitmapCreateFlags, ninePatchChunk, ninePatchInsets); } @@ -498,6 +500,8 @@ static jobject ImageDecoder_nDecodeBitmap(JNIEnv* env, jobject /*clazz*/, jlong nativeBitmap->setImmutable(); } + + uirenderer::logBitmapDecode(*nativeBitmap); return bitmap::createBitmap(env, nativeBitmap.release(), bitmapCreateFlags, ninePatchChunk, ninePatchInsets); } diff --git a/libs/hwui/jni/RuntimeEffectUtils.cpp b/libs/hwui/jni/RuntimeEffectUtils.cpp new file mode 100644 index 000000000000..46db8633c66e --- /dev/null +++ b/libs/hwui/jni/RuntimeEffectUtils.cpp @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "RuntimeEffectUtils.h" + +#include "include/effects/SkRuntimeEffect.h" + +namespace android { +namespace uirenderer { + +static inline int ThrowIAEFmt(JNIEnv* env, const char* fmt, ...) { + va_list args; + va_start(args, fmt); + int ret = jniThrowExceptionFmt(env, "java/lang/IllegalArgumentException", fmt, args); + va_end(args); + return ret; +} + +bool isIntUniformType(const SkRuntimeEffect::Uniform::Type& type) { + switch (type) { + case SkRuntimeEffect::Uniform::Type::kFloat: + case SkRuntimeEffect::Uniform::Type::kFloat2: + case SkRuntimeEffect::Uniform::Type::kFloat3: + case SkRuntimeEffect::Uniform::Type::kFloat4: + case SkRuntimeEffect::Uniform::Type::kFloat2x2: + case SkRuntimeEffect::Uniform::Type::kFloat3x3: + case SkRuntimeEffect::Uniform::Type::kFloat4x4: + return false; + case SkRuntimeEffect::Uniform::Type::kInt: + case SkRuntimeEffect::Uniform::Type::kInt2: + case SkRuntimeEffect::Uniform::Type::kInt3: + case SkRuntimeEffect::Uniform::Type::kInt4: + return true; + } +} + +void UpdateFloatUniforms(JNIEnv* env, SkRuntimeEffectBuilder* builder, const char* uniformName, + const float values[], int count, bool isColor) { + SkRuntimeEffectBuilder::BuilderUniform uniform = builder->uniform(uniformName); + if (uniform.fVar == nullptr) { + ThrowIAEFmt(env, "unable to find uniform named %s", uniformName); + } else if (isColor != ((uniform.fVar->flags & SkRuntimeEffect::Uniform::kColor_Flag) != 0)) { + if (isColor) { + jniThrowExceptionFmt( + env, "java/lang/IllegalArgumentException", + "attempting to set a color uniform using the non-color specific APIs: %s %x", + uniformName, uniform.fVar->flags); + } else { + ThrowIAEFmt(env, + "attempting to set a non-color uniform using the setColorUniform APIs: %s", + uniformName); + } + } else if (isIntUniformType(uniform.fVar->type)) { + ThrowIAEFmt(env, "attempting to set a int uniform using the setUniform APIs: %s", + uniformName); + } else if (!uniform.set<float>(values, count)) { + ThrowIAEFmt(env, "mismatch in byte size for uniform [expected: %zu actual: %zu]", + uniform.fVar->sizeInBytes(), sizeof(float) * count); + } +} + +void UpdateIntUniforms(JNIEnv* env, SkRuntimeEffectBuilder* builder, const char* uniformName, + const int values[], int count) { + SkRuntimeEffectBuilder::BuilderUniform uniform = builder->uniform(uniformName); + if (uniform.fVar == nullptr) { + ThrowIAEFmt(env, "unable to find uniform named %s", uniformName); + } else if (!isIntUniformType(uniform.fVar->type)) { + ThrowIAEFmt(env, "attempting to set a non-int uniform using the setIntUniform APIs: %s", + uniformName); + } else if (!uniform.set<int>(values, count)) { + ThrowIAEFmt(env, "mismatch in byte size for uniform [expected: %zu actual: %zu]", + uniform.fVar->sizeInBytes(), sizeof(float) * count); + } +} + +void UpdateChild(JNIEnv* env, SkRuntimeEffectBuilder* builder, const char* childName, + SkFlattenable* childEffect) { + SkRuntimeShaderBuilder::BuilderChild builderChild = builder->child(childName); + if (builderChild.fChild == nullptr) { + ThrowIAEFmt(env, "unable to find shader named %s", childName); + return; + } + + builderChild = sk_ref_sp(childEffect); +} + +} // namespace uirenderer +} // namespace android
\ No newline at end of file diff --git a/libs/hwui/jni/RuntimeEffectUtils.h b/libs/hwui/jni/RuntimeEffectUtils.h new file mode 100644 index 000000000000..75623c0f7ac1 --- /dev/null +++ b/libs/hwui/jni/RuntimeEffectUtils.h @@ -0,0 +1,37 @@ +/* + * 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. + */ + +#ifndef RUNTIMEEFFECTUTILS_H +#define RUNTIMEEFFECTUTILS_H + +#include "GraphicsJNI.h" +#include "include/effects/SkRuntimeEffect.h" + +namespace android { +namespace uirenderer { + +void UpdateFloatUniforms(JNIEnv* env, SkRuntimeEffectBuilder* builder, const char* uniformName, + const float values[], int count, bool isColor); + +void UpdateIntUniforms(JNIEnv* env, SkRuntimeEffectBuilder* builder, const char* uniformName, + const int values[], int count); + +void UpdateChild(JNIEnv* env, SkRuntimeEffectBuilder* builder, const char* childName, + SkFlattenable* childEffect); +} // namespace uirenderer +} // namespace android + +#endif // MAIN_RUNTIMEEFFECTUTILS_H diff --git a/libs/hwui/jni/android_graphics_RenderNode.cpp b/libs/hwui/jni/android_graphics_RenderNode.cpp index 6e03bbd0fa16..d9dc8eb8c1b1 100644 --- a/libs/hwui/jni/android_graphics_RenderNode.cpp +++ b/libs/hwui/jni/android_graphics_RenderNode.cpp @@ -593,9 +593,9 @@ static void android_view_RenderNode_requestPositionUpdates(JNIEnv* env, jobject, Matrix4 transform; SkIRect clipBounds; + uirenderer::Rect initialClipBounds; + const auto clipFlags = props.getClippingFlags(); if (enableClip) { - uirenderer::Rect initialClipBounds; - const auto clipFlags = props.getClippingFlags(); if (clipFlags) { props.getClippingRectForFlags(clipFlags, &initialClipBounds); } else { @@ -659,8 +659,8 @@ static void android_view_RenderNode_requestPositionUpdates(JNIEnv* env, jobject, static_cast<jint>(bounds.left), static_cast<jint>(bounds.top), static_cast<jint>(bounds.right), static_cast<jint>(bounds.bottom), static_cast<jint>(clipBounds.fLeft), static_cast<jint>(clipBounds.fTop), - static_cast<jint>(clipBounds.fRight), - static_cast<jint>(clipBounds.fBottom)); + static_cast<jint>(clipBounds.fRight), static_cast<jint>(clipBounds.fBottom), + static_cast<jint>(props.getWidth()), static_cast<jint>(props.getHeight())); } if (!keepListening) { env->DeleteGlobalRef(mListener); @@ -891,7 +891,7 @@ int register_android_view_RenderNode(JNIEnv* env) { gPositionListener.callPositionChanged = GetStaticMethodIDOrDie( env, clazz, "callPositionChanged", "(Ljava/lang/ref/WeakReference;JIIII)Z"); gPositionListener.callPositionChanged2 = GetStaticMethodIDOrDie( - env, clazz, "callPositionChanged2", "(Ljava/lang/ref/WeakReference;JIIIIIIII)Z"); + env, clazz, "callPositionChanged2", "(Ljava/lang/ref/WeakReference;JIIIIIIIIII)Z"); gPositionListener.callApplyStretch = GetStaticMethodIDOrDie( env, clazz, "callApplyStretch", "(Ljava/lang/ref/WeakReference;JFFFFFFFFFF)Z"); gPositionListener.callPositionLost = GetStaticMethodIDOrDie( diff --git a/libs/hwui/libhwui.map.txt b/libs/hwui/libhwui.map.txt index 2414299321a9..b5591941453d 100644 --- a/libs/hwui/libhwui.map.txt +++ b/libs/hwui/libhwui.map.txt @@ -67,6 +67,7 @@ LIBHWUI_PLATFORM { SkFILEStream::SkFILEStream*; SkImageInfo::*; SkMemoryStream::SkMemoryStream*; + android::uirenderer::logBitmapDecode*; }; local: *; diff --git a/libs/hwui/platform/host/android/api-level.h b/libs/hwui/platform/host/android/api-level.h deleted file mode 120000 index 4fb4784f9f60..000000000000 --- a/libs/hwui/platform/host/android/api-level.h +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bionic/libc/include/android/api-level.h
\ No newline at end of file diff --git a/libs/hwui/utils/StatsUtils.cpp b/libs/hwui/utils/StatsUtils.cpp new file mode 100644 index 000000000000..5c4027e1a846 --- /dev/null +++ b/libs/hwui/utils/StatsUtils.cpp @@ -0,0 +1,102 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifdef __ANDROID__ +#include <dlfcn.h> +#include <log/log.h> +#include <statslog_hwui.h> +#include <statssocket_lazy.h> +#include <utils/Errors.h> + +#include <mutex> +#endif + +#include <unistd.h> + +#include "StatsUtils.h" + +namespace android { +namespace uirenderer { + +#ifdef __ANDROID__ + +namespace { + +int32_t toStatsColorSpaceTransfer(skcms_TFType transferType) { + switch (transferType) { + case skcms_TFType_sRGBish: + return stats::IMAGE_DECODED__COLOR_SPACE_TRANSFER__COLOR_SPACE_TRANSFER_SRGBISH; + case skcms_TFType_PQish: + return stats::IMAGE_DECODED__COLOR_SPACE_TRANSFER__COLOR_SPACE_TRANSFER_PQISH; + case skcms_TFType_HLGish: + return stats::IMAGE_DECODED__COLOR_SPACE_TRANSFER__COLOR_SPACE_TRANSFER_HLGISH; + default: + return stats::IMAGE_DECODED__COLOR_SPACE_TRANSFER__COLOR_SPACE_TRANSFER_UNKNOWN; + } +} + +int32_t toStatsBitmapFormat(SkColorType type) { + switch (type) { + case kAlpha_8_SkColorType: + return stats::IMAGE_DECODED__FORMAT__BITMAP_FORMAT_A_8; + case kRGB_565_SkColorType: + return stats::IMAGE_DECODED__FORMAT__BITMAP_FORMAT_RGB_565; + case kN32_SkColorType: + return stats::IMAGE_DECODED__FORMAT__BITMAP_FORMAT_ARGB_8888; + case kRGBA_F16_SkColorType: + return stats::IMAGE_DECODED__FORMAT__BITMAP_FORMAT_RGBA_F16; + case kRGBA_1010102_SkColorType: + return stats::IMAGE_DECODED__FORMAT__BITMAP_FORMAT_RGBA_1010102; + default: + return stats::IMAGE_DECODED__FORMAT__BITMAP_FORMAT_UNKNOWN; + } +} + +} // namespace + +#endif + +void logBitmapDecode(const SkImageInfo& info, bool hasGainmap) { +#ifdef __ANDROID__ + + if (!statssocket::lazy::IsAvailable()) { + std::once_flag once; + std::call_once(once, []() { ALOGD("libstatssocket not available, dropping stats"); }); + return; + } + + skcms_TFType tfnType = skcms_TFType_Invalid; + + if (info.colorSpace()) { + skcms_TransferFunction tfn; + info.colorSpace()->transferFn(&tfn); + tfnType = skcms_TransferFunction_getType(&tfn); + } + + auto status = + stats::stats_write(uirenderer::stats::IMAGE_DECODED, static_cast<int32_t>(getuid()), + uirenderer::toStatsColorSpaceTransfer(tfnType), hasGainmap, + uirenderer::toStatsBitmapFormat(info.colorType())); + ALOGW_IF(status != OK, "Image decoding logging dropped!"); +#endif +} + +void logBitmapDecode(const Bitmap& bitmap) { + logBitmapDecode(bitmap.info(), bitmap.hasGainmap()); +} + +} // namespace uirenderer +} // namespace android diff --git a/libs/hwui/utils/StatsUtils.h b/libs/hwui/utils/StatsUtils.h new file mode 100644 index 000000000000..0c247014a8eb --- /dev/null +++ b/libs/hwui/utils/StatsUtils.h @@ -0,0 +1,33 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include <SkBitmap.h> +#include <SkColorSpace.h> +#include <SkColorType.h> +#include <cutils/compiler.h> +#include <hwui/Bitmap.h> + +namespace android { +namespace uirenderer { + +ANDROID_API void logBitmapDecode(const SkImageInfo& info, bool hasGainmap); + +ANDROID_API void logBitmapDecode(const Bitmap& bitmap); + +} // namespace uirenderer +} // namespace android diff --git a/media/java/android/media/AudioAttributes.java b/media/java/android/media/AudioAttributes.java index 1024a5519965..5b9054797bed 100644 --- a/media/java/android/media/AudioAttributes.java +++ b/media/java/android/media/AudioAttributes.java @@ -16,11 +16,15 @@ package android.media; +import static android.media.audio.Flags.FLAG_SPEAKER_CLEANUP_USAGE; + +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.annotation.TestApi; +// TODO switch from HIDL imports to AIDL import android.audio.policy.configuration.V7_0.AudioUsage; import android.compat.annotation.UnsupportedAppUsage; import android.media.audiopolicy.AudioProductStrategy; @@ -247,6 +251,16 @@ public final class AudioAttributes implements Parcelable { public static final int USAGE_ANNOUNCEMENT = SYSTEM_USAGE_OFFSET + 3; /** + * @hide + * Usage value to use when a system application plays a signal intended to clean up the + * speaker transducers and free them of deposits of dust or water. + */ + @FlaggedApi(FLAG_SPEAKER_CLEANUP_USAGE) + @SystemApi + @RequiresPermission(android.Manifest.permission.MODIFY_AUDIO_ROUTING) + public static final int USAGE_SPEAKER_CLEANUP = SYSTEM_USAGE_OFFSET + 4; + + /** * IMPORTANT: when adding new usage types, add them to SDK_USAGES and update SUPPRESSIBLE_USAGES * if applicable, as well as audioattributes.proto. * Also consider adding them to <aaudio/AAudio.h> for the NDK. @@ -1202,7 +1216,6 @@ public final class AudioAttributes implements Parcelable { break; case AudioSystem.STREAM_BLUETOOTH_SCO: mContentType = CONTENT_TYPE_SPEECH; - mFlags |= FLAG_SCO; break; case AudioSystem.STREAM_DTMF: mContentType = CONTENT_TYPE_SONIFICATION; @@ -1522,6 +1535,8 @@ public final class AudioAttributes implements Parcelable { return "USAGE_VEHICLE_STATUS"; case USAGE_ANNOUNCEMENT: return "USAGE_ANNOUNCEMENT"; + case USAGE_SPEAKER_CLEANUP: + return "USAGE_SPEAKER_CLEANUP"; default: return "unknown usage " + usage; } @@ -1662,12 +1677,8 @@ public final class AudioAttributes implements Parcelable { } /** - * @param usage one of {@link AttributeSystemUsage}, - * {@link AttributeSystemUsage#USAGE_CALL_ASSISTANT}, - * {@link AttributeSystemUsage#USAGE_EMERGENCY}, - * {@link AttributeSystemUsage#USAGE_SAFETY}, - * {@link AttributeSystemUsage#USAGE_VEHICLE_STATUS}, - * {@link AttributeSystemUsage#USAGE_ANNOUNCEMENT} + * Returns whether the given usage can only be used by system-privileged components + * @param usage one of {@link AttributeSystemUsage}. * @return boolean indicating if the usage is a system usage or not * @hide */ @@ -1677,7 +1688,8 @@ public final class AudioAttributes implements Parcelable { || usage == USAGE_EMERGENCY || usage == USAGE_SAFETY || usage == USAGE_VEHICLE_STATUS - || usage == USAGE_ANNOUNCEMENT); + || usage == USAGE_ANNOUNCEMENT + || usage == USAGE_SPEAKER_CLEANUP); } /** @@ -1750,8 +1762,7 @@ public final class AudioAttributes implements Parcelable { AudioSystem.STREAM_SYSTEM : AudioSystem.STREAM_SYSTEM_ENFORCED; } if ((aa.getAllFlags() & FLAG_SCO) == FLAG_SCO) { - return fromGetVolumeControlStream ? - AudioSystem.STREAM_VOICE_CALL : AudioSystem.STREAM_BLUETOOTH_SCO; + return AudioSystem.STREAM_VOICE_CALL; } if ((aa.getAllFlags() & FLAG_BEACON) == FLAG_BEACON) { return fromGetVolumeControlStream ? @@ -1792,6 +1803,7 @@ public final class AudioAttributes implements Parcelable { case USAGE_SAFETY: case USAGE_VEHICLE_STATUS: case USAGE_ANNOUNCEMENT: + case USAGE_SPEAKER_CLEANUP: case USAGE_UNKNOWN: return AudioSystem.STREAM_MUSIC; default: @@ -1831,7 +1843,8 @@ public final class AudioAttributes implements Parcelable { USAGE_EMERGENCY, USAGE_SAFETY, USAGE_VEHICLE_STATUS, - USAGE_ANNOUNCEMENT + USAGE_ANNOUNCEMENT, + USAGE_SPEAKER_CLEANUP }) @Retention(RetentionPolicy.SOURCE) public @interface AttributeSystemUsage {} @@ -1881,6 +1894,7 @@ public final class AudioAttributes implements Parcelable { USAGE_SAFETY, USAGE_VEHICLE_STATUS, USAGE_ANNOUNCEMENT, + USAGE_SPEAKER_CLEANUP, }) @Retention(RetentionPolicy.SOURCE) public @interface AttributeUsage {} diff --git a/media/java/android/media/AudioDeviceInfo.java b/media/java/android/media/AudioDeviceInfo.java index 39b29d0156d2..2da8eecedc4d 100644 --- a/media/java/android/media/AudioDeviceInfo.java +++ b/media/java/android/media/AudioDeviceInfo.java @@ -16,11 +16,15 @@ package android.media; +import static android.media.audio.Flags.FLAG_ENABLE_MULTICHANNEL_GROUP_DEVICE; + import android.Manifest; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.RequiresPermission; import android.annotation.TestApi; +import android.media.audio.Flags; import android.util.SparseIntArray; import com.android.internal.annotations.VisibleForTesting; @@ -192,6 +196,15 @@ public final class AudioDeviceInfo { */ public static final int TYPE_DOCK_ANALOG = 31; + /** + * A device type describing a speaker group that supports multichannel contents. The speakers in + * the group are connected together using local network based protocols. The speaker group + * requires additional input of the physical positions of each individual speaker to provide a + * better experience on multichannel contents. + */ + @FlaggedApi(FLAG_ENABLE_MULTICHANNEL_GROUP_DEVICE) + public static final int TYPE_MULTICHANNEL_GROUP = 32; + /** @hide */ @IntDef(flag = false, prefix = "TYPE", value = { TYPE_BUILTIN_EARPIECE, @@ -224,7 +237,8 @@ public final class AudioDeviceInfo { TYPE_BLE_SPEAKER, TYPE_ECHO_REFERENCE, TYPE_BLE_BROADCAST, - TYPE_DOCK_ANALOG} + TYPE_DOCK_ANALOG, + TYPE_MULTICHANNEL_GROUP} ) @Retention(RetentionPolicy.SOURCE) public @interface AudioDeviceType {} @@ -285,7 +299,8 @@ public final class AudioDeviceInfo { TYPE_BLE_HEADSET, TYPE_BLE_SPEAKER, TYPE_BLE_BROADCAST, - TYPE_DOCK_ANALOG} + TYPE_DOCK_ANALOG, + TYPE_MULTICHANNEL_GROUP} ) @Retention(RetentionPolicy.SOURCE) public @interface AudioDeviceTypeOut {} @@ -321,7 +336,13 @@ public final class AudioDeviceInfo { case TYPE_BLE_BROADCAST: case TYPE_DOCK_ANALOG: return true; + default: + if (Flags.enableMultichannelGroupDevice()) { + if (type == TYPE_MULTICHANNEL_GROUP) { + return true; + } + } return false; } } @@ -665,6 +686,10 @@ public final class AudioDeviceInfo { INT_TO_EXT_DEVICE_MAPPING.put(AudioSystem.DEVICE_OUT_BLE_HEADSET, TYPE_BLE_HEADSET); INT_TO_EXT_DEVICE_MAPPING.put(AudioSystem.DEVICE_OUT_BLE_SPEAKER, TYPE_BLE_SPEAKER); INT_TO_EXT_DEVICE_MAPPING.put(AudioSystem.DEVICE_OUT_BLE_BROADCAST, TYPE_BLE_BROADCAST); + if (Flags.enableMultichannelGroupDevice()) { + INT_TO_EXT_DEVICE_MAPPING.put( + AudioSystem.DEVICE_OUT_MULTICHANNEL_GROUP, TYPE_MULTICHANNEL_GROUP); + } INT_TO_EXT_DEVICE_MAPPING.put(AudioSystem.DEVICE_IN_BUILTIN_MIC, TYPE_BUILTIN_MIC); INT_TO_EXT_DEVICE_MAPPING.put(AudioSystem.DEVICE_IN_BLUETOOTH_SCO_HEADSET, TYPE_BLUETOOTH_SCO); @@ -721,6 +746,10 @@ public final class AudioDeviceInfo { EXT_TO_INT_DEVICE_MAPPING.put(TYPE_BLE_HEADSET, AudioSystem.DEVICE_OUT_BLE_HEADSET); EXT_TO_INT_DEVICE_MAPPING.put(TYPE_BLE_SPEAKER, AudioSystem.DEVICE_OUT_BLE_SPEAKER); EXT_TO_INT_DEVICE_MAPPING.put(TYPE_BLE_BROADCAST, AudioSystem.DEVICE_OUT_BLE_BROADCAST); + if (Flags.enableMultichannelGroupDevice()) { + EXT_TO_INT_DEVICE_MAPPING.put( + TYPE_MULTICHANNEL_GROUP, AudioSystem.DEVICE_OUT_MULTICHANNEL_GROUP); + } // privileges mapping to input device EXT_TO_INT_INPUT_DEVICE_MAPPING = new SparseIntArray(); diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index 8250a536552e..9beeef4c160f 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -21,6 +21,7 @@ import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_AUDIO; import static android.content.Context.DEVICE_ID_DEFAULT; import static android.media.audio.Flags.autoPublicVolumeApiHardening; import static android.media.audio.Flags.automaticBtDeviceType; +import static android.media.audio.Flags.FLAG_DEPRECATE_STREAM_BT_SCO; import static android.media.audio.Flags.FLAG_FOCUS_EXCLUSIVE_WITH_RECORDING; import static android.media.audio.Flags.FLAG_FOCUS_FREEZE_TEST_API; import static android.media.audio.Flags.FLAG_SUPPORTED_DEVICE_TYPES_API; @@ -406,8 +407,10 @@ public class AudioManager { /** Used to identify the volume of audio streams for notifications */ public static final int STREAM_NOTIFICATION = AudioSystem.STREAM_NOTIFICATION; /** @hide Used to identify the volume of audio streams for phone calls when connected - * to bluetooth */ + * to bluetooth + * @deprecated use {@link #STREAM_VOICE_CALL} instead */ @SystemApi + @FlaggedApi(FLAG_DEPRECATE_STREAM_BT_SCO) public static final int STREAM_BLUETOOTH_SCO = AudioSystem.STREAM_BLUETOOTH_SCO; /** @hide Used to identify the volume of audio streams for enforced system sounds * in certain countries (e.g camera in Japan) */ @@ -6156,6 +6159,11 @@ public class AudioManager { */ public static final int DEVICE_OUT_BLE_BROADCAST = AudioSystem.DEVICE_OUT_BLE_BROADCAST; /** @hide + * The audio output device code for a wireless speaker group supporting multichannel content. + */ + public static final int DEVICE_OUT_MULTICHANNEL_GROUP = + AudioSystem.DEVICE_OUT_MULTICHANNEL_GROUP; + /** @hide * This is not used as a returned value from {@link #getDevicesForStream}, but could be * used in the future in a set method to select whatever default device is chosen by the * platform-specific implementation. diff --git a/media/java/android/media/AudioSystem.java b/media/java/android/media/AudioSystem.java index bf09cb07a8ed..d0d91ba599f9 100644 --- a/media/java/android/media/AudioSystem.java +++ b/media/java/android/media/AudioSystem.java @@ -93,7 +93,8 @@ public class AudioSystem /** @hide Used to identify the volume of audio streams for notifications */ public static final int STREAM_NOTIFICATION = 5; /** @hide - * Used to identify the volume of audio streams for phone calls when connected on bluetooth */ + * Used to identify the volume of audio streams for phone calls when connected on bluetooth + * @deprecated use {@link #STREAM_VOICE_CALL} instead */ public static final int STREAM_BLUETOOTH_SCO = 6; /** @hide Used to identify the volume of audio streams for enforced system sounds in certain * countries (e.g camera in Japan) */ @@ -110,7 +111,7 @@ public class AudioSystem public static final int STREAM_ASSISTANT = 11; /** * @hide - * @deprecated Use {@link #numStreamTypes() instead} + * @deprecated Use {@link #numStreamTypes()} instead */ public static final int NUM_STREAMS = 5; @@ -1066,6 +1067,8 @@ public class AudioSystem /** @hide */ public static final int DEVICE_OUT_IP = 0x800000; /** @hide */ + public static final int DEVICE_OUT_MULTICHANNEL_GROUP = 0x800001; + /** @hide */ public static final int DEVICE_OUT_BUS = 0x1000000; /** @hide */ public static final int DEVICE_OUT_PROXY = 0x2000000; @@ -1134,6 +1137,7 @@ public class AudioSystem DEVICE_OUT_ALL_SET.add(DEVICE_OUT_AUX_LINE); DEVICE_OUT_ALL_SET.add(DEVICE_OUT_SPEAKER_SAFE); DEVICE_OUT_ALL_SET.add(DEVICE_OUT_IP); + DEVICE_OUT_ALL_SET.add(DEVICE_OUT_MULTICHANNEL_GROUP); DEVICE_OUT_ALL_SET.add(DEVICE_OUT_BUS); DEVICE_OUT_ALL_SET.add(DEVICE_OUT_PROXY); DEVICE_OUT_ALL_SET.add(DEVICE_OUT_USB_HEADSET); @@ -1422,6 +1426,8 @@ public class AudioSystem /** @hide */ public static final String DEVICE_OUT_AUX_LINE_NAME = "aux_line"; /** @hide */ public static final String DEVICE_OUT_SPEAKER_SAFE_NAME = "speaker_safe"; /** @hide */ public static final String DEVICE_OUT_IP_NAME = "ip"; + /** @hide */ + public static final String DEVICE_OUT_MULTICHANNEL_GROUP_NAME = "multichannel_group"; /** @hide */ public static final String DEVICE_OUT_BUS_NAME = "bus"; /** @hide */ public static final String DEVICE_OUT_PROXY_NAME = "proxy"; /** @hide */ public static final String DEVICE_OUT_USB_HEADSET_NAME = "usb_headset"; @@ -1515,6 +1521,8 @@ public class AudioSystem return DEVICE_OUT_SPEAKER_SAFE_NAME; case DEVICE_OUT_IP: return DEVICE_OUT_IP_NAME; + case DEVICE_OUT_MULTICHANNEL_GROUP: + return DEVICE_OUT_MULTICHANNEL_GROUP_NAME; case DEVICE_OUT_BUS: return DEVICE_OUT_BUS_NAME; case DEVICE_OUT_PROXY: diff --git a/media/java/android/media/MediaCas.java b/media/java/android/media/MediaCas.java index b022ea11d848..88efed55c11f 100644 --- a/media/java/android/media/MediaCas.java +++ b/media/java/android/media/MediaCas.java @@ -16,12 +16,16 @@ package android.media; +import static android.media.tv.flags.Flags.FLAG_SET_RESOURCE_HOLDER_RETAIN; + import static com.android.media.flags.Flags.FLAG_UPDATE_CLIENT_PROFILE_PRIORITY; import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.RequiresPermission; +import android.annotation.SystemApi; import android.annotation.TestApi; import android.content.Context; import android.hardware.cas.AidlCasPluginDescriptor; @@ -511,18 +515,20 @@ public final class MediaCas implements AutoCloseable { private final TunerResourceManager.ResourcesReclaimListener mResourceListener = new TunerResourceManager.ResourcesReclaimListener() { - @Override - public void onReclaimResources() { - synchronized (mSessionMap) { - List<Session> sessionList = new ArrayList<>(mSessionMap.keySet()); - for (Session casSession: sessionList) { - casSession.close(); + @Override + public void onReclaimResources() { + synchronized (mSessionMap) { + List<Session> sessionList = new ArrayList<>(mSessionMap.keySet()); + for (Session casSession : sessionList) { + casSession.close(); + } + } + if (mEventHandler != null) { + mEventHandler.sendMessage( + mEventHandler.obtainMessage(EventHandler.MSG_CAS_RESOURCE_LOST)); } } - mEventHandler.sendMessage(mEventHandler.obtainMessage( - EventHandler.MSG_CAS_RESOURCE_LOST)); - } - }; + }; /** * Describe a CAS plugin with its CA_system_ID and string name. @@ -988,12 +994,32 @@ public final class MediaCas implements AutoCloseable { * @param priority the new priority. Any negative value would cause no-op on priority setting * and the API would only process nice value setting in that case. * @param niceValue the nice value. + * @hide */ @FlaggedApi(FLAG_UPDATE_CLIENT_PROFILE_PRIORITY) + @SystemApi + @RequiresPermission(android.Manifest.permission.TUNER_RESOURCE_ACCESS) public boolean updateResourcePriority(int priority, int niceValue) { return mTunerResourceManager.updateClientPriority(mClientId, priority, niceValue); } + /** + * Determines whether the resource holder retains ownership of the resource during a challenge + * scenario, when both resource holder and resource challenger have same processId and same + * priority. + * + * @param resourceHolderRetain Set to {@code true} to allow the resource holder to retain + * ownership, or false to allow the resource challenger to acquire the resource. + * If not explicitly set, resourceHolderRetain is set to {@code false}. + * @hide + */ + @FlaggedApi(FLAG_SET_RESOURCE_HOLDER_RETAIN) + @SystemApi + @RequiresPermission(android.Manifest.permission.TUNER_RESOURCE_ACCESS) + public void setResourceHolderRetain(boolean resourceHolderRetain) { + mTunerResourceManager.setResourceHolderRetain(mClientId, resourceHolderRetain); + } + IHwBinder getBinder() { if (mICas != null) { return null; // Return IHwBinder only for HIDL diff --git a/media/java/android/media/MediaCodecInfo.java b/media/java/android/media/MediaCodecInfo.java index 3a19f466f7c1..96edd63a9b12 100644 --- a/media/java/android/media/MediaCodecInfo.java +++ b/media/java/android/media/MediaCodecInfo.java @@ -23,6 +23,7 @@ import static android.media.codec.Flags.FLAG_HLG_EDITING; import static android.media.codec.Flags.FLAG_IN_PROCESS_SW_AUDIO_CODEC; import static android.media.codec.Flags.FLAG_NULL_OUTPUT_SURFACE; import static android.media.codec.Flags.FLAG_REGION_OF_INTEREST; +import static android.media.codec.Flags.FLAG_APV_SUPPORT; import static android.media.MediaCodec.GetFlag; import android.annotation.FlaggedApi; @@ -4496,6 +4497,265 @@ public final class MediaCodecInfo { @SuppressLint("AllUpper") public static final int AC4Level4 = 0x10; + // Profiles and levels/bands for APV Codec, corresponding to the definitions in + // "Advanced Professional Video", 10.1.3 Profiles, 10.1.4 Levels and Bands + // found at https://www.ietf.org/archive/id/draft-lim-apv-02.html + + /** + * APV codec profile 422-10 as per IETF lim-apv-02, 10.1.3.1.1 + */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVProfile422_10 = 0x01; + + /** + * APV codec profile 422-10 as per IETF lim-apv-02, 10.1.3.1.1 + * with HDR10. + */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVProfile422_10HDR10 = 0x1000; + + /** + * APV codec profile 422-10 as per IETF lim-apv-02, 10.1.3.1.1 + * with HDR10Plus. + */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVProfile422_10HDR10Plus = 0x2000; + + // For APV Levels, the numerical values are constructed as follows: + // ((0x100 << (level_num - 1)) | (1 << band)) + // where: + // - "level_num" is the APV Level numbered consecutively + // (i.e., Level 1 == 1, Level 1.1 == 2, etc.) + // - "band" is the APV Band + + /** APV Codec Level 1, Band 0 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel1Band0 = 0x101; + /** APV Codec Level 1, Band 1 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel1Band1 = 0x102; + /** APV Codec Level 1, Band 2 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel1Band2 = 0x104; + /** APV Codec Level 1, Band 3 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel1Band3 = 0x108; + /** APV Codec Level 1.1, Band 0 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel11Band0 = 0x201; + /** APV Codec Level 1.1, Band 1 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel11Band1 = 0x202; + /** APV Codec Level 1.1, Band 2 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel11Band2 = 0x204; + /** APV Codec Level 1.1, Band 3 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel11Band3 = 0x208; + /** APV Codec Level 2, Band 0 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel2Band0 = 0x401; + /** APV Codec Level 2, Band 1 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel2Band1 = 0x402; + /** APV Codec Level 2, Band 2 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel2Band2 = 0x404; + /** APV Codec Level 2, Band 3 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel2Band3 = 0x408; + /** APV Codec Level 2.1, Band 0 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel21Band0 = 0x801; + /** APV Codec Level 2.1, Band 1 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel21Band1 = 0x802; + /** APV Codec Level 2.1, Band 2 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel21Band2 = 0x804; + /** APV Codec Level 2.1, Band 3 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel21Band3 = 0x808; + /** APV Codec Level 3, Band 0 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel3Band0 = 0x1001; + /** APV Codec Level 3, Band 1 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel3Band1 = 0x1002; + /** APV Codec Level 3, Band 2 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel3Band2 = 0x1004; + /** APV Codec Level 3, Band 3 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel3Band3 = 0x1008; + /** APV Codec Level 3.1, Band 0 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel31Band0 = 0x2001; + /** APV Codec Level 3.1, Band 1 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel31Band1 = 0x2002; + /** APV Codec Level 3.1, Band 2 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel31Band2 = 0x2004; + /** APV Codec Level 3.1, Band 3 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel31Band3 = 0x2008; + /** APV Codec Level 4, Band 0 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel4Band0 = 0x4001; + /** APV Codec Level 4, Band 1 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel4Band1 = 0x4002; + /** APV Codec Level 4, Band 2 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel4Band2 = 0x4004; + /** APV Codec Level 4, Band 3 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel4Band3 = 0x4008; + /** APV Codec Level 4.1, Band 0 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel41Band0 = 0x8001; + /** APV Codec Level 4.1, Band 1 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel41Band1 = 0x8002; + /** APV Codec Level 4.1, Band 2 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel41Band2 = 0x8004; + /** APV Codec Level 4.1, Band 3 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel41Band3 = 0x8008; + /** APV Codec Level 5, Band 0 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel5Band0 = 0x10001; + /** APV Codec Level 5, Band 1 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel5Band1 = 0x10002; + /** APV Codec Level 5, Band 2 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel5Band2 = 0x10004; + /** APV Codec Level 5, Band 3 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel5Band3 = 0x10008; + /** APV Codec Level 5.1, Band 0 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel51Band0 = 0x20001; + /** APV Codec Level 5.1, Band 1 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel51Band1 = 0x20002; + /** APV Codec Level 5.1, Band 2 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel51Band2 = 0x20004; + /** APV Codec Level 5.1, Band 3 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel51Band3 = 0x20008; + /** APV Codec Level 6, Band 0 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel6Band0 = 0x40001; + /** APV Codec Level 6, Band 1 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel6Band1 = 0x40002; + /** APV Codec Level 6, Band 2 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel6Band2 = 0x40004; + /** APV Codec Level 6, Band 3 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel6Band3 = 0x40008; + /** APV Codec Level 6.1, Band 0 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel61Band0 = 0x80001; + /** APV Codec Level 6.1, Band 1 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel61Band1 = 0x80002; + /** APV Codec Level 6.1, Band 2 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel61Band2 = 0x80004; + /** APV Codec Level 6.1, Band 3 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel61Band3 = 0x80008; + /** APV Codec Level 7, Band 0 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel7Band0 = 0x100001; + /** APV Codec Level 7, Band 1 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel7Band1 = 0x100002; + /** APV Codec Level 7, Band 2 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel7Band2 = 0x100004; + /** APV Codec Level 7, Band 3 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel7Band3 = 0x100008; + /** APV Codec Level 7.1, Band 0 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel71Band0 = 0x200001; + /** APV Codec Level 7.1, Band 1 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel71Band1 = 0x200002; + /** APV Codec Level 7.1, Band 2 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel71Band2 = 0x200004; + /** APV Codec Level 7.1, Band 3 as per IETF lim-apv-02, 10.1.4 */ + @SuppressLint("AllUpper") + @FlaggedApi(FLAG_APV_SUPPORT) + public static final int APVLevel71Band3 = 0x200008; + /** * The profile of the media content. Depending on the type of media this can be * one of the profile values defined in this class. diff --git a/media/java/android/media/MediaFormat.java b/media/java/android/media/MediaFormat.java index cd0654ceb348..b08a86ee8f46 100644 --- a/media/java/android/media/MediaFormat.java +++ b/media/java/android/media/MediaFormat.java @@ -18,6 +18,7 @@ package android.media; import static android.media.codec.Flags.FLAG_IN_PROCESS_SW_AUDIO_CODEC; import static android.media.codec.Flags.FLAG_REGION_OF_INTEREST; +import static android.media.codec.Flags.FLAG_APV_SUPPORT; import static com.android.media.codec.flags.Flags.FLAG_CODEC_IMPORTANCE; import static com.android.media.codec.flags.Flags.FLAG_LARGE_AUDIO_FRAME; @@ -157,6 +158,8 @@ import java.util.stream.Collectors; public final class MediaFormat { public static final String MIMETYPE_VIDEO_VP8 = "video/x-vnd.on2.vp8"; public static final String MIMETYPE_VIDEO_VP9 = "video/x-vnd.on2.vp9"; + @FlaggedApi(FLAG_APV_SUPPORT) + public static final String MIMETYPE_VIDEO_APV = "video/apv"; public static final String MIMETYPE_VIDEO_AV1 = "video/av01"; public static final String MIMETYPE_VIDEO_AVC = "video/avc"; public static final String MIMETYPE_VIDEO_HEVC = "video/hevc"; diff --git a/media/java/android/media/MediaRecorder.java b/media/java/android/media/MediaRecorder.java index bdfa63010adc..2d17bf500f12 100644 --- a/media/java/android/media/MediaRecorder.java +++ b/media/java/android/media/MediaRecorder.java @@ -387,13 +387,13 @@ public class MediaRecorder implements AudioRouting, /** * Audio source for capturing broadcast radio tuner output. * Capturing the radio tuner output requires the - * {@link android.Manifest.permission#CAPTURE_AUDIO_OUTPUT} permission. + * {@link android.Manifest.permission#CAPTURE_TUNER_AUDIO_INPUT} permission. * This permission is reserved for use by system components and is not available to * third-party applications. * @hide */ @SystemApi - @RequiresPermission(android.Manifest.permission.CAPTURE_AUDIO_OUTPUT) + @RequiresPermission(android.Manifest.permission.CAPTURE_TUNER_AUDIO_INPUT) public static final int RADIO_TUNER = 1998; /** diff --git a/media/java/android/media/MediaRoute2Info.java b/media/java/android/media/MediaRoute2Info.java index e048d5c56cfe..816729da5ad4 100644 --- a/media/java/android/media/MediaRoute2Info.java +++ b/media/java/android/media/MediaRoute2Info.java @@ -17,6 +17,7 @@ package android.media; import static android.media.MediaRouter2Utils.toUniqueId; +import static android.media.audio.Flags.FLAG_ENABLE_MULTICHANNEL_GROUP_DEVICE; import static com.android.media.flags.Flags.FLAG_ENABLE_AUDIO_POLICIES_DEVICE_AND_BLUETOOTH_CONTROLLER; import static com.android.media.flags.Flags.FLAG_ENABLE_BUILT_IN_SPEAKER_ROUTE_SUITABILITY_STATUSES; @@ -272,6 +273,19 @@ public final class MediaRoute2Info implements Parcelable { public static final int TYPE_BLE_HEADSET = AudioDeviceInfo.TYPE_BLE_HEADSET; /** + * Indicates the route is a speaker group supporting multichannel contents. + * + * <p>The speakers in the group are connected together using local network based protocols. The + * speaker group requires additional input of the physical positions of each individual speaker + * to provide a better experience on multichannel contents. + * + * @see #getType + */ + @FlaggedApi(FLAG_ENABLE_MULTICHANNEL_GROUP_DEVICE) + public static final int TYPE_MULTICHANNEL_SPEAKER_GROUP = + AudioDeviceInfo.TYPE_MULTICHANNEL_GROUP; + + /** * Indicates the route is a remote TV. * * <p>A remote device uses a routing protocol managed by the application, as opposed to the diff --git a/media/java/android/media/quality/AmbientBacklightEvent.aidl b/media/java/android/media/quality/AmbientBacklightEvent.aidl new file mode 100644 index 000000000000..174cd461e846 --- /dev/null +++ b/media/java/android/media/quality/AmbientBacklightEvent.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.quality; + +parcelable AmbientBacklightEvent; diff --git a/media/java/android/media/quality/AmbientBacklightEvent.java b/media/java/android/media/quality/AmbientBacklightEvent.java new file mode 100644 index 000000000000..3bc6b86c0615 --- /dev/null +++ b/media/java/android/media/quality/AmbientBacklightEvent.java @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.quality; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Parcel; +import android.os.Parcelable; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Objects; + +/** + * @hide + */ +public final class AmbientBacklightEvent implements Parcelable { + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({AMBIENT_BACKLIGHT_EVENT_ENABLED, AMBIENT_BACKLIGHT_EVENT_DISABLED, + AMBIENT_BACKLIGHT_EVENT_METADATA, + AMBIENT_BACKLIGHT_EVENT_INTERRUPTED}) + public @interface AmbientBacklightEventTypes {} + + /** + * Event type for ambient backlight events. The ambient backlight is enabled. + */ + public static final int AMBIENT_BACKLIGHT_EVENT_ENABLED = 1; + + /** + * Event type for ambient backlight events. The ambient backlight is disabled. + */ + public static final int AMBIENT_BACKLIGHT_EVENT_DISABLED = 2; + + /** + * Event type for ambient backlight events. The ambient backlight metadata is + * available. + */ + public static final int AMBIENT_BACKLIGHT_EVENT_METADATA = 3; + + /** + * Event type for ambient backlight events. The ambient backlight event is preempted by another + * application. + */ + public static final int AMBIENT_BACKLIGHT_EVENT_INTERRUPTED = 4; + + private final int mEventType; + @Nullable + private final AmbientBacklightMetadata mMetadata; + + public AmbientBacklightEvent(int eventType, + @Nullable AmbientBacklightMetadata metadata) { + mEventType = eventType; + mMetadata = metadata; + } + + private AmbientBacklightEvent(Parcel in) { + mEventType = in.readInt(); + mMetadata = in.readParcelable(AmbientBacklightMetadata.class.getClassLoader()); + } + + public int getEventType() { + return mEventType; + } + + @Nullable + public AmbientBacklightMetadata getMetadata() { + return mMetadata; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mEventType); + dest.writeParcelable(mMetadata, flags); + } + + @Override + public int describeContents() { + return 0; + } + + public static final @NonNull Parcelable.Creator<AmbientBacklightEvent> CREATOR = + new Parcelable.Creator<AmbientBacklightEvent>() { + public AmbientBacklightEvent createFromParcel(Parcel in) { + return new AmbientBacklightEvent(in); + } + + public AmbientBacklightEvent[] newArray(int size) { + return new AmbientBacklightEvent[size]; + } + }; + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (!(obj instanceof AmbientBacklightEvent)) { + return false; + } + + AmbientBacklightEvent other = (AmbientBacklightEvent) obj; + return mEventType == other.mEventType + && Objects.equals(mMetadata, other.mMetadata); + } + + @Override + public int hashCode() { + return mEventType * 31 + (mMetadata != null ? mMetadata.hashCode() : 0); + } + + @Override + public String toString() { + return "AmbientBacklightEvent{" + + "mEventType=" + mEventType + + ", mMetadata=" + mMetadata + + '}'; + } +} diff --git a/media/java/android/media/quality/AmbientBacklightMetadata.aidl b/media/java/android/media/quality/AmbientBacklightMetadata.aidl new file mode 100644 index 000000000000..b95a474fbf90 --- /dev/null +++ b/media/java/android/media/quality/AmbientBacklightMetadata.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.quality; + +parcelable AmbientBacklightMetadata;
\ No newline at end of file diff --git a/media/java/android/media/quality/AmbientBacklightMetadata.java b/media/java/android/media/quality/AmbientBacklightMetadata.java new file mode 100644 index 000000000000..fc779348de11 --- /dev/null +++ b/media/java/android/media/quality/AmbientBacklightMetadata.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.quality; + +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.NonNull; + +import java.util.Arrays; + +/** + * @hide + */ +public class AmbientBacklightMetadata implements Parcelable { + @NonNull + private final String mPackageName; + private final int mCompressAlgorithm; + private final int mSource; + private final int mColorFormat; + private final int mHorizontalZonesNumber; + private final int mVerticalZonesNumber; + @NonNull + private final int[] mZonesColors; + + public AmbientBacklightMetadata(@NonNull String packageName, int compressAlgorithm, + int source, int colorFormat, int horizontalZonesNumber, int verticalZonesNumber, + @NonNull int[] zonesColors) { + mPackageName = packageName; + mCompressAlgorithm = compressAlgorithm; + mSource = source; + mColorFormat = colorFormat; + mHorizontalZonesNumber = horizontalZonesNumber; + mVerticalZonesNumber = verticalZonesNumber; + mZonesColors = zonesColors; + } + + private AmbientBacklightMetadata(Parcel in) { + mPackageName = in.readString(); + mCompressAlgorithm = in.readInt(); + mSource = in.readInt(); + mColorFormat = in.readInt(); + mHorizontalZonesNumber = in.readInt(); + mVerticalZonesNumber = in.readInt(); + mZonesColors = in.createIntArray(); + } + + @NonNull + public String getPackageName() { + return mPackageName; + } + + public int getCompressAlgorithm() { + return mCompressAlgorithm; + } + + public int getSource() { + return mSource; + } + + public int getColorFormat() { + return mColorFormat; + } + + public int getHorizontalZonesNumber() { + return mHorizontalZonesNumber; + } + + public int getVerticalZonesNumber() { + return mVerticalZonesNumber; + } + + @NonNull + public int[] getZonesColors() { + return mZonesColors; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeString(mPackageName); + dest.writeInt(mCompressAlgorithm); + dest.writeInt(mSource); + dest.writeInt(mColorFormat); + dest.writeInt(mHorizontalZonesNumber); + dest.writeInt(mVerticalZonesNumber); + dest.writeIntArray(mZonesColors); + } + + @Override + public int describeContents() { + return 0; + } + + public static final @NonNull Parcelable.Creator<AmbientBacklightMetadata> CREATOR = + new Parcelable.Creator<AmbientBacklightMetadata>() { + public AmbientBacklightMetadata createFromParcel(Parcel in) { + return new AmbientBacklightMetadata(in); + } + + public AmbientBacklightMetadata[] newArray(int size) { + return new AmbientBacklightMetadata[size]; + } + }; + + @Override + public String toString() { + return "AmbientBacklightMetadata{packageName=" + mPackageName + + ", compressAlgorithm=" + mCompressAlgorithm + ", source=" + mSource + + ", colorFormat=" + mColorFormat + ", horizontalZonesNumber=" + + mHorizontalZonesNumber + ", verticalZonesNumber=" + mVerticalZonesNumber + + ", zonesColors=" + Arrays.toString(mZonesColors) + "}"; + } +} diff --git a/media/java/android/media/quality/AmbientBacklightSettings.aidl b/media/java/android/media/quality/AmbientBacklightSettings.aidl new file mode 100644 index 000000000000..e2cdd03194cd --- /dev/null +++ b/media/java/android/media/quality/AmbientBacklightSettings.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.quality; + +parcelable AmbientBacklightSettings; diff --git a/media/java/android/media/quality/AmbientBacklightSettings.java b/media/java/android/media/quality/AmbientBacklightSettings.java new file mode 100644 index 000000000000..391eb225bcab --- /dev/null +++ b/media/java/android/media/quality/AmbientBacklightSettings.java @@ -0,0 +1,203 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.quality; + +import android.annotation.IntDef; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.NonNull; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * @hide + */ +public class AmbientBacklightSettings implements Parcelable { + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({SOURCE_NONE, SOURCE_AUDIO, SOURCE_VIDEO, SOURCE_AUDIO_VIDEO}) + public @interface Source {} + + /** + * The detection is disabled. + */ + public static final int SOURCE_NONE = 0; + + /** + * The detection is enabled for audio. + */ + public static final int SOURCE_AUDIO = 1; + + /** + * The detection is enabled for video. + */ + public static final int SOURCE_VIDEO = 2; + + /** + * The detection is enabled for audio and video. + */ + public static final int SOURCE_AUDIO_VIDEO = 3; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({COLOR_FORMAT_RGB888}) + public @interface ColorFormat {} + + /** + * The color format is RGB888. + */ + public static final int COLOR_FORMAT_RGB888 = 1; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({ALGORITHM_NONE, ALGORITHM_RLE}) + public @interface CompressAlgorithm {} + + /** + * The compress algorithm is disabled. + */ + public static final int ALGORITHM_NONE = 0; + + /** + * The compress algorithm is RLE. + */ + public static final int ALGORITHM_RLE = 1; + + /** + * The source of the ambient backlight. + */ + private final int mSource; + + /** + * The maximum framerate for the ambient backlight. + */ + private final int mMaxFps; + + /** + * The color format for the ambient backlight. + */ + private final int mColorFormat; + + /** + * The number of zones in horizontal direction. + */ + private final int mHorizontalZonesNumber; + + /** + * The number of zones in vertical direction. + */ + private final int mVerticalZonesNumber; + + /** + * The flag to indicate whether the letterbox is omitted. + */ + private final boolean mIsLetterboxOmitted; + + /** + * The color threshold for the ambient backlight. + */ + private final int mThreshold; + + public AmbientBacklightSettings(int source, int maxFps, int colorFormat, + int horizontalZonesNumber, int verticalZonesNumber, boolean isLetterboxOmitted, + int threshold) { + mSource = source; + mMaxFps = maxFps; + mColorFormat = colorFormat; + mHorizontalZonesNumber = horizontalZonesNumber; + mVerticalZonesNumber = verticalZonesNumber; + mIsLetterboxOmitted = isLetterboxOmitted; + mThreshold = threshold; + } + + private AmbientBacklightSettings(Parcel in) { + mSource = in.readInt(); + mMaxFps = in.readInt(); + mColorFormat = in.readInt(); + mHorizontalZonesNumber = in.readInt(); + mVerticalZonesNumber = in.readInt(); + mIsLetterboxOmitted = in.readBoolean(); + mThreshold = in.readInt(); + } + + @Source + public int getSource() { + return mSource; + } + + public int getMaxFps() { + return mMaxFps; + } + + @ColorFormat + public int getColorFormat() { + return mColorFormat; + } + + public int getHorizontalZonesNumber() { + return mHorizontalZonesNumber; + } + + public int getVerticalZonesNumber() { + return mVerticalZonesNumber; + } + + public boolean isLetterboxOmitted() { + return mIsLetterboxOmitted; + } + + public int getThreshold() { + return mThreshold; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(mSource); + dest.writeInt(mMaxFps); + dest.writeInt(mColorFormat); + dest.writeInt(mHorizontalZonesNumber); + dest.writeInt(mVerticalZonesNumber); + dest.writeBoolean(mIsLetterboxOmitted); + dest.writeInt(mThreshold); + } + + @Override + public int describeContents() { + return 0; + } + + public static final @NonNull Parcelable.Creator<AmbientBacklightSettings> CREATOR = + new Parcelable.Creator<AmbientBacklightSettings>() { + public AmbientBacklightSettings createFromParcel(Parcel in) { + return new AmbientBacklightSettings(in); + } + + public AmbientBacklightSettings[] newArray(int size) { + return new AmbientBacklightSettings[size]; + } + }; + + @Override + public String toString() { + return "AmbientBacklightSettings{Source=" + mSource + ", MaxFps=" + mMaxFps + + ", ColorFormat=" + mColorFormat + ", HorizontalZonesNumber=" + + mHorizontalZonesNumber + ", VerticalZonesNumber=" + mVerticalZonesNumber + + ", IsLetterboxOmitted=" + mIsLetterboxOmitted + ", Threshold=" + mThreshold + "}"; + } +} diff --git a/media/java/android/media/quality/IAmbientBacklightCallback.aidl b/media/java/android/media/quality/IAmbientBacklightCallback.aidl new file mode 100644 index 000000000000..159f5b7b5e71 --- /dev/null +++ b/media/java/android/media/quality/IAmbientBacklightCallback.aidl @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.quality; + +import android.media.quality.AmbientBacklightEvent; + +/** @hide */ +oneway interface IAmbientBacklightCallback { + void onAmbientBacklightEvent(in AmbientBacklightEvent event); +} diff --git a/media/java/android/media/quality/IMediaQualityManager.aidl b/media/java/android/media/quality/IMediaQualityManager.aidl index 4f1210a7c7c1..e6c79dd9681f 100644 --- a/media/java/android/media/quality/IMediaQualityManager.aidl +++ b/media/java/android/media/quality/IMediaQualityManager.aidl @@ -16,9 +16,49 @@ package android.media.quality; +import android.media.quality.AmbientBacklightSettings; +import android.media.quality.IAmbientBacklightCallback; +import android.media.quality.IPictureProfileCallback; +import android.media.quality.ISoundProfileCallback; +import android.media.quality.ParamCapability; +import android.media.quality.PictureProfile; +import android.media.quality.SoundProfile; + /** * Interface for Media Quality Manager * @hide */ interface IMediaQualityManager { -}
\ No newline at end of file + PictureProfile createPictureProfile(in PictureProfile pp); + void updatePictureProfile(in long id, in PictureProfile pp); + void removePictureProfile(in long id); + PictureProfile getPictureProfileById(in long id); + List<PictureProfile> getPictureProfilesByPackage(in String packageName); + List<PictureProfile> getAvailablePictureProfiles(); + List<PictureProfile> getAllPictureProfiles(); + + SoundProfile createSoundProfile(in SoundProfile pp); + void updateSoundProfile(in long id, in SoundProfile pp); + void removeSoundProfile(in long id); + SoundProfile getSoundProfileById(in long id); + List<SoundProfile> getSoundProfilesByPackage(in String packageName); + List<SoundProfile> getAvailableSoundProfiles(); + List<SoundProfile> getAllSoundProfiles(); + + void registerPictureProfileCallback(in IPictureProfileCallback cb); + void registerSoundProfileCallback(in ISoundProfileCallback cb); + void registerAmbientBacklightCallback(in IAmbientBacklightCallback cb); + + List<ParamCapability> getParamCapabilities(in List<String> names); + + boolean isSupported(); + void setAutoPictureQualityEnabled(in boolean enabled); + boolean isAutoPictureQualityEnabled(); + void setSuperResolutionEnabled(in boolean enabled); + boolean isSuperResolutionEnabled(); + void setAutoSoundQualityEnabled(in boolean enabled); + boolean isAutoSoundQualityEnabled(); + + void setAmbientBacklightSettings(in AmbientBacklightSettings settings); + void setAmbientBacklightEnabled(in boolean enabled); +} diff --git a/media/java/android/media/quality/IPictureProfileCallback.aidl b/media/java/android/media/quality/IPictureProfileCallback.aidl new file mode 100644 index 000000000000..05441cde31e7 --- /dev/null +++ b/media/java/android/media/quality/IPictureProfileCallback.aidl @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package android.media.quality; + +import android.media.quality.PictureProfile; + +/** + * Interface to receive callbacks from IMediaQuality. + * @hide + */ +oneway interface IPictureProfileCallback { + void onPictureProfileAdded(in long id, in PictureProfile p); + void onPictureProfileUpdated(in long id, in PictureProfile p); + void onPictureProfileRemoved(in long id, in PictureProfile p); +} diff --git a/media/java/android/media/quality/ISoundProfileCallback.aidl b/media/java/android/media/quality/ISoundProfileCallback.aidl new file mode 100644 index 000000000000..72d1524198fd --- /dev/null +++ b/media/java/android/media/quality/ISoundProfileCallback.aidl @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +package android.media.quality; + +import android.media.quality.SoundProfile; + +/** + * Interface to receive callbacks from IMediaQuality. + * @hide + */ +oneway interface ISoundProfileCallback { + void onSoundProfileAdded(in long id, in SoundProfile p); + void onSoundProfileUpdated(in long id, in SoundProfile p); + void onSoundProfileRemoved(in long id, in SoundProfile p); +} diff --git a/media/java/android/media/quality/MediaQualityContract.java b/media/java/android/media/quality/MediaQualityContract.java new file mode 100644 index 000000000000..5cbc81d5f92d --- /dev/null +++ b/media/java/android/media/quality/MediaQualityContract.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.quality; + + +import android.annotation.FlaggedApi; +import android.media.tv.flags.Flags; + +/** + * @hide + */ +@FlaggedApi(Flags.FLAG_MEDIA_QUALITY_FW) +public class MediaQualityContract { + + public interface BaseParameters { + String PARAMETER_ID = "_id"; + String PARAMETER_NAME = "_name"; + String PARAMETER_PACKAGE = "_package"; + String PARAMETER_INPUT_ID = "_input_id"; + + } + public static final class PictureQuality implements BaseParameters { + public static final String PARAMETER_BRIGHTNESS = "brightness"; + public static final String PARAMETER_CONTRAST = "contrast"; + public static final String PARAMETER_SHARPNESS = "sharpness"; + public static final String PARAMETER_SATURATION = "saturation"; + } + + public static final class SoundQuality implements BaseParameters { + public static final String PARAMETER_BALANCE = "balance"; + public static final String PARAMETER_BASS = "bass"; + public static final String PARAMETER_TREBLE = "treble"; + } + + private MediaQualityContract() { + } +} diff --git a/media/java/android/media/quality/MediaQualityManager.java b/media/java/android/media/quality/MediaQualityManager.java index 86dbcdbbdf5e..1237d673162c 100644 --- a/media/java/android/media/quality/MediaQualityManager.java +++ b/media/java/android/media/quality/MediaQualityManager.java @@ -16,10 +16,22 @@ package android.media.quality; +import android.annotation.CallbackExecutor; import android.annotation.FlaggedApi; +import android.annotation.NonNull; import android.annotation.SystemService; import android.content.Context; import android.media.tv.flags.Flags; +import android.os.RemoteException; + +import androidx.annotation.RequiresPermission; + +import com.android.internal.util.Preconditions; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.Executor; /** * Expose TV setting APIs for the application to use @@ -27,12 +39,20 @@ import android.media.tv.flags.Flags; */ @FlaggedApi(Flags.FLAG_MEDIA_QUALITY_FW) @SystemService(Context.MEDIA_QUALITY_SERVICE) -public class MediaQualityManager { +public final class MediaQualityManager { // TODO: unhide the APIs for api review private static final String TAG = "MediaQualityManager"; private final IMediaQualityManager mService; private final Context mContext; + private final Object mLock = new Object(); + // @GuardedBy("mLock") + private final List<PictureProfileCallbackRecord> mPpCallbackRecords = new ArrayList<>(); + // @GuardedBy("mLock") + private final List<SoundProfileCallbackRecord> mSpCallbackRecords = new ArrayList<>(); + // @GuardedBy("mLock") + private final List<AmbientBacklightCallbackRecord> mAbCallbackRecords = new ArrayList<>(); + /** * @hide @@ -40,5 +60,648 @@ public class MediaQualityManager { public MediaQualityManager(Context context, IMediaQualityManager service) { mContext = context; mService = service; + IPictureProfileCallback ppCallback = new IPictureProfileCallback.Stub() { + @Override + public void onPictureProfileAdded(long profileId, PictureProfile profile) { + synchronized (mLock) { + for (PictureProfileCallbackRecord record : mPpCallbackRecords) { + // TODO: filter callback record + record.postPictureProfileAdded(profileId, profile); + } + } + } + @Override + public void onPictureProfileUpdated(long profileId, PictureProfile profile) { + synchronized (mLock) { + for (PictureProfileCallbackRecord record : mPpCallbackRecords) { + // TODO: filter callback record + record.postPictureProfileUpdated(profileId, profile); + } + } + } + @Override + public void onPictureProfileRemoved(long profileId, PictureProfile profile) { + synchronized (mLock) { + for (PictureProfileCallbackRecord record : mPpCallbackRecords) { + // TODO: filter callback record + record.postPictureProfileRemoved(profileId, profile); + } + } + } + }; + ISoundProfileCallback spCallback = new ISoundProfileCallback.Stub() { + @Override + public void onSoundProfileAdded(long profileId, SoundProfile profile) { + synchronized (mLock) { + for (SoundProfileCallbackRecord record : mSpCallbackRecords) { + // TODO: filter callback record + record.postSoundProfileAdded(profileId, profile); + } + } + } + @Override + public void onSoundProfileUpdated(long profileId, SoundProfile profile) { + synchronized (mLock) { + for (SoundProfileCallbackRecord record : mSpCallbackRecords) { + // TODO: filter callback record + record.postSoundProfileUpdated(profileId, profile); + } + } + } + @Override + public void onSoundProfileRemoved(long profileId, SoundProfile profile) { + synchronized (mLock) { + for (SoundProfileCallbackRecord record : mSpCallbackRecords) { + // TODO: filter callback record + record.postSoundProfileRemoved(profileId, profile); + } + } + } + }; + IAmbientBacklightCallback abCallback = new IAmbientBacklightCallback.Stub() { + @Override + public void onAmbientBacklightEvent(AmbientBacklightEvent event) { + synchronized (mLock) { + for (AmbientBacklightCallbackRecord record : mAbCallbackRecords) { + record.postAmbientBacklightEvent(event); + } + } + } + }; + + try { + if (mService != null) { + mService.registerPictureProfileCallback(ppCallback); + mService.registerSoundProfileCallback(spCallback); + mService.registerAmbientBacklightCallback(abCallback); + } + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Registers a {@link PictureProfileCallback}. + * @hide + */ + public void registerPictureProfileCallback( + @NonNull @CallbackExecutor Executor executor, + @NonNull PictureProfileCallback callback) { + Preconditions.checkNotNull(callback); + Preconditions.checkNotNull(executor); + synchronized (mLock) { + mPpCallbackRecords.add(new PictureProfileCallbackRecord(callback, executor)); + } + } + + /** + * Unregisters the existing {@link PictureProfileCallback}. + * @hide + */ + public void unregisterPictureProfileCallback(@NonNull final PictureProfileCallback callback) { + Preconditions.checkNotNull(callback); + synchronized (mLock) { + for (Iterator<PictureProfileCallbackRecord> it = mPpCallbackRecords.iterator(); + it.hasNext(); ) { + PictureProfileCallbackRecord record = it.next(); + if (record.getCallback() == callback) { + it.remove(); + break; + } + } + } + } + + + /** + * Gets picture profile by given profile ID. + * @return the corresponding picture profile if available; {@code null} if the ID doesn't + * exist or the profile is not accessible to the caller. + */ + public PictureProfile getPictureProfileById(long profileId) { + try { + return mService.getPictureProfileById(profileId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + + /** @SystemApi gets profiles that available to the given package */ + @RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE) + public List<PictureProfile> getPictureProfilesByPackage(String packageName) { + try { + return mService.getPictureProfilesByPackage(packageName); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** Gets profiles that available to the caller package */ + public List<PictureProfile> getAvailablePictureProfiles() { + try { + return mService.getAvailablePictureProfiles(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** @SystemApi all stored picture profiles of all packages */ + @RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE) + public List<PictureProfile> getAllPictureProfiles() { + try { + return mService.getAllPictureProfiles(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + + /** + * Creates a picture profile and store it in the system. + * + * @return the stored profile with an assigned profile ID. + */ + public PictureProfile createPictureProfile(PictureProfile pp) { + try { + return mService.createPictureProfile(pp); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + + /** + * Updates an existing picture profile and store it in the system. + */ + public void updatePictureProfile(long profileId, PictureProfile pp) { + try { + mService.updatePictureProfile(profileId, pp); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + + /** + * Removes a picture profile from the system. + */ + public void removePictureProfile(long profileId) { + try { + mService.removePictureProfile(profileId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Registers a {@link SoundProfileCallback}. + * @hide + */ + public void registerSoundProfileCallback( + @NonNull @CallbackExecutor Executor executor, + @NonNull SoundProfileCallback callback) { + Preconditions.checkNotNull(callback); + Preconditions.checkNotNull(executor); + synchronized (mLock) { + mSpCallbackRecords.add(new SoundProfileCallbackRecord(callback, executor)); + } + } + + /** + * Unregisters the existing {@link SoundProfileCallback}. + * @hide + */ + public void unregisterSoundProfileCallback(@NonNull final SoundProfileCallback callback) { + Preconditions.checkNotNull(callback); + synchronized (mLock) { + for (Iterator<SoundProfileCallbackRecord> it = mSpCallbackRecords.iterator(); + it.hasNext(); ) { + SoundProfileCallbackRecord record = it.next(); + if (record.getCallback() == callback) { + it.remove(); + break; + } + } + } + } + + + /** + * Gets sound profile by given profile ID. + * @return the corresponding sound profile if available; {@code null} if the ID doesn't + * exist or the profile is not accessible to the caller. + */ + public SoundProfile getSoundProfileById(long profileId) { + try { + return mService.getSoundProfileById(profileId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + + /** @SystemApi gets profiles that available to the given package */ + @RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_SOUND_QUALITY_SERVICE) + public List<SoundProfile> getSoundProfilesByPackage(String packageName) { + try { + return mService.getSoundProfilesByPackage(packageName); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** Gets profiles that available to the caller package */ + public List<SoundProfile> getAvailableSoundProfiles() { + try { + return mService.getAvailableSoundProfiles(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** @SystemApi all stored sound profiles of all packages */ + @RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_SOUND_QUALITY_SERVICE) + public List<SoundProfile> getAllSoundProfiles() { + try { + return mService.getAllSoundProfiles(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + + /** + * Creates a sound profile and store it in the system. + * + * @return the stored profile with an assigned profile ID. + */ + public SoundProfile createSoundProfile(SoundProfile sp) { + try { + return mService.createSoundProfile(sp); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + + /** + * Updates an existing sound profile and store it in the system. + */ + public void updateSoundProfile(long profileId, SoundProfile sp) { + try { + mService.updateSoundProfile(profileId, sp); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + + /** + * Removes a sound profile from the system. + */ + public void removeSoundProfile(long profileId) { + try { + mService.removeSoundProfile(profileId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Gets capability information of the given parameters. + */ + public List<ParamCapability> getParamCapabilities(List<String> names) { + try { + return mService.getParamCapabilities(names); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Returns {@code true} if media quality HAL is implemented; {@code false} otherwise. + */ + public boolean isSupported() { + try { + return mService.isSupported(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Enables or disables auto picture quality. + * <p>If enabled, picture quality parameters can be adjusted dynamically by hardware based on + * different use cases. + * + * @param enabled {@code true} to enable, {@code false} to disable. + */ + @RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE) + public void setAutoPictureQualityEnabled(boolean enabled) { + try { + mService.setAutoPictureQualityEnabled(enabled); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Returns {@code true} if auto picture quality is enabled; {@code false} otherwise. + */ + public boolean isAutoPictureQualityEnabled() { + try { + return mService.isAutoPictureQualityEnabled(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Enables or disables super resolution. + * <p>Super resolution is a feature to improve resolution. + * + * @param enabled {@code true} to enable, {@code false} to disable. + */ + @RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_PICTURE_QUALITY_SERVICE) + public void setSuperResolutionEnabled(boolean enabled) { + try { + mService.setSuperResolutionEnabled(enabled); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Returns {@code true} if super resolution is enabled; {@code false} otherwise. + */ + public boolean isSuperResolutionEnabled() { + try { + return mService.isSuperResolutionEnabled(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Enables or disables auto sound quality. + * <p>If enabled, sound quality parameters can be adjusted dynamically by hardware based on + * different use cases. + * + * @param enabled {@code true} to enable, {@code false} to disable. + */ + @RequiresPermission(android.Manifest.permission.MANAGE_GLOBAL_SOUND_QUALITY_SERVICE) + public void setAutoSoundQualityEnabled(boolean enabled) { + try { + mService.setAutoSoundQualityEnabled(enabled); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Returns {@code true} if auto sound quality is enabled; {@code false} otherwise. + */ + public boolean isAutoSoundQualityEnabled() { + try { + return mService.isAutoSoundQualityEnabled(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Registers a {@link AmbientBacklightCallback}. + * @hide + */ + public void registerAmbientBacklightCallback( + @NonNull @CallbackExecutor Executor executor, + @NonNull AmbientBacklightCallback callback) { + Preconditions.checkNotNull(callback); + Preconditions.checkNotNull(executor); + synchronized (mLock) { + mAbCallbackRecords.add(new AmbientBacklightCallbackRecord(callback, executor)); + } + } + + /** + * Unregisters the existing {@link AmbientBacklightCallback}. + * @hide + */ + public void unregisterAmbientBacklightCallback( + @NonNull final AmbientBacklightCallback callback) { + Preconditions.checkNotNull(callback); + synchronized (mLock) { + for (Iterator<AmbientBacklightCallbackRecord> it = mAbCallbackRecords.iterator(); + it.hasNext(); ) { + AmbientBacklightCallbackRecord record = it.next(); + if (record.getCallback() == callback) { + it.remove(); + break; + } + } + } + } + + /** + * Set the ambient backlight settings. + * + * @param settings The settings to use for the backlight detector. + */ + public void setAmbientBacklightSettings( + @NonNull AmbientBacklightSettings settings) { + Preconditions.checkNotNull(settings); + try { + mService.setAmbientBacklightSettings(settings); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Enables or disables the ambient backlight detection. + * + * @param enabled {@code true} to enable, {@code false} to disable. + */ + public void setAmbientBacklightEnabled(boolean enabled) { + try { + mService.setAmbientBacklightEnabled(enabled); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + + private static final class PictureProfileCallbackRecord { + private final PictureProfileCallback mCallback; + private final Executor mExecutor; + + PictureProfileCallbackRecord(PictureProfileCallback callback, Executor executor) { + mCallback = callback; + mExecutor = executor; + } + + public PictureProfileCallback getCallback() { + return mCallback; + } + + public void postPictureProfileAdded(final long id, PictureProfile profile) { + + mExecutor.execute(new Runnable() { + @Override + public void run() { + mCallback.onPictureProfileAdded(id, profile); + } + }); + } + + public void postPictureProfileUpdated(final long id, PictureProfile profile) { + mExecutor.execute(new Runnable() { + @Override + public void run() { + mCallback.onPictureProfileUpdated(id, profile); + } + }); + } + + public void postPictureProfileRemoved(final long id, PictureProfile profile) { + mExecutor.execute(new Runnable() { + @Override + public void run() { + mCallback.onPictureProfileRemoved(id, profile); + } + }); + } + } + + private static final class SoundProfileCallbackRecord { + private final SoundProfileCallback mCallback; + private final Executor mExecutor; + + SoundProfileCallbackRecord(SoundProfileCallback callback, Executor executor) { + mCallback = callback; + mExecutor = executor; + } + + public SoundProfileCallback getCallback() { + return mCallback; + } + + public void postSoundProfileAdded(final long id, SoundProfile profile) { + + mExecutor.execute(new Runnable() { + @Override + public void run() { + mCallback.onSoundProfileAdded(id, profile); + } + }); + } + + public void postSoundProfileUpdated(final long id, SoundProfile profile) { + mExecutor.execute(new Runnable() { + @Override + public void run() { + mCallback.onSoundProfileUpdated(id, profile); + } + }); + } + + public void postSoundProfileRemoved(final long id, SoundProfile profile) { + mExecutor.execute(new Runnable() { + @Override + public void run() { + mCallback.onSoundProfileRemoved(id, profile); + } + }); + } + } + + private static final class AmbientBacklightCallbackRecord { + private final AmbientBacklightCallback mCallback; + private final Executor mExecutor; + + AmbientBacklightCallbackRecord(AmbientBacklightCallback callback, Executor executor) { + mCallback = callback; + mExecutor = executor; + } + + public AmbientBacklightCallback getCallback() { + return mCallback; + } + + public void postAmbientBacklightEvent(AmbientBacklightEvent event) { + mExecutor.execute(new Runnable() { + @Override + public void run() { + mCallback.onAmbientBacklightEvent(event); + } + }); + } + } + + /** + * Callback used to monitor status of picture profiles. + * @hide + */ + public abstract static class PictureProfileCallback { + /** + * @hide + */ + public void onPictureProfileAdded(long id, PictureProfile profile) { + } + /** + * @hide + */ + public void onPictureProfileUpdated(long id, PictureProfile profile) { + } + /** + * @hide + */ + public void onPictureProfileRemoved(long id, PictureProfile profile) { + } + /** + * @hide + */ + public void onError(int errorCode) { + } + } + + /** + * Callback used to monitor status of sound profiles. + * @hide + */ + public abstract static class SoundProfileCallback { + /** + * @hide + */ + public void onSoundProfileAdded(long id, SoundProfile profile) { + } + /** + * @hide + */ + public void onSoundProfileUpdated(long id, SoundProfile profile) { + } + /** + * @hide + */ + public void onSoundProfileRemoved(long id, SoundProfile profile) { + } + /** + * @hide + */ + public void onError(int errorCode) { + } + } + + /** + * Callback used to monitor status of ambient backlight. + * @hide + */ + public abstract static class AmbientBacklightCallback { + /** + * Called when new ambient backlight event is emitted. + */ + public void onAmbientBacklightEvent(AmbientBacklightEvent event) { + } } } diff --git a/media/java/android/media/quality/ParamCapability.aidl b/media/java/android/media/quality/ParamCapability.aidl new file mode 100644 index 000000000000..b43409d039f2 --- /dev/null +++ b/media/java/android/media/quality/ParamCapability.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.quality; + +parcelable ParamCapability; diff --git a/media/java/android/media/quality/ParamCapability.java b/media/java/android/media/quality/ParamCapability.java new file mode 100644 index 000000000000..70e85920c12f --- /dev/null +++ b/media/java/android/media/quality/ParamCapability.java @@ -0,0 +1,187 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.quality; + +import android.annotation.FlaggedApi; +import android.annotation.IntDef; +import android.annotation.StringDef; +import android.media.tv.flags.Flags; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.NonNull; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * Capability info of media quality parameters + * @hide + */ +@FlaggedApi(Flags.FLAG_MEDIA_QUALITY_FW) +public class ParamCapability implements Parcelable { + + /** @hide */ + @IntDef(flag = true, prefix = { "TYPE_" }, value = { + TYPE_INT, + TYPE_LONG, + TYPE_DOUBLE, + TYPE_STRING, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ParamType {} + + /** + * Integer parameter type + */ + public static final int TYPE_INT = 1; + + /** + * Long integer parameter type + */ + public static final int TYPE_LONG = 2; + + /** + * Double parameter type + */ + public static final int TYPE_DOUBLE = 3; + + /** + * String parameter type + */ + public static final int TYPE_STRING = 4; + + /** @hide */ + @StringDef(prefix = { "CAPABILITY_" }, value = { + CAPABILITY_MAX, + CAPABILITY_MIN, + CAPABILITY_DEFAULT, + CAPABILITY_ENUM, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface Capability {} + + /** + * The key for the max possible value of this parameter. + */ + public static final String CAPABILITY_MAX = "max"; + + /** + * The key for the min possible value of this parameter. + */ + public static final String CAPABILITY_MIN = "min"; + + /** + * The key for the default value of this parameter. + */ + public static final String CAPABILITY_DEFAULT = "default"; + + /** + * The key for the enumeration of this parameter. + */ + public static final String CAPABILITY_ENUM = "enum"; + + @NonNull + private final String mName; + private final boolean mIsSupported; + @ParamType + private final int mType; + @NonNull + private final Bundle mCaps; + + protected ParamCapability(Parcel in) { + mName = in.readString(); + mIsSupported = in.readBoolean(); + mType = in.readInt(); + mCaps = in.readBundle(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mName); + dest.writeBoolean(mIsSupported); + dest.writeInt(mType); + dest.writeBundle(mCaps); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator<ParamCapability> CREATOR = new Creator<ParamCapability>() { + @Override + public ParamCapability createFromParcel(Parcel in) { + return new ParamCapability(in); + } + + @Override + public ParamCapability[] newArray(int size) { + return new ParamCapability[size]; + } + }; + + + /** + * Creates a new ParamCapability. + * + * @hide + */ + public ParamCapability( + @NonNull String name, + boolean isSupported, + int type, + @NonNull Bundle caps) { + this.mName = name; + this.mIsSupported = isSupported; + this.mType = type; + this.mCaps = caps; + } + + /** + * Gets parameter name. + */ + @NonNull + public String getParamName() { + return mName; + } + + /** + * Returns whether this parameter is supported or not. + */ + public boolean isSupported() { + return mIsSupported; + } + + /** + * Gets parameter type. + */ + @ParamType + public int getParamType() { + return mType; + } + + /** + * Gets capability information. + * <p>e.g. use the key {@link #CAPABILITY_MAX} to get the max value. + */ + @NonNull + public Bundle getCapabilities() { + return new Bundle(mCaps); + } +} diff --git a/media/java/android/media/quality/PictureProfile.aidl b/media/java/android/media/quality/PictureProfile.aidl new file mode 100644 index 000000000000..41d018b12f33 --- /dev/null +++ b/media/java/android/media/quality/PictureProfile.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.quality; + +parcelable PictureProfile; diff --git a/media/java/android/media/quality/PictureProfile.java b/media/java/android/media/quality/PictureProfile.java new file mode 100644 index 000000000000..36c49d80e13d --- /dev/null +++ b/media/java/android/media/quality/PictureProfile.java @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.quality; + +import android.annotation.FlaggedApi; +import android.media.tv.flags.Flags; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * @hide + */ + +@FlaggedApi(Flags.FLAG_MEDIA_QUALITY_FW) +public class PictureProfile implements Parcelable { + @Nullable + private Long mId; + @NonNull + private final String mName; + @Nullable + private final String mInputId; + @Nullable + private final String mPackageName; + @NonNull + private final Bundle mParams; + + protected PictureProfile(Parcel in) { + if (in.readByte() == 0) { + mId = null; + } else { + mId = in.readLong(); + } + mName = in.readString(); + mInputId = in.readString(); + mPackageName = in.readString(); + mParams = in.readBundle(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + if (mId == null) { + dest.writeByte((byte) 0); + } else { + dest.writeByte((byte) 1); + dest.writeLong(mId); + } + dest.writeString(mName); + dest.writeString(mInputId); + dest.writeString(mPackageName); + dest.writeBundle(mParams); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator<PictureProfile> CREATOR = new Creator<PictureProfile>() { + @Override + public PictureProfile createFromParcel(Parcel in) { + return new PictureProfile(in); + } + + @Override + public PictureProfile[] newArray(int size) { + return new PictureProfile[size]; + } + }; + + + /** + * Creates a new PictureProfile. + * + * @hide + */ + public PictureProfile( + @Nullable Long id, + @NonNull String name, + @Nullable String inputId, + @Nullable String packageName, + @NonNull Bundle params) { + this.mId = id; + this.mName = name; + com.android.internal.util.AnnotationValidations.validate(NonNull.class, null, name); + this.mInputId = inputId; + this.mPackageName = packageName; + this.mParams = params; + } + + @Nullable + public Long getProfileId() { + return mId; + } + + @NonNull + public String getName() { + return mName; + } + + @Nullable + public String getInputId() { + return mInputId; + } + + @Nullable + public String getPackageName() { + return mPackageName; + } + @NonNull + public Bundle getParameters() { + return new Bundle(mParams); + } + + /** + * A builder for {@link PictureProfile} + */ + public static class Builder { + @Nullable + private Long mId; + @NonNull + private String mName; + @Nullable + private String mInputId; + @Nullable + private String mPackageName; + @NonNull + private Bundle mParams; + + /** + * Creates a new Builder. + * + * @hide + */ + public Builder(@NonNull String name) { + mName = name; + com.android.internal.util.AnnotationValidations.validate(NonNull.class, null, name); + } + + /** + * Copy constructor. + * + * @hide + */ + public Builder(@NonNull PictureProfile p) { + mId = null; // ID needs to be reset + mName = p.getName(); + mPackageName = p.getPackageName(); + mInputId = p.getInputId(); + } + + /* @hide using by MediaQualityService */ + + /** + * Sets profile ID. + * @hide using by MediaQualityService + */ + @NonNull + public Builder setProfileId(@Nullable Long id) { + mId = id; + return this; + } + + /** + * Sets input ID. + */ + @NonNull + public Builder setInputId(@NonNull String value) { + mInputId = value; + return this; + } + + /** + * Sets package name of the profile. + */ + @NonNull + public Builder setPackageName(@NonNull String value) { + mPackageName = value; + return this; + } + + /** + * Sets profile parameters. + */ + @NonNull + public Builder setParameters(@NonNull Bundle params) { + mParams = new Bundle(params); + return this; + } + + /** + * Builds the instance. + */ + @NonNull + public PictureProfile build() { + + PictureProfile o = new PictureProfile( + mId, + mName, + mInputId, + mPackageName, + mParams); + return o; + } + } +} diff --git a/media/java/android/media/quality/SoundProfile.aidl b/media/java/android/media/quality/SoundProfile.aidl new file mode 100644 index 000000000000..e79fcaac97be --- /dev/null +++ b/media/java/android/media/quality/SoundProfile.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.quality; + +parcelable SoundProfile; diff --git a/media/java/android/media/quality/SoundProfile.java b/media/java/android/media/quality/SoundProfile.java new file mode 100644 index 000000000000..20d117bf15cf --- /dev/null +++ b/media/java/android/media/quality/SoundProfile.java @@ -0,0 +1,219 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.media.quality; + +import android.annotation.FlaggedApi; +import android.media.tv.flags.Flags; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +/** + * @hide + */ +@FlaggedApi(Flags.FLAG_MEDIA_QUALITY_FW) +public class SoundProfile implements Parcelable { + @Nullable + private Long mId; + @NonNull + private final String mName; + @Nullable + private final String mInputId; + @Nullable + private final String mPackageName; + @NonNull + private final Bundle mParams; + + protected SoundProfile(Parcel in) { + if (in.readByte() == 0) { + mId = null; + } else { + mId = in.readLong(); + } + mName = in.readString(); + mInputId = in.readString(); + mPackageName = in.readString(); + mParams = in.readBundle(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + if (mId == null) { + dest.writeByte((byte) 0); + } else { + dest.writeByte((byte) 1); + dest.writeLong(mId); + } + dest.writeString(mName); + dest.writeString(mInputId); + dest.writeString(mPackageName); + dest.writeBundle(mParams); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator<SoundProfile> CREATOR = new Creator<SoundProfile>() { + @Override + public SoundProfile createFromParcel(Parcel in) { + return new SoundProfile(in); + } + + @Override + public SoundProfile[] newArray(int size) { + return new SoundProfile[size]; + } + }; + + + /** + * Creates a new SoundProfile. + * + * @hide + */ + public SoundProfile( + @Nullable Long id, + @NonNull String name, + @Nullable String inputId, + @Nullable String packageName, + @NonNull Bundle params) { + this.mId = id; + this.mName = name; + com.android.internal.util.AnnotationValidations.validate(NonNull.class, null, name); + this.mInputId = inputId; + this.mPackageName = packageName; + this.mParams = params; + } + + @Nullable + public Long getProfileId() { + return mId; + } + + @NonNull + public String getName() { + return mName; + } + + @Nullable + public String getInputId() { + return mInputId; + } + + @Nullable + public String getPackageName() { + return mPackageName; + } + @NonNull + public Bundle getParameters() { + return new Bundle(mParams); + } + + /** + * A builder for {@link SoundProfile} + */ + public static class Builder { + @Nullable + private Long mId; + @NonNull + private String mName; + @Nullable + private String mInputId; + @Nullable + private String mPackageName; + @NonNull + private Bundle mParams; + + /** + * Creates a new Builder. + * + * @hide + */ + public Builder(@NonNull String name) { + mName = name; + com.android.internal.util.AnnotationValidations.validate(NonNull.class, null, name); + } + + /** + * Copy constructor. + * + * @hide + */ + public Builder(@NonNull SoundProfile p) { + mId = null; // ID needs to be reset + mName = p.getName(); + mPackageName = p.getPackageName(); + mInputId = p.getInputId(); + } + + /** + * Sets profile ID. + * @hide using by MediaQualityService + */ + @NonNull + public Builder setProfileId(@Nullable Long id) { + mId = id; + return this; + } + + /** + * Sets input ID. + */ + @NonNull + public Builder setInputId(@NonNull String value) { + mInputId = value; + return this; + } + + /** + * Sets package name of the profile. + */ + @NonNull + public Builder setPackageName(@NonNull String value) { + mPackageName = value; + return this; + } + + /** + * Sets profile parameters. + */ + @NonNull + public Builder setParameters(@NonNull Bundle params) { + mParams = new Bundle(params); + return this; + } + /** + * Builds the instance. + */ + @NonNull + public SoundProfile build() { + + SoundProfile o = new SoundProfile( + mId, + mName, + mInputId, + mPackageName, + mParams); + return o; + } + } +} diff --git a/media/java/android/media/tv/TvInputManager.java b/media/java/android/media/tv/TvInputManager.java index b673e032b1ff..c2f689679852 100644 --- a/media/java/android/media/tv/TvInputManager.java +++ b/media/java/android/media/tv/TvInputManager.java @@ -27,6 +27,7 @@ import android.annotation.StringDef; import android.annotation.SystemApi; import android.annotation.SystemService; import android.annotation.TestApi; +import android.annotation.UserIdInt; import android.content.AttributionSource; import android.content.Context; import android.content.Intent; @@ -760,6 +761,7 @@ public final class TvInputManager { * @hide */ public static final int UNKNOWN_CLIENT_PID = -1; + /** * An unknown state of the client userId gets from the TvInputManager. Client gets this value * when query through {@link #getClientUserId(String sessionId)} fails. @@ -2526,9 +2528,10 @@ public final class TvInputManager { * * @hide */ - @RequiresPermission(android.Manifest.permission.TUNER_RESOURCE_ACCESS) + @SystemApi @FlaggedApi(Flags.FLAG_KIDS_MODE_TVDB_SHARING) - public int getClientUserId(@NonNull String sessionId) { + @RequiresPermission(android.Manifest.permission.SINGLE_USER_TIS_ACCESS) + public @UserIdInt int getClientUserId(@NonNull String sessionId) { return getClientUserIdInternal(sessionId); } diff --git a/media/java/android/media/tv/flags/media_tv.aconfig b/media/java/android/media/tv/flags/media_tv.aconfig index d49f7dd92bf7..4de68634af5e 100644 --- a/media/java/android/media/tv/flags/media_tv.aconfig +++ b/media/java/android/media/tv/flags/media_tv.aconfig @@ -71,4 +71,12 @@ flag { namespace: "media_tv" description: "Standardize AIDL Extension Interface of TIS" bug: "330366987" -}
\ No newline at end of file +} + +flag { + name: "set_resource_holder_retain" + is_exported: true + namespace: "media_tv" + description : "Feature flag to add setResourceHolderRetain api to MediaCas and Tuner JAVA." + bug: "372973197" +} diff --git a/media/java/android/media/tv/tuner/Tuner.java b/media/java/android/media/tv/tuner/Tuner.java index cdf50ec963d8..b1adb77f9543 100644 --- a/media/java/android/media/tv/tuner/Tuner.java +++ b/media/java/android/media/tv/tuner/Tuner.java @@ -16,6 +16,8 @@ package android.media.tv.tuner; +import static android.media.tv.flags.Flags.FLAG_SET_RESOURCE_HOLDER_RETAIN; + import android.annotation.BytesLong; import android.annotation.CallbackExecutor; import android.annotation.FlaggedApi; @@ -751,6 +753,21 @@ public class Tuner implements AutoCloseable { } /** + * Determines whether the resource holder retains ownership of the resource during a challenge + * scenario, when both resource holder and resource challenger have same processId and same + * priority. + * + * @param resourceHolderRetain Set to true to allow the resource holder to retain ownership, or + * false to allow the resource challenger to acquire the resource. If not explicitly set, + * resourceHolderRetain is set to false. + */ + @FlaggedApi(FLAG_SET_RESOURCE_HOLDER_RETAIN) + @RequiresPermission(android.Manifest.permission.TUNER_RESOURCE_ACCESS) + public void setResourceHolderRetain(boolean resourceHolderRetain) { + mTunerResourceManager.setResourceHolderRetain(mClientId, resourceHolderRetain); + } + + /** * Checks if there is an unused frontend resource available. * * @param frontendType {@link android.media.tv.tuner.frontend.FrontendSettings.Type} for the diff --git a/media/java/android/media/tv/tunerresourcemanager/TunerResourceManager.java b/media/java/android/media/tv/tunerresourcemanager/TunerResourceManager.java index bb581ebe1778..be65ad98c35b 100644 --- a/media/java/android/media/tv/tunerresourcemanager/TunerResourceManager.java +++ b/media/java/android/media/tv/tunerresourcemanager/TunerResourceManager.java @@ -40,8 +40,11 @@ import java.util.concurrent.Executor; * <p>Resources include: * <ul> * <li>TunerFrontend {@link android.media.tv.tuner.frontend}. + * <li>Demux {@link com.android.server.tv.tunerresourcemanager.DemuxResource}. + * <li>Descrambler {@link android.media.tv.tuner.Descrambler}. * <li>TunerLnb {@link android.media.tv.tuner.Lnb}. * <li>MediaCas {@link android.media.MediaCas}. + * <li>CiCam {@link com.android.server.tv.tunerresourcemanager.CiCamResource}. * <ul> * * <p>Expected workflow is: @@ -78,7 +81,7 @@ public class TunerResourceManager { TUNER_RESOURCE_TYPE_LNB, TUNER_RESOURCE_TYPE_CAS_SESSION, TUNER_RESOURCE_TYPE_FRONTEND_CICAM, - TUNER_RESOURCE_TYPE_MAX, + TUNER_RESOURCE_TYPE_MAX, // upper bound of constants }) @Retention(RetentionPolicy.SOURCE) public @interface TunerResourceType {} @@ -220,6 +223,25 @@ public class TunerResourceManager { } /** + * Determines whether the resource holder retains ownership of the resource during a challenge + * scenario, when both resource holder and resource challenger have same processId and same + * priority. + * + * @param clientId The client id used to set ownership of resource to owner in case of resource + * challenger situation. + * @param resourceHolderRetain Set to true to allow the resource holder to retain ownership, or + * false to allow the resource challenger to acquire the resource. If not explicitly set, + * resourceHolderRetain is set to false. + */ + public void setResourceHolderRetain(int clientId, boolean resourceHolderRetain) { + try { + mService.setResourceHolderRetain(clientId, resourceHolderRetain); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * Stores the frontend resource map if it was stored before. * * <p>This API is only for testing purpose and should be used in pair with diff --git a/media/java/android/media/tv/tunerresourcemanager/aidl/android/media/tv/tunerresourcemanager/ITunerResourceManager.aidl b/media/java/android/media/tv/tunerresourcemanager/aidl/android/media/tv/tunerresourcemanager/ITunerResourceManager.aidl index 109c791c1748..c57be1b09b66 100644 --- a/media/java/android/media/tv/tunerresourcemanager/aidl/android/media/tv/tunerresourcemanager/ITunerResourceManager.aidl +++ b/media/java/android/media/tv/tunerresourcemanager/aidl/android/media/tv/tunerresourcemanager/ITunerResourceManager.aidl @@ -151,6 +151,18 @@ interface ITunerResourceManager { */ void setLnbInfoList(in long[] lnbIds); + /** + * Determines whether the Resource Holder retains ownership of the resource during a challenge + * scenario, when both Resource Holder and Resource Challenger have same processId and same + * priority. + * + * @param clientId The resourceHolderRetain of the client is updated using client ID. + * @param resourceHolderRetain set to true to allow the Resource Holder to retain ownership, or + * false to allow the Resource Challenger to acquire the resource. If not explicitly set, + * resourceHolderRetain is set to false. + */ + void setResourceHolderRetain(int clientId, boolean resourceHolderRetain); + /* * This API is used by the Tuner framework to request a frontend from the TunerHAL. * diff --git a/native/android/libandroid.map.txt b/native/android/libandroid.map.txt index 202535d45191..b025cb880ee7 100644 --- a/native/android/libandroid.map.txt +++ b/native/android/libandroid.map.txt @@ -283,6 +283,7 @@ LIBANDROID { ASurfaceTransaction_setEnableBackPressure; # introduced=31 ASurfaceTransaction_setFrameRate; # introduced=30 ASurfaceTransaction_setFrameRateWithChangeStrategy; # introduced=31 + ASurfaceTransaction_setFrameRateParams; # introduced=36 ASurfaceTransaction_clearFrameRate; # introduced=34 ASurfaceTransaction_setFrameTimeline; # introduced=Tiramisu ASurfaceTransaction_setGeometry; # introduced=29 diff --git a/native/android/surface_control.cpp b/native/android/surface_control.cpp index e46db6bb3727..698bc84a78b9 100644 --- a/native/android/surface_control.cpp +++ b/native/android/surface_control.cpp @@ -731,6 +731,28 @@ void ASurfaceTransaction_setFrameRateWithChangeStrategy(ASurfaceTransaction* aSu transaction->setFrameRate(surfaceControl, frameRate, compatibility, changeFrameRateStrategy); } +void ASurfaceTransaction_setFrameRateParams( + ASurfaceTransaction* aSurfaceTransaction, ASurfaceControl* aSurfaceControl, + float desiredMinRate, float desiredMaxRate, float fixedSourceRate, + ANativeWindow_ChangeFrameRateStrategy changeFrameRateStrategy) { + CHECK_NOT_NULL(aSurfaceTransaction); + CHECK_NOT_NULL(aSurfaceControl); + Transaction* transaction = ASurfaceTransaction_to_Transaction(aSurfaceTransaction); + sp<SurfaceControl> surfaceControl = ASurfaceControl_to_SurfaceControl(aSurfaceControl); + + if (desiredMaxRate < desiredMinRate) { + ALOGW("desiredMaxRate must be greater than or equal to desiredMinRate"); + return; + } + // TODO(b/362798998): Fix plumbing to send modern params + int compatibility = fixedSourceRate == 0 ? ANATIVEWINDOW_FRAME_RATE_COMPATIBILITY_DEFAULT + : ANATIVEWINDOW_FRAME_RATE_COMPATIBILITY_FIXED_SOURCE; + double frameRate = compatibility == ANATIVEWINDOW_FRAME_RATE_COMPATIBILITY_FIXED_SOURCE + ? fixedSourceRate + : desiredMinRate; + transaction->setFrameRate(surfaceControl, frameRate, compatibility, changeFrameRateStrategy); +} + void ASurfaceTransaction_clearFrameRate(ASurfaceTransaction* aSurfaceTransaction, ASurfaceControl* aSurfaceControl) { CHECK_NOT_NULL(aSurfaceTransaction); diff --git a/native/graphics/jni/Android.bp b/native/graphics/jni/Android.bp index 0fb3049f63d8..23dd9b7aee37 100644 --- a/native/graphics/jni/Android.bp +++ b/native/graphics/jni/Android.bp @@ -48,7 +48,9 @@ cc_library_shared { "libhwui_internal_headers", ], - static_libs: ["libarect"], + static_libs: [ + "libarect", + ], host_supported: true, target: { @@ -60,6 +62,11 @@ cc_library_shared { shared_libs: [ "libandroid", ], + static_libs: [ + "libstatslog_hwui", + "libstatspull_lazy", + "libstatssocket_lazy", + ], version_script: "libjnigraphics.map.txt", }, host: { diff --git a/native/graphics/jni/imagedecoder.cpp b/native/graphics/jni/imagedecoder.cpp index e18b4a9d2420..cb95bcf5d2b1 100644 --- a/native/graphics/jni/imagedecoder.cpp +++ b/native/graphics/jni/imagedecoder.cpp @@ -14,18 +14,9 @@ * limitations under the License. */ -#include "aassetstreamadaptor.h" - -#include <android/asset_manager.h> -#include <android/bitmap.h> -#include <android/data_space.h> -#include <android/imagedecoder.h> #include <MimeType.h> -#include <android/rect.h> -#include <hwui/ImageDecoder.h> -#include <log/log.h> -#include <SkAndroidCodec.h> #include <SkAlphaType.h> +#include <SkAndroidCodec.h> #include <SkCodec.h> #include <SkCodecAnimation.h> #include <SkColorSpace.h> @@ -35,14 +26,24 @@ #include <SkRefCnt.h> #include <SkSize.h> #include <SkStream.h> -#include <utils/Color.h> - +#include <android/asset_manager.h> +#include <android/bitmap.h> +#include <android/data_space.h> +#include <android/imagedecoder.h> +#include <android/rect.h> #include <fcntl.h> -#include <limits> -#include <optional> +#include <hwui/ImageDecoder.h> +#include <log/log.h> #include <sys/stat.h> #include <sys/types.h> #include <unistd.h> +#include <utils/Color.h> +#include <utils/StatsUtils.h> + +#include <limits> +#include <optional> + +#include "aassetstreamadaptor.h" using namespace android; @@ -400,9 +401,7 @@ size_t AImageDecoder_getMinimumStride(AImageDecoder* decoder) { return info.minRowBytes(); } -int AImageDecoder_decodeImage(AImageDecoder* decoder, - void* pixels, size_t stride, - size_t size) { +int AImageDecoder_decodeImage(AImageDecoder* decoder, void* pixels, size_t stride, size_t size) { if (!decoder || !pixels || !stride) { return ANDROID_IMAGE_DECODER_BAD_PARAMETER; } @@ -419,7 +418,13 @@ int AImageDecoder_decodeImage(AImageDecoder* decoder, return ANDROID_IMAGE_DECODER_FINISHED; } - return ResultToErrorCode(imageDecoder->decode(pixels, stride)); + const auto result = ResultToErrorCode(imageDecoder->decode(pixels, stride)); + + if (result == ANDROID_IMAGE_DECODER_SUCCESS) { + uirenderer::logBitmapDecode(imageDecoder->getOutputInfo(), false); + } + + return result; } void AImageDecoder_delete(AImageDecoder* decoder) { diff --git a/nfc/java/android/nfc/NfcOemExtension.java b/nfc/java/android/nfc/NfcOemExtension.java index 520ba896f01f..474ff8c663e6 100644 --- a/nfc/java/android/nfc/NfcOemExtension.java +++ b/nfc/java/android/nfc/NfcOemExtension.java @@ -82,6 +82,13 @@ public final class NfcOemExtension { private boolean mRfDiscoveryStarted = false; /** + * Broadcast Action: Sent on NFC stack initialization when NFC OEM extensions are enabled. + * <p> OEM extension modules should use this intent to start their extension service </p> + * @hide + */ + public static final String ACTION_OEM_EXTENSION_INIT = "android.nfc.action.OEM_EXTENSION_INIT"; + + /** * Mode Type for {@link #setControllerAlwaysOnMode(int)}. * Enables the controller in default mode when NFC is disabled (existing API behavior). * works same as {@link NfcAdapter#setControllerAlwaysOn(boolean)}. diff --git a/nfc/java/android/nfc/flags.aconfig b/nfc/java/android/nfc/flags.aconfig index 6a7e6939e773..34f020012d3c 100644 --- a/nfc/java/android/nfc/flags.aconfig +++ b/nfc/java/android/nfc/flags.aconfig @@ -165,3 +165,11 @@ flag { description: "Enabling security log for nfc state change" bug: "319934052" } + +flag { + name: "nfc_associated_role_services" + is_exported: true + namespace: "nfc" + description: "Share wallet role routing priority with associated services" + bug: "366243361" +} diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/HandlerExecutor.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/HandlerExecutor.kt index be086061f5cb..99d3c8db7716 100644 --- a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/HandlerExecutor.kt +++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/HandlerExecutor.kt @@ -24,7 +24,7 @@ import java.util.concurrent.Executor * Adapter of [Handler] and [Executor], where the task is executed on handler with given looper. * * When current looper is same with the given looper, task passed to [Executor.execute] will be - * executed immediately to improve better performance. + * executed immediately to achieve better performance. * * @param looper Looper of the handler. */ diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/KeyedObserver.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/KeyedObserver.kt index 9cb0ebb995a8..843d2aadf333 100644 --- a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/KeyedObserver.kt +++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/KeyedObserver.kt @@ -114,6 +114,10 @@ interface KeyedObservable<K> { fun notifyChange(key: K, reason: Int) } +/** Delegation of [KeyedObservable]. */ +open class KeyedObservableDelegate<K>(delegate: KeyedObservable<K>) : + KeyedObservable<K> by delegate + /** A thread safe implementation of [KeyedObservable]. */ open class KeyedDataObservable<K> : KeyedObservable<K> { // Instead of @GuardedBy("this"), guarded by itself because KeyedDataObservable object could be diff --git a/packages/SettingsLib/Graph/graph.proto b/packages/SettingsLib/Graph/graph.proto index e93d756d0285..cbe602e00406 100644 --- a/packages/SettingsLib/Graph/graph.proto +++ b/packages/SettingsLib/Graph/graph.proto @@ -74,6 +74,9 @@ message PreferenceProto { optional ActionTarget action_target = 12; // Preference value (if present, it means `persistent` is true). optional PreferenceValueProto value = 13; + // Intent to show and locate the preference (might have highlight animation on + // the preference). + optional IntentProto launch_intent = 14; // Target of an Intent message ActionTarget { diff --git a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt index 5e78a290179e..cf6bf7012ac2 100644 --- a/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt +++ b/packages/SettingsLib/Graph/src/com/android/settingslib/graph/PreferenceGraphBuilder.kt @@ -51,9 +51,9 @@ import com.android.settingslib.metadata.PreferenceSummaryProvider import com.android.settingslib.metadata.PreferenceTitleProvider import com.android.settingslib.preference.PreferenceScreenFactory import com.android.settingslib.preference.PreferenceScreenProvider +import java.util.Locale import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import java.util.Locale private const val TAG = "PreferenceGraphBuilder" @@ -140,7 +140,7 @@ private constructor(private val context: Context, private val request: GetPrefer addPreferenceScreen(metadata.key) { preferenceScreenProto { completeHierarchy = metadata.hasCompleteHierarchy() - root = metadata.getPreferenceHierarchy(context).toProto(true) + root = metadata.getPreferenceHierarchy(context).toProto(metadata, true) } } @@ -237,23 +237,29 @@ private constructor(private val context: Context, private val request: GetPrefer this@toProto.intent?.let { actionTarget = it.toActionTarget() } } - private suspend fun PreferenceHierarchy.toProto(isRoot: Boolean): PreferenceGroupProto = - preferenceGroupProto { - preference = toProto(this@toProto, isRoot) - forEachAsync { - addPreferences( - preferenceOrGroupProto { - if (it is PreferenceHierarchy) { - group = it.toProto(false) - } else { - preference = toProto(it, false) - } + private suspend fun PreferenceHierarchy.toProto( + screenMetadata: PreferenceScreenMetadata, + isRoot: Boolean, + ): PreferenceGroupProto = preferenceGroupProto { + preference = toProto(screenMetadata, this@toProto, isRoot) + forEachAsync { + addPreferences( + preferenceOrGroupProto { + if (it is PreferenceHierarchy) { + group = it.toProto(screenMetadata, false) + } else { + preference = toProto(screenMetadata, it, false) } - ) - } + } + ) } + } - private suspend fun toProto(node: PreferenceHierarchyNode, isRoot: Boolean) = preferenceProto { + private suspend fun toProto( + screenMetadata: PreferenceScreenMetadata, + node: PreferenceHierarchyNode, + isRoot: Boolean, + ) = preferenceProto { val metadata = node.metadata key = metadata.key metadata.getTitleTextProto(isRoot)?.let { title = it } @@ -264,7 +270,8 @@ private constructor(private val context: Context, private val request: GetPrefer summary = textProto { string = it.toString() } } } - if (metadata.icon != 0) icon = metadata.icon + val metadataIcon = metadata.getPreferenceIcon(context) + if (metadataIcon != 0) icon = metadataIcon if (metadata.keywords != 0) keywords = metadata.keywords val preferenceExtras = metadata.extras(context) preferenceExtras?.let { extras = it.toProto() } @@ -291,6 +298,7 @@ private constructor(private val context: Context, private val request: GetPrefer @Suppress("CheckReturnValue") addPreferenceScreenMetadata(metadata) } metadata.intent(context)?.let { actionTarget = it.toActionTarget() } + screenMetadata.getLaunchIntent(context, metadata)?.let { launchIntent = it.toProto() } } private fun PreferenceMetadata.getTitleTextProto(isRoot: Boolean): TextProto? { diff --git a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt index 10087a712bcf..386b6d94c631 100644 --- a/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt +++ b/packages/SettingsLib/Metadata/src/com/android/settingslib/metadata/PreferenceMetadata.kt @@ -162,8 +162,8 @@ interface PreferenceMetadata { /** * Returns the preference icon. * - * Implement [PreferenceIconProvider] interface if icon content is provided dynamically - * (e.g. icon is provided based on flag value). + * Implement [PreferenceIconProvider] interface if icon is provided dynamically (e.g. icon is + * provided based on flag value). */ fun getPreferenceIcon(context: Context): Int = when { @@ -212,4 +212,11 @@ interface PreferenceScreenMetadata : PreferenceMetadata { * conditions. DO NOT check any condition (except compile time flag) before adding a preference. */ fun getPreferenceHierarchy(context: Context): PreferenceHierarchy + + /** + * Returns the [Intent] to show current preference screen. + * + * @param metadata the preference to locate when show the screen + */ + fun getLaunchIntent(context: Context, metadata: PreferenceMetadata?): Intent? = null } diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindings.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindings.kt index d40a6f6c55f4..762f5eaf6325 100644 --- a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindings.kt +++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceBindings.kt @@ -23,7 +23,6 @@ import androidx.preference.PreferenceScreen import androidx.preference.SwitchPreferenceCompat import androidx.preference.TwoStatePreference import com.android.settingslib.metadata.EXTRA_BINDING_SCREEN_KEY -import com.android.settingslib.metadata.PersistentPreference import com.android.settingslib.metadata.PreferenceMetadata import com.android.settingslib.metadata.PreferenceScreenMetadata import com.android.settingslib.metadata.PreferenceTitleProvider @@ -71,10 +70,10 @@ interface TwoStatePreferenceBinding : PreferenceBinding { override fun bind(preference: Preference, metadata: PreferenceMetadata) { super.bind(preference, metadata) - (metadata as? PersistentPreference<*>) - ?.storage(preference.context) - ?.getValue(metadata.key, Boolean::class.javaObjectType) - ?.let { (preference as TwoStatePreference).isChecked = it } + (preference as TwoStatePreference).apply { + // "false" is kind of placeholder, metadata datastore should provide the default value + isChecked = preferenceDataStore!!.getBoolean(key, false) + } } } diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceHierarchyInflater.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceHierarchyInflater.kt index c26ba1814ac1..657f69a738f2 100644 --- a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceHierarchyInflater.kt +++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceHierarchyInflater.kt @@ -40,11 +40,11 @@ fun PreferenceGroup.inflatePreferenceHierarchy( addPreference(preferenceGroup) preferenceGroup.inflatePreferenceHierarchy(preferenceBindingFactory, it) } else { - preferenceBindingFactory.bind(widget, it, preferenceBinding) (metadata as? PersistentPreference<*>)?.storage(context)?.let { storage -> widget.preferenceDataStore = storages.getOrPut(storage) { PreferenceDataStoreAdapter(storage) } } + preferenceBindingFactory.bind(widget, it, preferenceBinding) // MUST add preference after binding for persistent preference to get initial value // (preference key is set within bind method) addPreference(widget) diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt index 7af9c81a61c1..022fb1dbe99c 100644 --- a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt +++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceScreenBindingHelper.kt @@ -236,11 +236,12 @@ class PreferenceScreenBindingHelper( preference.bindRecursively(preferenceBindingFactory, preferences, storages) } else { preferences[preference.key]?.let { - preferenceBindingFactory.bind(preference, it) - (it as? PersistentPreference<*>)?.storage(context)?.let { storage -> + val metadata = it.metadata + (metadata as? PersistentPreference<*>)?.storage(context)?.let { storage -> preference.preferenceDataStore = storages.getOrPut(storage) { PreferenceDataStoreAdapter(storage) } } + preferenceBindingFactory.bind(preference, it) } } } diff --git a/packages/SettingsLib/Preference/testutils/com/android/settingslib/preference/PreferenceBindingTestUtils.kt b/packages/SettingsLib/Preference/testutils/com/android/settingslib/preference/PreferenceBindingTestUtils.kt new file mode 100644 index 000000000000..f3142d031aa9 --- /dev/null +++ b/packages/SettingsLib/Preference/testutils/com/android/settingslib/preference/PreferenceBindingTestUtils.kt @@ -0,0 +1,37 @@ +/* + * 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.settingslib.preference + +import android.content.Context +import androidx.annotation.VisibleForTesting +import androidx.preference.Preference +import com.android.settingslib.metadata.PersistentPreference +import com.android.settingslib.metadata.PreferenceMetadata + +/** Creates [Preference] widget and binds with metadata. */ +@VisibleForTesting +fun <P : Preference> PreferenceMetadata.createAndBindWidget(context: Context): P { + val binding = DefaultPreferenceBindingFactory.getPreferenceBinding(this) + return (binding.createWidget(context) as P).also { + if (this is PersistentPreference<*>) { + storage(context)?.let { keyValueStore -> + it.preferenceDataStore = PreferenceDataStoreAdapter(keyValueStore) + } + } + binding.bind(it, this) + } +} diff --git a/packages/SettingsLib/res/values-bs/strings.xml b/packages/SettingsLib/res/values-bs/strings.xml index b76b028ac4b9..4d334660895e 100644 --- a/packages/SettingsLib/res/values-bs/strings.xml +++ b/packages/SettingsLib/res/values-bs/strings.xml @@ -82,7 +82,7 @@ <string name="speed_label_very_fast" msgid="8215718029533182439">"Veoma brzo"</string> <string name="wifi_passpoint_expired" msgid="6540867261754427561">"Isteklo"</string> <string name="preference_summary_default_combination" msgid="2644094566845577901">"<xliff:g id="STATE">%1$s</xliff:g> / <xliff:g id="DESCRIPTION">%2$s</xliff:g>"</string> - <string name="bluetooth_disconnected" msgid="7739366554710388701">"Isključen"</string> + <string name="bluetooth_disconnected" msgid="7739366554710388701">"Nije povezano"</string> <string name="bluetooth_disconnecting" msgid="7638892134401574338">"Prekidanje veze…"</string> <string name="bluetooth_connecting" msgid="5871702668260192755">"Povezivanje…"</string> <string name="bluetooth_connected" msgid="8065345572198502293">"Povezano<xliff:g id="ACTIVE_DEVICE">%1$s</xliff:g>"</string> diff --git a/packages/SettingsLib/res/values-in/arrays.xml b/packages/SettingsLib/res/values-in/arrays.xml index 038800cf4bfa..00ea04d68c16 100644 --- a/packages/SettingsLib/res/values-in/arrays.xml +++ b/packages/SettingsLib/res/values-in/arrays.xml @@ -29,7 +29,7 @@ <item msgid="4613015005934755724">"Terhubung"</item> <item msgid="3763530049995655072">"Ditangguhkan"</item> <item msgid="7852381437933824454">"Memutus sambungan..."</item> - <item msgid="5046795712175415059">"Sambungan terputus"</item> + <item msgid="5046795712175415059">"Tidak terhubung"</item> <item msgid="2473654476624070462">"Gagal"</item> <item msgid="9146847076036105115">"Diblokir"</item> <item msgid="4543924085816294893">"Menghindari sambungan buruk untuk sementara"</item> @@ -43,7 +43,7 @@ <item msgid="1043944043827424501">"Terhubung ke <xliff:g id="NETWORK_NAME">%1$s</xliff:g>"</item> <item msgid="7445993821842009653">"Ditangguhkan"</item> <item msgid="1175040558087735707">"Diputus dari <xliff:g id="NETWORK_NAME">%1$s</xliff:g>…"</item> - <item msgid="699832486578171722">"Sambungan terputus"</item> + <item msgid="699832486578171722">"Tidak terhubung"</item> <item msgid="522383512264986901">"Gagal"</item> <item msgid="3602596701217484364">"Diblokir"</item> <item msgid="1999413958589971747">"Menghindari sambungan buruk untuk sementara"</item> diff --git a/packages/SettingsLib/res/values-in/strings.xml b/packages/SettingsLib/res/values-in/strings.xml index 3b265f8e9d38..bfd8f397be60 100644 --- a/packages/SettingsLib/res/values-in/strings.xml +++ b/packages/SettingsLib/res/values-in/strings.xml @@ -50,7 +50,7 @@ <string name="wifi_security_owe" msgid="3343421403561657809">"Enhanced Open"</string> <string name="wifi_security_eap_suiteb" msgid="415842785991698142">"WPA3-Enterprise 192-bit"</string> <string name="wifi_remembered" msgid="3266709779723179188">"Disimpan"</string> - <string name="wifi_disconnected" msgid="7054450256284661757">"Terputus"</string> + <string name="wifi_disconnected" msgid="7054450256284661757">"Tidak terhubung"</string> <string name="wifi_disabled_generic" msgid="2651916945380294607">"Nonaktif"</string> <string name="wifi_disabled_network_failure" msgid="2660396183242399585">"Kegagalan Konfigurasi IP"</string> <string name="wifi_disabled_password_failure" msgid="6892387079613226738">"Masalah autentikasi"</string> @@ -82,7 +82,7 @@ <string name="speed_label_very_fast" msgid="8215718029533182439">"Sangat Cepat"</string> <string name="wifi_passpoint_expired" msgid="6540867261754427561">"Sudah tidak berlaku"</string> <string name="preference_summary_default_combination" msgid="2644094566845577901">"<xliff:g id="STATE">%1$s</xliff:g> / <xliff:g id="DESCRIPTION">%2$s</xliff:g>"</string> - <string name="bluetooth_disconnected" msgid="7739366554710388701">"Sambungan terputus"</string> + <string name="bluetooth_disconnected" msgid="7739366554710388701">"Tidak terhubung"</string> <string name="bluetooth_disconnecting" msgid="7638892134401574338">"Memutus sambungan..."</string> <string name="bluetooth_connecting" msgid="5871702668260192755">"Menghubungkan…"</string> <string name="bluetooth_connected" msgid="8065345572198502293">"Terhubung<xliff:g id="ACTIVE_DEVICE">%1$s</xliff:g>"</string> diff --git a/packages/SettingsLib/res/values-ky/strings.xml b/packages/SettingsLib/res/values-ky/strings.xml index 2fb036f8356c..785bf43bb4d1 100644 --- a/packages/SettingsLib/res/values-ky/strings.xml +++ b/packages/SettingsLib/res/values-ky/strings.xml @@ -258,7 +258,7 @@ <string name="adb_pair_method_qrcode_summary" msgid="7130694277228970888">"QR кодунун сканерин колдонуп, жаңы түзмөктөрдү жупташтырыңыз"</string> <string name="adb_pair_method_code_title" msgid="1122590300445142904">"Түзмөктү атайын код аркылуу жупташтыруу"</string> <string name="adb_pair_method_code_summary" msgid="6370414511333685185">"Жаңы түзмөктөрдү алты сандан турган код аркылуу жупташтырасыз"</string> - <string name="adb_paired_devices_title" msgid="5268997341526217362">"Жупташтырылган түзмөктөр"</string> + <string name="adb_paired_devices_title" msgid="5268997341526217362">"Байланышкан түзмөктөр"</string> <string name="adb_wireless_device_connected_summary" msgid="3039660790249148713">"Учурда туташып турган түзмөктөр"</string> <string name="adb_wireless_device_details_title" msgid="7129369670526565786">"Түзмөктүн чоо-жайы"</string> <string name="adb_device_forget" msgid="193072400783068417">"Унутулсун"</string> diff --git a/packages/SettingsLib/res/values-mk/strings.xml b/packages/SettingsLib/res/values-mk/strings.xml index f5e84a23aa07..b9d970d8388f 100644 --- a/packages/SettingsLib/res/values-mk/strings.xml +++ b/packages/SettingsLib/res/values-mk/strings.xml @@ -85,7 +85,7 @@ <string name="bluetooth_disconnected" msgid="7739366554710388701">"Не е поврзано"</string> <string name="bluetooth_disconnecting" msgid="7638892134401574338">"Се исклучува..."</string> <string name="bluetooth_connecting" msgid="5871702668260192755">"Се поврзува..."</string> - <string name="bluetooth_connected" msgid="8065345572198502293">"Поврзан со <xliff:g id="ACTIVE_DEVICE">%1$s</xliff:g>"</string> + <string name="bluetooth_connected" msgid="8065345572198502293">"Поврзано<xliff:g id="ACTIVE_DEVICE">%1$s</xliff:g>"</string> <string name="bluetooth_pairing" msgid="4269046942588193600">"Се спарува..."</string> <string name="bluetooth_connected_no_headset" msgid="2224101138659967604">"Поврзан со <xliff:g id="ACTIVE_DEVICE">%1$s</xliff:g> (без телефон)"</string> <string name="bluetooth_connected_no_a2dp" msgid="8566874395813947092">"Поврзан со <xliff:g id="ACTIVE_DEVICE">%1$s</xliff:g> (без аудиовизуелни содржини)"</string> diff --git a/packages/SettingsLib/res/values-or/strings.xml b/packages/SettingsLib/res/values-or/strings.xml index 73b099695da8..ae1df210bce3 100644 --- a/packages/SettingsLib/res/values-or/strings.xml +++ b/packages/SettingsLib/res/values-or/strings.xml @@ -82,7 +82,7 @@ <string name="speed_label_very_fast" msgid="8215718029533182439">"ଅତି ଦ୍ରୁତ"</string> <string name="wifi_passpoint_expired" msgid="6540867261754427561">"ମିଆଦ ଶେଷ ହୋଇଯାଇଛି"</string> <string name="preference_summary_default_combination" msgid="2644094566845577901">"<xliff:g id="STATE">%1$s</xliff:g> / <xliff:g id="DESCRIPTION">%2$s</xliff:g>"</string> - <string name="bluetooth_disconnected" msgid="7739366554710388701">"ବିଛିନ୍ନ ହେଲା"</string> + <string name="bluetooth_disconnected" msgid="7739366554710388701">"ଡିସକନେକ୍ଟ ହୋଇଛି"</string> <string name="bluetooth_disconnecting" msgid="7638892134401574338">"ବିଚ୍ଛିନ୍ନ କରୁଛି…"</string> <string name="bluetooth_connecting" msgid="5871702668260192755">"କନେକ୍ଟ ହେଉଛି…"</string> <string name="bluetooth_connected" msgid="8065345572198502293">"ସଂଯୁକ୍ତ ହେଲା<xliff:g id="ACTIVE_DEVICE">%1$s</xliff:g>"</string> diff --git a/packages/SettingsLib/res/values-sk/strings.xml b/packages/SettingsLib/res/values-sk/strings.xml index 74c294b84db7..4c41b7d4e397 100644 --- a/packages/SettingsLib/res/values-sk/strings.xml +++ b/packages/SettingsLib/res/values-sk/strings.xml @@ -82,7 +82,7 @@ <string name="speed_label_very_fast" msgid="8215718029533182439">"Veľmi rýchla"</string> <string name="wifi_passpoint_expired" msgid="6540867261754427561">"Vypršalo"</string> <string name="preference_summary_default_combination" msgid="2644094566845577901">"<xliff:g id="STATE">%1$s</xliff:g> / <xliff:g id="DESCRIPTION">%2$s</xliff:g>"</string> - <string name="bluetooth_disconnected" msgid="7739366554710388701">"Odpojený"</string> + <string name="bluetooth_disconnected" msgid="7739366554710388701">"Odpojené"</string> <string name="bluetooth_disconnecting" msgid="7638892134401574338">"Prebieha odpájanie..."</string> <string name="bluetooth_connecting" msgid="5871702668260192755">"Prebieha pripájanie…"</string> <string name="bluetooth_connected" msgid="8065345572198502293">"Pripojené k zariadeniu <xliff:g id="ACTIVE_DEVICE">%1$s</xliff:g>"</string> diff --git a/packages/SettingsLib/res/values-sv/strings.xml b/packages/SettingsLib/res/values-sv/strings.xml index 1c1117d39d5b..1d7f62ac6c93 100644 --- a/packages/SettingsLib/res/values-sv/strings.xml +++ b/packages/SettingsLib/res/values-sv/strings.xml @@ -82,7 +82,7 @@ <string name="speed_label_very_fast" msgid="8215718029533182439">"Mycket snabb"</string> <string name="wifi_passpoint_expired" msgid="6540867261754427561">"Har upphört att gälla"</string> <string name="preference_summary_default_combination" msgid="2644094566845577901">"<xliff:g id="STATE">%1$s</xliff:g>/<xliff:g id="DESCRIPTION">%2$s</xliff:g>"</string> - <string name="bluetooth_disconnected" msgid="7739366554710388701">"Kopplas ifrån"</string> + <string name="bluetooth_disconnected" msgid="7739366554710388701">"Frånkopplad"</string> <string name="bluetooth_disconnecting" msgid="7638892134401574338">"Kopplar ifrån…"</string> <string name="bluetooth_connecting" msgid="5871702668260192755">"Ansluter…"</string> <string name="bluetooth_connected" msgid="8065345572198502293">"Ansluten<xliff:g id="ACTIVE_DEVICE">%1$s</xliff:g>"</string> diff --git a/packages/SettingsLib/res/values-th/strings.xml b/packages/SettingsLib/res/values-th/strings.xml index 57b8f5914e71..060b7b267624 100644 --- a/packages/SettingsLib/res/values-th/strings.xml +++ b/packages/SettingsLib/res/values-th/strings.xml @@ -82,7 +82,7 @@ <string name="speed_label_very_fast" msgid="8215718029533182439">"เร็วมาก"</string> <string name="wifi_passpoint_expired" msgid="6540867261754427561">"หมดอายุแล้ว"</string> <string name="preference_summary_default_combination" msgid="2644094566845577901">"<xliff:g id="STATE">%1$s</xliff:g>/<xliff:g id="DESCRIPTION">%2$s</xliff:g>"</string> - <string name="bluetooth_disconnected" msgid="7739366554710388701">"ตัดการเชื่อมต่อ"</string> + <string name="bluetooth_disconnected" msgid="7739366554710388701">"ยกเลิกการเชื่อมต่อแล้ว"</string> <string name="bluetooth_disconnecting" msgid="7638892134401574338">"กำลังตัดการเชื่อมต่อ..."</string> <string name="bluetooth_connecting" msgid="5871702668260192755">"กำลังเชื่อมต่อ…"</string> <string name="bluetooth_connected" msgid="8065345572198502293">"เชื่อมต่อแล้ว<xliff:g id="ACTIVE_DEVICE">%1$s</xliff:g>"</string> diff --git a/packages/SettingsLib/src/com/android/settingslib/Utils.java b/packages/SettingsLib/src/com/android/settingslib/Utils.java index 744e97e53fef..1998d0c6721c 100644 --- a/packages/SettingsLib/src/com/android/settingslib/Utils.java +++ b/packages/SettingsLib/src/com/android/settingslib/Utils.java @@ -1,7 +1,6 @@ package com.android.settingslib; import static android.app.admin.DevicePolicyResources.Strings.Settings.WORK_PROFILE_USER_LABEL; -import static android.webkit.Flags.updateServiceV2; import android.annotation.ColorInt; import android.app.admin.DevicePolicyManager; @@ -496,7 +495,7 @@ public class Utils { || packageName.equals(sServicesSystemSharedLibPackageName) || packageName.equals(sSharedSystemSharedLibPackageName) || packageName.equals(PrintManager.PRINT_SPOOLER_PACKAGE_NAME) - || (updateServiceV2() && packageName.equals(getDefaultWebViewPackageName(pm))) + || packageName.equals(getDefaultWebViewPackageName(pm)) || isDeviceProvisioningPackage(resources, packageName); } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingId.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingId.java index e7c7476d4797..f4b79db67d88 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingId.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/DeviceSettingId.java @@ -120,4 +120,10 @@ public @interface DeviceSettingId { /** Device setting ID for ANC. */ int DEVICE_SETTING_ID_ANC = 1001; + + /** Device setting expandable ID 1. */ + int DEVICE_SETTING_ID_EXPANDABLE_1 = 3001; + + /** Device setting expandable ID 2. */ + int DEVICE_SETTING_ID_EXPANDABLE_2 = 3100; } diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepository.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepository.kt index 8537897cb329..c68dbeead26b 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepository.kt +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepository.kt @@ -106,26 +106,40 @@ class DeviceSettingRepositoryImpl( private fun DeviceSettingsConfig.toModel(): DeviceSettingConfigModel = DeviceSettingConfigModel( - mainItems = mainContentItems.map { it.toModel() }, - moreSettingsItems = moreSettingsItems.map { it.toModel() }, + mainItems = mainContentItems.toModel(), + moreSettingsItems = moreSettingsItems.toModel(), moreSettingsHelpItem = moreSettingsHelpItem?.toModel(), ) - private fun DeviceSettingItem.toModel(): DeviceSettingConfigItemModel { + private fun List<DeviceSettingItem>.toModel(): List<DeviceSettingConfigItemModel> { + return this.flatMap { item -> + if (item.settingId in EXPANDABLE_SETTING_IDS) { + IntRange(item.settingId, item.settingId + SETTING_ID_EXPAND_LIMIT - 1).map { + item.toModel(overrideSettingId = it) + } + } else { + listOf(item.toModel()) + } + } + } + + private fun DeviceSettingItem.toModel( + overrideSettingId: Int? = null + ): DeviceSettingConfigItemModel { return if (!TextUtils.isEmpty(preferenceKey)) { if (settingId == DeviceSettingId.DEVICE_SETTING_ID_BLUETOOTH_PROFILES) { BluetoothProfilesItem( - settingId, + overrideSettingId ?: settingId, highlighted, preferenceKey!!, extras.getStringArrayList(DeviceSettingContract.INVISIBLE_PROFILES) ?: emptyList(), ) } else { - CommonBuiltinItem(settingId, highlighted, preferenceKey!!) + CommonBuiltinItem(overrideSettingId ?: settingId, highlighted, preferenceKey!!) } } else { - AppProvidedItem(settingId, highlighted) + AppProvidedItem(overrideSettingId ?: settingId, highlighted) } } @@ -134,6 +148,7 @@ class DeviceSettingRepositoryImpl( is DeviceSettingIntentAction -> DeviceSettingActionModel.IntentAction(this.intent) is DeviceSettingPendingIntentAction -> DeviceSettingActionModel.PendingIntentAction(this.pendingIntent) + else -> null } @@ -150,7 +165,7 @@ class DeviceSettingRepositoryImpl( summary = pref.summary, icon = pref.icon?.let { DeviceSettingIcon.BitmapIcon(it) }, isAllowedChangingState = pref.isAllowedChangingState, - action = pref.action?.toModel(), + action = pref.action.toModel(), switchState = if (pref.hasSwitch()) { DeviceSettingStateModel.ActionSwitchPreferenceState(pref.checked) @@ -163,6 +178,7 @@ class DeviceSettingRepositoryImpl( } }, ) + is MultiTogglePreference -> DeviceSettingModel.MultiTogglePreference( cachedDevice = cachedDevice, @@ -178,21 +194,33 @@ class DeviceSettingRepositoryImpl( } }, ) + is DeviceSettingFooterPreference -> DeviceSettingModel.FooterPreference( cachedDevice = cachedDevice, id = settingId, footerText = pref.footerText, ) + is DeviceSettingHelpPreference -> DeviceSettingModel.HelpPreference( cachedDevice = cachedDevice, id = settingId, intent = pref.intent, ) + else -> DeviceSettingModel.Unknown(cachedDevice, settingId) } private fun ToggleInfo.toModel(): ToggleModel = ToggleModel(label, DeviceSettingIcon.BitmapIcon(icon)) + + companion object { + private val EXPANDABLE_SETTING_IDS = + listOf( + DeviceSettingId.DEVICE_SETTING_ID_EXPANDABLE_1, + DeviceSettingId.DEVICE_SETTING_ID_EXPANDABLE_2, + ) + private const val SETTING_ID_EXPAND_LIMIT = 15 + } } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt index 4e62fd3b27c5..3d4e4492ffd1 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/devicesettings/data/repository/DeviceSettingRepositoryTest.kt @@ -167,6 +167,26 @@ class DeviceSettingRepositoryTest { } @Test + fun getDeviceSettingsConfig_expandable_success() { + testScope.runTest { + setUpConfigService(true, DEVICE_SETTING_CONFIG_EXPANDABLE) + `when`(settingProviderService1.serviceStatus) + .thenReturn(DeviceSettingsProviderServiceStatus(true)) + `when`(settingProviderService2.serviceStatus) + .thenReturn(DeviceSettingsProviderServiceStatus(true)) + + val config = underTest.getDeviceSettingsConfig(cachedDevice)!! + + assertThat(config.mainItems.map { it.settingId }).isEqualTo( + IntRange( + DeviceSettingId.DEVICE_SETTING_ID_EXPANDABLE_1, + DeviceSettingId.DEVICE_SETTING_ID_EXPANDABLE_1 + 14 + ).toList() + ) + } + } + + @Test fun getDeviceSettingsConfig_noMetadata_returnNull() { testScope.runTest { `when`( @@ -510,6 +530,13 @@ class DeviceSettingRepositoryTest { SETTING_PROVIDER_SERVICE_CLASS_NAME_2, SETTING_PROVIDER_SERVICE_INTENT_ACTION_2, ) + val DEVICE_SETTING_APP_PROVIDED_ITEM_EXPANDABLE = + DeviceSettingItem( + DeviceSettingId.DEVICE_SETTING_ID_EXPANDABLE_1, + SETTING_PROVIDER_SERVICE_PACKAGE_NAME_1, + SETTING_PROVIDER_SERVICE_CLASS_NAME_1, + SETTING_PROVIDER_SERVICE_INTENT_ACTION_1, + ) val DEVICE_SETTING_BUILT_IN_ITEM = DeviceSettingItem( DeviceSettingId.DEVICE_SETTING_ID_BLUETOOTH_AUDIO_DEVICE_TYPE_GROUP, @@ -581,5 +608,13 @@ class DeviceSettingRepositoryTest { listOf(DEVICE_SETTING_APP_PROVIDED_ITEM_2), DEVICE_SETTING_HELP_ITEM, ) + val DEVICE_SETTING_CONFIG_EXPANDABLE = + DeviceSettingsConfig( + listOf( + DEVICE_SETTING_APP_PROVIDED_ITEM_EXPANDABLE, + ), + listOf(), + DEVICE_SETTING_HELP_ITEM, + ) } } diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java index 509b88b257fe..558ccaf7a707 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java @@ -263,5 +263,6 @@ public class SystemSettingsValidators { VALIDATORS.put(System.NOTIFICATION_COOLDOWN_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put(System.NOTIFICATION_COOLDOWN_ALL, BOOLEAN_VALIDATOR); VALIDATORS.put(System.NOTIFICATION_COOLDOWN_VIBRATE_UNLOCKED, BOOLEAN_VALIDATOR); + VALIDATORS.put(System.HEARING_DEVICE_LOCAL_AMBIENT_VOLUME, ANY_STRING_VALIDATOR); } } diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java index 1659c9eb67f2..c2beaa8048f6 100644 --- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java +++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java @@ -944,7 +944,8 @@ public class SettingsBackupTest { Settings.System.WEAR_ACCESSIBILITY_GESTURE_ENABLED_DURING_OOBE, Settings.System.WEAR_TTS_PREWARM_ENABLED, Settings.System.SCREEN_AUTO_BRIGHTNESS_ADJ, - Settings.System.MULTI_AUDIO_FOCUS_ENABLED // form-factor/OEM specific + Settings.System.MULTI_AUDIO_FOCUS_ENABLED, // form-factor/OEM specific + Settings.System.HEARING_DEVICE_LOCAL_AMBIENT_VOLUME // internal cache ); if (!Flags.backUpSmoothDisplayAndForcePeakRefreshRate()) { settings.add(Settings.System.MIN_REFRESH_RATE); diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index 05c5e5d9f82d..1919572ff571 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -951,6 +951,9 @@ <!-- Permission required for CTS test - CtsAppTestCases --> <uses-permission android:name="android.permission.KILL_UID" /> + <!-- Permission required for CTS test - CtsTelephonyTestCases --> + <uses-permission android:name="android.permission.READ_BASIC_PHONE_STATE" /> + <application android:label="@string/app_label" android:theme="@android:style/Theme.DeviceDefault.DayNight" diff --git a/packages/StatementService/Android.bp b/packages/StatementService/Android.bp index ff1a756479b6..90e1808b7d44 100644 --- a/packages/StatementService/Android.bp +++ b/packages/StatementService/Android.bp @@ -35,6 +35,7 @@ android_app { privileged: true, certificate: "platform", static_libs: [ + "StatementServiceParser", "androidx.appcompat_appcompat", "androidx.collection_collection-ktx", "androidx.work_work-runtime", diff --git a/packages/StatementService/src/com/android/statementservice/network/retriever/StatementParser.kt b/packages/StatementService/src/com/android/statementservice/network/retriever/StatementParser.kt index 455e8085af50..ad137400fa86 100644 --- a/packages/StatementService/src/com/android/statementservice/network/retriever/StatementParser.kt +++ b/packages/StatementService/src/com/android/statementservice/network/retriever/StatementParser.kt @@ -28,6 +28,8 @@ import java.io.StringReader import java.util.ArrayList import com.android.statementservice.retriever.WebAsset import com.android.statementservice.retriever.AndroidAppAsset +import com.android.statementservice.retriever.DynamicAppLinkComponent +import org.json.JSONObject /** * Parses JSON from the Digital Asset Links specification. For examples, see [WebAsset], @@ -97,13 +99,45 @@ object StatementParser { FIELD_NOT_ARRAY_FORMAT_STRING.format(StatementUtils.ASSET_DESCRIPTOR_FIELD_RELATION) ) val target = AssetFactory.create(targetObject) + val dynamicAppLinkComponents = parseDynamicAppLinkComponents( + statement.optJSONObject(StatementUtils.ASSET_DESCRIPTOR_FIELD_RELATION_EXTENSIONS) + ) val statements = (0 until relations.length()) .map { relations.getString(it) } .map(Relation::create) - .map { Statement.create(source, target, it) } + .map { Statement.create(source, target, it, dynamicAppLinkComponents) } return Result.Success(ParsedStatement(statements, listOfNotNull(delegate))) } + private fun parseDynamicAppLinkComponents( + statement: JSONObject? + ): List<DynamicAppLinkComponent> { + val relationExtensions = statement?.optJSONObject( + StatementUtils.ASSET_DESCRIPTOR_FIELD_RELATION_EXTENSIONS + ) ?: return emptyList() + val handleAllUrlsRelationExtension = relationExtensions.optJSONObject( + StatementUtils.RELATION.toString() + ) ?: return emptyList() + val components = handleAllUrlsRelationExtension.optJSONArray( + StatementUtils.RELATION_EXTENSION_FIELD_DAL_COMPONENTS + ) ?: return emptyList() + + return (0 until components.length()) + .map { components.getJSONObject(it) } + .map { parseComponent(it) } + } + + private fun parseComponent(component: JSONObject): DynamicAppLinkComponent { + val query = component.optJSONObject("?") + return DynamicAppLinkComponent.create( + component.optBoolean("exclude", false), + component.optString("#"), + component.optString("/"), + query?.keys()?.asSequence()?.associateWith { query.getString(it) }, + component.optString("comments") + ) + } + data class ParsedStatement(val statements: List<Statement>, val delegates: List<String>) } diff --git a/packages/StatementService/src/com/android/statementservice/retriever/DynamicAppLinkComponent.java b/packages/StatementService/src/com/android/statementservice/retriever/DynamicAppLinkComponent.java new file mode 100644 index 000000000000..dc27e125e204 --- /dev/null +++ b/packages/StatementService/src/com/android/statementservice/retriever/DynamicAppLinkComponent.java @@ -0,0 +1,145 @@ +/* + * 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.statementservice.retriever; + +import android.annotation.Nullable; + +import java.util.Map; + +/** + * A immutable value type representing a dynamic app link component + */ +public final class DynamicAppLinkComponent { + private final boolean mExclude; + private final String mFragment; + private final String mPath; + private final Map<String, String> mQuery; + private final String mComments; + + private DynamicAppLinkComponent(boolean exclude, String fragment, String path, + Map<String, String> query, String comments) { + mExclude = exclude; + mFragment = fragment; + mPath = path; + mQuery = query; + mComments = comments; + } + + /** + * Returns true or false indicating whether this rule should be a exclusion rule. + */ + public boolean getExclude() { + return mExclude; + } + + /** + * Returns a optional pattern string for matching URL fragments. + */ + @Nullable + public String getFragment() { + return mFragment; + } + + /** + * Returns a optional pattern string for matching URL paths. + */ + @Nullable + public String getPath() { + return mPath; + } + + /** + * Returns a optional pattern string for matching a single key-value pair in the URL query + * params. + */ + @Nullable + public Map<String, String> getQuery() { + return mQuery; + } + + /** + * Returns a optional comment string for this component. + */ + @Nullable + public String getComments() { + return mComments; + } + + /** + * Creates a new DynamicAppLinkComponent object. + */ + public static DynamicAppLinkComponent create(boolean exclude, String fragment, String path, + Map<String, String> query, String comments) { + return new DynamicAppLinkComponent(exclude, fragment, path, query, comments); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + + DynamicAppLinkComponent rule = (DynamicAppLinkComponent) o; + + if (mExclude != rule.mExclude) { + return false; + } + if (!mFragment.equals(rule.mFragment)) { + return false; + } + if (!mPath.equals(rule.mPath)) { + return false; + } + if (!mQuery.equals(rule.mQuery)) { + return false; + } + if (!mComments.equals(rule.mComments)) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + int result = Boolean.hashCode(mExclude); + result = 31 * result + mFragment.hashCode(); + result = 31 * result + mPath.hashCode(); + result = 31 * result + mQuery.hashCode(); + result = 31 * result + mComments.hashCode(); + return result; + } + + @Override + public String toString() { + StringBuilder statement = new StringBuilder(); + statement.append("HandleAllUriRule: "); + statement.append(mExclude); + statement.append(", "); + statement.append(mFragment); + statement.append(", "); + statement.append(mPath); + statement.append(", "); + statement.append(mQuery); + statement.append(", "); + statement.append(mComments); + return statement.toString(); + } +} diff --git a/packages/StatementService/src/com/android/statementservice/retriever/JsonParser.java b/packages/StatementService/src/com/android/statementservice/retriever/JsonParser.java index ce063ea5c143..7635e8234dc0 100644 --- a/packages/StatementService/src/com/android/statementservice/retriever/JsonParser.java +++ b/packages/StatementService/src/com/android/statementservice/retriever/JsonParser.java @@ -46,12 +46,6 @@ public final class JsonParser { while (reader.hasNext()) { String fieldName = reader.nextName(); - if (output.has(fieldName)) { - errorMsg = "Duplicate field name."; - reader.skipValue(); - continue; - } - JsonToken token = reader.peek(); if (token.equals(JsonToken.BEGIN_ARRAY)) { output.put(fieldName, new JSONArray(parseArray(reader))); diff --git a/packages/StatementService/src/com/android/statementservice/retriever/Statement.java b/packages/StatementService/src/com/android/statementservice/retriever/Statement.java index f8bab3ef170f..b5e204651307 100644 --- a/packages/StatementService/src/com/android/statementservice/retriever/Statement.java +++ b/packages/StatementService/src/com/android/statementservice/retriever/Statement.java @@ -23,6 +23,10 @@ import com.android.statementservice.network.retriever.StatementRetriever; import kotlin.coroutines.Continuation; +import java.util.Collections; +import java.util.List; + + /** * An immutable value type representing a statement, consisting of a source, target, and relation. * This reflects an assertion that the relation holds for the source, target pair. For example, if a @@ -32,7 +36,21 @@ import kotlin.coroutines.Continuation; * { * "relation": ["delegate_permission/common.handle_all_urls"], * "target" : {"namespace": "android_app", "package_name": "com.example.app", - * "sha256_cert_fingerprints": ["00:11:22:33"] } + * "sha256_cert_fingerprints": ["00:11:22:33"] }, + * "relation_extensions": { + * "delegate_permission/common_handle_all_urls": { + * "dynamic_app_link_components": [ + * { + * "/": "/foo*", + * "exclude": true, + * "comments": "App should not handle paths that start with foo" + * }, + * { + * "/": "*", + * "comments": "Catch all other paths" + * } + * ] + * } * } * </pre> * @@ -40,7 +58,7 @@ import kotlin.coroutines.Continuation; * return a {@link Statement} with {@link #getSource} equal to the input parameter, * {@link #getRelation} equal to * - * <pre>Relation.create("delegate_permission", "common.get_login_creds");</pre> + * <pre>Relation.create("delegate_permission", "common.handle_all_urls");</pre> * * and with {@link #getTarget} equal to * @@ -48,17 +66,23 @@ import kotlin.coroutines.Continuation; * + "\"package_name\": \"com.example.app\"}" * + "\"sha256_cert_fingerprints\": \"[\"00:11:22:33\"]\"}"); * </pre> + * + * If extensions exist for the handle_all_urls relation then {@link #getDynamicAppLinkComponents} + * will return a list of parsed {@link DynamicAppLinkComponent}s. */ public final class Statement { private final AbstractAsset mTarget; private final Relation mRelation; private final AbstractAsset mSource; + private final List<DynamicAppLinkComponent> mDynamicAppLinkComponents; - private Statement(AbstractAsset source, AbstractAsset target, Relation relation) { + private Statement(AbstractAsset source, AbstractAsset target, Relation relation, + List<DynamicAppLinkComponent> components) { mSource = source; mTarget = target; mRelation = relation; + mDynamicAppLinkComponents = Collections.unmodifiableList(components); } /** @@ -86,6 +110,14 @@ public final class Statement { } /** + * Returns the relation matching rules of the statement. + */ + @NonNull + public List<DynamicAppLinkComponent> getDynamicAppLinkComponents() { + return mDynamicAppLinkComponents; + } + + /** * Creates a new Statement object for the specified target asset and relation. For example: * <pre> * Asset asset = Asset.Factory.create( @@ -95,8 +127,9 @@ public final class Statement { * </pre> */ public static Statement create(@NonNull AbstractAsset source, @NonNull AbstractAsset target, - @NonNull Relation relation) { - return new Statement(source, target, relation); + @NonNull Relation relation, + @NonNull List<DynamicAppLinkComponent> components) { + return new Statement(source, target, relation, components); } @Override @@ -119,6 +152,9 @@ public final class Statement { if (!mSource.equals(statement.mSource)) { return false; } + if (!mDynamicAppLinkComponents.equals(statement.mDynamicAppLinkComponents)) { + return false; + } return true; } @@ -128,6 +164,7 @@ public final class Statement { int result = mTarget.hashCode(); result = 31 * result + mRelation.hashCode(); result = 31 * result + mSource.hashCode(); + result = 31 * result + mDynamicAppLinkComponents.hashCode(); return result; } @@ -140,6 +177,10 @@ public final class Statement { statement.append(mTarget); statement.append(", "); statement.append(mRelation); + if (!mDynamicAppLinkComponents.isEmpty()) { + statement.append(", "); + statement.append(mDynamicAppLinkComponents); + } return statement.toString(); } } diff --git a/packages/StatementService/src/com/android/statementservice/utils/StatementUtils.kt b/packages/StatementService/src/com/android/statementservice/utils/StatementUtils.kt index 4837aad3a025..47c69b4b3a44 100644 --- a/packages/StatementService/src/com/android/statementservice/utils/StatementUtils.kt +++ b/packages/StatementService/src/com/android/statementservice/utils/StatementUtils.kt @@ -17,8 +17,17 @@ package com.android.statementservice.utils import android.content.Context +import android.content.UriRelativeFilter +import android.content.UriRelativeFilter.FRAGMENT +import android.content.UriRelativeFilter.PATH +import android.content.UriRelativeFilter.QUERY +import android.content.UriRelativeFilterGroup +import android.content.UriRelativeFilterGroup.ACTION_ALLOW +import android.content.UriRelativeFilterGroup.ACTION_BLOCK import android.content.pm.PackageManager import android.util.Patterns +import com.android.statementservice.parser.parseMatchingExpression +import com.android.statementservice.retriever.DynamicAppLinkComponent import com.android.statementservice.retriever.Relation import java.net.URL import java.security.MessageDigest @@ -52,7 +61,9 @@ internal object StatementUtils { */ const val ASSET_DESCRIPTOR_FIELD_RELATION = "relation" const val ASSET_DESCRIPTOR_FIELD_TARGET = "target" + const val ASSET_DESCRIPTOR_FIELD_RELATION_EXTENSIONS = "relation_extensions" const val DELEGATE_FIELD_DELEGATE = "include" + const val RELATION_EXTENSION_FIELD_DAL_COMPONENTS = "dynamic_app_link_components" val HEX_DIGITS = charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F') @@ -160,4 +171,23 @@ internal object StatementUtils { // Hosts with *. for wildcard subdomain support are verified against their root domain fun createWebAssetString(host: String) = WEB_ASSET_FORMAT.format(URL("https", host.removePrefix("*."), "").toString()) + + fun createUriRelativeFilterGroup(component: DynamicAppLinkComponent): UriRelativeFilterGroup { + val group = UriRelativeFilterGroup(if (component.exclude) ACTION_BLOCK else ACTION_ALLOW) + component.fragment?.let { + val (type, filter) = parseMatchingExpression(it) + group.addUriRelativeFilter(UriRelativeFilter(FRAGMENT, type, filter)) + } + component.path?.let { + val (type, filter) = parseMatchingExpression(it) + group.addUriRelativeFilter(UriRelativeFilter(PATH, type, filter)) + } + component.query?.let { + for ((k, v) in it) { + val (type, filter) = parseMatchingExpression(k + "=" + v) + group.addUriRelativeFilter(UriRelativeFilter(QUERY, type, filter)) + } + } + return group + } } diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 4531b7932eaf..f0e1b437ec51 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -30,6 +30,7 @@ <uses-permission android:name="android.permission.READ_WALLPAPER_INTERNAL" /> <!-- Used to read storage for all users --> + <uses-permission android:name="android.permission.STORAGE_INTERNAL" /> <uses-permission android:name="android.permission.WRITE_MEDIA_STORAGE" /> <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WAKE_LOCK" /> @@ -1015,6 +1016,10 @@ android:exported="false"> </activity> + <service + android:name="com.android.systemui.communal.widgets.GlanceableHubWidgetManagerService" + android:exported="false" /> + <!-- Doze with notifications, run in main sysui process for every user --> <service android:name=".doze.DozeService" @@ -1137,5 +1142,11 @@ android:name="android.service.dream" android:resource="@xml/home_controls_dream_metadata" /> </service> + + <service android:name="com.android.systemui.dreams.homecontrols.system.HomeControlsRemoteService" + android:singleUser="true" + android:exported="false" + /> + </application> </manifest> diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index d50a92b4e726..5c5edb1d00ba 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -26,6 +26,13 @@ flag { } flag { + name: "modes_ui_dialog_paging" + namespace: "systemui" + description: "Add pagination to the Modes dialog in quick settings." + bug: "376450983" +} + +flag { name: "priority_people_section" namespace: "systemui" description: "Add a new section for priority people (aka important conversations)." @@ -59,7 +66,7 @@ flag { flag { name: "notification_over_expansion_clipping_fix" namespace: "systemui" - description: "fix NSSL clipping when over-expanding; fixes split shade bug." + description: "Fix NSSL clipping when over-expanding; fixes split shade bug." bug: "288553572" metadata { purpose: PURPOSE_BUGFIX @@ -67,6 +74,14 @@ flag { } flag { + name: "notification_add_x_on_hover_to_dismiss" + namespace: "systemui" + description: "Adds an x to notifications which shows up on mouse hover, allowing the user to " + "dismiss a notification with mouse." + bug: "376297472" +} + +flag { name: "notification_async_group_header_inflation" namespace: "systemui" description: "Inflates the notification group summary header views from the background thread." @@ -740,10 +755,13 @@ flag { } flag { - name: "smartspace_swipe_event_logging" + name: "smartspace_swipe_event_logging_fix" namespace: "systemui" description: "Log card swipe events in smartspace" bug: "374150422" + metadata { + purpose: PURPOSE_BUGFIX + } } flag { @@ -908,16 +926,6 @@ flag { } flag { - name: "delayed_wakelock_release_on_background_thread" - namespace: "systemui" - description: "Released delayed wakelocks on background threads to avoid janking screen transitions." - bug: "316128516" - metadata { - purpose: PURPOSE_BUGFIX - } -} - -flag { name: "notify_power_manager_user_activity_background" namespace: "systemui" description: "Decide whether to notify the user activity to power manager in the background thread." @@ -1153,6 +1161,13 @@ flag { } flag { + name: "communal_hub_on_mobile" + namespace: "systemui" + description: "Brings the glanceable hub experience to mobile phones" + bug: "375689917" +} + +flag { name: "dream_overlay_updated_font" namespace: "systemui" description: "Flag to enable updated font settings for dream overlay" @@ -1625,6 +1640,16 @@ flag { } flag { + name: "home_controls_dream_hsum" + namespace: "systemui" + description: "Enables the home controls dream in HSUM" + bug: "370691405" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "only_show_media_stream_slider_in_single_volume_mode" namespace: "systemui" description: "When the device is in single volume mode, only show media stream slider and hide all other stream (e.g. call, notification, alarm, etc) sliders in volume panel" @@ -1652,6 +1677,13 @@ flag { } flag { + name: "bouncer_ui_revamp" + namespace: "systemui" + description: "Updates to background (blur), button animations and font changes." + bug: "376491880" +} + +flag { name: "ensure_enr_views_visibility" namespace: "systemui" description: "Ensures public and private visibilities" diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/Icon.kt b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/Icon.kt index 6e8312436d96..82d14369f239 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/Icon.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/common/ui/compose/Icon.kt @@ -32,11 +32,7 @@ import com.android.systemui.common.shared.model.Icon * Note: You can use [Color.Unspecified] to disable the tint and keep the original icon colors. */ @Composable -fun Icon( - icon: Icon, - modifier: Modifier = Modifier, - tint: Color = LocalContentColor.current, -) { +fun Icon(icon: Icon, modifier: Modifier = Modifier, tint: Color = LocalContentColor.current) { val contentDescription = icon.contentDescription?.load() when (icon) { is Icon.Loaded -> { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/StatusBarSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/StatusBarSection.kt index f9e22525237b..0d8a47019a08 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/StatusBarSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/StatusBarSection.kt @@ -21,12 +21,10 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import com.android.compose.animation.scene.SceneScope import com.android.compose.modifiers.height @@ -53,17 +51,13 @@ constructor( @SuppressLint("InflateParams") val view = remember(context) { - (LayoutInflater.from(context) - .inflate( - R.layout.keyguard_status_bar, - null, - false, - ) as KeyguardStatusBarView) + (LayoutInflater.from(context).inflate(R.layout.keyguard_status_bar, null, false) + as KeyguardStatusBarView) .also { it.layoutParams = ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT + ViewGroup.LayoutParams.WRAP_CONTENT, ) } } @@ -92,10 +86,8 @@ constructor( view }, modifier = - Modifier.fillMaxWidth().padding(horizontal = 16.dp).height { - Utils.getStatusBarHeaderHeightKeyguard(context) - }, - update = { viewController.setDisplayCutout(viewDisplayCutout) } + modifier.fillMaxWidth().height { Utils.getStatusBarHeaderHeightKeyguard(context) }, + update = { viewController.setDisplayCutout(viewDisplayCutout) }, ) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimNestedScrollConnection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimNestedScrollConnection.kt index 5cb45e5bd914..94c18cdbef5a 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimNestedScrollConnection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimNestedScrollConnection.kt @@ -16,11 +16,13 @@ package com.android.systemui.notifications.ui.composable +import androidx.compose.foundation.gestures.FlingBehavior import androidx.compose.foundation.gestures.Orientation import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.util.fastCoerceAtLeast import androidx.compose.ui.util.fastCoerceAtMost +import com.android.compose.nestedscroll.OnStopScope import com.android.compose.nestedscroll.PriorityNestedScrollConnection import com.android.compose.nestedscroll.ScrollController @@ -43,6 +45,7 @@ fun NotificationScrimNestedScrollConnection( isCurrentGestureOverscroll: () -> Boolean, onStart: (Float) -> Unit = {}, onStop: (Float) -> Unit = {}, + flingBehavior: FlingBehavior, ): PriorityNestedScrollConnection { return PriorityNestedScrollConnection( orientation = Orientation.Vertical, @@ -77,8 +80,9 @@ fun NotificationScrimNestedScrollConnection( return amountConsumed } - override suspend fun onStop(initialVelocity: Float): Float { - onStop(initialVelocity) + override suspend fun OnStopScope.onStop(initialVelocity: Float): Float { + val consumedByScroll = flingToScroll(initialVelocity, flingBehavior) + onStop(initialVelocity - consumedByScroll) if (scrimOffset() < minScrimOffset()) { animateScrimOffset(minScrimOffset()) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationStackNestedScrollConnection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationStackNestedScrollConnection.kt index e1b74a968caa..d8abfd7a4b94 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationStackNestedScrollConnection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationStackNestedScrollConnection.kt @@ -18,7 +18,9 @@ package com.android.systemui.notifications.ui.composable import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.tween +import androidx.compose.foundation.gestures.FlingBehavior import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.ScrollableDefaults import androidx.compose.foundation.layout.offset import androidx.compose.runtime.Composable import androidx.compose.runtime.remember @@ -30,6 +32,7 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastCoerceAtLeast +import com.android.compose.nestedscroll.OnStopScope import com.android.compose.nestedscroll.PriorityNestedScrollConnection import com.android.compose.nestedscroll.ScrollController import kotlin.math.max @@ -46,32 +49,35 @@ fun Modifier.stackVerticalOverscroll( val screenHeight = with(LocalDensity.current) { LocalConfiguration.current.screenHeightDp.dp.toPx() } val overscrollOffset = remember { Animatable(0f) } - val stackNestedScrollConnection = remember { - NotificationStackNestedScrollConnection( - stackOffset = { overscrollOffset.value }, - canScrollForward = canScrollForward, - onScroll = { offsetAvailable -> - coroutineScope.launch { - val maxProgress = screenHeight * 0.2f - val tilt = 3f - var offset = - overscrollOffset.value + - maxProgress * tanh(x = offsetAvailable / (maxProgress * tilt)) - offset = max(offset, -1f * maxProgress) - overscrollOffset.snapTo(offset) - } - }, - onStop = { velocityAvailable -> - coroutineScope.launch { - overscrollOffset.animateTo( - targetValue = 0f, - initialVelocity = velocityAvailable, - animationSpec = tween(), - ) - } - }, - ) - } + val flingBehavior = ScrollableDefaults.flingBehavior() + val stackNestedScrollConnection = + remember(flingBehavior) { + NotificationStackNestedScrollConnection( + stackOffset = { overscrollOffset.value }, + canScrollForward = canScrollForward, + onScroll = { offsetAvailable -> + coroutineScope.launch { + val maxProgress = screenHeight * 0.2f + val tilt = 3f + var offset = + overscrollOffset.value + + maxProgress * tanh(x = offsetAvailable / (maxProgress * tilt)) + offset = max(offset, -1f * maxProgress) + overscrollOffset.snapTo(offset) + } + }, + onStop = { velocityAvailable -> + coroutineScope.launch { + overscrollOffset.animateTo( + targetValue = 0f, + initialVelocity = velocityAvailable, + animationSpec = tween(), + ) + } + }, + flingBehavior = flingBehavior, + ) + } return this.then( Modifier.nestedScroll(stackNestedScrollConnection).offset { @@ -86,6 +92,7 @@ fun NotificationStackNestedScrollConnection( onStart: (Float) -> Unit = {}, onScroll: (Float) -> Unit, onStop: (Float) -> Unit = {}, + flingBehavior: FlingBehavior, ): PriorityNestedScrollConnection { return PriorityNestedScrollConnection( orientation = Orientation.Vertical, @@ -106,8 +113,9 @@ fun NotificationStackNestedScrollConnection( return consumed } - override suspend fun onStop(initialVelocity: Float): Float { - onStop(initialVelocity) + override suspend fun OnStopScope.onStop(initialVelocity: Float): Float { + val consumedByScroll = flingToScroll(initialVelocity, flingBehavior) + onStop(initialVelocity - consumedByScroll) return initialVelocity } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt index 4fa1984661da..ae273d8e2ad9 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt @@ -25,6 +25,7 @@ import androidx.compose.foundation.ScrollState import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.ScrollableDefaults import androidx.compose.foundation.gestures.animateScrollBy import androidx.compose.foundation.gestures.rememberScrollableState import androidx.compose.foundation.gestures.scrollBy @@ -451,12 +452,14 @@ fun SceneScope.NotificationScrollingStack( } } + val flingBehavior = ScrollableDefaults.flingBehavior() val scrimNestedScrollConnection = shadeSession.rememberSession( scrimOffset, maxScrimTop, minScrimTop, isCurrentGestureOverscroll, + flingBehavior, ) { NotificationScrimNestedScrollConnection( scrimOffset = { scrimOffset.value }, @@ -469,6 +472,7 @@ fun SceneScope.NotificationScrollingStack( contentHeight = { stackHeight.intValue.toFloat() }, minVisibleScrimHeight = minVisibleScrimHeight, isCurrentGestureOverscroll = { isCurrentGestureOverscroll.value }, + flingBehavior = flingBehavior, ) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt index 6f1349f20e4d..46f5ecd99301 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt @@ -45,11 +45,13 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.unit.dp import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.LowestZIndexContentPicker import com.android.compose.animation.scene.SceneScope import com.android.compose.windowsizeclass.LocalWindowSizeClass +import com.android.systemui.res.R /** Renders a lightweight shade UI container, as an overlay. */ @Composable @@ -104,13 +106,11 @@ private fun SceneScope.Panel(modifier: Modifier = Modifier, content: @Composable @Composable private fun Modifier.panelSize(): Modifier { val widthSizeClass = LocalWindowSizeClass.current.widthSizeClass - return this.then( - when (widthSizeClass) { - WindowWidthSizeClass.Compact -> Modifier.fillMaxWidth() - WindowWidthSizeClass.Medium -> Modifier.width(OverlayShade.Dimensions.PanelWidthMedium) - WindowWidthSizeClass.Expanded -> Modifier.width(OverlayShade.Dimensions.PanelWidthLarge) - else -> error("Unsupported WindowWidthSizeClass \"$widthSizeClass\"") + if (widthSizeClass == WindowWidthSizeClass.Compact) { + Modifier.fillMaxWidth() + } else { + Modifier.width(dimensionResource(id = R.dimen.shade_panel_width)) } ) } @@ -121,14 +121,15 @@ private fun Modifier.panelPadding(): Modifier { val systemBars = WindowInsets.systemBarsIgnoringVisibility val displayCutout = WindowInsets.displayCutout val waterfall = WindowInsets.waterfall - val contentPadding = PaddingValues(all = OverlayShade.Dimensions.ScrimContentPadding) + val horizontalPadding = + PaddingValues(horizontal = dimensionResource(id = R.dimen.shade_panel_margin_horizontal)) val combinedPadding = combinePaddings( systemBars.asPaddingValues(), displayCutout.asPaddingValues(), waterfall.asPaddingValues(), - contentPadding, + horizontalPadding, ) return if (widthSizeClass == WindowWidthSizeClass.Compact) { @@ -174,10 +175,7 @@ object OverlayShade { } object Dimensions { - val ScrimContentPadding = 16.dp val PanelCornerRadius = 46.dp - val PanelWidthMedium = 390.dp - val PanelWidthLarge = 474.dp val OverscrollLimit = 32.dp } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/ColumnVolumeSliders.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/ColumnVolumeSliders.kt index 28a12f813cf5..581fb9d77c59 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/ColumnVolumeSliders.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/ColumnVolumeSliders.kt @@ -92,7 +92,7 @@ fun ColumnVolumeSliders( onValueChangeFinished = { sliderViewModel.onValueChangeFinished() }, onIconTapped = { sliderViewModel.toggleMuted(sliderState) }, sliderColors = sliderColors, - hapticsViewModelFactory = sliderViewModel.hapticsViewModelFactory, + hapticsViewModelFactory = sliderViewModel.getSliderHapticsViewModelFactory(), ) ExpandButton( @@ -142,7 +142,8 @@ fun ColumnVolumeSliders( onValueChangeFinished = { sliderViewModel.onValueChangeFinished() }, onIconTapped = { sliderViewModel.toggleMuted(sliderState) }, sliderColors = sliderColors, - hapticsViewModelFactory = sliderViewModel.hapticsViewModelFactory, + hapticsViewModelFactory = + sliderViewModel.getSliderHapticsViewModelFactory(), ) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/GridVolumeSliders.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/GridVolumeSliders.kt index a0e46d51c73a..3718b10cabde 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/GridVolumeSliders.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/GridVolumeSliders.kt @@ -49,7 +49,7 @@ fun GridVolumeSliders( onValueChangeFinished = { sliderViewModel.onValueChangeFinished() }, onIconTapped = { sliderViewModel.toggleMuted(sliderState) }, sliderColors = sliderColors, - hapticsViewModelFactory = sliderViewModel.hapticsViewModelFactory, + hapticsViewModelFactory = sliderViewModel.getSliderHapticsViewModelFactory(), ) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt index eb79b906e5f8..97ce429cf938 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt @@ -48,7 +48,6 @@ import androidx.compose.ui.semantics.stateDescription import androidx.compose.ui.unit.dp import com.android.compose.PlatformSlider import com.android.compose.PlatformSliderColors -import com.android.systemui.Flags import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.ui.compose.Icon import com.android.systemui.compose.modifiers.sysuiResTag @@ -66,15 +65,15 @@ fun VolumeSlider( onIconTapped: () -> Unit, modifier: Modifier = Modifier, sliderColors: PlatformSliderColors, - hapticsViewModelFactory: SliderHapticsViewModel.Factory, + hapticsViewModelFactory: SliderHapticsViewModel.Factory?, ) { val value by valueState(state) val interactionSource = remember { MutableInteractionSource() } val sliderStepSize = 1f / (state.valueRange.endInclusive - state.valueRange.start) val hapticsViewModel: SliderHapticsViewModel? = - if (Flags.hapticsForComposeSliders()) { + hapticsViewModelFactory?.let { rememberViewModel(traceName = "SliderHapticsViewModel") { - hapticsViewModelFactory.create( + it.create( interactionSource, state.valueRange, Orientation.Horizontal, @@ -93,8 +92,6 @@ fun VolumeSlider( ), ) } - } else { - null } // Perform haptics due to UI composition diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt index 7c7202a5c7f2..7872ffad6cec 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt @@ -27,6 +27,7 @@ import androidx.compose.ui.util.fastCoerceIn import com.android.compose.animation.scene.content.Content import com.android.compose.animation.scene.content.state.TransitionState import com.android.compose.animation.scene.content.state.TransitionState.HasOverscrollProperties.Companion.DistanceUnspecified +import com.android.compose.nestedscroll.OnStopScope import com.android.compose.nestedscroll.PriorityNestedScrollConnection import com.android.compose.nestedscroll.ScrollController import kotlin.math.absoluteValue @@ -749,7 +750,7 @@ private fun scrollController( return dragController.onDrag(delta = deltaScroll) } - override suspend fun onStop(initialVelocity: Float): Float { + override suspend fun OnStopScope.onStop(initialVelocity: Float): Float { return dragController .onStop(velocity = initialVelocity, canChangeContent = canChangeScene) .invoke() diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt index f14622fe7b65..63c5d7aed3e1 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt @@ -48,7 +48,6 @@ import androidx.compose.ui.unit.round import androidx.compose.ui.util.fastCoerceIn import androidx.compose.ui.util.fastForEachReversed import androidx.compose.ui.util.lerp -import com.android.compose.animation.scene.Element.State import com.android.compose.animation.scene.content.Content import com.android.compose.animation.scene.content.state.TransitionState import com.android.compose.animation.scene.transformation.PropertyTransformation @@ -163,7 +162,7 @@ private fun Modifier.maybeElevateInContent( transitionStates: List<TransitionState>, ): Modifier { fun isSharedElement( - stateByContent: Map<ContentKey, State>, + stateByContent: Map<ContentKey, Element.State>, transition: TransitionState.Transition, ): Boolean { fun inFromContent() = transition.fromContent in stateByContent @@ -1281,14 +1280,14 @@ private inline fun <T> computeValue( checkNotNull(if (currentContent == toContent) toState else fromState) val idleValue = contentValue(overscrollState) val targetValue = - propertySpec.transform( - layoutImpl, - currentContent, - element, - overscrollState, - transition, - idleValue, - ) + with(propertySpec) { + layoutImpl.propertyTransformationScope.transform( + currentContent, + element.key, + transition, + idleValue, + ) + } // Make sure we don't read progress if values are the same and we don't need to // interpolate, so we don't invalidate the phase where this is read. @@ -1376,24 +1375,26 @@ private inline fun <T> computeValue( val idleValue = contentValue(contentState) val isEntering = content == toContent val previewTargetValue = - previewTransformation.transform( - layoutImpl, - content, - element, - contentState, - transition, - idleValue, - ) + with(previewTransformation) { + layoutImpl.propertyTransformationScope.transform( + content, + element.key, + transition, + idleValue, + ) + } val targetValueOrNull = - transformation?.transform( - layoutImpl, - content, - element, - contentState, - transition, - idleValue, - ) + transformation?.let { transformation -> + with(transformation) { + layoutImpl.propertyTransformationScope.transform( + content, + element.key, + transition, + idleValue, + ) + } + } // Make sure we don't read progress if values are the same and we don't need to interpolate, // so we don't invalidate the phase where this is read. @@ -1460,7 +1461,14 @@ private inline fun <T> computeValue( val idleValue = contentValue(contentState) val targetValue = - transformation.transform(layoutImpl, content, element, contentState, transition, idleValue) + with(transformation) { + layoutImpl.propertyTransformationScope.transform( + content, + element.key, + transition, + idleValue, + ) + } // Make sure we don't read progress if values are the same and we don't need to interpolate, so // we don't invalidate the phase where this is read. diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt index b00c8ade07eb..8a6a0d6dbb99 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt @@ -70,6 +70,7 @@ internal fun PredictiveBackHandler( // The predictive back APIs will automatically animate the progress for us in this case // so there is no need to animate it. cancelSpec = snap(), + animationScope = layoutImpl.animationScope, ) } } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt index d58d3f24862f..e93cf8f714cd 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt @@ -129,6 +129,7 @@ internal class SceneTransitionLayoutImpl( private val verticalDraggableHandler: DraggableHandlerImpl internal val elementStateScope = ElementStateScopeImpl(this) + internal val propertyTransformationScope = PropertyTransformationScopeImpl(this) private var _userActionDistanceScope: UserActionDistanceScope? = null internal val userActionDistanceScope: UserActionDistanceScope get() = diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt index 690c809aa56d..8457481b3e14 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt @@ -18,6 +18,8 @@ package com.android.compose.animation.scene import androidx.compose.ui.geometry.Offset import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.LayoutDirection +import com.android.compose.animation.scene.transformation.PropertyTransformationScope internal class ElementStateScopeImpl(private val layoutImpl: SceneTransitionLayoutImpl) : ElementStateScope { @@ -46,3 +48,15 @@ internal class UserActionDistanceScopeImpl(private val layoutImpl: SceneTransiti override val fontScale: Float get() = layoutImpl.density.fontScale } + +internal class PropertyTransformationScopeImpl(private val layoutImpl: SceneTransitionLayoutImpl) : + PropertyTransformationScope, ElementStateScope by layoutImpl.elementStateScope { + override val density: Float + get() = layoutImpl.density.density + + override val fontScale: Float + get() = layoutImpl.density.fontScale + + override val layoutDirection: LayoutDirection + get() = layoutImpl.layoutDirection +} diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt index c5a3067cc4d9..5936d2595465 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredSize.kt @@ -18,10 +18,8 @@ package com.android.compose.animation.scene.transformation import androidx.compose.ui.unit.IntSize import com.android.compose.animation.scene.ContentKey -import com.android.compose.animation.scene.Element import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.ElementMatcher -import com.android.compose.animation.scene.SceneTransitionLayoutImpl import com.android.compose.animation.scene.content.state.TransitionState /** Anchor the size of an element to the size of another element. */ @@ -31,19 +29,15 @@ internal class AnchoredSize( private val anchorWidth: Boolean, private val anchorHeight: Boolean, ) : PropertyTransformation<IntSize> { - override fun transform( - layoutImpl: SceneTransitionLayoutImpl, + override fun PropertyTransformationScope.transform( content: ContentKey, - element: Element, - stateInContent: Element.State, + element: ElementKey, transition: TransitionState.Transition, value: IntSize, ): IntSize { fun anchorSizeIn(content: ContentKey): IntSize { val size = - layoutImpl.elements[anchor]?.stateByContent?.get(content)?.targetSize?.takeIf { - it != Element.SizeUnspecified - } + anchor.targetSize(content) ?: throwMissingAnchorException( transformation = "AnchoredSize", anchor = anchor, diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt index 86e06ab1f243..0a59dfe515fc 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/AnchoredTranslate.kt @@ -17,12 +17,9 @@ package com.android.compose.animation.scene.transformation import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.isSpecified import com.android.compose.animation.scene.ContentKey -import com.android.compose.animation.scene.Element import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.ElementMatcher -import com.android.compose.animation.scene.SceneTransitionLayoutImpl import com.android.compose.animation.scene.content.state.TransitionState /** Anchor the translation of an element to another element. */ @@ -30,11 +27,9 @@ internal class AnchoredTranslate( override val matcher: ElementMatcher, private val anchor: ElementKey, ) : PropertyTransformation<Offset> { - override fun transform( - layoutImpl: SceneTransitionLayoutImpl, + override fun PropertyTransformationScope.transform( content: ContentKey, - element: Element, - stateInContent: Element.State, + element: ElementKey, transition: TransitionState.Transition, value: Offset, ): Offset { @@ -46,18 +41,13 @@ internal class AnchoredTranslate( ) } - val anchor = layoutImpl.elements[anchor] ?: throwException(content = null) - fun anchorOffsetIn(content: ContentKey): Offset? { - return anchor.stateByContent[content]?.targetOffset?.takeIf { it.isSpecified } - } - // [element] will move the same amount as [anchor] does. // TODO(b/290184746): Also support anchors that are not shared but translated because of // other transformations, like an edge translation. val anchorFromOffset = - anchorOffsetIn(transition.fromContent) ?: throwException(transition.fromContent) + anchor.targetOffset(transition.fromContent) ?: throwException(transition.fromContent) val anchorToOffset = - anchorOffsetIn(transition.toContent) ?: throwException(transition.toContent) + anchor.targetOffset(transition.toContent) ?: throwException(transition.toContent) val offset = anchorToOffset - anchorFromOffset return if (content == transition.toContent) { diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt index 7f8647976873..7223dad43a2e 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/DrawScale.kt @@ -18,10 +18,9 @@ package com.android.compose.animation.scene.transformation import androidx.compose.ui.geometry.Offset import com.android.compose.animation.scene.ContentKey -import com.android.compose.animation.scene.Element +import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.ElementMatcher import com.android.compose.animation.scene.Scale -import com.android.compose.animation.scene.SceneTransitionLayoutImpl import com.android.compose.animation.scene.content.state.TransitionState /** @@ -35,11 +34,9 @@ internal class DrawScale( private val pivot: Offset = Offset.Unspecified, ) : PropertyTransformation<Scale> { - override fun transform( - layoutImpl: SceneTransitionLayoutImpl, + override fun PropertyTransformationScope.transform( content: ContentKey, - element: Element, - stateInContent: Element.State, + element: ElementKey, transition: TransitionState.Transition, value: Scale, ): Scale { diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt index 031f50e17225..4ae07c541011 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/EdgeTranslate.kt @@ -19,9 +19,8 @@ package com.android.compose.animation.scene.transformation import androidx.compose.ui.geometry.Offset import com.android.compose.animation.scene.ContentKey import com.android.compose.animation.scene.Edge -import com.android.compose.animation.scene.Element +import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.ElementMatcher -import com.android.compose.animation.scene.SceneTransitionLayoutImpl import com.android.compose.animation.scene.content.state.TransitionState /** Translate an element from an edge of the layout. */ @@ -30,21 +29,18 @@ internal class EdgeTranslate( private val edge: Edge, private val startsOutsideLayoutBounds: Boolean = true, ) : PropertyTransformation<Offset> { - override fun transform( - layoutImpl: SceneTransitionLayoutImpl, + override fun PropertyTransformationScope.transform( content: ContentKey, - element: Element, - stateInContent: Element.State, + element: ElementKey, transition: TransitionState.Transition, value: Offset, ): Offset { - val sceneSize = layoutImpl.content(content).targetSize - val elementSize = stateInContent.targetSize - if (elementSize == Element.SizeUnspecified) { - return value - } + val sceneSize = + content.targetSize() + ?: error("Content ${content.debugName} does not have a target size") + val elementSize = element.targetSize(content) ?: return value - return when (edge.resolve(layoutImpl.layoutDirection)) { + return when (edge.resolve(layoutDirection)) { Edge.Resolved.Top -> if (startsOutsideLayoutBounds) { Offset(value.x, -elementSize.height.toFloat()) diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt index 078aa0f7efe9..c11ec977fe2b 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Fade.kt @@ -17,18 +17,15 @@ package com.android.compose.animation.scene.transformation import com.android.compose.animation.scene.ContentKey -import com.android.compose.animation.scene.Element +import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.ElementMatcher -import com.android.compose.animation.scene.SceneTransitionLayoutImpl import com.android.compose.animation.scene.content.state.TransitionState /** Fade an element in or out. */ internal class Fade(override val matcher: ElementMatcher) : PropertyTransformation<Float> { - override fun transform( - layoutImpl: SceneTransitionLayoutImpl, + override fun PropertyTransformationScope.transform( content: ContentKey, - element: Element, - stateInContent: Element.State, + element: ElementKey, transition: TransitionState.Transition, value: Float, ): Float { diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt index 5f3fdaf67e5f..a159a5b5b2bd 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/ScaleSize.kt @@ -18,9 +18,8 @@ package com.android.compose.animation.scene.transformation import androidx.compose.ui.unit.IntSize import com.android.compose.animation.scene.ContentKey -import com.android.compose.animation.scene.Element +import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.ElementMatcher -import com.android.compose.animation.scene.SceneTransitionLayoutImpl import com.android.compose.animation.scene.content.state.TransitionState import kotlin.math.roundToInt @@ -33,11 +32,9 @@ internal class ScaleSize( private val width: Float = 1f, private val height: Float = 1f, ) : PropertyTransformation<IntSize> { - override fun transform( - layoutImpl: SceneTransitionLayoutImpl, + override fun PropertyTransformationScope.transform( content: ContentKey, - element: Element, - stateInContent: Element.State, + element: ElementKey, transition: TransitionState.Transition, value: IntSize, ): IntSize { diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt index de7f418f219a..d38067d9af38 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Transformation.kt @@ -18,13 +18,15 @@ package com.android.compose.animation.scene.transformation import androidx.compose.animation.core.Easing import androidx.compose.animation.core.LinearEasing +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.util.fastCoerceAtLeast import androidx.compose.ui.util.fastCoerceAtMost import androidx.compose.ui.util.fastCoerceIn import com.android.compose.animation.scene.ContentKey -import com.android.compose.animation.scene.Element +import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.ElementMatcher -import com.android.compose.animation.scene.SceneTransitionLayoutImpl +import com.android.compose.animation.scene.ElementStateScope import com.android.compose.animation.scene.content.state.TransitionState /** A transformation applied to one or more elements during a transition. */ @@ -56,22 +58,30 @@ internal class SharedElementTransformation( ) : Transformation /** A transformation that changes the value of an element property, like its size or offset. */ -internal sealed interface PropertyTransformation<T> : Transformation { +interface PropertyTransformation<T> : Transformation { /** - * Transform [value], i.e. the value of the transformed property without this transformation. + * Return the transformed value for the given property, i.e.: + * - the value at progress = 0% for elements that are entering the layout (i.e. elements in the + * content we are transitioning to). + * - the value at progress = 100% for elements that are leaving the layout (i.e. elements in the + * content we are transitioning from). + * + * The returned value will be interpolated using the [transition] progress and [value], the + * value of the property when we are idle. */ - // TODO(b/290184746): Figure out a public API for custom transformations that don't have access - // to these internal classes. - fun transform( - layoutImpl: SceneTransitionLayoutImpl, + fun PropertyTransformationScope.transform( content: ContentKey, - element: Element, - stateInContent: Element.State, + element: ElementKey, transition: TransitionState.Transition, value: T, ): T } +interface PropertyTransformationScope : Density, ElementStateScope { + /** The current [direction][LayoutDirection] of the layout. */ + val layoutDirection: LayoutDirection +} + /** * A [PropertyTransformation] associated to a range. This is a helper class so that normal * implementations of [PropertyTransformation] don't have to take care of reversing their range when diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt index 70142717fffe..af0a6edfa2fb 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transformation/Translate.kt @@ -21,10 +21,9 @@ import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.android.compose.animation.scene.ContentKey -import com.android.compose.animation.scene.Element +import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.ElementMatcher import com.android.compose.animation.scene.OverscrollScope -import com.android.compose.animation.scene.SceneTransitionLayoutImpl import com.android.compose.animation.scene.content.state.TransitionState internal class Translate( @@ -32,15 +31,13 @@ internal class Translate( private val x: Dp = 0.dp, private val y: Dp = 0.dp, ) : PropertyTransformation<Offset> { - override fun transform( - layoutImpl: SceneTransitionLayoutImpl, + override fun PropertyTransformationScope.transform( content: ContentKey, - element: Element, - stateInContent: Element.State, + element: ElementKey, transition: TransitionState.Transition, value: Offset, ): Offset { - return with(layoutImpl.density) { Offset(value.x + x.toPx(), value.y + y.toPx()) } + return Offset(value.x + x.toPx(), value.y + y.toPx()) } } @@ -51,11 +48,9 @@ internal class OverscrollTranslate( ) : PropertyTransformation<Offset> { private val cachedOverscrollScope = CachedOverscrollScope() - override fun transform( - layoutImpl: SceneTransitionLayoutImpl, + override fun PropertyTransformationScope.transform( content: ContentKey, - element: Element, - stateInContent: Element.State, + element: ElementKey, transition: TransitionState.Transition, value: Offset, ): Offset { @@ -64,7 +59,7 @@ internal class OverscrollTranslate( // that this method was invoked after performing this check. val overscrollProperties = transition as TransitionState.HasOverscrollProperties val overscrollScope = - cachedOverscrollScope.getFromCacheOrCompute(layoutImpl.density, overscrollProperties) + cachedOverscrollScope.getFromCacheOrCompute(density = this, overscrollProperties) return Offset(x = value.x + overscrollScope.x(), y = value.y + overscrollScope.y()) } @@ -75,7 +70,7 @@ internal class OverscrollTranslate( * [TransitionState.HasOverscrollProperties]. This helps avoid recreating a scope every frame * whenever an overscroll transition is computed. */ -private class CachedOverscrollScope() { +private class CachedOverscrollScope { private var previousScope: OverscrollScope? = null private var previousDensity: Density? = null private var previousOverscrollProperties: TransitionState.HasOverscrollProperties? = null diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/Seek.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/Seek.kt index 715d979e116e..2b33224022fc 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/Seek.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/Seek.kt @@ -30,6 +30,8 @@ import com.android.compose.animation.scene.TransitionKey import com.android.compose.animation.scene.UserActionResult import com.android.compose.animation.scene.createSwipeAnimation import kotlin.coroutines.cancellation.CancellationException +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.collectLatest @@ -141,6 +143,7 @@ internal suspend fun <T : ContentKey> animateProgress( progress: Flow<Float>, commitSpec: AnimationSpec<Float>?, cancelSpec: AnimationSpec<Float>?, + animationScope: CoroutineScope? = null, ) { fun animateOffset(targetContent: T, spec: AnimationSpec<Float>?) { if (state.transitionState != animation.contentTransition || animation.isAnimatingOffset()) { @@ -176,12 +179,20 @@ internal suspend fun <T : ContentKey> animateProgress( } // Start the transition. - state.startTransition(animation.contentTransition) + animationScope?.launch { startTransition(state, animation, collectionJob) } + ?: startTransition(state, animation, collectionJob) + } +} - // The transition is done. Cancel the collection in case the transition was finished because - // it was interrupted by another transition. - if (collectionJob.isActive) { - collectionJob.cancel() - } +private suspend fun <T : ContentKey> startTransition( + state: MutableSceneTransitionLayoutStateImpl, + animation: SwipeAnimation<T>, + progressCollectionJob: Job, +) { + state.startTransition(animation.contentTransition) + // The transition is done. Cancel the collection in case the transition was finished + // because it was interrupted by another transition. + if (progressCollectionJob.isActive) { + progressCollectionJob.cancel() } } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/LargeTopAppBarNestedScrollConnection.kt b/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/LargeTopAppBarNestedScrollConnection.kt index 255da31719f3..a5be4dc195bc 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/LargeTopAppBarNestedScrollConnection.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/LargeTopAppBarNestedScrollConnection.kt @@ -16,6 +16,7 @@ package com.android.compose.nestedscroll +import androidx.compose.foundation.gestures.FlingBehavior import androidx.compose.foundation.gestures.Orientation import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollSource @@ -41,6 +42,7 @@ fun LargeTopAppBarNestedScrollConnection( onHeightChanged: (Float) -> Unit, minHeight: () -> Float, maxHeight: () -> Float, + flingBehavior: FlingBehavior, ): PriorityNestedScrollConnection { return PriorityNestedScrollConnection( orientation = Orientation.Vertical, @@ -55,7 +57,15 @@ fun LargeTopAppBarNestedScrollConnection( offsetAvailable > 0 && height() < maxHeight() }, canStartPostFling = { false }, - onStart = { LargeTopAppBarScrollController(height, maxHeight, minHeight, onHeightChanged) }, + onStart = { + LargeTopAppBarScrollController( + height = height, + maxHeight = maxHeight, + minHeight = minHeight, + onHeightChanged = onHeightChanged, + flingBehavior = flingBehavior, + ) + }, ) } @@ -64,6 +74,7 @@ private class LargeTopAppBarScrollController( val maxHeight: () -> Float, val minHeight: () -> Float, val onHeightChanged: (Float) -> Unit, + val flingBehavior: FlingBehavior, ) : ScrollController { override fun onScroll(deltaScroll: Float, source: NestedScrollSource): Float { val currentHeight = height() @@ -79,9 +90,8 @@ private class LargeTopAppBarScrollController( return amountConsumed } - override suspend fun onStop(initialVelocity: Float): Float { - // Don't consume the velocity on pre/post fling - return 0f + override suspend fun OnStopScope.onStop(initialVelocity: Float): Float { + return flingToScroll(initialVelocity, flingBehavior) } override fun onCancel() { diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt b/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt index ca44a5c21cab..e924ebfd2a8d 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/nestedscroll/PriorityNestedScrollConnection.kt @@ -16,7 +16,9 @@ package com.android.compose.nestedscroll +import androidx.compose.foundation.gestures.FlingBehavior import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.ScrollScope import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollSource @@ -83,7 +85,16 @@ interface ScrollController { * @param initialVelocity The initial velocity of the scroll when stopping. * @return The consumed [initialVelocity] when the animation completes. */ - suspend fun onStop(initialVelocity: Float): Float + suspend fun OnStopScope.onStop(initialVelocity: Float): Float +} + +interface OnStopScope { + /** + * Emits scroll events by using the [initialVelocity] and the [FlingBehavior]. + * + * @return consumed velocity + */ + suspend fun flingToScroll(initialVelocity: Float, flingBehavior: FlingBehavior): Float } /** @@ -307,7 +318,11 @@ class PriorityNestedScrollConnection( val controller = requireController(isStopping = false) return coroutineScope { try { - async { controller.onStop(velocity) } + async { + with(controller) { + OnStopScopeImpl(controller = controller).onStop(velocity) + } + } // Allows others to interrupt the job. .also { stoppingJob = it } // Note: this can be cancelled by [interruptStopping] @@ -336,3 +351,19 @@ class PriorityNestedScrollConnection( offsetScrolledBeforePriorityMode = 0f } } + +private class OnStopScopeImpl(private val controller: ScrollController) : OnStopScope { + override suspend fun flingToScroll( + initialVelocity: Float, + flingBehavior: FlingBehavior, + ): Float { + return with(flingBehavior) { + object : ScrollScope { + override fun scrollBy(pixels: Float): Float { + return controller.onScroll(pixels, NestedScrollSource.SideEffect) + } + } + .performFling(initialVelocity) + } + } +} diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/nestedscroll/LargeTopAppBarNestedScrollConnectionTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/nestedscroll/LargeTopAppBarNestedScrollConnectionTest.kt index a406e13904f5..e27f9b52153d 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/nestedscroll/LargeTopAppBarNestedScrollConnectionTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/nestedscroll/LargeTopAppBarNestedScrollConnectionTest.kt @@ -16,6 +16,8 @@ package com.android.compose.nestedscroll +import androidx.compose.foundation.gestures.FlingBehavior +import androidx.compose.foundation.gestures.ScrollScope import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollSource @@ -29,6 +31,13 @@ class LargeTopAppBarNestedScrollConnectionTest(testCase: TestCase) { val scrollSource = testCase.scrollSource private var height = 0f + private val customFlingBehavior = + object : FlingBehavior { + override suspend fun ScrollScope.performFling(initialVelocity: Float): Float { + scrollBy(initialVelocity) + return initialVelocity / 2f + } + } private fun buildScrollConnection(heightRange: ClosedFloatingPointRange<Float>) = LargeTopAppBarNestedScrollConnection( @@ -36,6 +45,7 @@ class LargeTopAppBarNestedScrollConnectionTest(testCase: TestCase) { onHeightChanged = { height = it }, minHeight = { heightRange.start }, maxHeight = { heightRange.endInclusive }, + flingBehavior = customFlingBehavior, ) private fun NestedScrollConnection.scroll( diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/nestedscroll/PriorityNestedScrollConnectionTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/nestedscroll/PriorityNestedScrollConnectionTest.kt index 0364cdc4166e..54428404bd0c 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/nestedscroll/PriorityNestedScrollConnectionTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/nestedscroll/PriorityNestedScrollConnectionTest.kt @@ -18,12 +18,15 @@ package com.android.compose.nestedscroll +import androidx.compose.foundation.gestures.FlingBehavior import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.ScrollScope import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.NestedScrollSource.Companion.UserInput import androidx.compose.ui.unit.Velocity import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.compose.test.runMonotonicClockTest import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest @@ -41,7 +44,16 @@ class PriorityNestedScrollConnectionTest { private var consumeScroll = true private var lastStop: Float? = null private var isCancelled: Boolean = false - private var consumeStop = true + private var onStopConsumeFlingToScroll = false + private var onStopConsumeAll = true + + private val customFlingBehavior = + object : FlingBehavior { + override suspend fun ScrollScope.performFling(initialVelocity: Float): Float { + scrollBy(initialVelocity) + return initialVelocity / 2f + } + } private val scrollConnection = PriorityNestedScrollConnection( @@ -57,9 +69,16 @@ class PriorityNestedScrollConnectionTest { return if (consumeScroll) deltaScroll else 0f } - override suspend fun onStop(initialVelocity: Float): Float { + override suspend fun OnStopScope.onStop(initialVelocity: Float): Float { lastStop = initialVelocity - return if (consumeStop) initialVelocity else 0f + var velocityConsumed = 0f + if (onStopConsumeFlingToScroll) { + velocityConsumed = flingToScroll(initialVelocity, customFlingBehavior) + } + if (onStopConsumeAll) { + velocityConsumed = initialVelocity + } + return velocityConsumed } override fun onCancel() { @@ -178,6 +197,22 @@ class PriorityNestedScrollConnectionTest { } @Test + fun onStopScrollUsingFlingToScroll() = runMonotonicClockTest { + startPriorityModePostScroll() + onStopConsumeFlingToScroll = true + onStopConsumeAll = false + lastScroll = Float.NaN + + val consumed = scrollConnection.onPreFling(available = Velocity(2f, 2f)) + + assertThat(lastStop).isEqualTo(2f) + // flingToScroll should try to scroll the content, customFlingBehavior uses the velocity. + assertThat(lastScroll).isEqualTo(2f) + // customFlingBehavior returns half of the vertical velocity. + assertThat(consumed).isEqualTo(Velocity(0f, 1f)) + } + + @Test fun ifCannotStopOnPreFling_shouldStopOnPostFling() = runTest { startPriorityModePostScroll() canStopOnPreFling = false diff --git a/packages/SystemUI/customization/res/drawable/clock_default_thumbnail.xml b/packages/SystemUI/customization/res/drawable/clock_default_thumbnail.xml index be72d0b82dbb..3951e4cb9476 100644 --- a/packages/SystemUI/customization/res/drawable/clock_default_thumbnail.xml +++ b/packages/SystemUI/customization/res/drawable/clock_default_thumbnail.xml @@ -14,7 +14,22 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License --> -<shape xmlns:android="http://schemas.android.com/apk/res/android" - android:shape="rectangle"> - <solid android:color="#FFFF00FF" /> -</shape> + <vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="36dp" + android:height="50dp" + android:viewportWidth="36" + android:viewportHeight="50"> + + <path + android:pathData="M8.592,22.712C11.052,22.712 13.063,21.635 14.628,19.48C16.193,17.326 16.976,14.623 16.976,11.372C16.976,8.059 16.198,5.346 14.644,3.232C13.099,1.109 11.082,0.047 8.592,0.047C6.113,0.047 4.096,1.109 2.541,3.232C0.997,5.346 0.225,8.059 0.225,11.372C0.225,14.664 0.997,17.377 2.541,19.511C4.096,21.645 6.113,22.712 8.592,22.712ZM8.592,18.901C7.322,18.901 6.301,18.225 5.529,16.874C4.757,15.523 4.37,13.699 4.37,11.402C4.37,9.045 4.757,7.206 5.529,5.885C6.301,4.553 7.322,3.888 8.592,3.888C9.873,3.888 10.899,4.543 11.671,5.854C12.444,7.155 12.83,9.004 12.83,11.402C12.83,13.78 12.449,15.624 11.686,16.935C10.924,18.246 9.893,18.901 8.592,18.901Z" + android:fillColor="#FFFFFF"/> + <path + android:pathData="M8.141,39.462C9.289,39.462 10.168,39.742 10.778,40.301C11.387,40.849 11.692,41.627 11.692,42.633C11.692,43.7 11.377,44.538 10.747,45.148C10.117,45.747 9.254,46.047 8.156,46.047C7.14,46.047 6.353,45.778 5.794,45.239C5.245,44.701 4.859,44.132 4.635,43.532C4.483,43.115 4.249,42.831 3.934,42.679C3.629,42.516 3.182,42.521 2.593,42.694C2.034,42.856 1.638,43.136 1.404,43.532C1.18,43.918 1.155,44.375 1.328,44.904C1.694,46.113 2.486,47.2 3.706,48.166C4.925,49.121 6.49,49.598 8.4,49.598C10.585,49.598 12.343,48.948 13.674,47.647C15.005,46.336 15.67,44.695 15.67,42.724C15.67,41.383 15.279,40.209 14.497,39.203C13.714,38.197 12.703,37.583 11.464,37.359V37.298C12.51,36.973 13.323,36.373 13.902,35.5C14.482,34.616 14.771,33.574 14.771,32.375C14.771,30.597 14.192,29.225 13.034,28.26C11.885,27.284 10.336,26.796 8.385,26.796C6.789,26.796 5.443,27.172 4.346,27.924C3.258,28.666 2.496,29.57 2.059,30.637C1.826,31.135 1.795,31.587 1.968,31.994C2.151,32.4 2.501,32.715 3.02,32.939C3.579,33.183 4.015,33.244 4.33,33.122C4.656,32.99 4.92,32.741 5.123,32.375C5.469,31.704 5.87,31.196 6.327,30.851C6.784,30.495 7.409,30.317 8.202,30.317H8.217C9.152,30.317 9.879,30.581 10.397,31.11C10.925,31.638 11.189,32.38 11.189,33.335C11.189,34.209 10.89,34.915 10.29,35.454C9.691,35.992 8.923,36.262 7.989,36.262H6.906C6.378,36.262 5.971,36.389 5.687,36.643C5.402,36.887 5.26,37.278 5.26,37.816C5.26,38.334 5.402,38.741 5.687,39.036C5.971,39.32 6.378,39.462 6.906,39.462H8.141Z" + android:fillColor="#FFFFFF"/> + <path + android:pathData="M23.263,7.531C23.263,6.372 23.644,5.432 24.406,4.711C25.178,3.979 26.153,3.613 27.332,3.613C28.531,3.613 29.507,3.984 30.259,4.726C31.021,5.458 31.402,6.393 31.402,7.531C31.402,8.679 30.99,9.639 30.167,10.411C29.354,11.184 28.404,11.57 27.317,11.57C26.169,11.57 25.203,11.194 24.421,10.442C23.649,9.68 23.263,8.709 23.263,7.531ZM26.936,22.026C27.271,21.558 27.759,20.867 28.399,19.953C29.049,19.038 30.04,17.595 31.371,15.624C32.144,14.476 32.956,13.236 33.81,11.905C34.664,10.574 35.09,9.116 35.09,7.531C35.09,5.468 34.333,3.705 32.819,2.242C31.315,0.778 29.466,0.047 27.271,0.047C25.198,0.047 23.4,0.778 21.875,2.242C20.361,3.705 19.604,5.498 19.604,7.622C19.604,9.756 20.331,11.524 21.784,12.926C23.237,14.318 24.959,15.014 26.951,15.014C27.754,15.014 28.303,14.959 28.597,14.847C28.892,14.725 29.151,14.603 29.375,14.481L28.536,13.292L28.277,13.643C27.932,14.12 27.545,14.659 27.119,15.258C26.702,15.858 26.285,16.468 25.869,17.087C25.351,17.829 24.929,18.429 24.604,18.886C24.289,19.343 24.06,19.678 23.918,19.892C23.573,20.4 23.456,20.872 23.567,21.309C23.689,21.736 24.004,22.107 24.512,22.422C25.01,22.767 25.457,22.905 25.854,22.834C26.26,22.773 26.621,22.503 26.936,22.026Z" + android:fillColor="#FFFFFF"/> + <path + android:pathData="M27.406,49.537C29.865,49.537 31.877,48.46 33.442,46.306C35.007,44.152 35.789,41.449 35.789,38.197C35.789,34.885 35.012,32.172 33.457,30.058C31.912,27.934 29.895,26.873 27.406,26.873C24.927,26.873 22.91,27.934 21.355,30.058C19.81,32.172 19.038,34.885 19.038,38.197C19.038,41.49 19.81,44.203 21.355,46.336C22.91,48.47 24.927,49.537 27.406,49.537ZM27.406,45.727C26.136,45.727 25.115,45.051 24.342,43.7C23.57,42.348 23.184,40.524 23.184,38.228C23.184,35.87 23.57,34.031 24.342,32.71C25.115,31.379 26.136,30.713 27.406,30.713C28.686,30.713 29.712,31.369 30.485,32.68C31.257,33.98 31.643,35.83 31.643,38.228C31.643,40.605 31.262,42.45 30.5,43.761C29.738,45.071 28.707,45.727 27.406,45.727Z" + android:fillColor="#FFFFFF"/> +</vector> diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockDesign.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockDesign.kt index f5e843234095..bcf055bd2c40 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockDesign.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockDesign.kt @@ -166,12 +166,6 @@ data class FontTextStyle( // A color literal like `#FF00FF` or a color resource like `@android:color/system_accent1_100` val fillColorDark: String? = null, override val fontSizeScale: Float? = null, - /** - * use `wdth` for width, `wght` for weight, 'opsz' for optical size single quote for tag name, - * and no quote for value separate different axis with `,` e.g. "'wght' 1000, 'wdth' 108, 'opsz' - * 90" - */ - var fontVariation: String? = null, // used when alternate in one font file is needed var fontFeatureSettings: String? = null, val renderType: RenderType = RenderType.STROKE_TEXT, diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt index 9da3022fc0d8..12b20a53df81 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ClockRegistry.kt @@ -44,6 +44,7 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.json.JSONObject private val KEY_TIMESTAMP = "appliedTimestamp" private val KNOWN_PLUGINS = @@ -299,7 +300,7 @@ open class ClockRegistry( ) } - ClockSettings.deserialize(json) + ClockSettings.fromJson(JSONObject(json)) } catch (ex: Exception) { logger.e("Failed to parse clock settings", ex) null @@ -312,21 +313,24 @@ open class ClockRegistry( assert.isNotMainThread() try { - value?.metadata?.put(KEY_TIMESTAMP, System.currentTimeMillis()) + val json = + value?.let { + it.metadata.put(KEY_TIMESTAMP, System.currentTimeMillis()) + ClockSettings.toJson(it) + } ?: "" - val json = ClockSettings.serialize(value) if (handleAllUsers) { Settings.Secure.putStringForUser( context.contentResolver, Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE, - json, + json.toString(), ActivityManager.getCurrentUser(), ) } else { Settings.Secure.putString( context.contentResolver, Settings.Secure.LOCK_SCREEN_CUSTOM_CLOCK_FACE, - json, + json.toString(), ) } } catch (ex: Exception) { @@ -557,14 +561,15 @@ open class ClockRegistry( } fun getClocks(): List<ClockMetadata> { - if (!isEnabled) { - return listOf(availableClocks[DEFAULT_CLOCK_ID]!!.metadata) - } + if (!isEnabled) return listOf(availableClocks[DEFAULT_CLOCK_ID]!!.metadata) return availableClocks.map { (_, clock) -> clock.metadata } } - fun getClockPickerConfig(clockId: ClockId): ClockPickerConfig? = - availableClocks[clockId]?.provider?.getClockPickerConfig(clockId) + fun getClockPickerConfig(clockId: ClockId): ClockPickerConfig? { + val clockSettings = + settings?.let { if (clockId == it.clockId) it else null } ?: ClockSettings(clockId) + return availableClocks[clockId]?.provider?.getClockPickerConfig(clockSettings) + } fun createExampleClock(clockId: ClockId): ClockController? = createClock(clockId) diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt index a8839779c4e9..4ed8fd8f631d 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt @@ -27,7 +27,7 @@ import com.android.systemui.plugins.clocks.ClockAnimations import com.android.systemui.plugins.clocks.ClockEvents import com.android.systemui.plugins.clocks.ClockFaceConfig import com.android.systemui.plugins.clocks.ClockFaceEvents -import com.android.systemui.plugins.clocks.ClockReactiveSetting +import com.android.systemui.plugins.clocks.ClockFontAxisSetting import com.android.systemui.plugins.clocks.ThemeConfig import com.android.systemui.plugins.clocks.WeatherData import com.android.systemui.plugins.clocks.ZenData @@ -103,7 +103,9 @@ class ComposedDigitalLayerController( view.onZenDataChanged(data) } - override fun onReactiveAxesChanged(axes: List<ClockReactiveSetting>) {} + override fun onFontAxesChanged(axes: List<ClockFontAxisSetting>) { + view.updateAxes(axes) + } override var isReactiveTouchInteractionEnabled get() = view.isReactiveTouchInteractionEnabled diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt index c5b751820ebd..7014826f6262 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockController.kt @@ -33,8 +33,8 @@ import com.android.systemui.plugins.clocks.ClockEvents import com.android.systemui.plugins.clocks.ClockFaceConfig import com.android.systemui.plugins.clocks.ClockFaceController import com.android.systemui.plugins.clocks.ClockFaceEvents +import com.android.systemui.plugins.clocks.ClockFontAxisSetting import com.android.systemui.plugins.clocks.ClockMessageBuffers -import com.android.systemui.plugins.clocks.ClockReactiveSetting import com.android.systemui.plugins.clocks.ClockSettings import com.android.systemui.plugins.clocks.DefaultClockFaceLayout import com.android.systemui.plugins.clocks.ThemeConfig @@ -264,7 +264,7 @@ class DefaultClockController( override fun onZenDataChanged(data: ZenData) {} - override fun onReactiveAxesChanged(axes: List<ClockReactiveSetting>) {} + override fun onFontAxesChanged(axes: List<ClockFontAxisSetting>) {} } open inner class DefaultClockAnimations( diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt index a89e6fb35fa8..e9b58b01291f 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt @@ -19,14 +19,13 @@ import android.view.LayoutInflater import com.android.systemui.customization.R import com.android.systemui.log.core.LogLevel import com.android.systemui.log.core.LogcatOnlyMessageBuffer -import com.android.systemui.plugins.clocks.AxisType import com.android.systemui.plugins.clocks.ClockController -import com.android.systemui.plugins.clocks.ClockId +import com.android.systemui.plugins.clocks.ClockFontAxis +import com.android.systemui.plugins.clocks.ClockFontAxisSetting import com.android.systemui.plugins.clocks.ClockMessageBuffers import com.android.systemui.plugins.clocks.ClockMetadata import com.android.systemui.plugins.clocks.ClockPickerConfig import com.android.systemui.plugins.clocks.ClockProvider -import com.android.systemui.plugins.clocks.ClockReactiveAxis import com.android.systemui.plugins.clocks.ClockSettings import com.android.systemui.shared.clocks.view.HorizontalAlignment import com.android.systemui.shared.clocks.view.VerticalAlignment @@ -61,7 +60,15 @@ class DefaultClockProvider( messageBuffers?.infraMessageBuffer ?: LogcatOnlyMessageBuffer(LogLevel.INFO) val assets = AssetLoader(ctx, ctx, "clocks/", buffer) assets.setSeedColor(settings.seedColor, null) - FlexClockController(ctx, resources, assets, FLEX_DESIGN, messageBuffers) + val fontAxes = ClockFontAxis.merge(FlexClockController.FONT_AXES, settings.axes) + FlexClockController( + ctx, + resources, + settings.copy(axes = fontAxes.map { it.toSetting() }), + assets, + FLEX_DESIGN, + messageBuffers, + ) } else { DefaultClockController( ctx, @@ -75,37 +82,42 @@ class DefaultClockProvider( } } - override fun getClockPickerConfig(id: ClockId): ClockPickerConfig { - if (id != DEFAULT_CLOCK_ID) { - throw IllegalArgumentException("$id is unsupported by $TAG") + override fun getClockPickerConfig(settings: ClockSettings): ClockPickerConfig { + if (settings.clockId != DEFAULT_CLOCK_ID) { + throw IllegalArgumentException("${settings.clockId} is unsupported by $TAG") } + val fontAxes = + if (!isClockReactiveVariantsEnabled) listOf() + else ClockFontAxis.merge(FlexClockController.FONT_AXES, settings.axes) return ClockPickerConfig( DEFAULT_CLOCK_ID, resources.getString(R.string.clock_default_name), resources.getString(R.string.clock_default_description), - // TODO(b/352049256): Update placeholder to actual resource resources.getDrawable(R.drawable.clock_default_thumbnail, null), isReactiveToTone = true, - // TODO(b/364673969): Populate the rest of this - axes = - if (isClockReactiveVariantsEnabled) - listOf( - ClockReactiveAxis( - key = "wdth", - type = AxisType.Slider, - maxValue = 1000f, - minValue = 100f, - currentValue = 400f, - name = "Width", - description = "Glyph Width", - ) - ) - else listOf(), + axes = fontAxes, ) } companion object { + // TODO(b/364681643): Variations for retargetted DIGITAL_CLOCK_FLEX + val LEGACY_FLEX_LS_VARIATION = + listOf( + ClockFontAxisSetting("wght", 600f), + ClockFontAxisSetting("wdth", 100f), + ClockFontAxisSetting("ROND", 100f), + ClockFontAxisSetting("slnt", 0f), + ) + + val LEGACY_FLEX_AOD_VARIATION = + listOf( + ClockFontAxisSetting("wght", 74f), + ClockFontAxisSetting("wdth", 43f), + ClockFontAxisSetting("ROND", 100f), + ClockFontAxisSetting("slnt", 0f), + ) + val FLEX_DESIGN = run { val largeLayer = listOf( @@ -117,16 +129,9 @@ class DefaultClockProvider( DigitalHandLayer( layerBounds = LayerBounds.FIT, timespec = DigitalTimespec.FIRST_DIGIT, - style = - FontTextStyle( - lineHeight = 147.25f, - fontVariation = - "'wght' 603, 'wdth' 100, 'opsz' 144, 'ROND' 100", - ), + style = FontTextStyle(lineHeight = 147.25f), aodStyle = FontTextStyle( - fontVariation = - "'wght' 74, 'wdth' 43, 'opsz' 144, 'ROND' 100", fillColorLight = "#FFFFFFFF", outlineColor = "#00000000", renderType = RenderType.CHANGE_WEIGHT, @@ -143,16 +148,9 @@ class DefaultClockProvider( DigitalHandLayer( layerBounds = LayerBounds.FIT, timespec = DigitalTimespec.SECOND_DIGIT, - style = - FontTextStyle( - lineHeight = 147.25f, - fontVariation = - "'wght' 603, 'wdth' 100, 'opsz' 144, 'ROND' 100", - ), + style = FontTextStyle(lineHeight = 147.25f), aodStyle = FontTextStyle( - fontVariation = - "'wght' 74, 'wdth' 43, 'opsz' 144, 'ROND' 100", fillColorLight = "#FFFFFFFF", outlineColor = "#00000000", renderType = RenderType.CHANGE_WEIGHT, @@ -169,16 +167,9 @@ class DefaultClockProvider( DigitalHandLayer( layerBounds = LayerBounds.FIT, timespec = DigitalTimespec.FIRST_DIGIT, - style = - FontTextStyle( - lineHeight = 147.25f, - fontVariation = - "'wght' 603, 'wdth' 100, 'opsz' 144, 'ROND' 100", - ), + style = FontTextStyle(lineHeight = 147.25f), aodStyle = FontTextStyle( - fontVariation = - "'wght' 74, 'wdth' 43, 'opsz' 144, 'ROND' 100", fillColorLight = "#FFFFFFFF", outlineColor = "#00000000", renderType = RenderType.CHANGE_WEIGHT, @@ -195,16 +186,9 @@ class DefaultClockProvider( DigitalHandLayer( layerBounds = LayerBounds.FIT, timespec = DigitalTimespec.SECOND_DIGIT, - style = - FontTextStyle( - lineHeight = 147.25f, - fontVariation = - "'wght' 603, 'wdth' 100, 'opsz' 144, 'ROND' 100", - ), + style = FontTextStyle(lineHeight = 147.25f), aodStyle = FontTextStyle( - fontVariation = - "'wght' 74, 'wdth' 43, 'opsz' 144, 'ROND' 100", fillColorLight = "#FFFFFFFF", outlineColor = "#00000000", renderType = RenderType.CHANGE_WEIGHT, @@ -227,14 +211,9 @@ class DefaultClockProvider( DigitalHandLayer( layerBounds = LayerBounds.FIT, timespec = DigitalTimespec.TIME_FULL_FORMAT, - style = - FontTextStyle( - fontVariation = "'wght' 600, 'wdth' 100, 'opsz' 144, 'ROND' 100", - fontSizeScale = 0.98f, - ), + style = FontTextStyle(fontSizeScale = 0.98f), aodStyle = FontTextStyle( - fontVariation = "'wght' 133, 'wdth' 43, 'opsz' 144, 'ROND' 100", fillColorLight = "#FFFFFFFF", outlineColor = "#00000000", renderType = RenderType.CHANGE_WEIGHT, diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt index a75022a40422..6c627e27a19b 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockController.kt @@ -20,11 +20,14 @@ import android.content.Context import android.content.res.Resources import com.android.systemui.customization.R import com.android.systemui.plugins.clocks.AlarmData +import com.android.systemui.plugins.clocks.AxisType import com.android.systemui.plugins.clocks.ClockConfig import com.android.systemui.plugins.clocks.ClockController import com.android.systemui.plugins.clocks.ClockEvents +import com.android.systemui.plugins.clocks.ClockFontAxis +import com.android.systemui.plugins.clocks.ClockFontAxisSetting import com.android.systemui.plugins.clocks.ClockMessageBuffers -import com.android.systemui.plugins.clocks.ClockReactiveSetting +import com.android.systemui.plugins.clocks.ClockSettings import com.android.systemui.plugins.clocks.ThemeConfig import com.android.systemui.plugins.clocks.WeatherData import com.android.systemui.plugins.clocks.ZenData @@ -37,6 +40,7 @@ import java.util.TimeZone class FlexClockController( private val ctx: Context, private val resources: Resources, + private val settings: ClockSettings, private val assets: AssetLoader, // TODO(b/364680879): Remove and replace w/ resources val design: ClockDesign, // TODO(b/364680879): Remove when done inlining val messageBuffers: ClockMessageBuffers?, @@ -113,14 +117,17 @@ class FlexClockController( largeClock.events.onZenDataChanged(data) } - override fun onReactiveAxesChanged(axes: List<ClockReactiveSetting>) { - smallClock.events.onReactiveAxesChanged(axes) - largeClock.events.onReactiveAxesChanged(axes) + override fun onFontAxesChanged(axes: List<ClockFontAxisSetting>) { + val fontAxes = ClockFontAxis.merge(FONT_AXES, axes).map { it.toSetting() } + smallClock.events.onFontAxesChanged(fontAxes) + largeClock.events.onFontAxesChanged(fontAxes) } } override fun initialize(isDarkTheme: Boolean, dozeFraction: Float, foldFraction: Float) { val theme = ThemeConfig(isDarkTheme, assets.seedColor) + events.onFontAxesChanged(settings.axes) + smallClock.run { events.onThemeChanged(theme) animations.doze(dozeFraction) @@ -137,4 +144,46 @@ class FlexClockController( } override fun dump(pw: PrintWriter) {} + + companion object { + val FONT_AXES = + listOf( + ClockFontAxis( + key = "wght", + type = AxisType.Float, + minValue = 1f, + currentValue = 400f, + maxValue = 1000f, + name = "Weight", + description = "Glyph Weight", + ), + ClockFontAxis( + key = "wdth", + type = AxisType.Float, + minValue = 25f, + currentValue = 100f, + maxValue = 151f, + name = "Width", + description = "Glyph Width", + ), + ClockFontAxis( + key = "ROND", + type = AxisType.Boolean, + minValue = 0f, + currentValue = 0f, + maxValue = 100f, + name = "Round", + description = "Glyph Roundness", + ), + ClockFontAxis( + key = "slnt", + type = AxisType.Boolean, + minValue = 0f, + currentValue = 0f, + maxValue = -10f, + name = "Slant", + description = "Glyph Slant", + ), + ) + } } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt index 8ffc00de5fe5..a4782acaed9b 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/FlexClockFaceController.kt @@ -32,7 +32,7 @@ import com.android.systemui.plugins.clocks.ClockFaceConfig import com.android.systemui.plugins.clocks.ClockFaceController import com.android.systemui.plugins.clocks.ClockFaceEvents import com.android.systemui.plugins.clocks.ClockFaceLayout -import com.android.systemui.plugins.clocks.ClockReactiveSetting +import com.android.systemui.plugins.clocks.ClockFontAxisSetting import com.android.systemui.plugins.clocks.DefaultClockFaceLayout import com.android.systemui.plugins.clocks.ThemeConfig import com.android.systemui.plugins.clocks.WeatherData @@ -136,13 +136,16 @@ class FlexClockFaceController( override fun onFontSettingChanged(fontSizePx: Float) { layerController.faceEvents.onFontSettingChanged(fontSizePx) + view.requestLayout() } override fun onThemeChanged(theme: ThemeConfig) { layerController.faceEvents.onThemeChanged(theme) } - override fun onReactiveAxesChanged(axes: List<ClockReactiveSetting>) {} + override fun onFontAxesChanged(axes: List<ClockFontAxisSetting>) { + layerController.events.onFontAxesChanged(axes) + } /** * targetRegion passed to all customized clock applies counter translationY of diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt index 7b1960efa1c6..143b28f1e82b 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt @@ -31,7 +31,7 @@ import com.android.systemui.plugins.clocks.ClockAnimations import com.android.systemui.plugins.clocks.ClockEvents import com.android.systemui.plugins.clocks.ClockFaceConfig import com.android.systemui.plugins.clocks.ClockFaceEvents -import com.android.systemui.plugins.clocks.ClockReactiveSetting +import com.android.systemui.plugins.clocks.ClockFontAxisSetting import com.android.systemui.plugins.clocks.ThemeConfig import com.android.systemui.plugins.clocks.WeatherData import com.android.systemui.plugins.clocks.ZenData @@ -246,7 +246,9 @@ open class SimpleDigitalHandLayerController<T>( override fun onZenDataChanged(data: ZenData) {} - override fun onReactiveAxesChanged(axes: List<ClockReactiveSetting>) {} + override fun onFontAxesChanged(axes: List<ClockFontAxisSetting>) { + view.updateAxes(axes) + } } override val animations = diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/DigitalClockFaceView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/DigitalClockFaceView.kt index ce4d71cff963..b09332f34e99 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/DigitalClockFaceView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/DigitalClockFaceView.kt @@ -25,6 +25,7 @@ import androidx.annotation.VisibleForTesting import com.android.systemui.log.core.Logger import com.android.systemui.log.core.MessageBuffer import com.android.systemui.plugins.clocks.AlarmData +import com.android.systemui.plugins.clocks.ClockFontAxisSetting import com.android.systemui.plugins.clocks.WeatherData import com.android.systemui.plugins.clocks.ZenData import com.android.systemui.shared.clocks.LogUtil @@ -126,6 +127,11 @@ abstract class DigitalClockFaceView(ctx: Context, messageBuffer: MessageBuffer) invalidate() } + fun updateAxes(axes: List<ClockFontAxisSetting>) { + digitalClockTextViewMap.forEach { _, view -> view.updateAxes(axes) } + requestLayout() + } + fun onFontSettingChanged(fontSizePx: Float) { digitalClockTextViewMap.forEach { _, view -> view.applyTextSize(fontSizePx) } } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt index 25b2ad772b32..0b55a6e3ffa1 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/FlexClockView.kt @@ -19,7 +19,6 @@ package com.android.systemui.shared.clocks.view import android.content.Context import android.graphics.Canvas import android.graphics.Point -import android.view.MotionEvent import android.view.View import android.view.ViewGroup import android.widget.RelativeLayout @@ -28,7 +27,6 @@ import com.android.systemui.customization.R import com.android.systemui.log.core.MessageBuffer import com.android.systemui.shared.clocks.AssetLoader import com.android.systemui.shared.clocks.DigitTranslateAnimator -import com.android.systemui.shared.clocks.FontTextStyle import kotlin.math.abs import kotlin.math.max import kotlin.math.min @@ -39,8 +37,7 @@ class FlexClockView(context: Context, val assets: AssetLoader, messageBuffer: Me DigitalClockFaceView(context, messageBuffer) { override var digitalClockTextViewMap = mutableMapOf<Int, SimpleDigitalClockTextView>() val digitLeftTopMap = mutableMapOf<Int, Point>() - var maxSingleDigitHeight = -1 - var maxSingleDigitWidth = -1 + var maxSingleDigitSize = Point(-1, -1) val lockscreenTranslate = Point(0, 0) val aodTranslate = Point(0, 0) @@ -53,65 +50,6 @@ class FlexClockView(context: Context, val assets: AssetLoader, messageBuffer: Me ) } - private var prevX = 0f - private var prevY = 0f - private var isDown = false - - private var wght = 603f - private var wdth = 100f - - private val MAX_WGHT = 950f - private val MIN_WGHT = 50f - private val WGHT_SCALE = 0.5f - - private val MAX_WDTH = 150f - private val MIN_WDTH = 0f - private val WDTH_SCALE = 0.2f - - override fun onTouchEvent(evt: MotionEvent): Boolean { - if (!isReactiveTouchInteractionEnabled) { - return super.onTouchEvent(evt) - } - - when (evt.action) { - MotionEvent.ACTION_DOWN -> { - isDown = true - prevX = evt.x - prevY = evt.y - return true - } - - MotionEvent.ACTION_MOVE -> { - if (!isDown) { - return super.onTouchEvent(evt) - } - - wdth = clamp(wdth + (evt.x - prevX) * WDTH_SCALE, MIN_WDTH, MAX_WDTH) - wght = clamp(wght + (evt.y - prevY) * WGHT_SCALE, MIN_WGHT, MAX_WGHT) - prevX = evt.x - prevY = evt.y - - val fvar = "'wght' $wght, 'wdth' $wdth, 'opsz' 144, 'ROND' 100" - digitalClockTextViewMap.forEach { (_, view) -> - val textStyle = view.textStyle as FontTextStyle - textStyle.fontVariation = fvar - view.applyStyles(assets, textStyle, view.aodStyle) - } - - requestLayout() - invalidate() - return true - } - - MotionEvent.ACTION_UP -> { - isDown = false - return true - } - } - - return super.onTouchEvent(evt) - } - override fun addView(child: View?) { super.addView(child) (child as SimpleDigitalClockTextView).digitTranslateAnimator = @@ -119,25 +57,26 @@ class FlexClockView(context: Context, val assets: AssetLoader, messageBuffer: Me } protected override fun calculateSize(widthMeasureSpec: Int, heightMeasureSpec: Int): Point { + maxSingleDigitSize = Point(-1, -1) digitalClockTextViewMap.forEach { (_, textView) -> textView.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED) + maxSingleDigitSize.x = max(maxSingleDigitSize.x, textView.measuredWidth) + maxSingleDigitSize.y = max(maxSingleDigitSize.y, textView.measuredHeight) } val textView = digitalClockTextViewMap[R.id.HOUR_FIRST_DIGIT]!! - maxSingleDigitHeight = textView.measuredHeight - maxSingleDigitWidth = textView.measuredWidth - aodTranslate.x = -(maxSingleDigitWidth * AOD_HORIZONTAL_TRANSLATE_RATIO).toInt() - aodTranslate.y = (maxSingleDigitHeight * AOD_VERTICAL_TRANSLATE_RATIO).toInt() + aodTranslate.x = -(maxSingleDigitSize.x * AOD_HORIZONTAL_TRANSLATE_RATIO).toInt() + aodTranslate.y = (maxSingleDigitSize.y * AOD_VERTICAL_TRANSLATE_RATIO).toInt() return Point( - ((maxSingleDigitWidth + abs(aodTranslate.x)) * 2), - ((maxSingleDigitHeight + abs(aodTranslate.y)) * 2), + ((maxSingleDigitSize.x + abs(aodTranslate.x)) * 2), + ((maxSingleDigitSize.y + abs(aodTranslate.y)) * 2), ) } protected override fun calculateLeftTopPosition() { digitLeftTopMap[R.id.HOUR_FIRST_DIGIT] = Point(0, 0) - digitLeftTopMap[R.id.HOUR_SECOND_DIGIT] = Point(maxSingleDigitWidth, 0) - digitLeftTopMap[R.id.MINUTE_FIRST_DIGIT] = Point(0, maxSingleDigitHeight) - digitLeftTopMap[R.id.MINUTE_SECOND_DIGIT] = Point(maxSingleDigitWidth, maxSingleDigitHeight) + digitLeftTopMap[R.id.HOUR_SECOND_DIGIT] = Point(maxSingleDigitSize.x, 0) + digitLeftTopMap[R.id.MINUTE_FIRST_DIGIT] = Point(0, maxSingleDigitSize.y) + digitLeftTopMap[R.id.MINUTE_SECOND_DIGIT] = Point(maxSingleDigitSize) digitLeftTopMap.forEach { _, point -> point.x += abs(aodTranslate.x) point.y += abs(aodTranslate.y) @@ -162,7 +101,7 @@ class FlexClockView(context: Context, val assets: AssetLoader, messageBuffer: Me override fun animateDoze(isDozing: Boolean, isAnimated: Boolean) { dozeControlState.animateDoze = { super.animateDoze(isDozing, isAnimated) - if (maxSingleDigitHeight == -1) { + if (maxSingleDigitSize.x < 0 || maxSingleDigitSize.y < 0) { measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED) } digitalClockTextViewMap.forEach { (id, textView) -> diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt index baed3ae5c7b2..c0899e3006a6 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockTextView.kt @@ -41,6 +41,7 @@ import com.android.systemui.animation.TypefaceVariantCache import com.android.systemui.customization.R import com.android.systemui.log.core.Logger import com.android.systemui.log.core.MessageBuffer +import com.android.systemui.plugins.clocks.ClockFontAxisSetting import com.android.systemui.shared.clocks.AssetLoader import com.android.systemui.shared.clocks.ClockAnimation import com.android.systemui.shared.clocks.DigitTranslateAnimator @@ -64,6 +65,9 @@ open class SimpleDigitalClockTextView( val lockScreenPaint = TextPaint() override lateinit var textStyle: FontTextStyle lateinit var aodStyle: FontTextStyle + + private var lsFontVariation = ClockFontAxisSetting.toFVar(DEFAULT_LS_VARIATION) + private var aodFontVariation = ClockFontAxisSetting.toFVar(DEFAULT_AOD_VARIATION) private val parser = DimensionParser(ctx) var maxSingleDigitHeight = -1 var maxSingleDigitWidth = -1 @@ -140,6 +144,17 @@ open class SimpleDigitalClockTextView( invalidate() } + override fun updateAxes(axes: List<ClockFontAxisSetting>) { + lsFontVariation = ClockFontAxisSetting.toFVar(axes + OPTICAL_SIZE_AXIS) + lockScreenPaint.typeface = typefaceCache.getTypefaceForVariant(lsFontVariation) + typeface = lockScreenPaint.typeface + textAnimator.setTextStyle(fvar = lsFontVariation, animate = true) + measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED) + recomputeMaxSingleDigitSizes() + requestLayout() + invalidate() + } + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { logger.d("onMeasure()") if (isVertical) { @@ -189,6 +204,7 @@ open class SimpleDigitalClockTextView( MeasureSpec.getMode(measuredHeight), ) } + if (MeasureSpec.getMode(widthMeasureSpec) == EXACTLY) { expectedWidth = widthMeasureSpec } else { @@ -266,15 +282,12 @@ open class SimpleDigitalClockTextView( } override fun animateDoze(isDozing: Boolean, isAnimated: Boolean) { - if (!this::textAnimator.isInitialized) { - return - } - val fvar = if (isDozing) aodStyle.fontVariation else textStyle.fontVariation + if (!this::textAnimator.isInitialized) return textAnimator.setTextStyle( animate = isAnimated && isAnimationEnabled, color = if (isDozing) AOD_COLOR else lockscreenColor, textSize = if (isDozing) aodFontSizePx else lockScreenPaint.textSize, - fvar = fvar, + fvar = if (isDozing) aodFontVariation else lsFontVariation, duration = aodStyle.transitionDuration, interpolator = aodDozingInterpolator, ) @@ -287,14 +300,15 @@ open class SimpleDigitalClockTextView( return } logger.d("animateCharge()") - val middleFvar = if (dozeFraction == 0F) aodStyle.fontVariation else textStyle.fontVariation - val endFvar = if (dozeFraction == 0F) textStyle.fontVariation else aodStyle.fontVariation val startAnimPhase2 = Runnable { - textAnimator.setTextStyle(fvar = endFvar, animate = isAnimationEnabled) + textAnimator.setTextStyle( + fvar = if (dozeFraction == 0F) lsFontVariation else aodFontVariation, + animate = isAnimationEnabled, + ) updateTextBoundsForTextAnimator() } textAnimator.setTextStyle( - fvar = middleFvar, + fvar = if (dozeFraction == 0F) aodFontVariation else lsFontVariation, animate = isAnimationEnabled, onAnimationEnd = startAnimPhase2, ) @@ -423,7 +437,7 @@ open class SimpleDigitalClockTextView( val typefaceName = "fonts/" + textStyle.fontFamily setTypefaceCache(assets.typefaceCache.getVariantCache(typefaceName)) lockScreenPaint.strokeJoin = Paint.Join.ROUND - lockScreenPaint.typeface = typefaceCache.getTypefaceForVariant(textStyle.fontVariation) + lockScreenPaint.typeface = typefaceCache.getTypefaceForVariant(lsFontVariation) textStyle.fontFeatureSettings?.let { lockScreenPaint.fontFeatureSettings = it fontFeatureSettings = it @@ -494,7 +508,7 @@ open class SimpleDigitalClockTextView( textAnimator.textInterpolator.targetPaint.set(lockScreenPaint) textAnimator.textInterpolator.onTargetPaintModified() textAnimator.setTextStyle( - fvar = textStyle.fontVariation, + fvar = lsFontVariation, textSize = lockScreenPaint.textSize, color = lockscreenColor, animate = false, @@ -534,5 +548,22 @@ open class SimpleDigitalClockTextView( companion object { val AOD_STROKE_WIDTH = "2dp" val AOD_COLOR = Color.WHITE + val OPTICAL_SIZE_AXIS = ClockFontAxisSetting("opsz", 144f) + val DEFAULT_LS_VARIATION = + listOf( + OPTICAL_SIZE_AXIS, + ClockFontAxisSetting("wght", 400f), + ClockFontAxisSetting("wdth", 100f), + ClockFontAxisSetting("ROND", 0f), + ClockFontAxisSetting("slnt", 0f), + ) + val DEFAULT_AOD_VARIATION = + listOf( + OPTICAL_SIZE_AXIS, + ClockFontAxisSetting("wght", 200f), + ClockFontAxisSetting("wdth", 100f), + ClockFontAxisSetting("ROND", 0f), + ClockFontAxisSetting("slnt", 0f), + ) } } diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockView.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockView.kt index 30b54d881d92..3d2ed4a1ef40 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockView.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/view/SimpleDigitalClockView.kt @@ -17,6 +17,7 @@ package com.android.systemui.shared.clocks.view import androidx.annotation.VisibleForTesting +import com.android.systemui.plugins.clocks.ClockFontAxisSetting import com.android.systemui.shared.clocks.AssetLoader import com.android.systemui.shared.clocks.TextStyle @@ -34,6 +35,8 @@ interface SimpleDigitalClockView { fun updateColor(color: Int) + fun updateAxes(axes: List<ClockFontAxisSetting>) + fun refreshTime() fun animateCharge() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractorImplTest.kt index 4856f156c4c7..e1421691a92d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractorImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractorImplTest.kt @@ -3,7 +3,9 @@ package com.android.systemui.biometrics.domain.interactor import android.app.admin.DevicePolicyManager import android.app.admin.DevicePolicyResourcesManager import android.content.pm.UserInfo +import android.hardware.biometrics.Flags import android.os.UserManager +import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.widget.LockPatternUtils @@ -33,6 +35,7 @@ import org.mockito.Mockito.verify import org.mockito.junit.MockitoJUnit private const val USER_ID = 22 +private const val OWNER_ID = 10 private const val OPERATION_ID = 100L private const val MAX_ATTEMPTS = 5 @@ -67,7 +70,7 @@ class CredentialInteractorImplTest : SysuiTestCase() { lockPatternUtils, userManager, devicePolicyManager, - systemClock + systemClock, ) } @@ -115,58 +118,87 @@ class CredentialInteractorImplTest : SysuiTestCase() { @Test fun pinCredentialWhenBadAndThrottled() = pinCredential(badCredential(timeout = 5_000)) - private fun pinCredential(result: VerifyCredentialResponse) = runTest { - val usedAttempts = 1 - whenever(lockPatternUtils.getCurrentFailedPasswordAttempts(eq(USER_ID))) - .thenReturn(usedAttempts) - whenever(lockPatternUtils.verifyCredential(any(), eq(USER_ID), anyInt())).thenReturn(result) - whenever(lockPatternUtils.verifyGatekeeperPasswordHandle(anyLong(), anyLong(), eq(USER_ID))) - .thenReturn(result) - whenever(lockPatternUtils.setLockoutAttemptDeadline(anyInt(), anyInt())).thenAnswer { - systemClock.elapsedRealtime() + (it.arguments[1] as Int) - } + @EnableFlags(Flags.FLAG_PRIVATE_SPACE_BP) + @Test + fun pinCredentialTiedProfileWhenGood() = pinCredential(goodCredential(), OWNER_ID) - // wrap in an async block so the test can advance the clock if throttling credential - // checks prevents the method from returning - val statusList = mutableListOf<CredentialStatus>() - interactor - .verifyCredential(pinRequest(), LockscreenCredential.createPin("1234")) - .toList(statusList) + @EnableFlags(Flags.FLAG_PRIVATE_SPACE_BP) + @Test + fun pinCredentialTiedProfileWhenBad() = pinCredential(badCredential(), OWNER_ID) - val last = statusList.removeLastOrNull() - if (result.isMatched) { - assertThat(statusList).isEmpty() - val successfulResult = last as? CredentialStatus.Success.Verified - assertThat(successfulResult).isNotNull() - assertThat(successfulResult!!.hat).isEqualTo(result.gatekeeperHAT) - - verify(lockPatternUtils).userPresent(eq(USER_ID)) - verify(lockPatternUtils) - .removeGatekeeperPasswordHandle(eq(result.gatekeeperPasswordHandle)) - } else { - val failedResult = last as? CredentialStatus.Fail.Error - assertThat(failedResult).isNotNull() - assertThat(failedResult!!.remainingAttempts) - .isEqualTo(if (result.timeout > 0) null else MAX_ATTEMPTS - usedAttempts - 1) - assertThat(failedResult.urgentMessage).isNull() - - if (result.timeout > 0) { // failed and throttled - // messages are in the throttled errors, so the final Error.error is empty - assertThat(failedResult.error).isEmpty() - assertThat(statusList).isNotEmpty() - assertThat(statusList.filterIsInstance(CredentialStatus.Fail.Throttled::class.java)) - .hasSize(statusList.size) - - verify(lockPatternUtils).setLockoutAttemptDeadline(eq(USER_ID), eq(result.timeout)) - } else { // failed - assertThat(failedResult.error) - .matches(Regex("(.*)try again(.*)", RegexOption.IGNORE_CASE).toPattern()) - assertThat(statusList).isEmpty() + @EnableFlags(Flags.FLAG_PRIVATE_SPACE_BP) + @Test + fun pinCredentialTiedProfileWhenBadAndThrottled() = + pinCredential(badCredential(timeout = 5_000), OWNER_ID) + + private fun pinCredential(result: VerifyCredentialResponse, credentialOwner: Int = USER_ID) = + runTest { + val usedAttempts = 1 + whenever(lockPatternUtils.getCurrentFailedPasswordAttempts(eq(USER_ID))) + .thenReturn(usedAttempts) + whenever(lockPatternUtils.verifyCredential(any(), eq(USER_ID), anyInt())) + .thenReturn(result) + whenever(lockPatternUtils.verifyTiedProfileChallenge(any(), eq(USER_ID), anyInt())) + .thenReturn(result) + whenever( + lockPatternUtils.verifyGatekeeperPasswordHandle( + anyLong(), + anyLong(), + eq(USER_ID), + ) + ) + .thenReturn(result) + whenever(lockPatternUtils.setLockoutAttemptDeadline(anyInt(), anyInt())).thenAnswer { + systemClock.elapsedRealtime() + (it.arguments[1] as Int) + } - verify(lockPatternUtils).reportFailedPasswordAttempt(eq(USER_ID)) + // wrap in an async block so the test can advance the clock if throttling credential + // checks prevents the method from returning + val statusList = mutableListOf<CredentialStatus>() + interactor + .verifyCredential( + pinRequest(credentialOwner), + LockscreenCredential.createPin("1234"), + ) + .toList(statusList) + + val last = statusList.removeLastOrNull() + if (result.isMatched) { + assertThat(statusList).isEmpty() + val successfulResult = last as? CredentialStatus.Success.Verified + assertThat(successfulResult).isNotNull() + assertThat(successfulResult!!.hat).isEqualTo(result.gatekeeperHAT) + + verify(lockPatternUtils).userPresent(eq(USER_ID)) + verify(lockPatternUtils) + .removeGatekeeperPasswordHandle(eq(result.gatekeeperPasswordHandle)) + } else { + val failedResult = last as? CredentialStatus.Fail.Error + assertThat(failedResult).isNotNull() + assertThat(failedResult!!.remainingAttempts) + .isEqualTo(if (result.timeout > 0) null else MAX_ATTEMPTS - usedAttempts - 1) + assertThat(failedResult.urgentMessage).isNull() + + if (result.timeout > 0) { // failed and throttled + // messages are in the throttled errors, so the final Error.error is empty + assertThat(failedResult.error).isEmpty() + assertThat(statusList).isNotEmpty() + assertThat( + statusList.filterIsInstance(CredentialStatus.Fail.Throttled::class.java) + ) + .hasSize(statusList.size) + + verify(lockPatternUtils) + .setLockoutAttemptDeadline(eq(USER_ID), eq(result.timeout)) + } else { // failed + assertThat(failedResult.error) + .matches(Regex("(.*)try again(.*)", RegexOption.IGNORE_CASE).toPattern()) + assertThat(statusList).isEmpty() + + verify(lockPatternUtils).reportFailedPasswordAttempt(eq(USER_ID)) + } } } - } @Test fun pinCredentialWhenBadAndFinalAttempt() = runTest { @@ -212,11 +244,11 @@ class CredentialInteractorImplTest : SysuiTestCase() { } } -private fun pinRequest(): BiometricPromptRequest.Credential.Pin = +private fun pinRequest(credentialOwner: Int = USER_ID): BiometricPromptRequest.Credential.Pin = BiometricPromptRequest.Credential.Pin( promptInfo(), - BiometricUserInfo(USER_ID), - BiometricOperationInfo(OPERATION_ID) + BiometricUserInfo(userId = USER_ID, deviceCredentialOwnerId = credentialOwner), + BiometricOperationInfo(OPERATION_ID), ) private fun goodCredential( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt index dc499cd2fe8d..b39a888dad91 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt @@ -77,6 +77,7 @@ class PromptSelectorInteractorImplTest : SysuiTestCase() { private val fingerprintRepository = FakeFingerprintPropertyRepository() private val promptRepository = FakePromptRepository() private val fakeExecutor = FakeExecutor(FakeSystemClock()) + private val credentialInteractor = FakeCredentialInteractor() private lateinit var displayStateRepository: FakeDisplayStateRepository private lateinit var displayRepository: FakeDisplayRepository @@ -99,8 +100,9 @@ class PromptSelectorInteractorImplTest : SysuiTestCase() { PromptSelectorInteractorImpl( fingerprintRepository, displayStateInteractor, + credentialInteractor, promptRepository, - lockPatternUtils + lockPatternUtils, ) } @@ -134,13 +136,13 @@ class PromptSelectorInteractorImplTest : SysuiTestCase() { testScope.runTest { useBiometricsAndReset( allowCredentialFallback = true, - setComponentNameForConfirmDeviceCredentialActivity = true + setComponentNameForConfirmDeviceCredentialActivity = true, ) } private fun TestScope.useBiometricsAndReset( allowCredentialFallback: Boolean, - setComponentNameForConfirmDeviceCredentialActivity: Boolean = false + setComponentNameForConfirmDeviceCredentialActivity: Boolean = false, ) { setUserCredentialType(isPassword = true) @@ -357,7 +359,7 @@ class PromptSelectorInteractorImplTest : SysuiTestCase() { private fun setPrompt( info: PromptInfo = basicPromptInfo(), - onSwitchToCredential: Boolean = false + onSwitchToCredential: Boolean = false, ) { interactor.setPrompt( info, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt index 55fd3440ea07..c803097134de 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt @@ -28,7 +28,6 @@ import android.graphics.Point import android.graphics.Rect import android.graphics.drawable.BitmapDrawable import android.hardware.biometrics.BiometricFingerprintConstants -import android.hardware.biometrics.Flags.FLAG_CUSTOM_BIOMETRIC_PROMPT import android.hardware.biometrics.PromptContentItemBulletedText import android.hardware.biometrics.PromptContentView import android.hardware.biometrics.PromptContentViewWithMoreOptionsButton @@ -163,7 +162,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa naturalDisplayWidth = 1000, naturalDisplayHeight = 3000, scaleFactor = 1f, - rotation = if (isLandscape) Surface.ROTATION_90 else Surface.ROTATION_0 + rotation = if (isLandscape) Surface.ROTATION_90 else Surface.ROTATION_0, ) private lateinit var promptContentView: PromptContentView @@ -180,21 +179,21 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa whenever( kosmos.packageManager.getApplicationInfo( eq(OP_PACKAGE_NAME_WITH_APP_LOGO), - anyInt() + anyInt(), ) ) .thenReturn(applicationInfoWithIconAndDescription) whenever( kosmos.packageManager.getApplicationInfo( eq(OP_PACKAGE_NAME_WITH_ACTIVITY_LOGO), - anyInt() + anyInt(), ) ) .thenReturn(applicationInfoWithIconAndDescription) whenever( kosmos.packageManager.getApplicationInfo( eq(OP_PACKAGE_NAME_CAN_NOT_BE_FOUND), - anyInt() + anyInt(), ) ) .thenThrow(NameNotFoundException()) @@ -220,13 +219,13 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa overrideResource(logoResFromApp, logoDrawableFromAppRes) overrideResource( R.array.config_useActivityLogoForBiometricPrompt, - arrayOf(OP_PACKAGE_NAME_WITH_ACTIVITY_LOGO) + arrayOf(OP_PACKAGE_NAME_WITH_ACTIVITY_LOGO), ) overrideResource(R.dimen.biometric_dialog_fingerprint_icon_width, mockFingerprintIconWidth) overrideResource( R.dimen.biometric_dialog_fingerprint_icon_height, - mockFingerprintIconHeight + mockFingerprintIconHeight, ) overrideResource(R.dimen.biometric_dialog_face_icon_size, mockFaceIconSize) @@ -243,7 +242,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa it.sensorType.toSensorType(), it.allLocations.associateBy { sensorLocationInternal -> sensorLocationInternal.displayId - } + }, ) } @@ -441,7 +440,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa kosmos.promptViewModel.showAuthenticated( modality = testCase.authenticatedModality, - dismissAfterDelay = DELAY + dismissAfterDelay = DELAY, ) // SFPS test cases @@ -513,7 +512,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa kosmos.promptViewModel.showAuthenticated( modality = testCase.authenticatedModality, dismissAfterDelay = DELAY, - "TEST" + "TEST", ) if (testCase.isFingerprintOnly) { @@ -558,7 +557,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa kosmos.promptViewModel.showAuthenticated( modality = testCase.authenticatedModality, - dismissAfterDelay = DELAY + dismissAfterDelay = DELAY, ) if (testCase.isFaceOnly) { @@ -598,7 +597,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa kosmos.promptViewModel.showAuthenticated( modality = testCase.authenticatedModality, - dismissAfterDelay = DELAY + dismissAfterDelay = DELAY, ) kosmos.promptViewModel.confirmAuthenticated() @@ -701,7 +700,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa .isEqualTo( Pair( expectedUdfpsOverlayParams.sensorBounds.width(), - expectedUdfpsOverlayParams.sensorBounds.height() + expectedUdfpsOverlayParams.sensorBounds.height(), ) ) } else { @@ -834,7 +833,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa kosmos.promptViewModel.showAuthenticated( modality = testCase.authenticatedModality, - dismissAfterDelay = DELAY + dismissAfterDelay = DELAY, ) kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_0) @@ -907,10 +906,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } ) - assertButtonsVisible( - cancel = expectConfirmation, - confirm = expectConfirmation, - ) + assertButtonsVisible(cancel = expectConfirmation, confirm = expectConfirmation) } @Test @@ -1158,10 +1154,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa testScheduler.runCurrent() assertThat(messages) - .containsExactly( - PromptMessage.Empty, - PromptMessage.Error(expectedErrorMessage), - ) + .containsExactly(PromptMessage.Empty, PromptMessage.Error(expectedErrorMessage)) .inOrder() testScheduler.advanceUntilIdle() @@ -1221,10 +1214,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa assertThat(authenticated?.needsUserConfirmation).isEqualTo(expectConfirmation) if (expectConfirmation) { assertThat(size).isEqualTo(PromptSize.MEDIUM) - assertButtonsVisible( - cancel = true, - confirm = true, - ) + assertButtonsVisible(cancel = true, confirm = true) kosmos.promptViewModel.confirmAuthenticated() assertThat(message).isEqualTo(PromptMessage.Empty) @@ -1251,10 +1241,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa assertThat(authenticated?.needsUserConfirmation).isEqualTo(expectConfirmation) if (expectConfirmation) { assertThat(size).isEqualTo(PromptSize.MEDIUM) - assertButtonsVisible( - cancel = true, - confirm = true, - ) + assertButtonsVisible(cancel = true, confirm = true) if (testCase.modalities.hasSfps) { kosmos.promptViewModel.showAuthenticated(BiometricModality.Fingerprint, 0) @@ -1290,10 +1277,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa if (expectConfirmation) { if (testCase.isFaceOnly) { assertThat(size).isEqualTo(PromptSize.MEDIUM) - assertButtonsVisible( - cancel = true, - confirm = true, - ) + assertButtonsVisible(cancel = true, confirm = true) kosmos.promptViewModel.confirmAuthenticated() } else if (testCase.isCoex) { @@ -1323,10 +1307,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa assertThat(authenticated?.needsUserConfirmation).isEqualTo(expectConfirmation) if (expectConfirmation) { assertThat(size).isEqualTo(PromptSize.MEDIUM) - assertButtonsVisible( - cancel = true, - confirm = true, - ) + assertButtonsVisible(cancel = true, confirm = true) kosmos.promptViewModel.confirmAuthenticated() assertThat(message).isEqualTo(PromptMessage.Empty) @@ -1398,10 +1379,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa assertThat(authenticating).isFalse() assertThat(authenticated?.isAuthenticated).isTrue() assertThat(authenticated?.needsUserConfirmation).isEqualTo(expectConfirmation) - assertButtonsVisible( - cancel = expectConfirmation, - confirm = expectConfirmation, - ) + assertButtonsVisible(cancel = expectConfirmation, confirm = expectConfirmation) } @Test @@ -1421,7 +1399,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa errorMessage, messageAfterError = helpMessage, authenticateAfterError = false, - failedModality = testCase.authenticatedModality + failedModality = testCase.authenticatedModality, ) } @@ -1472,7 +1450,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa kosmos.promptViewModel.onAnnounceAccessibilityHint( obtainMotionEvent(MotionEvent.ACTION_HOVER_ENTER), - true + true, ) if (testCase.modalities.hasUdfps) { @@ -1497,14 +1475,13 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa kosmos.promptViewModel.onAnnounceAccessibilityHint( obtainMotionEvent(MotionEvent.ACTION_HOVER_ENTER), - true + true, ) assertThat(hint.isNullOrBlank()).isTrue() } @Test - @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) fun descriptionOverriddenByVerticalListContentView() = runGenericTest(description = "test description", contentView = promptContentView) { val contentView by collectLastValue(kosmos.promptViewModel.contentView) @@ -1515,11 +1492,10 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) fun descriptionOverriddenByContentViewWithMoreOptionsButton() = runGenericTest( description = "test description", - contentView = promptContentViewWithMoreOptionsButton + contentView = promptContentViewWithMoreOptionsButton, ) { val contentView by collectLastValue(kosmos.promptViewModel.contentView) val description by collectLastValue(kosmos.promptViewModel.description) @@ -1529,7 +1505,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) fun descriptionWithoutContentView() = runGenericTest(description = "test description") { val contentView by collectLastValue(kosmos.promptViewModel.contentView) @@ -1540,7 +1515,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) fun logo_nullIfPkgNameNotFound() = runGenericTest(packageName = OP_PACKAGE_NAME_CAN_NOT_BE_FOUND) { val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo) @@ -1549,7 +1523,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) fun logo_defaultFromActivityInfo() = runGenericTest(packageName = OP_PACKAGE_NAME_WITH_ACTIVITY_LOGO) { val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo) @@ -1564,7 +1537,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) fun logo_defaultIsNull() = runGenericTest(packageName = OP_PACKAGE_NAME_NO_ICON) { val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo) @@ -1573,7 +1545,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) fun logo_default() = runGenericTest { val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo) assertThat(logoInfo).isNotNull() @@ -1581,7 +1552,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) fun logo_resSetByApp() = runGenericTest(logoRes = logoResFromApp) { val expectedBitmap = context.getDrawable(logoResFromApp).toBitmap() @@ -1591,7 +1561,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) fun logo_bitmapSetByApp() = runGenericTest(logoBitmap = logoBitmapFromApp) { val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo) @@ -1599,7 +1568,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) fun logoDescription_emptyIfPkgNameNotFound() = runGenericTest(packageName = OP_PACKAGE_NAME_CAN_NOT_BE_FOUND) { val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo) @@ -1607,7 +1575,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) fun logoDescription_defaultFromActivityInfo() = runGenericTest(packageName = OP_PACKAGE_NAME_WITH_ACTIVITY_LOGO) { val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo) @@ -1619,7 +1586,6 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) fun logoDescription_defaultIsEmpty() = runGenericTest(packageName = OP_PACKAGE_NAME_NO_ICON) { val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo) @@ -1627,14 +1593,12 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa } @Test - @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) fun logoDescription_default() = runGenericTest { val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo) assertThat(logoInfo!!.second).isEqualTo(defaultLogoDescriptionFromAppInfo) } @Test - @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) fun logoDescription_setByApp() = runGenericTest(logoDescription = logoDescriptionFromApp) { val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo) @@ -1826,7 +1790,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa kosmos.biometricStatusRepository.setFingerprintAcquiredStatus( AcquiredFingerprintAuthenticationStatus( AuthenticationReason.BiometricPromptAuthentication, - BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_UNKNOWN + BiometricFingerprintConstants.FINGERPRINT_ACQUIRED_UNKNOWN, ) ) @@ -1893,7 +1857,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa fingerprint = fingerprintSensorPropertiesInternal( strong = true, - sensorType = FingerprintSensorProperties.TYPE_POWER_BUTTON + sensorType = FingerprintSensorProperties.TYPE_POWER_BUTTON, ) .first(), authenticatedModality = BiometricModality.Fingerprint, @@ -1903,7 +1867,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa fingerprint = fingerprintSensorPropertiesInternal( strong = true, - sensorType = FingerprintSensorProperties.TYPE_POWER_BUTTON + sensorType = FingerprintSensorProperties.TYPE_POWER_BUTTON, ) .first(), authenticatedModality = BiometricModality.Fingerprint, @@ -1913,7 +1877,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa fingerprint = fingerprintSensorPropertiesInternal( strong = true, - sensorType = FingerprintSensorProperties.TYPE_UDFPS_OPTICAL + sensorType = FingerprintSensorProperties.TYPE_UDFPS_OPTICAL, ) .first(), authenticatedModality = BiometricModality.Fingerprint, @@ -1932,7 +1896,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa fingerprint = fingerprintSensorPropertiesInternal( strong = true, - sensorType = FingerprintSensorProperties.TYPE_POWER_BUTTON + sensorType = FingerprintSensorProperties.TYPE_POWER_BUTTON, ) .first(), authenticatedModality = BiometricModality.Fingerprint, @@ -1958,7 +1922,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa fingerprint = fingerprintSensorPropertiesInternal( strong = true, - sensorType = FingerprintSensorProperties.TYPE_POWER_BUTTON + sensorType = FingerprintSensorProperties.TYPE_POWER_BUTTON, ) .first(), authenticatedModality = BiometricModality.Fingerprint, @@ -1969,7 +1933,7 @@ internal class PromptViewModelTest(private val testCase: TestCase) : SysuiTestCa fingerprint = fingerprintSensorPropertiesInternal( strong = true, - sensorType = FingerprintSensorProperties.TYPE_UDFPS_OPTICAL + sensorType = FingerprintSensorProperties.TYPE_UDFPS_OPTICAL, ) .first(), authenticatedModality = BiometricModality.Fingerprint, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryLocalImplTest.kt index 8ae9d2e8f7e8..55d7d08e8519 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryLocalImplTest.kt @@ -73,7 +73,7 @@ import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) -class CommunalWidgetRepositoryImplTest : SysuiTestCase() { +class CommunalWidgetRepositoryLocalImplTest : SysuiTestCase() { @Mock private lateinit var appWidgetHost: CommunalAppWidgetHost @Mock private lateinit var providerInfoA: AppWidgetProviderInfo @Mock private lateinit var providerInfoB: AppWidgetProviderInfo @@ -105,14 +105,14 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { "com.android.fake/WidgetProviderC", ) - private lateinit var underTest: CommunalWidgetRepositoryImpl + private lateinit var underTest: CommunalWidgetRepositoryLocalImpl @Before fun setUp() { MockitoAnnotations.initMocks(this) fakeWidgets = MutableStateFlow(emptyMap()) fakeProviders = MutableStateFlow(emptyMap()) - logBuffer = logcatLogBuffer(name = "CommunalWidgetRepoImplTest") + logBuffer = logcatLogBuffer(name = "CommunalWidgetRepoLocalImplTest") backupUtils = CommunalBackupUtils(kosmos.applicationContext) setAppWidgetIds(emptyList()) @@ -126,7 +126,7 @@ class CommunalWidgetRepositoryImplTest : SysuiTestCase() { restoreUser(mainUser) underTest = - CommunalWidgetRepositoryImpl( + CommunalWidgetRepositoryLocalImpl( appWidgetHost, testScope.backgroundScope, kosmos.testDispatcher, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModelTest.kt new file mode 100644 index 000000000000..c21015402f01 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModelTest.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.shared.model + +import android.appwidget.AppWidgetProviderInfo +import android.content.ComponentName +import android.os.Parcel +import android.os.UserHandle +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class CommunalWidgetContentModelTest : SysuiTestCase() { + @Test + fun testParcelizeAvailableWidget() { + val widgetToParcelize = + CommunalWidgetContentModel.Available( + appWidgetId = 1, + providerInfo = + AppWidgetProviderInfo().apply { provider = ComponentName("pkg", "cls") }, + rank = 2, + spanY = 3, + ) + + val parcel = Parcel.obtain() + widgetToParcelize.writeToParcel(parcel, flags = 0) + + parcel.setDataPosition(0) + + // Only checking fields are equal and not complete equality because not all fields are + // specified in the fake AppWidgetProviderInfo + val widgetFromParcel = + CommunalWidgetContentModel.createFromParcel(parcel) + as CommunalWidgetContentModel.Available + assertThat(widgetFromParcel.appWidgetId).isEqualTo(widgetToParcelize.appWidgetId) + assertThat(widgetFromParcel.rank).isEqualTo(widgetToParcelize.rank) + assertThat(widgetFromParcel.spanY).isEqualTo(widgetToParcelize.spanY) + assertThat(widgetFromParcel.providerInfo.provider) + .isEqualTo(widgetToParcelize.providerInfo.provider) + } + + @Test + fun testParcelizePendingWidget() { + val widgetToParcelize = + CommunalWidgetContentModel.Pending( + appWidgetId = 2, + rank = 3, + componentName = ComponentName("pkg", "cls"), + icon = null, + user = UserHandle(0), + spanY = 6, + ) + + val parcel = Parcel.obtain() + widgetToParcelize.writeToParcel(parcel, flags = 0) + + parcel.setDataPosition(0) + + val widgetFromParcel = CommunalWidgetContentModel.createFromParcel(parcel) + assertThat(widgetFromParcel).isEqualTo(widgetToParcelize) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModelTest.kt index 7816d3b131cb..bea1010d8887 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModelTest.kt @@ -367,6 +367,39 @@ class ResizeableItemFrameViewModelTest : SysuiTestCase() { } @Test + fun testMaxSpansLessThanCurrentSpan() = + testScope.runTest { + // heightPerSpan = + // (viewportHeightPx - verticalContentPaddingPx - (totalSpans - 1) + // * verticalItemSpacingPx) / totalSpans + // = 145.3333 + // maxSpans = (maxHeightPx + verticalItemSpacing) / + // (heightPerSpanPx + verticalItemSpacingPx) + // = 4.72 + // This is invalid because the max span calculation comes out to be less than + // the current span. Ensure we handle this case correctly. + val layout = + GridLayout( + verticalItemSpacingPx = 100f, + currentRow = 0, + minHeightPx = 480, + maxHeightPx = 1060, + currentSpan = 6, + resizeMultiple = 3, + totalSpans = 6, + viewportHeightPx = 1600, + verticalContentPaddingPx = 228f, + ) + updateGridLayout(layout) + + val topState = underTest.topDragState + val bottomState = underTest.bottomDragState + + assertThat(topState.anchors.toList()).containsExactly(0 to 0f) + assertThat(bottomState.anchors.toList()).containsExactly(0 to 0f, -3 to -736f) + } + + @Test fun testCanExpand_atTopPosition_withMultipleAnchors_returnsTrue() = testScope.runTest { val twoRowGrid = singleSpanGrid.copy(totalSpans = 2, currentSpan = 1, currentRow = 0) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/widgets/CommunalAppWidgetHostTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/widgets/CommunalAppWidgetHostTest.kt index 1e79112eefe3..18513fc496b4 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/widgets/CommunalAppWidgetHostTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/widgets/CommunalAppWidgetHostTest.kt @@ -21,6 +21,7 @@ import android.testing.TestableLooper.RunWithLooper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.communal.shared.model.fakeGlanceableHubMultiUserHelper import com.android.systemui.communal.widgets.CommunalAppWidgetHost import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.applicationCoroutineScope @@ -61,6 +62,7 @@ class CommunalAppWidgetHostTest : SysuiTestCase() { backgroundScope = kosmos.applicationCoroutineScope, hostId = 116, logBuffer = logcatLogBuffer("CommunalAppWidgetHostTest"), + glanceableHubMultiUserHelper = kosmos.fakeGlanceableHubMultiUserHelper, ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt index c9f3f1453492..9ef2b190fdd7 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt @@ -24,10 +24,14 @@ import com.android.systemui.Flags.FLAG_COMMUNAL_HUB import com.android.systemui.SysuiTestCase import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepository import com.android.systemui.communal.domain.interactor.communalInteractor +import com.android.systemui.communal.domain.interactor.communalSettingsInteractor +import com.android.systemui.communal.shared.model.FakeGlanceableHubMultiUserHelper +import com.android.systemui.communal.shared.model.fakeGlanceableHubMultiUserHelper import com.android.systemui.coroutines.collectLastValue import com.android.systemui.flags.Flags import com.android.systemui.flags.fakeFeatureFlagsClassic import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope @@ -58,6 +62,9 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() { @Mock private lateinit var appWidgetHost: CommunalAppWidgetHost @Mock private lateinit var communalWidgetHost: CommunalWidgetHost + private lateinit var widgetManager: GlanceableHubWidgetManager + private lateinit var helper: FakeGlanceableHubMultiUserHelper + private lateinit var appWidgetIdToRemove: MutableSharedFlow<Int> private lateinit var underTest: CommunalAppWidgetHostStartable @@ -69,17 +76,23 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() { kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true) mSetFlagsRule.enableFlags(FLAG_COMMUNAL_HUB) + widgetManager = kosmos.mockGlanceableHubWidgetManager + helper = kosmos.fakeGlanceableHubMultiUserHelper appWidgetIdToRemove = MutableSharedFlow() whenever(appWidgetHost.appWidgetIdToRemove).thenReturn(appWidgetIdToRemove) underTest = CommunalAppWidgetHostStartable( - appWidgetHost, - communalWidgetHost, - kosmos.communalInteractor, - kosmos.fakeUserTracker, + { appWidgetHost }, + { communalWidgetHost }, + { kosmos.communalInteractor }, + { kosmos.communalSettingsInteractor }, + { kosmos.keyguardInteractor }, + { kosmos.fakeUserTracker }, kosmos.applicationCoroutineScope, kosmos.testDispatcher, + { widgetManager }, + helper, ) } @@ -211,7 +224,7 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() { fakeCommunalWidgetRepository.addWidget(appWidgetId = 1, userId = USER_INFO_WORK.id) fakeCommunalWidgetRepository.addPendingWidget( appWidgetId = 2, - userId = USER_INFO_WORK.id + userId = USER_INFO_WORK.id, ) fakeCommunalWidgetRepository.addWidget(appWidgetId = 3, userId = MAIN_USER_INFO.id) @@ -246,16 +259,42 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() { } } - private suspend fun setCommunalAvailable(available: Boolean) = + @Test + fun onStartHeadlessSystemUser_registerWidgetManager_whenCommunalIsAvailable() = + with(kosmos) { + testScope.runTest { + helper.setIsInHeadlessSystemUser(true) + underTest.start() + runCurrent() + verify(widgetManager, never()).register() + verify(widgetManager, never()).unregister() + + // Binding to the service does not require keyguard showing + setCommunalAvailable(true, setKeyguardShowing = false) + runCurrent() + verify(widgetManager).register() + + setCommunalAvailable(false) + runCurrent() + verify(widgetManager).unregister() + } + } + + private suspend fun setCommunalAvailable( + available: Boolean, + setKeyguardShowing: Boolean = true, + ) = with(kosmos) { fakeKeyguardRepository.setIsEncryptedOrLockdown(false) fakeUserRepository.setSelectedUserInfo(MAIN_USER_INFO) - fakeKeyguardRepository.setKeyguardShowing(true) + if (setKeyguardShowing) { + fakeKeyguardRepository.setKeyguardShowing(true) + } val settingsValue = if (available) 1 else 0 fakeSettings.putIntForUser( Settings.Secure.GLANCEABLE_HUB_ENABLED, settingsValue, - MAIN_USER_INFO.id + MAIN_USER_INFO.id, ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalWidgetHostTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalWidgetHostTest.kt index 054e516db943..017c77828cdc 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalWidgetHostTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalWidgetHostTest.kt @@ -26,6 +26,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.communal.shared.model.fakeGlanceableHubMultiUserHelper import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues import com.android.systemui.kosmos.applicationCoroutineScope @@ -84,7 +85,7 @@ class CommunalWidgetHostTest : SysuiTestCase() { any<Int>(), any<UserHandle>(), any<ComponentName>(), - any<Bundle>() + any<Bundle>(), ) ) .thenReturn(true) @@ -96,6 +97,7 @@ class CommunalWidgetHostTest : SysuiTestCase() { appWidgetHost, selectedUserInteractor, logcatLogBuffer("CommunalWidgetHostTest"), + glanceableHubMultiUserHelper = kosmos.fakeGlanceableHubMultiUserHelper, ) } @@ -162,7 +164,7 @@ class CommunalWidgetHostTest : SysuiTestCase() { any<Int>(), any<UserHandle>(), any<ComponentName>(), - any<Bundle>() + any<Bundle>(), ) ) .thenReturn(false) @@ -283,12 +285,7 @@ class CommunalWidgetHostTest : SysuiTestCase() { // all providers are emitted at once assertThat(providerInfoValues).hasSize(2) assertThat(providerInfoValues[1]) - .containsExactlyEntriesIn( - mapOf( - Pair(1, providerInfo1), - Pair(2, providerInfo2), - ) - ) + .containsExactlyEntriesIn(mapOf(Pair(1, providerInfo1), Pair(2, providerInfo2))) } @Test @@ -304,12 +301,7 @@ class CommunalWidgetHostTest : SysuiTestCase() { // Assert that the provider info map is populated val providerInfo by collectLastValue(underTest.appWidgetProviders) assertThat(providerInfo) - .containsExactlyEntriesIn( - mapOf( - Pair(1, providerInfo1), - Pair(2, providerInfo2), - ) - ) + .containsExactlyEntriesIn(mapOf(Pair(1, providerInfo1), Pair(2, providerInfo2))) // Host stop listening observer.onHostStopListening() @@ -332,12 +324,7 @@ class CommunalWidgetHostTest : SysuiTestCase() { // Assert that the provider info map is populated assertThat(providerInfo) - .containsExactlyEntriesIn( - mapOf( - Pair(1, providerInfo1), - Pair(2, providerInfo2), - ) - ) + .containsExactlyEntriesIn(mapOf(Pair(1, providerInfo1), Pair(2, providerInfo2))) // Provider info for widget 1 updated val listener = @@ -349,12 +336,7 @@ class CommunalWidgetHostTest : SysuiTestCase() { // Assert that the update is reflected in the flow assertThat(providerInfo) - .containsExactlyEntriesIn( - mapOf( - Pair(1, providerInfo3), - Pair(2, providerInfo2), - ) - ) + .containsExactlyEntriesIn(mapOf(Pair(1, providerInfo3), Pair(2, providerInfo2))) } @Test @@ -371,12 +353,7 @@ class CommunalWidgetHostTest : SysuiTestCase() { // Assert that the provider info map is populated assertThat(providerInfo) - .containsExactlyEntriesIn( - mapOf( - Pair(1, providerInfo1), - Pair(2, providerInfo2), - ) - ) + .containsExactlyEntriesIn(mapOf(Pair(1, providerInfo1), Pair(2, providerInfo2))) // Bind a new widget whenever(appWidgetHost.allocateAppWidgetId()).thenReturn(3) @@ -388,11 +365,7 @@ class CommunalWidgetHostTest : SysuiTestCase() { // Assert that the new provider is reflected in the flow assertThat(providerInfo) .containsExactlyEntriesIn( - mapOf( - Pair(1, providerInfo1), - Pair(2, providerInfo2), - Pair(3, providerInfo3), - ) + mapOf(Pair(1, providerInfo1), Pair(2, providerInfo2), Pair(3, providerInfo3)) ) } @@ -410,31 +383,21 @@ class CommunalWidgetHostTest : SysuiTestCase() { // Assert that the provider info map is populated assertThat(providerInfo) - .containsExactlyEntriesIn( - mapOf( - Pair(1, providerInfo1), - Pair(2, providerInfo2), - ) - ) + .containsExactlyEntriesIn(mapOf(Pair(1, providerInfo1), Pair(2, providerInfo2))) // Remove widget 1 observer.onDeleteAppWidgetId(1) runCurrent() // Assert that provider info for widget 1 is removed - assertThat(providerInfo) - .containsExactlyEntriesIn( - mapOf( - Pair(2, providerInfo2), - ) - ) + assertThat(providerInfo).containsExactlyEntriesIn(mapOf(Pair(2, providerInfo2))) } private fun selectUser() { kosmos.fakeUserRepository.selectedUser.value = SelectedUserModel( userInfo = UserInfo(0, "Current user", 0), - selectionStatus = SelectionStatus.SELECTION_COMPLETE + selectionStatus = SelectionStatus.SELECTION_COMPLETE, ) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/GlanceableHubWidgetManagerServiceTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/GlanceableHubWidgetManagerServiceTest.kt new file mode 100644 index 000000000000..44ce08514dee --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/GlanceableHubWidgetManagerServiceTest.kt @@ -0,0 +1,319 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.widgets + +import android.appwidget.AppWidgetHost +import android.appwidget.AppWidgetProviderInfo +import android.content.ComponentName +import android.content.Intent +import android.os.UserHandle +import android.testing.TestableLooper +import android.widget.RemoteViews +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepository +import com.android.systemui.communal.shared.model.CommunalWidgetContentModel +import com.android.systemui.communal.shared.model.fakeGlanceableHubMultiUserHelper +import com.android.systemui.communal.widgets.IGlanceableHubWidgetManagerService.IGlanceableHubWidgetsListener +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testScope +import com.android.systemui.log.logcatLogBuffer +import com.android.systemui.testKosmos +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@TestableLooper.RunWithLooper(setAsMainLooper = true) +@RunWith(AndroidJUnit4::class) +class GlanceableHubWidgetManagerServiceTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + + private val appWidgetHostListenerCaptor = argumentCaptor<AppWidgetHost.AppWidgetHostListener>() + + private val widgetRepository = kosmos.fakeCommunalWidgetRepository + private val appWidgetHost = mock<CommunalAppWidgetHost>() + private val communalWidgetHost = mock<CommunalWidgetHost>() + private val multiUserHelper = kosmos.fakeGlanceableHubMultiUserHelper + + private lateinit var underTest: GlanceableHubWidgetManagerService + + @Before + fun setup() { + underTest = + GlanceableHubWidgetManagerService( + widgetRepository, + appWidgetHost, + communalWidgetHost, + multiUserHelper, + logcatLogBuffer("GlanceableHubWidgetManagerServiceTest"), + ) + } + + @Test + fun appWidgetHost_listenWhenServiceIsBound() { + underTest.onCreate() + verify(appWidgetHost).startListening() + verify(communalWidgetHost).startObservingHost() + verify(appWidgetHost, never()).stopListening() + verify(communalWidgetHost, never()).stopObservingHost() + + underTest.onDestroy() + verify(appWidgetHost).stopListening() + verify(communalWidgetHost).stopObservingHost() + } + + @Test + fun widgetsListener_getWidgetUpdates() = + testScope.runTest { + setupWidgets() + + // Bind service + val binder = underTest.onBind(Intent()) + val service = IGlanceableHubWidgetManagerService.Stub.asInterface(binder) + + // Verify the update is as expected + val widgets by collectLastValue(service.listenForWidgetUpdates()) + assertThat(widgets).hasSize(3) + assertThat(widgets?.get(0)?.has(1, "pkg_1/cls_1", 0, 3)).isTrue() + assertThat(widgets?.get(1)?.has(2, "pkg_2/cls_2", 1, 3)).isTrue() + assertThat(widgets?.get(2)?.has(3, "pkg_3/cls_3", 2, 6)).isTrue() + } + + @Test + fun widgetsListener_multipleListeners_eachGetsWidgetUpdates() = + testScope.runTest { + setupWidgets() + + // Bind service + val binder = underTest.onBind(Intent()) + val service = IGlanceableHubWidgetManagerService.Stub.asInterface(binder) + + // Verify the update for the first listener is as expected + val widgets1 by collectLastValue(service.listenForWidgetUpdates()) + assertThat(widgets1).hasSize(3) + assertThat(widgets1?.get(0)?.has(1, "pkg_1/cls_1", 0, 3)).isTrue() + assertThat(widgets1?.get(1)?.has(2, "pkg_2/cls_2", 1, 3)).isTrue() + assertThat(widgets1?.get(2)?.has(3, "pkg_3/cls_3", 2, 6)).isTrue() + + // Verify the update for the second listener is as expected + val widgets2 by collectLastValue(service.listenForWidgetUpdates()) + assertThat(widgets2).hasSize(3) + assertThat(widgets2?.get(0)?.has(1, "pkg_1/cls_1", 0, 3)).isTrue() + assertThat(widgets2?.get(1)?.has(2, "pkg_2/cls_2", 1, 3)).isTrue() + assertThat(widgets2?.get(2)?.has(3, "pkg_3/cls_3", 2, 6)).isTrue() + } + + @Test + fun setAppWidgetHostListener_getUpdates() = + testScope.runTest { + // Bind service + val binder = underTest.onBind(Intent()) + val service = IGlanceableHubWidgetManagerService.Stub.asInterface(binder) + + // Set listener + val listener = mock<IGlanceableHubWidgetManagerService.IAppWidgetHostListener>() + service.setAppWidgetHostListener(1, listener) + + // Verify a listener is set on the host + verify(appWidgetHost).setListener(eq(1), appWidgetHostListenerCaptor.capture()) + val appWidgetHostListener = appWidgetHostListenerCaptor.firstValue + + // Each update should be passed to the listener + val providerInfo = mock<AppWidgetProviderInfo>() + appWidgetHostListener.onUpdateProviderInfo(providerInfo) + verify(listener).onUpdateProviderInfo(providerInfo) + + val remoteViews = mock<RemoteViews>() + appWidgetHostListener.updateAppWidget(remoteViews) + verify(listener).updateAppWidget(remoteViews) + + appWidgetHostListener.updateAppWidgetDeferred("pkg", 1) + verify(listener).updateAppWidgetDeferred("pkg", 1) + + appWidgetHostListener.onViewDataChanged(1) + verify(listener).onViewDataChanged(1) + } + + @Test + fun addWidget_getWidgetUpdate() = + testScope.runTest { + setupWidgets() + + // Bind service + val binder = underTest.onBind(Intent()) + val service = IGlanceableHubWidgetManagerService.Stub.asInterface(binder) + + // Verify the update is as expected + val widgets by collectLastValue(service.listenForWidgetUpdates()) + assertThat(widgets).hasSize(3) + assertThat(widgets?.get(0)?.has(1, "pkg_1/cls_1", 0, 3)).isTrue() + assertThat(widgets?.get(1)?.has(2, "pkg_2/cls_2", 1, 3)).isTrue() + assertThat(widgets?.get(2)?.has(3, "pkg_3/cls_3", 2, 6)).isTrue() + + // Add a widget + service.addWidget(ComponentName("pkg_4", "cls_4"), UserHandle.of(0), 3) + runCurrent() + + // Verify an update pushed with widget 4 added + assertThat(widgets).hasSize(4) + assertThat(widgets?.get(0)?.has(1, "pkg_1/cls_1", 0, 3)).isTrue() + assertThat(widgets?.get(1)?.has(2, "pkg_2/cls_2", 1, 3)).isTrue() + assertThat(widgets?.get(2)?.has(3, "pkg_3/cls_3", 2, 6)).isTrue() + assertThat(widgets?.get(3)?.has(4, "pkg_4/cls_4", 3, 3)).isTrue() + } + + @Test + fun deleteWidget_getWidgetUpdate() = + testScope.runTest { + setupWidgets() + + // Bind service + val binder = underTest.onBind(Intent()) + val service = IGlanceableHubWidgetManagerService.Stub.asInterface(binder) + + // Verify the update is as expected + val widgets by collectLastValue(service.listenForWidgetUpdates()) + assertThat(widgets).hasSize(3) + assertThat(widgets?.get(0)?.has(1, "pkg_1/cls_1", 0, 3)).isTrue() + assertThat(widgets?.get(1)?.has(2, "pkg_2/cls_2", 1, 3)).isTrue() + assertThat(widgets?.get(2)?.has(3, "pkg_3/cls_3", 2, 6)).isTrue() + + // Delete a widget + service.deleteWidget(1) + runCurrent() + + // Verify an update pushed with widget 1 removed + assertThat(widgets).hasSize(2) + assertThat(widgets?.get(0)?.has(2, "pkg_2/cls_2", 1, 3)).isTrue() + assertThat(widgets?.get(1)?.has(3, "pkg_3/cls_3", 2, 6)).isTrue() + } + + @Test + fun updateWidgetOrder_getWidgetUpdate() = + testScope.runTest { + setupWidgets() + + // Bind service + val binder = underTest.onBind(Intent()) + val service = IGlanceableHubWidgetManagerService.Stub.asInterface(binder) + + // Verify the update is as expected + val widgets by collectLastValue(service.listenForWidgetUpdates()) + assertThat(widgets).hasSize(3) + assertThat(widgets?.get(0)?.has(1, "pkg_1/cls_1", 0, 3)).isTrue() + assertThat(widgets?.get(1)?.has(2, "pkg_2/cls_2", 1, 3)).isTrue() + assertThat(widgets?.get(2)?.has(3, "pkg_3/cls_3", 2, 6)).isTrue() + + // Update widget order + service.updateWidgetOrder(intArrayOf(1, 2, 3), intArrayOf(2, 1, 0)) + runCurrent() + + // Verify an update pushed with the new order + assertThat(widgets).hasSize(3) + assertThat(widgets?.get(0)?.has(3, "pkg_3/cls_3", 0, 6)).isTrue() + assertThat(widgets?.get(1)?.has(2, "pkg_2/cls_2", 1, 3)).isTrue() + assertThat(widgets?.get(2)?.has(1, "pkg_1/cls_1", 2, 3)).isTrue() + } + + @Test + fun resizeWidget_getWidgetUpdate() = + testScope.runTest { + setupWidgets() + + // Bind service + val binder = underTest.onBind(Intent()) + val service = IGlanceableHubWidgetManagerService.Stub.asInterface(binder) + + // Verify the update is as expected + val widgets by collectLastValue(service.listenForWidgetUpdates()) + assertThat(widgets).hasSize(3) + assertThat(widgets?.get(0)?.has(1, "pkg_1/cls_1", 0, 3)).isTrue() + assertThat(widgets?.get(1)?.has(2, "pkg_2/cls_2", 1, 3)).isTrue() + assertThat(widgets?.get(2)?.has(3, "pkg_3/cls_3", 2, 6)).isTrue() + + // Resize widget 1 from spanY 3 to 6 + service.resizeWidget(1, 6, intArrayOf(1, 2, 3), intArrayOf(0, 1, 2)) + runCurrent() + + // Verify an update pushed with the new size for widget 1 + assertThat(widgets).hasSize(3) + assertThat(widgets?.get(0)?.has(1, "pkg_1/cls_1", 0, 6)).isTrue() + assertThat(widgets?.get(1)?.has(2, "pkg_2/cls_2", 1, 3)).isTrue() + assertThat(widgets?.get(2)?.has(3, "pkg_3/cls_3", 2, 6)).isTrue() + } + + private fun setupWidgets() { + widgetRepository.addWidget( + appWidgetId = 1, + componentName = "pkg_1/cls_1", + rank = 0, + spanY = 3, + ) + widgetRepository.addWidget( + appWidgetId = 2, + componentName = "pkg_2/cls_2", + rank = 1, + spanY = 3, + ) + widgetRepository.addWidget( + appWidgetId = 3, + componentName = "pkg_3/cls_3", + rank = 2, + spanY = 6, + ) + } + + private fun IGlanceableHubWidgetManagerService.listenForWidgetUpdates() = + conflatedCallbackFlow<List<CommunalWidgetContentModel>> { + val listener = + object : IGlanceableHubWidgetsListener.Stub() { + override fun onWidgetsUpdated(widgets: List<CommunalWidgetContentModel>) { + trySend(widgets) + } + } + addWidgetsListener(listener) + awaitClose { removeWidgetsListener(listener) } + } + + private fun CommunalWidgetContentModel.has( + appWidgetId: Int, + componentName: String, + rank: Int, + spanY: Int, + ): Boolean { + return this is CommunalWidgetContentModel.Available && + this.appWidgetId == appWidgetId && + this.providerInfo.provider.flattenToString() == componentName && + this.rank == rank && + this.spanY == spanY + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/WidgetConfigurationControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/WidgetConfigurationControllerTest.kt index 55fafdff795f..5d4eaf07be25 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/WidgetConfigurationControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/WidgetConfigurationControllerTest.kt @@ -22,6 +22,7 @@ import androidx.activity.ComponentActivity import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.communal.shared.model.fakeGlanceableHubMultiUserHelper import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.testKosmos @@ -55,7 +56,12 @@ class WidgetConfigurationControllerTest : SysuiTestCase() { fun setUp() { MockitoAnnotations.initMocks(this) underTest = - WidgetConfigurationController(ownerActivity, appWidgetHost, kosmos.testDispatcher) + WidgetConfigurationController( + ownerActivity, + { appWidgetHost }, + kosmos.testDispatcher, + kosmos.fakeGlanceableHubMultiUserHelper, + ) } @Test @@ -68,7 +74,7 @@ class WidgetConfigurationControllerTest : SysuiTestCase() { eq(123), anyInt(), eq(WidgetConfigurationController.REQUEST_CODE), - any() + any(), ) ) .thenThrow(ActivityNotFoundException()) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt index 2b7e7adbe022..20d66155e5ca 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorTest.kt @@ -16,34 +16,57 @@ package com.android.systemui.deviceentry.domain.interactor +import android.hardware.face.FaceManager import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.compose.animation.scene.ObservableTransitionState +import com.android.keyguard.keyguardUpdateMonitor import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.biometrics.authController import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository import com.android.systemui.biometrics.shared.model.FingerprintSensorType import com.android.systemui.biometrics.shared.model.SensorStrength +import com.android.systemui.bouncer.data.repository.keyguardBouncerRepository import com.android.systemui.coroutines.collectLastValue import com.android.systemui.deviceentry.shared.model.FailedFaceAuthenticationStatus +import com.android.systemui.deviceentry.shared.model.SuccessFaceAuthenticationStatus +import com.android.systemui.flags.DisableSceneContainer +import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.keyevent.data.repository.fakeKeyEventRepository import com.android.systemui.keyguard.data.repository.biometricSettingsRepository +import com.android.systemui.keyguard.data.repository.configureKeyguardBypass import com.android.systemui.keyguard.data.repository.deviceEntryFingerprintAuthRepository import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository +import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.shared.model.BiometricUnlockMode import com.android.systemui.keyguard.shared.model.BiometricUnlockSource import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus +import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus import com.android.systemui.kosmos.testScope import com.android.systemui.power.data.repository.powerRepository import com.android.systemui.power.shared.model.WakeSleepReason import com.android.systemui.power.shared.model.WakefulnessState +import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.scene.shared.flag.SceneContainerFlag +import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.statusbar.phone.dozeScrimController +import com.android.systemui.statusbar.phone.screenOffAnimationController import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @@ -51,24 +74,52 @@ import org.junit.runner.RunWith class DeviceEntryHapticsInteractorTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope - private val underTest = kosmos.deviceEntryHapticsInteractor + private lateinit var underTest: DeviceEntryHapticsInteractor + + @Before + fun setup() { + if (SceneContainerFlag.isEnabled) { + whenever(kosmos.authController.isUdfpsFingerDown).thenReturn(false) + whenever(kosmos.dozeScrimController.isPulsing).thenReturn(false) + whenever(kosmos.keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())) + .thenReturn(true) + whenever(kosmos.screenOffAnimationController.isKeyguardShowDelayed()).thenReturn(false) + + // Dependencies for DeviceEntrySourceInteractor#biometricUnlockStateOnKeyguardDismissed + whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true) + whenever(kosmos.keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())) + .thenReturn(true) + + // Mock authenticationMethodIsSecure true + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Pin + ) + + kosmos.keyguardBouncerRepository.setAlternateVisible(false) + kosmos.sceneInteractor.changeScene(Scenes.Lockscreen, "reason") + } else { + underTest = kosmos.deviceEntryHapticsInteractor + } + } + + @DisableSceneContainer @Test fun nonPowerButtonFPS_vibrateSuccess() = testScope.runTest { val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic) - setFingerprintSensorType(FingerprintSensorType.UDFPS_ULTRASONIC) + enrollFingerprint(FingerprintSensorType.UDFPS_ULTRASONIC) runCurrent() - enterDeviceFromBiometricUnlock() + enterDeviceFromFingerprintUnlockLegacy() assertThat(playSuccessHaptic).isNotNull() } + @DisableSceneContainer @Test fun powerButtonFPS_vibrateSuccess() = testScope.runTest { val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic) - setPowerButtonFingerprintProperty() - setFingerprintEnrolled() + enrollFingerprint(FingerprintSensorType.POWER_BUTTON) kosmos.fakeKeyEventRepository.setPowerButtonDown(false) // It's been 10 seconds since the last power button wakeup @@ -76,16 +127,16 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() { advanceTimeBy(10000) runCurrent() - enterDeviceFromBiometricUnlock() + enterDeviceFromFingerprintUnlockLegacy() assertThat(playSuccessHaptic).isNotNull() } + @DisableSceneContainer @Test fun powerButtonFPS_powerDown_doNotVibrateSuccess() = testScope.runTest { val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic) - setPowerButtonFingerprintProperty() - setFingerprintEnrolled() + enrollFingerprint(FingerprintSensorType.POWER_BUTTON) kosmos.fakeKeyEventRepository.setPowerButtonDown(true) // power button is currently DOWN // It's been 10 seconds since the last power button wakeup @@ -93,16 +144,16 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() { advanceTimeBy(10000) runCurrent() - enterDeviceFromBiometricUnlock() + enterDeviceFromFingerprintUnlockLegacy() assertThat(playSuccessHaptic).isNull() } + @DisableSceneContainer @Test fun powerButtonFPS_powerButtonRecentlyPressed_doNotVibrateSuccess() = testScope.runTest { val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic) - setPowerButtonFingerprintProperty() - setFingerprintEnrolled() + enrollFingerprint(FingerprintSensorType.POWER_BUTTON) kosmos.fakeKeyEventRepository.setPowerButtonDown(false) // It's only been 50ms since the last power button wakeup @@ -110,7 +161,7 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() { advanceTimeBy(50) runCurrent() - enterDeviceFromBiometricUnlock() + enterDeviceFromFingerprintUnlockLegacy() assertThat(playSuccessHaptic).isNull() } @@ -118,7 +169,7 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() { fun nonPowerButtonFPS_vibrateError() = testScope.runTest { val playErrorHaptic by collectLastValue(underTest.playErrorHaptic) - setFingerprintSensorType(FingerprintSensorType.UDFPS_ULTRASONIC) + enrollFingerprint(FingerprintSensorType.UDFPS_ULTRASONIC) runCurrent() fingerprintFailure() assertThat(playErrorHaptic).isNotNull() @@ -128,8 +179,8 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() { fun nonPowerButtonFPS_coExFaceFailure_doNotVibrateError() = testScope.runTest { val playErrorHaptic by collectLastValue(underTest.playErrorHaptic) - setFingerprintSensorType(FingerprintSensorType.UDFPS_ULTRASONIC) - coExEnrolledAndEnabled() + enrollFingerprint(FingerprintSensorType.UDFPS_ULTRASONIC) + enrollFace() runCurrent() faceFailure() assertThat(playErrorHaptic).isNull() @@ -139,8 +190,7 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() { fun powerButtonFPS_vibrateError() = testScope.runTest { val playErrorHaptic by collectLastValue(underTest.playErrorHaptic) - setPowerButtonFingerprintProperty() - setFingerprintEnrolled() + enrollFingerprint(FingerprintSensorType.POWER_BUTTON) runCurrent() fingerprintFailure() assertThat(playErrorHaptic).isNotNull() @@ -150,15 +200,143 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() { fun powerButtonFPS_powerDown_doNotVibrateError() = testScope.runTest { val playErrorHaptic by collectLastValue(underTest.playErrorHaptic) - setPowerButtonFingerprintProperty() - setFingerprintEnrolled() + enrollFingerprint(FingerprintSensorType.POWER_BUTTON) kosmos.fakeKeyEventRepository.setPowerButtonDown(true) runCurrent() fingerprintFailure() assertThat(playErrorHaptic).isNull() } - private suspend fun enterDeviceFromBiometricUnlock() { + @EnableSceneContainer + @Test + fun playSuccessHaptic_onDeviceEntryFromUdfps_sceneContainer() = + testScope.runTest { + kosmos.configureKeyguardBypass(isBypassAvailable = false) + underTest = kosmos.deviceEntryHapticsInteractor + val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic) + enrollFingerprint(FingerprintSensorType.UDFPS_ULTRASONIC) + runCurrent() + configureDeviceEntryFromBiometricSource(isFpUnlock = true) + verifyDeviceEntryFromFingerprintAuth() + assertThat(playSuccessHaptic).isNotNull() + } + + @EnableSceneContainer + @Test + fun playSuccessHaptic_onDeviceEntryFromSfps_sceneContainer() = + testScope.runTest { + kosmos.configureKeyguardBypass(isBypassAvailable = false) + underTest = kosmos.deviceEntryHapticsInteractor + val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic) + enrollFingerprint(FingerprintSensorType.POWER_BUTTON) + kosmos.fakeKeyEventRepository.setPowerButtonDown(false) + + // It's been 10 seconds since the last power button wakeup + setAwakeFromPowerButton() + advanceTimeBy(10000) + runCurrent() + + configureDeviceEntryFromBiometricSource(isFpUnlock = true) + verifyDeviceEntryFromFingerprintAuth() + assertThat(playSuccessHaptic).isNotNull() + } + + @EnableSceneContainer + @Test + fun playSuccessHaptic_onDeviceEntryFromFaceAuth_sceneContainer() = + testScope.runTest { + enrollFace() + kosmos.configureKeyguardBypass(isBypassAvailable = true) + underTest = kosmos.deviceEntryHapticsInteractor + val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic) + configureDeviceEntryFromBiometricSource(isFaceUnlock = true) + verifyDeviceEntryFromFaceAuth() + assertThat(playSuccessHaptic).isNotNull() + } + + @EnableSceneContainer + @Test + fun skipSuccessHaptic_onDeviceEntryFromSfps_whenPowerDown_sceneContainer() = + testScope.runTest { + kosmos.configureKeyguardBypass(isBypassAvailable = false) + underTest = kosmos.deviceEntryHapticsInteractor + val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic) + enrollFingerprint(FingerprintSensorType.POWER_BUTTON) + // power button is currently DOWN + kosmos.fakeKeyEventRepository.setPowerButtonDown(true) + + // It's been 10 seconds since the last power button wakeup + setAwakeFromPowerButton() + advanceTimeBy(10000) + runCurrent() + + configureDeviceEntryFromBiometricSource(isFpUnlock = true) + verifyDeviceEntryFromFingerprintAuth() + assertThat(playSuccessHaptic).isNull() + } + + @EnableSceneContainer + @Test + fun skipSuccessHaptic_onDeviceEntryFromSfps_whenPowerButtonRecentlyPressed_sceneContainer() = + testScope.runTest { + kosmos.configureKeyguardBypass(isBypassAvailable = false) + underTest = kosmos.deviceEntryHapticsInteractor + val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic) + enrollFingerprint(FingerprintSensorType.POWER_BUTTON) + kosmos.fakeKeyEventRepository.setPowerButtonDown(false) + + // It's only been 50ms since the last power button wakeup + setAwakeFromPowerButton() + advanceTimeBy(50) + runCurrent() + + configureDeviceEntryFromBiometricSource(isFpUnlock = true) + verifyDeviceEntryFromFingerprintAuth() + assertThat(playSuccessHaptic).isNull() + } + + // Mock dependencies for DeviceEntrySourceInteractor#deviceEntryFromBiometricSource + private fun configureDeviceEntryFromBiometricSource( + isFpUnlock: Boolean = false, + isFaceUnlock: Boolean = false, + ) { + // Mock DeviceEntrySourceInteractor#deviceEntryBiometricAuthSuccessState + if (isFpUnlock) { + kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( + SuccessFingerprintAuthenticationStatus(0, true) + ) + } + if (isFaceUnlock) { + kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus( + SuccessFaceAuthenticationStatus( + FaceManager.AuthenticationResult(null, null, 0, true) + ) + ) + + // Mock DeviceEntrySourceInteractor#faceWakeAndUnlockMode = MODE_UNLOCK_COLLAPSING + kosmos.sceneInteractor.setTransitionState( + MutableStateFlow<ObservableTransitionState>( + ObservableTransitionState.Idle(Scenes.Lockscreen) + ) + ) + } + underTest = kosmos.deviceEntryHapticsInteractor + } + + private fun TestScope.verifyDeviceEntryFromFingerprintAuth() { + val deviceEntryFromBiometricSource by + collectLastValue(kosmos.deviceEntrySourceInteractor.deviceEntryFromBiometricSource) + assertThat(deviceEntryFromBiometricSource) + .isEqualTo(BiometricUnlockSource.FINGERPRINT_SENSOR) + } + + private fun TestScope.verifyDeviceEntryFromFaceAuth() { + val deviceEntryFromBiometricSource by + collectLastValue(kosmos.deviceEntrySourceInteractor.deviceEntryFromBiometricSource) + assertThat(deviceEntryFromBiometricSource).isEqualTo(BiometricUnlockSource.FACE_SENSOR) + } + + private fun enterDeviceFromFingerprintUnlockLegacy() { kosmos.fakeKeyguardRepository.setBiometricUnlockSource( BiometricUnlockSource.FINGERPRINT_SENSOR ) @@ -177,21 +355,22 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() { ) } - private fun setFingerprintSensorType(fingerprintSensorType: FingerprintSensorType) { - kosmos.fingerprintPropertyRepository.setProperties( - sensorId = 0, - strength = SensorStrength.STRONG, - sensorType = fingerprintSensorType, - sensorLocations = mapOf(), - ) - } - - private fun setPowerButtonFingerprintProperty() { - setFingerprintSensorType(FingerprintSensorType.POWER_BUTTON) + private fun enrollFingerprint(fingerprintSensorType: FingerprintSensorType?) { + if (fingerprintSensorType == null) { + kosmos.biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false) + } else { + kosmos.biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true) + kosmos.fingerprintPropertyRepository.setProperties( + sensorId = 0, + strength = SensorStrength.STRONG, + sensorType = fingerprintSensorType, + sensorLocations = mapOf(), + ) + } } - private fun setFingerprintEnrolled() { - kosmos.biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true) + private fun enrollFace() { + kosmos.biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true) } private fun setAwakeFromPowerButton() { @@ -202,9 +381,4 @@ class DeviceEntryHapticsInteractorTest : SysuiTestCase() { powerButtonLaunchGestureTriggered = false, ) } - - private fun coExEnrolledAndEnabled() { - setFingerprintEnrolled() - kosmos.biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true) - } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntrySourceInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntrySourceInteractorTest.kt index 2e4c97bc80d0..b3c891dc9ac6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntrySourceInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntrySourceInteractorTest.kt @@ -16,21 +16,51 @@ package com.android.systemui.deviceentry.domain.interactor +import android.hardware.face.FaceManager import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.compose.animation.scene.ObservableTransitionState +import com.android.compose.animation.scene.SceneKey +import com.android.keyguard.keyguardUpdateMonitor import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.biometrics.authController +import com.android.systemui.bouncer.data.repository.keyguardBouncerRepository import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.deviceentry.shared.model.FaceAuthenticationStatus +import com.android.systemui.deviceentry.shared.model.SuccessFaceAuthenticationStatus +import com.android.systemui.flags.DisableSceneContainer +import com.android.systemui.flags.EnableSceneContainer +import com.android.systemui.keyguard.data.repository.configureKeyguardBypass +import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository +import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository +import com.android.systemui.keyguard.data.repository.keyguardBypassRepository +import com.android.systemui.keyguard.data.repository.keyguardOcclusionRepository +import com.android.systemui.keyguard.data.repository.verifyCallback import com.android.systemui.keyguard.shared.model.BiometricUnlockMode import com.android.systemui.keyguard.shared.model.BiometricUnlockSource +import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus import com.android.systemui.kosmos.testScope +import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.scene.shared.flag.SceneContainerFlag +import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.statusbar.phone.dozeScrimController +import com.android.systemui.statusbar.phone.screenOffAnimationController +import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_OPENED +import com.android.systemui.statusbar.policy.devicePostureController import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @@ -38,45 +68,328 @@ import org.junit.runner.RunWith class DeviceEntrySourceInteractorTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope - private val keyguardRepository = kosmos.fakeKeyguardRepository - private val underTest = kosmos.deviceEntrySourceInteractor + private lateinit var underTest: DeviceEntrySourceInteractor + @Before + fun setup() { + if (SceneContainerFlag.isEnabled) { + whenever(kosmos.authController.isUdfpsFingerDown).thenReturn(false) + whenever(kosmos.dozeScrimController.isPulsing).thenReturn(false) + whenever(kosmos.keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())) + .thenReturn(true) + whenever(kosmos.screenOffAnimationController.isKeyguardShowDelayed()).thenReturn(false) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.Pin + ) + } else { + underTest = kosmos.deviceEntrySourceInteractor + } + } + + @DisableSceneContainer @Test fun deviceEntryFromFaceUnlock() = testScope.runTest { val deviceEntryFromBiometricAuthentication by collectLastValue(underTest.deviceEntryFromBiometricSource) - keyguardRepository.setBiometricUnlockState( + + kosmos.fakeKeyguardRepository.setBiometricUnlockState( BiometricUnlockMode.WAKE_AND_UNLOCK, BiometricUnlockSource.FACE_SENSOR, ) runCurrent() + assertThat(deviceEntryFromBiometricAuthentication) .isEqualTo(BiometricUnlockSource.FACE_SENSOR) } + @DisableSceneContainer @Test - fun deviceEntryFromFingerprintUnlock() = runTest { - val deviceEntryFromBiometricAuthentication by - collectLastValue(underTest.deviceEntryFromBiometricSource) - keyguardRepository.setBiometricUnlockState( - BiometricUnlockMode.WAKE_AND_UNLOCK, - BiometricUnlockSource.FINGERPRINT_SENSOR, - ) - runCurrent() - assertThat(deviceEntryFromBiometricAuthentication) - .isEqualTo(BiometricUnlockSource.FINGERPRINT_SENSOR) - } + fun deviceEntryFromFingerprintUnlock() = + testScope.runTest { + val deviceEntryFromBiometricAuthentication by + collectLastValue(underTest.deviceEntryFromBiometricSource) + + kosmos.fakeKeyguardRepository.setBiometricUnlockState( + BiometricUnlockMode.WAKE_AND_UNLOCK, + BiometricUnlockSource.FINGERPRINT_SENSOR, + ) + runCurrent() + + assertThat(deviceEntryFromBiometricAuthentication) + .isEqualTo(BiometricUnlockSource.FINGERPRINT_SENSOR) + } + + @DisableSceneContainer + @Test + fun noDeviceEntry() = + testScope.runTest { + val deviceEntryFromBiometricAuthentication by + collectLastValue(underTest.deviceEntryFromBiometricSource) + + kosmos.fakeKeyguardRepository.setBiometricUnlockState( + BiometricUnlockMode.ONLY_WAKE, // doesn't dismiss keyguard: + BiometricUnlockSource.FINGERPRINT_SENSOR, + ) + runCurrent() + + assertThat(deviceEntryFromBiometricAuthentication).isNull() + } + + @EnableSceneContainer + @Test + fun deviceEntryFromFingerprintUnlockOnLockScreen_sceneContainerEnabled() = + testScope.runTest { + underTest = kosmos.deviceEntrySourceInteractor + val deviceEntryFromBiometricSource by + collectLastValue(underTest.deviceEntryFromBiometricSource) + + whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true) + configureDeviceEntryBiometricAuthSuccessState(isFingerprintAuth = true) + configureBiometricUnlockState( + alternateBouncerVisible = false, + sceneKey = Scenes.Lockscreen, + ) + runCurrent() + + kosmos.configureKeyguardBypass(isBypassAvailable = true) + underTest = kosmos.deviceEntrySourceInteractor + + assertThat(deviceEntryFromBiometricSource) + .isEqualTo(BiometricUnlockSource.FINGERPRINT_SENSOR) + } + + @EnableSceneContainer + @Test + fun deviceEntryFromFingerprintUnlockOnAod_sceneContainerEnabled() = + testScope.runTest { + underTest = kosmos.deviceEntrySourceInteractor + val deviceEntryFromBiometricSource by + collectLastValue(underTest.deviceEntryFromBiometricSource) + + whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(false) + configureDeviceEntryBiometricAuthSuccessState(isFingerprintAuth = true) + configureBiometricUnlockState(alternateBouncerVisible = false, sceneKey = Scenes.Dream) + runCurrent() + + assertThat(deviceEntryFromBiometricSource) + .isEqualTo(BiometricUnlockSource.FINGERPRINT_SENSOR) + } + + @EnableSceneContainer + @Test + fun deviceEntryFromFingerprintUnlockOnBouncer_sceneContainerEnabled() = + testScope.runTest { + underTest = kosmos.deviceEntrySourceInteractor + val deviceEntryFromBiometricSource by + collectLastValue(underTest.deviceEntryFromBiometricSource) + + whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true) + configureDeviceEntryBiometricAuthSuccessState(isFingerprintAuth = true) + configureBiometricUnlockState( + alternateBouncerVisible = false, + sceneKey = Scenes.Bouncer, + ) + runCurrent() + assertThat(deviceEntryFromBiometricSource) + .isEqualTo(BiometricUnlockSource.FINGERPRINT_SENSOR) + } + + @EnableSceneContainer @Test - fun noDeviceEntry() = runTest { - val deviceEntryFromBiometricAuthentication by - collectLastValue(underTest.deviceEntryFromBiometricSource) - keyguardRepository.setBiometricUnlockState( - BiometricUnlockMode.ONLY_WAKE, // doesn't dismiss keyguard: - BiometricUnlockSource.FINGERPRINT_SENSOR, + fun deviceEntryFromFingerprintUnlockOnShade_sceneContainerEnabled() = + testScope.runTest { + underTest = kosmos.deviceEntrySourceInteractor + val deviceEntryFromBiometricSource by + collectLastValue(underTest.deviceEntryFromBiometricSource) + + whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true) + configureDeviceEntryBiometricAuthSuccessState(isFingerprintAuth = true) + configureBiometricUnlockState( + alternateBouncerVisible = false, + sceneKey = Scenes.Lockscreen, + ) + runCurrent() + + assertThat(deviceEntryFromBiometricSource) + .isEqualTo(BiometricUnlockSource.FINGERPRINT_SENSOR) + } + + @EnableSceneContainer + @Test + fun deviceEntryFromFingerprintUnlockOnAlternateBouncer_sceneContainerEnabled() = + testScope.runTest { + underTest = kosmos.deviceEntrySourceInteractor + val deviceEntryFromBiometricSource by + collectLastValue(underTest.deviceEntryFromBiometricSource) + + whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true) + configureDeviceEntryBiometricAuthSuccessState(isFingerprintAuth = true) + configureBiometricUnlockState( + alternateBouncerVisible = true, + sceneKey = Scenes.Lockscreen, + ) + runCurrent() + + assertThat(deviceEntryFromBiometricSource) + .isEqualTo(BiometricUnlockSource.FINGERPRINT_SENSOR) + } + + @EnableSceneContainer + @Test + fun deviceEntryFromFaceUnlockOnLockScreen_bypassAvailable_sceneContainerEnabled() = + testScope.runTest { + kosmos.configureKeyguardBypass(isBypassAvailable = true) + underTest = kosmos.deviceEntrySourceInteractor + + val deviceEntryFromBiometricSource by + collectLastValue(underTest.deviceEntryFromBiometricSource) + + whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true) + configureDeviceEntryBiometricAuthSuccessState(isFaceAuth = true) + configureBiometricUnlockState( + alternateBouncerVisible = false, + sceneKey = Scenes.Lockscreen, + ) + runCurrent() + + assertThat(deviceEntryFromBiometricSource).isEqualTo(BiometricUnlockSource.FACE_SENSOR) + } + + @EnableSceneContainer + @Test + fun deviceEntryFromFaceUnlockOnLockScreen_bypassDisabled_sceneContainerEnabled() = + testScope.runTest { + kosmos.configureKeyguardBypass(isBypassAvailable = false) + underTest = kosmos.deviceEntrySourceInteractor + + collectLastValue(kosmos.keyguardBypassRepository.isBypassAvailable) + runCurrent() + + val postureControllerCallback = kosmos.devicePostureController.verifyCallback() + postureControllerCallback.onPostureChanged(DEVICE_POSTURE_OPENED) + + val deviceEntryFromBiometricSource by + collectLastValue(underTest.deviceEntryFromBiometricSource) + + whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true) + configureDeviceEntryBiometricAuthSuccessState(isFaceAuth = true) + configureBiometricUnlockState( + alternateBouncerVisible = false, + sceneKey = Scenes.Lockscreen, + ) + runCurrent() + + // MODE_NONE does not dismiss keyguard + assertThat(deviceEntryFromBiometricSource).isNull() + } + + @EnableSceneContainer + @Test + fun deviceEntryFromFaceUnlockOnBouncer_sceneContainerEnabled() = + testScope.runTest { + kosmos.configureKeyguardBypass(isBypassAvailable = true) + underTest = kosmos.deviceEntrySourceInteractor + val deviceEntryFromBiometricSource by + collectLastValue(underTest.deviceEntryFromBiometricSource) + + whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true) + configureDeviceEntryBiometricAuthSuccessState(isFaceAuth = true) + configureBiometricUnlockState( + alternateBouncerVisible = false, + sceneKey = Scenes.Bouncer, + ) + runCurrent() + + assertThat(deviceEntryFromBiometricSource).isEqualTo(BiometricUnlockSource.FACE_SENSOR) + } + + @EnableSceneContainer + @Test + fun deviceEntryFromFaceUnlockOnShade_bypassAvailable_sceneContainerEnabled() = + testScope.runTest { + kosmos.configureKeyguardBypass(isBypassAvailable = true) + underTest = kosmos.deviceEntrySourceInteractor + val deviceEntryFromBiometricSource by + collectLastValue(underTest.deviceEntryFromBiometricSource) + + whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true) + configureDeviceEntryBiometricAuthSuccessState(isFaceAuth = true) + configureBiometricUnlockState(alternateBouncerVisible = false, sceneKey = Scenes.Shade) + runCurrent() + + // MODE_NONE does not dismiss keyguard + assertThat(deviceEntryFromBiometricSource).isNull() + } + + @EnableSceneContainer + @Test + fun deviceEntryFromFaceUnlockOnShade_bypassDisabled_sceneContainerEnabled() = + testScope.runTest { + kosmos.configureKeyguardBypass(isBypassAvailable = false) + underTest = kosmos.deviceEntrySourceInteractor + val deviceEntryFromBiometricSource by + collectLastValue(underTest.deviceEntryFromBiometricSource) + + whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true) + configureDeviceEntryBiometricAuthSuccessState(isFaceAuth = true) + configureBiometricUnlockState( + alternateBouncerVisible = false, + sceneKey = Scenes.Lockscreen, + ) + runCurrent() + + assertThat(deviceEntryFromBiometricSource).isNull() + } + + @EnableSceneContainer + @Test + fun deviceEntryFromFaceUnlockOnAlternateBouncer_sceneContainerEnabled() = + testScope.runTest { + kosmos.configureKeyguardBypass(isBypassAvailable = true) + underTest = kosmos.deviceEntrySourceInteractor + val deviceEntryFromBiometricSource by + collectLastValue(underTest.deviceEntryFromBiometricSource) + + whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true) + kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(onTop = true) + configureDeviceEntryBiometricAuthSuccessState(isFaceAuth = true) + configureBiometricUnlockState( + alternateBouncerVisible = true, + sceneKey = Scenes.Lockscreen, + ) + runCurrent() + + assertThat(deviceEntryFromBiometricSource).isEqualTo(BiometricUnlockSource.FACE_SENSOR) + } + + private fun configureDeviceEntryBiometricAuthSuccessState( + isFingerprintAuth: Boolean = false, + isFaceAuth: Boolean = false, + ) { + if (isFingerprintAuth) { + val successStatus = SuccessFingerprintAuthenticationStatus(0, true) + kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(successStatus) + } + + if (isFaceAuth) { + val successStatus: FaceAuthenticationStatus = + SuccessFaceAuthenticationStatus( + FaceManager.AuthenticationResult(null, null, 0, true) + ) + kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(successStatus) + } + } + + private fun configureBiometricUnlockState( + alternateBouncerVisible: Boolean, + sceneKey: SceneKey, + ) { + kosmos.keyguardBouncerRepository.setAlternateVisible(alternateBouncerVisible) + kosmos.sceneInteractor.changeScene(sceneKey, "reason") + kosmos.sceneInteractor.setTransitionState( + MutableStateFlow<ObservableTransitionState>(ObservableTransitionState.Idle(sceneKey)) ) - runCurrent() - assertThat(deviceEntryFromBiometricAuthentication).isNull() } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractorTest.kt index 77337d36a6b1..a981e2083312 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractorTest.kt @@ -18,6 +18,7 @@ package com.android.systemui.deviceentry.domain.interactor import android.content.Intent import android.content.mockedContext +import android.content.res.Resources import android.hardware.fingerprint.FingerprintManager import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -41,13 +42,16 @@ import com.android.systemui.kosmos.testScope import com.android.systemui.plugins.ActivityStarter.OnDismissAction import com.android.systemui.plugins.activityStarter import com.android.systemui.power.data.repository.fakePowerRepository +import com.android.systemui.res.R import com.android.systemui.testKosmos import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor @@ -55,6 +59,7 @@ import org.mockito.ArgumentMatchers.eq import org.mockito.ArgumentMatchers.isNull import org.mockito.Mockito.never import org.mockito.Mockito.verify +import org.mockito.kotlin.mock @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @@ -63,8 +68,8 @@ class OccludingAppDeviceEntryInteractorTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope - private val underTest = kosmos.occludingAppDeviceEntryInteractor - + private lateinit var underTest: OccludingAppDeviceEntryInteractor + private lateinit var mockedResources: Resources private val fingerprintAuthRepository = kosmos.deviceEntryFingerprintAuthRepository private val keyguardRepository = kosmos.fakeKeyguardRepository private val bouncerRepository = kosmos.keyguardBouncerRepository @@ -74,9 +79,18 @@ class OccludingAppDeviceEntryInteractorTest : SysuiTestCase() { private val mockedContext = kosmos.mockedContext private val mockedActivityStarter = kosmos.activityStarter + @Before + fun setup() { + mockedResources = mock<Resources>() + whenever(mockedContext.resources).thenReturn(mockedResources) + whenever(mockedResources.getBoolean(R.bool.config_goToHomeFromOccludedApps)) + .thenReturn(true) + } + @Test fun fingerprintSuccess_goToHomeScreen() = testScope.runTest { + underTest = kosmos.occludingAppDeviceEntryInteractor givenOnOccludingApp(true) fingerprintAuthRepository.setAuthenticationStatus( SuccessFingerprintAuthenticationStatus(0, true) @@ -86,8 +100,23 @@ class OccludingAppDeviceEntryInteractorTest : SysuiTestCase() { } @Test + fun fingerprintSuccess_configOff_doesNotGoToHomeScreen() = + testScope.runTest { + whenever(mockedResources.getBoolean(R.bool.config_goToHomeFromOccludedApps)) + .thenReturn(false) + underTest = kosmos.occludingAppDeviceEntryInteractor + givenOnOccludingApp(true) + fingerprintAuthRepository.setAuthenticationStatus( + SuccessFingerprintAuthenticationStatus(0, true) + ) + runCurrent() + verifyNeverGoToHomeScreen() + } + + @Test fun fingerprintSuccess_notInteractive_doesNotGoToHomeScreen() = testScope.runTest { + underTest = kosmos.occludingAppDeviceEntryInteractor givenOnOccludingApp(true) powerRepository.setInteractive(false) fingerprintAuthRepository.setAuthenticationStatus( @@ -100,6 +129,7 @@ class OccludingAppDeviceEntryInteractorTest : SysuiTestCase() { @Test fun fingerprintSuccess_dreaming_doesNotGoToHomeScreen() = testScope.runTest { + underTest = kosmos.occludingAppDeviceEntryInteractor givenOnOccludingApp(true) keyguardRepository.setDreaming(true) fingerprintAuthRepository.setAuthenticationStatus( @@ -112,6 +142,7 @@ class OccludingAppDeviceEntryInteractorTest : SysuiTestCase() { @Test fun fingerprintSuccess_notOnOccludingApp_doesNotGoToHomeScreen() = testScope.runTest { + underTest = kosmos.occludingAppDeviceEntryInteractor givenOnOccludingApp(false) fingerprintAuthRepository.setAuthenticationStatus( SuccessFingerprintAuthenticationStatus(0, true) @@ -123,11 +154,12 @@ class OccludingAppDeviceEntryInteractorTest : SysuiTestCase() { @Test fun lockout_goToHomeScreenOnDismissAction() = testScope.runTest { + underTest = kosmos.occludingAppDeviceEntryInteractor givenOnOccludingApp(true) fingerprintAuthRepository.setAuthenticationStatus( ErrorFingerprintAuthenticationStatus( FingerprintManager.FINGERPRINT_ERROR_LOCKOUT, - "lockoutTest" + "lockoutTest", ) ) runCurrent() @@ -137,11 +169,12 @@ class OccludingAppDeviceEntryInteractorTest : SysuiTestCase() { @Test fun lockout_notOnOccludingApp_neverGoToHomeScreen() = testScope.runTest { + underTest = kosmos.occludingAppDeviceEntryInteractor givenOnOccludingApp(false) fingerprintAuthRepository.setAuthenticationStatus( ErrorFingerprintAuthenticationStatus( FingerprintManager.FINGERPRINT_ERROR_LOCKOUT, - "lockoutTest" + "lockoutTest", ) ) runCurrent() @@ -151,11 +184,12 @@ class OccludingAppDeviceEntryInteractorTest : SysuiTestCase() { @Test fun lockout_onOccludingApp_onCommunal_neverGoToHomeScreen() = testScope.runTest { + underTest = kosmos.occludingAppDeviceEntryInteractor givenOnOccludingApp(isOnOccludingApp = true, isOnCommunal = true) fingerprintAuthRepository.setAuthenticationStatus( ErrorFingerprintAuthenticationStatus( FingerprintManager.FINGERPRINT_ERROR_LOCKOUT, - "lockoutTest" + "lockoutTest", ) ) runCurrent() @@ -165,6 +199,7 @@ class OccludingAppDeviceEntryInteractorTest : SysuiTestCase() { @Test fun message_fpFailOnOccludingApp_thenNotOnOccludingApp() = testScope.runTest { + underTest = kosmos.occludingAppDeviceEntryInteractor val message by collectLastValue(underTest.message) givenOnOccludingApp(true) @@ -186,6 +221,7 @@ class OccludingAppDeviceEntryInteractorTest : SysuiTestCase() { @Test fun message_fpErrorHelpFailOnOccludingApp() = testScope.runTest { + underTest = kosmos.occludingAppDeviceEntryInteractor val message by collectLastValue(underTest.message) givenOnOccludingApp(true) @@ -218,6 +254,7 @@ class OccludingAppDeviceEntryInteractorTest : SysuiTestCase() { @Test fun message_fpError_lockoutFilteredOut() = testScope.runTest { + underTest = kosmos.occludingAppDeviceEntryInteractor val message by collectLastValue(underTest.message) givenOnOccludingApp(true) @@ -246,6 +283,7 @@ class OccludingAppDeviceEntryInteractorTest : SysuiTestCase() { @Test fun noMessage_fpErrorsWhileDozing() = testScope.runTest { + underTest = kosmos.occludingAppDeviceEntryInteractor val message by collectLastValue(underTest.message) givenOnOccludingApp(true) @@ -254,7 +292,7 @@ class OccludingAppDeviceEntryInteractorTest : SysuiTestCase() { kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( from = KeyguardState.OCCLUDED, to = KeyguardState.DOZING, - testScope + testScope, ) runCurrent() @@ -283,7 +321,7 @@ class OccludingAppDeviceEntryInteractorTest : SysuiTestCase() { private suspend fun givenOnOccludingApp( isOnOccludingApp: Boolean, - isOnCommunal: Boolean = false + isOnCommunal: Boolean = false, ) { powerRepository.setInteractive(true) keyguardRepository.setIsDozing(false) @@ -305,13 +343,13 @@ class OccludingAppDeviceEntryInteractorTest : SysuiTestCase() { kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( from = KeyguardState.LOCKSCREEN, to = KeyguardState.OCCLUDED, - testScope + testScope, ) } else { kosmos.fakeKeyguardTransitionRepository.sendTransitionSteps( from = KeyguardState.OCCLUDED, to = KeyguardState.LOCKSCREEN, - testScope + testScope, ) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepositoryImplTest.kt index 5a764892584e..f68a1b5a17e9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepositoryImplTest.kt @@ -19,6 +19,7 @@ package com.android.systemui.display.data.repository import android.content.testableContext import android.platform.test.annotations.EnableFlags import android.view.Display +import android.view.layoutInflater import android.view.mockWindowManager import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -49,14 +50,18 @@ class DisplayWindowPropertiesRepositoryImplTest : SysuiTestCase() { private val applicationContext = kosmos.testableContext private val applicationWindowManager = kosmos.mockWindowManager + private val applicationLayoutInflater = kosmos.layoutInflater - private val repo = + // Lazy so that @EnableFlags has time to run before this repo is instantiated + private val repo by lazy { DisplayWindowPropertiesRepositoryImpl( kosmos.applicationCoroutineScope, applicationContext, applicationWindowManager, + kosmos.layoutInflater, fakeDisplayRepository, ) + } @Before fun start() { @@ -81,6 +86,7 @@ class DisplayWindowPropertiesRepositoryImplTest : SysuiTestCase() { windowType = WINDOW_TYPE_FOO, context = applicationContext, windowManager = applicationWindowManager, + layoutInflater = applicationLayoutInflater, ) ) } @@ -102,6 +108,14 @@ class DisplayWindowPropertiesRepositoryImplTest : SysuiTestCase() { } @Test + fun get_nonDefaultDisplayId_returnsNewLayoutInflater() = + testScope.runTest { + val displayContext = repo.get(NON_DEFAULT_DISPLAY_ID, WINDOW_TYPE_FOO) + + assertThat(displayContext.layoutInflater).isNotSameInstanceAs(applicationLayoutInflater) + } + + @Test fun get_multipleCallsForDefaultDisplay_returnsSameInstance() = testScope.runTest { val displayContext = repo.get(DEFAULT_DISPLAY_ID, WINDOW_TYPE_FOO) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamServiceTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamServiceTest.kt index 9300db9a24c8..4317b9f27da6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamServiceTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamServiceTest.kt @@ -16,25 +16,37 @@ package com.android.systemui.dreams.homecontrols import android.app.Activity +import android.content.ComponentName import android.content.Intent +import android.os.powerManager import android.service.controls.ControlsProviderService.CONTROLS_SURFACE_ACTIVITY_PANEL import android.service.controls.ControlsProviderService.CONTROLS_SURFACE_DREAM import android.service.controls.ControlsProviderService.EXTRA_CONTROLS_SURFACE +import android.service.dreams.DreamService import android.window.TaskFragmentInfo +import androidx.lifecycle.testing.TestLifecycleOwner import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.controls.settings.FakeControlsSettingsRepository +import com.android.systemui.dreams.homecontrols.service.TaskFragmentComponent +import com.android.systemui.dreams.homecontrols.shared.model.HomeControlsComponentInfo +import com.android.systemui.dreams.homecontrols.shared.model.fakeHomeControlsDataSource +import com.android.systemui.dreams.homecontrols.shared.model.homeControlsDataSource import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.log.logcatLogBuffer import com.android.systemui.testKosmos +import com.android.systemui.util.time.fakeSystemClock import com.android.systemui.util.wakelock.WakeLockFake import com.google.common.truth.Truth.assertThat -import java.util.Optional +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.advanceUntilIdle +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -47,7 +59,6 @@ import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.times import org.mockito.kotlin.verify -import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @@ -62,13 +73,18 @@ class HomeControlsDreamServiceTest : SysuiTestCase() { WakeLockFake.Builder(context).apply { setWakeLock(fakeWakeLock) } } + private val lifecycleOwner = TestLifecycleOwner(coroutineDispatcher = kosmos.testDispatcher) + private val taskFragmentComponent = mock<TaskFragmentComponent>() private val activity = mock<Activity>() private val onCreateCallback = argumentCaptor<(TaskFragmentInfo) -> Unit>() private val onInfoChangedCallback = argumentCaptor<(TaskFragmentInfo) -> Unit>() private val hideCallback = argumentCaptor<() -> Unit>() - private val dreamServiceDelegate = - mock<DreamServiceDelegate> { on { getActivity(any()) } doReturn activity } + private var dreamService = + mock<DreamService> { + on { activity } doReturn activity + on { redirectWake } doReturn false + } private val taskFragmentComponentFactory = mock<TaskFragmentComponent.Factory> { @@ -82,12 +98,32 @@ class HomeControlsDreamServiceTest : SysuiTestCase() { } doReturn taskFragmentComponent } - private val underTest: HomeControlsDreamService by lazy { buildService() } + private val underTest: HomeControlsDreamServiceImpl by lazy { + with(kosmos) { + HomeControlsDreamServiceImpl( + taskFragmentFactory = taskFragmentComponentFactory, + wakeLockBuilder = fakeWakeLockBuilder, + powerManager = powerManager, + systemClock = fakeSystemClock, + dataSource = homeControlsDataSource, + logBuffer = logcatLogBuffer("HomeControlsDreamServiceTest"), + service = dreamService, + lifecycleOwner = lifecycleOwner, + ) + } + } @Before fun setup() { - whenever(kosmos.controlsComponent.getControlsListingController()) - .thenReturn(Optional.of(kosmos.controlsListingController)) + Dispatchers.setMain(kosmos.testDispatcher) + kosmos.fakeHomeControlsDataSource.setComponentInfo( + HomeControlsComponentInfo(PANEL_COMPONENT, true) + ) + } + + @After + fun tearDown() { + Dispatchers.resetMain() } @Test @@ -108,13 +144,10 @@ class HomeControlsDreamServiceTest : SysuiTestCase() { @Test fun testNotCreatingTaskFragmentComponentWhenActivityIsNull() = testScope.runTest { - val serviceWithNullActivity = - buildService( - mock<DreamServiceDelegate> { on { getActivity(underTest) } doReturn null } - ) - - serviceWithNullActivity.onAttachedToWindow() + dreamService = mock<DreamService> { on { activity } doReturn null } + underTest.onAttachedToWindow() verify(taskFragmentComponentFactory, never()).create(any(), any(), any(), any()) + verify(dreamService).finish() } @Test @@ -137,9 +170,9 @@ class HomeControlsDreamServiceTest : SysuiTestCase() { @Test fun testFinishesDreamWithoutRestartingActivityWhenNotRedirectingWakes() = testScope.runTest { - whenever(dreamServiceDelegate.redirectWake(any())).thenReturn(false) underTest.onAttachedToWindow() onCreateCallback.firstValue.invoke(mock<TaskFragmentInfo>()) + runCurrent() verify(taskFragmentComponent, times(1)).startActivityInTaskFragment(intentMatcher()) // Task fragment becomes empty @@ -149,16 +182,21 @@ class HomeControlsDreamServiceTest : SysuiTestCase() { advanceUntilIdle() // Dream is finished and activity is not restarted verify(taskFragmentComponent, times(1)).startActivityInTaskFragment(intentMatcher()) - verify(dreamServiceDelegate, never()).wakeUp(any()) - verify(dreamServiceDelegate).finish(any()) + verify(dreamService, never()).wakeUp() + verify(dreamService).finish() } @Test fun testRestartsActivityWhenRedirectingWakes() = testScope.runTest { - whenever(dreamServiceDelegate.redirectWake(any())).thenReturn(true) + dreamService = + mock<DreamService> { + on { activity } doReturn activity + on { redirectWake } doReturn true + } underTest.onAttachedToWindow() onCreateCallback.firstValue.invoke(mock<TaskFragmentInfo>()) + runCurrent() verify(taskFragmentComponent, times(1)).startActivityInTaskFragment(intentMatcher()) // Task fragment becomes empty @@ -166,30 +204,20 @@ class HomeControlsDreamServiceTest : SysuiTestCase() { mock<TaskFragmentInfo> { on { isEmpty } doReturn true } ) advanceUntilIdle() + // Activity is restarted instead of finishing the dream. verify(taskFragmentComponent, times(2)).startActivityInTaskFragment(intentMatcher()) - verify(dreamServiceDelegate).wakeUp(any()) - verify(dreamServiceDelegate, never()).finish(any()) + verify(dreamService).wakeUp() + verify(dreamService, never()).finish() } private fun intentMatcher() = argThat<Intent> { getIntExtra(EXTRA_CONTROLS_SURFACE, CONTROLS_SURFACE_ACTIVITY_PANEL) == - CONTROLS_SURFACE_DREAM + CONTROLS_SURFACE_DREAM && component == PANEL_COMPONENT } - private fun buildService( - activityProvider: DreamServiceDelegate = dreamServiceDelegate - ): HomeControlsDreamService = - with(kosmos) { - return HomeControlsDreamService( - controlsSettingsRepository = FakeControlsSettingsRepository(), - taskFragmentFactory = taskFragmentComponentFactory, - homeControlsComponentInteractor = homeControlsComponentInteractor, - wakeLockBuilder = fakeWakeLockBuilder, - dreamServiceDelegate = activityProvider, - bgDispatcher = testDispatcher, - logBuffer = logcatLogBuffer("HomeControlsDreamServiceTest") - ) - } + private companion object { + val PANEL_COMPONENT = ComponentName("test.pkg", "test.panel") + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartableTest.kt index 1adf414c9ef0..ef02817eb4ac 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartableTest.kt @@ -23,82 +23,86 @@ import android.content.pm.ServiceInfo import android.content.pm.UserInfo import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.FlagsParameterization import android.service.controls.flags.Flags.FLAG_HOME_PANEL_DREAM -import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.systemui.Flags.FLAG_HOME_CONTROLS_DREAM_HSUM +import com.android.systemui.Flags.homeControlsDreamHsum import com.android.systemui.SysuiTestCase import com.android.systemui.controls.ControlsServiceInfo -import com.android.systemui.controls.dagger.ControlsComponent -import com.android.systemui.controls.management.ControlsListingController -import com.android.systemui.controls.panels.AuthorizedPanelsRepository import com.android.systemui.controls.panels.SelectedComponentRepository import com.android.systemui.controls.panels.authorizedPanelsRepository import com.android.systemui.controls.panels.selectedComponentRepository -import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor +import com.android.systemui.dreams.homecontrols.system.HomeControlsDreamStartable +import com.android.systemui.dreams.homecontrols.system.domain.interactor.controlsComponent +import com.android.systemui.dreams.homecontrols.system.domain.interactor.controlsListingController +import com.android.systemui.dreams.homecontrols.system.domain.interactor.homeControlsComponentInteractor import com.android.systemui.kosmos.applicationCoroutineScope 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.android.systemui.user.data.repository.FakeUserRepository import com.android.systemui.user.data.repository.fakeUserRepository -import com.android.systemui.util.mockito.eq -import com.android.systemui.util.mockito.whenever import java.util.Optional +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mock import org.mockito.Mockito.verify -import org.mockito.MockitoAnnotations - +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.stub +import org.mockito.kotlin.whenever +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters + +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest -@RunWith(AndroidJUnit4::class) -class HomeControlsDreamStartableTest : SysuiTestCase() { +@RunWith(ParameterizedAndroidJunit4::class) +class HomeControlsDreamStartableTest(flags: FlagsParameterization) : SysuiTestCase() { + init { + mSetFlagsRule.setFlagsParameterization(flags) + } private val kosmos = testKosmos() + private val testScope = kosmos.testScope - @Mock private lateinit var packageManager: PackageManager + private val systemUserPackageManager = mock<PackageManager>() + private val userPackageManager = mock<PackageManager>() - private lateinit var homeControlsComponentInteractor: HomeControlsComponentInteractor - private lateinit var selectedComponentRepository: SelectedComponentRepository - private lateinit var authorizedPanelsRepository: AuthorizedPanelsRepository - private lateinit var userRepository: FakeUserRepository - private lateinit var controlsComponent: ControlsComponent - private lateinit var controlsListingController: ControlsListingController + private val selectedComponentRepository = kosmos.selectedComponentRepository + private val userRepository = + kosmos.fakeUserRepository.apply { setUserInfos(listOf(PRIMARY_USER)) } + private val controlsListingController = + kosmos.controlsListingController.stub { + on { getCurrentServices() } doReturn + listOf(buildControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true)) + } + private val controlsComponent = + kosmos.controlsComponent.stub { + on { getControlsListingController() } doReturn Optional.of(controlsListingController) + } - private lateinit var startable: HomeControlsDreamStartable + private val underTest by lazy { + HomeControlsDreamStartable( + mContext, + systemUserPackageManager, + kosmos.userTracker, + kosmos.homeControlsComponentInteractor, + kosmos.applicationCoroutineScope, + ) + } private val componentName = ComponentName(context, HomeControlsDreamService::class.java) - private val testScope = kosmos.testScope @Before fun setUp() { - MockitoAnnotations.initMocks(this) - - selectedComponentRepository = kosmos.selectedComponentRepository - authorizedPanelsRepository = kosmos.authorizedPanelsRepository - userRepository = kosmos.fakeUserRepository - controlsComponent = kosmos.controlsComponent - controlsListingController = kosmos.controlsListingController - - userRepository.setUserInfos(listOf(PRIMARY_USER)) - - authorizedPanelsRepository.addAuthorizedPanels(setOf(TEST_PACKAGE_PANEL)) - + kosmos.authorizedPanelsRepository.addAuthorizedPanels(setOf(TEST_PACKAGE_PANEL)) whenever(controlsComponent.getControlsListingController()) .thenReturn(Optional.of(controlsListingController)) - whenever(controlsListingController.getCurrentServices()) - .thenReturn(listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true))) - - homeControlsComponentInteractor = kosmos.homeControlsComponentInteractor - - startable = - HomeControlsDreamStartable( - mContext, - packageManager, - homeControlsComponentInteractor, - kosmos.applicationCoroutineScope - ) + whenever(kosmos.fakeUserTracker.userContext.packageManager).thenReturn(userPackageManager) } @Test @@ -107,13 +111,19 @@ class HomeControlsDreamStartableTest : SysuiTestCase() { testScope.runTest { userRepository.setSelectedUserInfo(PRIMARY_USER) selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL) - startable.start() + underTest.start() runCurrent() + val packageManager = + if (homeControlsDreamHsum()) { + userPackageManager + } else { + systemUserPackageManager + } verify(packageManager) .setComponentEnabledSetting( - eq(componentName), - eq(PackageManager.COMPONENT_ENABLED_STATE_ENABLED), - eq(PackageManager.DONT_KILL_APP) + componentName, + PackageManager.COMPONENT_ENABLED_STATE_ENABLED, + PackageManager.DONT_KILL_APP, ) } @@ -122,13 +132,19 @@ class HomeControlsDreamStartableTest : SysuiTestCase() { fun testStartDisablesHomeControlsDreamServiceWhenPanelComponentIsNull() = testScope.runTest { selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_NON_PANEL) - startable.start() + underTest.start() runCurrent() + val packageManager = + if (homeControlsDreamHsum()) { + userPackageManager + } else { + systemUserPackageManager + } verify(packageManager) .setComponentEnabledSetting( - eq(componentName), - eq(PackageManager.COMPONENT_ENABLED_STATE_DISABLED), - eq(PackageManager.DONT_KILL_APP) + componentName, + PackageManager.COMPONENT_ENABLED_STATE_DISABLED, + PackageManager.DONT_KILL_APP, ) } @@ -137,20 +153,26 @@ class HomeControlsDreamStartableTest : SysuiTestCase() { fun testStartDisablesDreamServiceWhenFlagIsDisabled() = testScope.runTest { selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_NON_PANEL) - startable.start() + underTest.start() runCurrent() + val packageManager = + if (homeControlsDreamHsum()) { + userPackageManager + } else { + systemUserPackageManager + } verify(packageManager) .setComponentEnabledSetting( eq(componentName), eq(PackageManager.COMPONENT_ENABLED_STATE_DISABLED), - eq(PackageManager.DONT_KILL_APP) + eq(PackageManager.DONT_KILL_APP), ) } - private fun ControlsServiceInfo( + private fun buildControlsServiceInfo( componentName: ComponentName, label: CharSequence, - hasPanel: Boolean + hasPanel: Boolean, ): ControlsServiceInfo { val serviceInfo = ServiceInfo().apply { @@ -165,7 +187,7 @@ class HomeControlsDreamStartableTest : SysuiTestCase() { context: Context, serviceInfo: ServiceInfo, private val label: CharSequence, - hasPanel: Boolean + hasPanel: Boolean, ) : ControlsServiceInfo(context, serviceInfo) { init { @@ -180,12 +202,16 @@ class HomeControlsDreamStartableTest : SysuiTestCase() { } companion object { + @get:Parameters(name = "{0}") + @JvmStatic + val params = FlagsParameterization.allCombinationsOf(FLAG_HOME_CONTROLS_DREAM_HSUM) + private const val PRIMARY_USER_ID = 0 private val PRIMARY_USER = UserInfo( /* id= */ PRIMARY_USER_ID, /* name= */ "primary user", - /* flags= */ UserInfo.FLAG_PRIMARY + /* flags= */ UserInfo.FLAG_PRIMARY, ) private const val TEST_PACKAGE_PANEL = "pkg.panel" private val TEST_COMPONENT_PANEL = ComponentName(TEST_PACKAGE_PANEL, "service") @@ -193,13 +219,13 @@ class HomeControlsDreamStartableTest : SysuiTestCase() { SelectedComponentRepository.SelectedComponent( TEST_PACKAGE_PANEL, TEST_COMPONENT_PANEL, - true + true, ) private val TEST_SELECTED_COMPONENT_NON_PANEL = SelectedComponentRepository.SelectedComponent( TEST_PACKAGE_PANEL, TEST_COMPONENT_PANEL, - false + false, ) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/service/HomeControlsRemoteProxyTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/service/HomeControlsRemoteProxyTest.kt new file mode 100644 index 000000000000..e57776f4db1b --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/service/HomeControlsRemoteProxyTest.kt @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.dreams.homecontrols.service + +import android.content.ComponentName +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.dreams.homecontrols.shared.model.HomeControlsComponentInfo +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class HomeControlsRemoteProxyTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + + private val fakeBinder = kosmos.fakeHomeControlsRemoteBinder + + private val underTest by lazy { kosmos.homeControlsRemoteProxy } + + @Test + fun testRegistersOnlyWhileSubscribed() = + testScope.runTest { + assertThat(fakeBinder.callbacks).isEmpty() + + val job = launch { underTest.componentInfo.collect {} } + runCurrent() + assertThat(fakeBinder.callbacks).hasSize(1) + + job.cancel() + runCurrent() + assertThat(fakeBinder.callbacks).isEmpty() + } + + @Test + fun testEmitsOnCallback() = + testScope.runTest { + val componentInfo by collectLastValue(underTest.componentInfo) + assertThat(componentInfo).isNull() + + fakeBinder.notifyCallbacks(TEST_COMPONENT, allowTrivialControlsOnLockscreen = true) + assertThat(componentInfo) + .isEqualTo( + HomeControlsComponentInfo( + TEST_COMPONENT, + allowTrivialControlsOnLockscreen = true, + ) + ) + } + + @Test + fun testOnlyRegistersSingleCallbackForMultipleSubscribers() = + testScope.runTest { + assertThat(fakeBinder.callbacks).isEmpty() + + // 2 collectors + val job = launch { + launch { underTest.componentInfo.collect {} } + launch { underTest.componentInfo.collect {} } + } + runCurrent() + assertThat(fakeBinder.callbacks).hasSize(1) + job.cancel() + } + + private companion object { + val TEST_COMPONENT = ComponentName("pkg.test", "class.test") + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/service/RemoteHomeControlsDataSourceDelegatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/service/RemoteHomeControlsDataSourceDelegatorTest.kt new file mode 100644 index 000000000000..400217503299 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/service/RemoteHomeControlsDataSourceDelegatorTest.kt @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.dreams.homecontrols.service + +import android.content.ComponentName +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.dreams.homecontrols.dagger.HomeControlsRemoteServiceComponent +import com.android.systemui.dreams.homecontrols.shared.model.HomeControlsComponentInfo +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.android.systemui.util.service.ObservableServiceConnection +import com.android.systemui.util.service.PersistentConnectionManager +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.stub +import org.mockito.kotlin.times +import org.mockito.kotlin.verify + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class RemoteHomeControlsDataSourceDelegatorTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + + private val proxy = kosmos.homeControlsRemoteProxy + private val fakeBinder = kosmos.fakeHomeControlsRemoteBinder + + private val callbackCaptor = + argumentCaptor<ObservableServiceConnection.Callback<HomeControlsRemoteProxy>>() + + private val connectionManager = + mock<PersistentConnectionManager<HomeControlsRemoteProxy>> { + on { start() } doAnswer { simulateConnect() } + on { stop() } doAnswer { simulateDisconnect() } + } + private val serviceComponent = + mock<HomeControlsRemoteServiceComponent> { + on { connectionManager } doReturn connectionManager + } + + private val underTest by lazy { kosmos.remoteHomeControlsDataSourceDelegator } + + @Before + fun setUp() { + kosmos.homeControlsRemoteServiceFactory = + mock<HomeControlsRemoteServiceComponent.Factory>().stub { + on { create(callbackCaptor.capture()) } doReturn serviceComponent + } + } + + @Test + fun testQueriesComponentInfoFromBinder() = + testScope.runTest { + assertThat(fakeBinder.callbacks).isEmpty() + + val componentInfo by collectLastValue(underTest.componentInfo) + + assertThat(componentInfo).isNull() + assertThat(fakeBinder.callbacks).hasSize(1) + + fakeBinder.notifyCallbacks(TEST_COMPONENT, allowTrivialControlsOnLockscreen = true) + assertThat(componentInfo) + .isEqualTo( + HomeControlsComponentInfo( + TEST_COMPONENT, + allowTrivialControlsOnLockscreen = true, + ) + ) + } + + @Test + fun testOnlyConnectToServiceOnSubscription() = + testScope.runTest { + verify(connectionManager, never()).start() + + val job = launch { underTest.componentInfo.collect {} } + runCurrent() + verify(connectionManager, times(1)).start() + verify(connectionManager, never()).stop() + + job.cancel() + runCurrent() + verify(connectionManager, times(1)).start() + verify(connectionManager, times(1)).stop() + } + + private fun simulateConnect() { + callbackCaptor.lastValue.onConnected(mock(), proxy) + } + + private fun simulateDisconnect() { + callbackCaptor.lastValue.onDisconnected(mock(), 0) + } + + private companion object { + val TEST_COMPONENT = ComponentName("pkg.test", "class.test") + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/system/HomeControlsRemoteServiceBinderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/system/HomeControlsRemoteServiceBinderTest.kt new file mode 100644 index 000000000000..f8a45e82c2ab --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/system/HomeControlsRemoteServiceBinderTest.kt @@ -0,0 +1,236 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.dreams.homecontrols.system + +import android.content.ComponentName +import android.content.Context +import android.content.pm.ApplicationInfo +import android.content.pm.ServiceInfo +import android.content.pm.UserInfo +import androidx.lifecycle.testing.TestLifecycleOwner +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.controls.ControlsServiceInfo +import com.android.systemui.controls.panels.SelectedComponentRepository +import com.android.systemui.controls.panels.authorizedPanelsRepository +import com.android.systemui.controls.panels.selectedComponentRepository +import com.android.systemui.controls.settings.FakeControlsSettingsRepository +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.dreams.homecontrols.shared.IOnControlsSettingsChangeListener +import com.android.systemui.dreams.homecontrols.system.domain.interactor.controlsComponent +import com.android.systemui.dreams.homecontrols.system.domain.interactor.controlsListingController +import com.android.systemui.dreams.homecontrols.system.domain.interactor.homeControlsComponentInteractor +import com.android.systemui.kosmos.backgroundCoroutineContext +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.log.logcatLogBuffer +import com.android.systemui.settings.fakeUserTracker +import com.android.systemui.testKosmos +import com.android.systemui.user.data.repository.fakeUserRepository +import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.mockito.withArgCaptor +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow +import com.google.common.truth.Truth.assertThat +import java.util.Optional +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class HomeControlsRemoteServiceBinderTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + + private val lifecycleOwner = TestLifecycleOwner(coroutineDispatcher = kosmos.testDispatcher) + private val fakeControlsSettingsRepository = FakeControlsSettingsRepository() + + private val underTest by lazy { + HomeControlsRemoteServiceBinder( + kosmos.homeControlsComponentInteractor, + fakeControlsSettingsRepository, + kosmos.backgroundCoroutineContext, + logcatLogBuffer(), + lifecycleOwner, + ) + } + + @Before + fun setUp() { + with(kosmos) { + fakeUserRepository.setUserInfos(listOf(PRIMARY_USER)) + whenever(controlsComponent.getControlsListingController()) + .thenReturn(Optional.of(controlsListingController)) + } + } + + @Test + fun testRegisterSingleListener() = + testScope.runTest { + setup() + val controlsSettings by collectLastValue(addCallback()) + runServicesUpdate() + + assertThat(controlsSettings) + .isEqualTo( + CallbackArgs( + panelComponent = TEST_COMPONENT, + allowTrivialControlsOnLockscreen = false, + ) + ) + } + + @Test + fun testRegisterMultipleListeners() = + testScope.runTest { + setup() + val controlsSettings1 by collectLastValue(addCallback()) + val controlsSettings2 by collectLastValue(addCallback()) + runServicesUpdate() + + assertThat(controlsSettings1) + .isEqualTo( + CallbackArgs( + panelComponent = TEST_COMPONENT, + allowTrivialControlsOnLockscreen = false, + ) + ) + assertThat(controlsSettings2) + .isEqualTo( + CallbackArgs( + panelComponent = TEST_COMPONENT, + allowTrivialControlsOnLockscreen = false, + ) + ) + } + + @Test + fun testListenerCalledWhenStateChanges() = + testScope.runTest { + setup() + val controlsSettings by collectLastValue(addCallback()) + runServicesUpdate() + + assertThat(controlsSettings) + .isEqualTo( + CallbackArgs( + panelComponent = TEST_COMPONENT, + allowTrivialControlsOnLockscreen = false, + ) + ) + + kosmos.authorizedPanelsRepository.removeAuthorizedPanels(setOf(TEST_PACKAGE)) + + // Updated with null component now that we are no longer authorized. + assertThat(controlsSettings) + .isEqualTo( + CallbackArgs(panelComponent = null, allowTrivialControlsOnLockscreen = false) + ) + } + + private fun TestScope.runServicesUpdate() { + runCurrent() + val listings = listOf(ControlsServiceInfo(TEST_COMPONENT, "panel", hasPanel = true)) + val callback = withArgCaptor { + Mockito.verify(kosmos.controlsListingController).addCallback(capture()) + } + callback.onServicesUpdated(listings) + runCurrent() + } + + private fun addCallback() = conflatedCallbackFlow { + val callback = + object : IOnControlsSettingsChangeListener.Stub() { + override fun onControlsSettingsChanged( + panelComponent: ComponentName?, + allowTrivialControlsOnLockscreen: Boolean, + ) { + trySend(CallbackArgs(panelComponent, allowTrivialControlsOnLockscreen)) + } + } + underTest.registerListenerForCurrentUser(callback) + awaitClose { underTest.unregisterListenerForCurrentUser(callback) } + } + + private suspend fun TestScope.setup() { + kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER) + kosmos.fakeUserTracker.set(listOf(PRIMARY_USER), 0) + kosmos.authorizedPanelsRepository.addAuthorizedPanels(setOf(TEST_PACKAGE)) + kosmos.selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL) + runCurrent() + } + + private data class CallbackArgs( + val panelComponent: ComponentName?, + val allowTrivialControlsOnLockscreen: Boolean, + ) + + private fun ControlsServiceInfo( + componentName: ComponentName, + label: CharSequence, + hasPanel: Boolean, + ): ControlsServiceInfo { + val serviceInfo = + ServiceInfo().apply { + applicationInfo = ApplicationInfo() + packageName = componentName.packageName + name = componentName.className + } + return FakeControlsServiceInfo(context, serviceInfo, label, hasPanel) + } + + private class FakeControlsServiceInfo( + context: Context, + serviceInfo: ServiceInfo, + private val label: CharSequence, + hasPanel: Boolean, + ) : ControlsServiceInfo(context, serviceInfo) { + + init { + if (hasPanel) { + panelActivity = serviceInfo.componentName + } + } + + override fun loadLabel(): CharSequence { + return label + } + } + + private companion object { + const val PRIMARY_USER_ID = 0 + val PRIMARY_USER = + UserInfo( + /* id= */ PRIMARY_USER_ID, + /* name= */ "primary user", + /* flags= */ UserInfo.FLAG_PRIMARY, + ) + + private const val TEST_PACKAGE = "pkg" + private val TEST_COMPONENT = ComponentName(TEST_PACKAGE, "service") + private val TEST_SELECTED_COMPONENT_PANEL = + SelectedComponentRepository.SelectedComponent(TEST_PACKAGE, TEST_COMPONENT, true) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/system/domain/interactor/HomeControlsComponentInteractorTest.kt index 7292985b2dba..c950523f7854 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/system/domain/interactor/HomeControlsComponentInteractorTest.kt @@ -20,40 +20,32 @@ import android.content.Context import android.content.pm.ApplicationInfo import android.content.pm.ServiceInfo import android.content.pm.UserInfo -import android.os.PowerManager -import android.os.UserHandle -import android.os.powerManager -import android.service.dream.dreamManager import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.common.data.repository.fakePackageChangeRepository import com.android.systemui.controls.ControlsServiceInfo import com.android.systemui.controls.panels.SelectedComponentRepository import com.android.systemui.controls.panels.authorizedPanelsRepository import com.android.systemui.controls.panels.selectedComponentRepository import com.android.systemui.coroutines.collectLastValue -import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor.Companion.MAX_UPDATE_CORRELATION_DELAY +import com.android.systemui.dreams.homecontrols.system.domain.interactor.controlsComponent +import com.android.systemui.dreams.homecontrols.system.domain.interactor.controlsListingController +import com.android.systemui.dreams.homecontrols.system.domain.interactor.homeControlsComponentInteractor import com.android.systemui.kosmos.testScope import com.android.systemui.settings.fakeUserTracker import com.android.systemui.testKosmos import com.android.systemui.user.data.repository.fakeUserRepository import com.android.systemui.util.mockito.whenever import com.android.systemui.util.mockito.withArgCaptor -import com.android.systemui.util.time.fakeSystemClock import com.google.common.truth.Truth.assertThat import java.util.Optional import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.launch import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers.anyInt -import org.mockito.Mockito.anyLong -import org.mockito.Mockito.never import org.mockito.Mockito.verify @OptIn(ExperimentalCoroutinesApi::class) @@ -68,7 +60,6 @@ class HomeControlsComponentInteractorTest : SysuiTestCase() { @Before fun setUp() = with(kosmos) { - fakeSystemClock.setCurrentTimeMillis(0) fakeUserRepository.setUserInfos(listOf(PRIMARY_USER, ANOTHER_USER)) whenever(controlsComponent.getControlsListingController()) .thenReturn(Optional.of(controlsListingController)) @@ -172,113 +163,6 @@ class HomeControlsComponentInteractorTest : SysuiTestCase() { } } - @Test - fun testMonitoringUpdatesAndRestart() = - with(kosmos) { - testScope.runTest { - setActiveUser(PRIMARY_USER) - authorizedPanelsRepository.addAuthorizedPanels(setOf(TEST_PACKAGE)) - selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL) - whenever(controlsListingController.getCurrentServices()) - .thenReturn( - listOf(ControlsServiceInfo(TEST_COMPONENT, "panel", hasPanel = true)) - ) - - val job = launch { underTest.monitorUpdatesAndRestart() } - val panelComponent by collectLastValue(underTest.panelComponent) - - assertThat(panelComponent).isEqualTo(TEST_COMPONENT) - verify(dreamManager, never()).startDream() - - fakeSystemClock.advanceTime(100) - // The package update is started. - fakePackageChangeRepository.notifyUpdateStarted( - TEST_PACKAGE, - UserHandle.of(PRIMARY_USER_ID), - ) - fakeSystemClock.advanceTime(MAX_UPDATE_CORRELATION_DELAY.inWholeMilliseconds) - // Task fragment becomes empty as a result of the update. - underTest.onDreamEndUnexpectedly() - - runCurrent() - verify(dreamManager, never()).startDream() - - fakeSystemClock.advanceTime(500) - // The package update is finished. - fakePackageChangeRepository.notifyUpdateFinished( - TEST_PACKAGE, - UserHandle.of(PRIMARY_USER_ID), - ) - - runCurrent() - verify(dreamManager).startDream() - job.cancel() - } - } - - @Test - fun testMonitoringUpdatesAndRestart_dreamEndsAfterDelay() = - with(kosmos) { - testScope.runTest { - setActiveUser(PRIMARY_USER) - authorizedPanelsRepository.addAuthorizedPanels(setOf(TEST_PACKAGE)) - selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL) - whenever(controlsListingController.getCurrentServices()) - .thenReturn( - listOf(ControlsServiceInfo(TEST_COMPONENT, "panel", hasPanel = true)) - ) - - val job = launch { underTest.monitorUpdatesAndRestart() } - val panelComponent by collectLastValue(underTest.panelComponent) - - assertThat(panelComponent).isEqualTo(TEST_COMPONENT) - verify(dreamManager, never()).startDream() - - fakeSystemClock.advanceTime(100) - // The package update is started. - fakePackageChangeRepository.notifyUpdateStarted( - TEST_PACKAGE, - UserHandle.of(PRIMARY_USER_ID), - ) - fakeSystemClock.advanceTime(MAX_UPDATE_CORRELATION_DELAY.inWholeMilliseconds + 100) - // Task fragment becomes empty as a result of the update. - underTest.onDreamEndUnexpectedly() - - runCurrent() - verify(dreamManager, never()).startDream() - - fakeSystemClock.advanceTime(500) - // The package update is finished. - fakePackageChangeRepository.notifyUpdateFinished( - TEST_PACKAGE, - UserHandle.of(PRIMARY_USER_ID), - ) - - runCurrent() - verify(dreamManager, never()).startDream() - job.cancel() - } - } - - @Test - fun testDreamUnexpectedlyEnds_triggersUserActivity() = - with(kosmos) { - testScope.runTest { - fakeSystemClock.setUptimeMillis(100000L) - verify(powerManager, never()).userActivity(anyLong(), anyInt(), anyInt()) - - // Dream ends unexpectedly - underTest.onDreamEndUnexpectedly() - - verify(powerManager) - .userActivity( - 100000L, - PowerManager.USER_ACTIVITY_EVENT_OTHER, - PowerManager.USER_ACTIVITY_FLAG_NO_CHANGE_LIGHTS - ) - } - } - private fun runServicesUpdate(hasPanelBoolean: Boolean = true) { val listings = listOf(ControlsServiceInfo(TEST_COMPONENT, "panel", hasPanel = hasPanelBoolean)) @@ -297,7 +181,7 @@ class HomeControlsComponentInteractorTest : SysuiTestCase() { private fun ControlsServiceInfo( componentName: ComponentName, label: CharSequence, - hasPanel: Boolean + hasPanel: Boolean, ): ControlsServiceInfo { val serviceInfo = ServiceInfo().apply { @@ -312,7 +196,7 @@ class HomeControlsComponentInteractorTest : SysuiTestCase() { context: Context, serviceInfo: ServiceInfo, private val label: CharSequence, - hasPanel: Boolean + hasPanel: Boolean, ) : ControlsServiceInfo(context, serviceInfo) { init { @@ -332,7 +216,7 @@ class HomeControlsComponentInteractorTest : SysuiTestCase() { UserInfo( /* id= */ PRIMARY_USER_ID, /* name= */ "primary user", - /* flags= */ UserInfo.FLAG_PRIMARY + /* flags= */ UserInfo.FLAG_PRIMARY, ) private const val ANOTHER_USER_ID = 1 @@ -340,7 +224,7 @@ class HomeControlsComponentInteractorTest : SysuiTestCase() { UserInfo( /* id= */ ANOTHER_USER_ID, /* name= */ "another user", - /* flags= */ UserInfo.FLAG_PRIMARY + /* flags= */ UserInfo.FLAG_PRIMARY, ) private const val TEST_PACKAGE = "pkg" private val TEST_COMPONENT = ComponentName(TEST_PACKAGE, "service") diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt index 21679f960198..2a6d29c61890 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/interactor/KeyboardTouchpadEduInteractorTest.kt @@ -427,7 +427,7 @@ class KeyboardTouchpadEduInteractorTest(private val gestureType: GestureType) : @After fun clear() { - testScope.launch { tutorialSchedulerRepository.clearDataStore() } + testScope.launch { tutorialSchedulerRepository.clear() } } private suspend fun triggerMaxEducationSignals(gestureType: GestureType) { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/inputdevice/data/repository/TutorialSchedulerRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/inputdevice/data/repository/TutorialSchedulerRepositoryTest.kt index 1d96c4d67c77..8bb6962a2d8e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/inputdevice/data/repository/TutorialSchedulerRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/inputdevice/data/repository/TutorialSchedulerRepositoryTest.kt @@ -47,13 +47,13 @@ class TutorialSchedulerRepositoryTest : SysuiTestCase() { TutorialSchedulerRepository( context, testScope.backgroundScope, - "TutorialSchedulerRepositoryTest" + "TutorialSchedulerRepositoryTest", ) } @After fun clear() { - testScope.launch { underTest.clearDataStore() } + testScope.launch { underTest.clear() } } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialNotificationCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialNotificationCoordinatorTest.kt index 38e4ae101d06..bcac086033ed 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialNotificationCoordinatorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialNotificationCoordinatorTest.kt @@ -26,10 +26,11 @@ import com.android.systemui.inputdevice.tutorial.data.repository.TutorialSchedul import com.android.systemui.inputdevice.tutorial.inputDeviceTutorialLogger import com.android.systemui.inputdevice.tutorial.ui.TutorialNotificationCoordinator import com.android.systemui.keyboard.data.repository.FakeKeyboardRepository -import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope import com.android.systemui.res.R import com.android.systemui.settings.userTracker +import com.android.systemui.statusbar.commandline.commandRegistry +import com.android.systemui.testKosmos import com.android.systemui.touchpad.data.repository.FakeTouchpadRepository import com.google.common.truth.Truth.assertThat import kotlin.time.Duration.Companion.hours @@ -60,7 +61,7 @@ import org.mockito.kotlin.verify class TutorialNotificationCoordinatorTest : SysuiTestCase() { private lateinit var underTest: TutorialNotificationCoordinator - private val kosmos = Kosmos() + private val kosmos = testKosmos() private val testScope = kosmos.testScope private val keyboardRepository = FakeKeyboardRepository() private val touchpadRepository = FakeTouchpadRepository() @@ -85,6 +86,7 @@ class TutorialNotificationCoordinatorTest : SysuiTestCase() { touchpadRepository, repository, kosmos.inputDeviceTutorialLogger, + kosmos.commandRegistry, ) underTest = TutorialNotificationCoordinator( @@ -100,7 +102,7 @@ class TutorialNotificationCoordinatorTest : SysuiTestCase() { @After fun clear() { - runBlocking { repository.clearDataStore() } + runBlocking { repository.clear() } dataStoreScope.cancel() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialSchedulerInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialSchedulerInteractorTest.kt index b0ffc47cbc6c..5df9b7b8d5b8 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialSchedulerInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialSchedulerInteractorTest.kt @@ -24,8 +24,9 @@ import com.android.systemui.inputdevice.tutorial.data.repository.TutorialSchedul import com.android.systemui.inputdevice.tutorial.domain.interactor.TutorialSchedulerInteractor.TutorialType import com.android.systemui.inputdevice.tutorial.inputDeviceTutorialLogger import com.android.systemui.keyboard.data.repository.FakeKeyboardRepository -import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope +import com.android.systemui.statusbar.commandline.commandRegistry +import com.android.systemui.testKosmos import com.android.systemui.touchpad.data.repository.FakeTouchpadRepository import com.google.common.truth.Truth.assertThat import kotlin.time.Duration.Companion.hours @@ -49,7 +50,7 @@ import org.junit.runner.RunWith class TutorialSchedulerInteractorTest : SysuiTestCase() { private lateinit var underTest: TutorialSchedulerInteractor - private val kosmos = Kosmos() + private val kosmos = testKosmos() private val testScope = kosmos.testScope private lateinit var dataStoreScope: CoroutineScope private val keyboardRepository = FakeKeyboardRepository() @@ -71,12 +72,13 @@ class TutorialSchedulerInteractorTest : SysuiTestCase() { touchpadRepository, schedulerRepository, kosmos.inputDeviceTutorialLogger, + kosmos.commandRegistry, ) } @After fun clear() { - runBlocking { schedulerRepository.clearDataStore() } + runBlocking { schedulerRepository.clear() } dataStoreScope.cancel() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepositoryTest.kt index 9e20e7d0f98c..620b8b63c71a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepositoryTest.kt @@ -16,7 +16,11 @@ package com.android.systemui.keyboard.shortcut.data.repository +import android.graphics.drawable.Drawable +import android.hardware.input.KeyGlyphMap import android.hardware.input.fakeInputManager +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import android.view.KeyEvent.KEYCODE_1 import android.view.KeyEvent.KEYCODE_A import android.view.KeyEvent.KEYCODE_B @@ -26,10 +30,12 @@ import android.view.KeyEvent.KEYCODE_E import android.view.KeyEvent.KEYCODE_F import android.view.KeyEvent.KEYCODE_G import android.view.KeyEvent.META_FUNCTION_ON +import android.view.KeyEvent.META_META_ON import android.view.KeyboardShortcutGroup import android.view.KeyboardShortcutInfo import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest +import com.android.systemui.Flags.FLAG_SHORTCUT_HELPER_KEY_GLYPH import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyboard.shortcut.data.source.FakeKeyboardShortcutGroupsSource @@ -49,6 +55,7 @@ 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.res.R import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -57,6 +64,9 @@ import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mockito.mock +import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @@ -187,6 +197,79 @@ class ShortcutHelperCategoriesRepositoryTest : SysuiTestCase() { ) } + @EnableFlags(FLAG_SHORTCUT_HELPER_KEY_GLYPH) + @Test + fun modifierMappedToCustomDrawableWhenKeyGlyphMapExists() = + testScope.runTest { + val metaDrawable = mock(Drawable::class.java) + val keyGlyph = mock(KeyGlyphMap::class.java) + whenever(keyGlyph.getDrawableForModifierState(context, META_META_ON)) + .thenReturn(metaDrawable) + whenever(kosmos.fakeInputManager.inputManager.getKeyGlyphMap(anyInt())) + .thenReturn(keyGlyph) + fakeSystemSource.setGroups(simpleGroup(simpleShortcutInfo(KEYCODE_1, META_META_ON))) + helper.toggle(deviceId = 123) + + val categories by collectLastValue(repo.categories) + val systemCategory = categories?.firstOrNull { it.type == ShortcutCategoryType.System } + + val expectedCategory = + ShortcutCategory( + type = ShortcutCategoryType.System, + simpleSubCategory( + simpleDrawableModifierShortcut("1", modifierDrawable = metaDrawable) + ), + ) + + assertThat(systemCategory).isEqualTo(expectedCategory) + } + + @EnableFlags(FLAG_SHORTCUT_HELPER_KEY_GLYPH) + @Test + fun modifierMappedToDefaultDrawableWhenNoKeyGlyphMapExists() = + testScope.runTest { + fakeSystemSource.setGroups(simpleGroup(simpleShortcutInfo(KEYCODE_1, META_META_ON))) + helper.toggle(deviceId = 123) + + val categories by collectLastValue(repo.categories) + val systemCategory = categories?.firstOrNull { it.type == ShortcutCategoryType.System } + + val expectedCategory = + ShortcutCategory( + type = ShortcutCategoryType.System, + simpleSubCategory( + simpleResIdModifierShortcut("1", modifierResId = R.drawable.ic_ksh_key_meta) + ), + ) + assertThat(systemCategory).isEqualTo(expectedCategory) + } + + @DisableFlags(FLAG_SHORTCUT_HELPER_KEY_GLYPH) + @Test + fun modifierMappedToDefaultDrawableWhenKeyGlyphDisabled() = + testScope.runTest { + val metaDrawable = mock(Drawable::class.java) + val keyGlyph = mock(KeyGlyphMap::class.java) + whenever(keyGlyph.getDrawableForModifierState(context, META_META_ON)) + .thenReturn(metaDrawable) + whenever(kosmos.fakeInputManager.inputManager.getKeyGlyphMap(anyInt())) + .thenReturn(keyGlyph) + fakeSystemSource.setGroups(simpleGroup(simpleShortcutInfo(KEYCODE_1, META_META_ON))) + helper.toggle(deviceId = 123) + + val categories by collectLastValue(repo.categories) + val systemCategory = categories?.firstOrNull { it.type == ShortcutCategoryType.System } + + val expectedCategory = + ShortcutCategory( + type = ShortcutCategoryType.System, + simpleSubCategory( + simpleResIdModifierShortcut("1", modifierResId = R.drawable.ic_ksh_key_meta) + ), + ) + assertThat(systemCategory).isEqualTo(expectedCategory) + } + private fun simpleSubCategory(vararg shortcuts: Shortcut) = ShortcutSubCategory(simpleGroupLabel, shortcuts.asList()) @@ -196,6 +279,37 @@ class ShortcutHelperCategoriesRepositoryTest : SysuiTestCase() { commands = listOf(ShortcutCommand(keys.map { ShortcutKey.Text(it) })), ) + private fun simpleDrawableModifierShortcut( + vararg keys: String, + modifierDrawable: Drawable, + ): Shortcut { + val keyShortcuts = keys.map { ShortcutKey.Text(it) } + return Shortcut( + label = simpleShortcutLabel, + commands = + listOf( + ShortcutCommand( + listOf(ShortcutKey.Icon.DrawableIcon(drawable = modifierDrawable)) + + keyShortcuts + ) + ), + ) + } + + private fun simpleResIdModifierShortcut(vararg keys: String, modifierResId: Int): Shortcut { + val keyShortcuts = keys.map { ShortcutKey.Text(it) } + return Shortcut( + label = simpleShortcutLabel, + commands = + listOf( + ShortcutCommand( + listOf(ShortcutKey.Icon.ResIdIcon(drawableResId = modifierResId)) + + keyShortcuts + ) + ), + ) + } + private fun simpleGroup(vararg shortcuts: KeyboardShortcutInfo) = KeyboardShortcutGroup(simpleGroupLabel, shortcuts.asList()) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt index 9ca3ce6929b0..e9e3e1b37776 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt @@ -81,7 +81,7 @@ class FromDreamingTransitionInteractorTest(flags: FlagsParameterization?) : Sysu this.fakeKeyguardTransitionRepository = FakeKeyguardTransitionRepository( // This test sends transition steps manually in the test cases. - sendTransitionStepsOnStartTransition = false, + initiallySendTransitionStepsOnStartTransition = false, testScope = testScope, ) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorTest.kt index 9c2e6313a1f0..b29a5f4e456f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractorTest.kt @@ -27,15 +27,13 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectValues import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository -import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepositorySpy +import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository import com.android.systemui.keyguard.data.repository.keyguardOcclusionRepository -import com.android.systemui.keyguard.data.repository.keyguardTransitionRepository import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.StatusBarState import com.android.systemui.keyguard.shared.model.StatusBarState.KEYGUARD import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep -import com.android.systemui.keyguard.util.KeyguardTransitionRepositorySpySubject.Companion.assertThat as assertThatRepository import com.android.systemui.kosmos.testScope import com.android.systemui.shade.data.repository.FlingInfo import com.android.systemui.shade.data.repository.fakeShadeRepository @@ -48,6 +46,8 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito.reset +import org.mockito.Mockito.spy +import com.android.systemui.keyguard.util.KeyguardTransitionRepositorySpySubject.Companion.assertThat as assertThatRepository @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @@ -55,7 +55,9 @@ import org.mockito.Mockito.reset class FromLockscreenTransitionInteractorTest : SysuiTestCase() { private val kosmos = testKosmos().apply { - this.keyguardTransitionRepository = fakeKeyguardTransitionRepositorySpy + this.fakeKeyguardTransitionRepository = spy(FakeKeyguardTransitionRepository( + testScope = testScope, + )) } private val testScope = kosmos.testScope @@ -66,7 +68,7 @@ class FromLockscreenTransitionInteractorTest : SysuiTestCase() { @Before fun setup() { - transitionRepository = kosmos.fakeKeyguardTransitionRepositorySpy + transitionRepository = kosmos.fakeKeyguardTransitionRepository } @Test @@ -302,4 +304,74 @@ class FromLockscreenTransitionInteractorTest : SysuiTestCase() { to = KeyguardState.LOCKSCREEN, ) } + + /** + * External signals can cause us to transition from PRIMARY_BOUNCER -> * while a manual + * transition is in progress. This test was added after a bug that caused the manual transition + * ID to get stuck in this scenario, preventing subsequent transitions to PRIMARY_BOUNCER. + */ + @Test + fun testExternalTransitionAwayFromBouncer_transitionIdNotStuck() = + testScope.runTest { + underTest.start() + keyguardRepository.setStatusBarState(StatusBarState.KEYGUARD) + keyguardRepository.setKeyguardDismissible(false) + shadeRepository.setLegacyShadeTracking(true) + keyguardRepository.setKeyguardOccluded(false) + runCurrent() + + reset(transitionRepository) + + // Disable automatic sending of transition steps so we can send steps through RUNNING + // to simulate a cancellation. + transitionRepository.sendTransitionStepsOnStartTransition = false + shadeRepository.setLegacyShadeExpansion(0.5f) + runCurrent() + + assertThatRepository(transitionRepository) + .startedTransition( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.PRIMARY_BOUNCER, + ) + + // Partially transition to PRIMARY_BOUNCER. + transitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.PRIMARY_BOUNCER, + throughTransitionState = TransitionState.RUNNING, + testScope = testScope, + ) + + // Start a transition to GONE, which will cancel LS -> BOUNCER. + transitionRepository.sendTransitionSteps( + from = KeyguardState.PRIMARY_BOUNCER, + to = KeyguardState.GONE, + testScope = testScope, + ) + + // Go to AOD, then LOCKSCREEN. + transitionRepository.sendTransitionSteps( + from = KeyguardState.GONE, + to = KeyguardState.AOD, + testScope = testScope, + ) + transitionRepository.sendTransitionSteps( + from = KeyguardState.AOD, + to = KeyguardState.LOCKSCREEN, + testScope = testScope, + ) + + reset(transitionRepository) + + // Start a swipe up to the bouncer, and verify that we started a transition to + // PRIMARY_BOUNCER, verifying the transition ID did not get stuck. + shadeRepository.setLegacyShadeExpansion(0.25f) + runCurrent() + + assertThatRepository(transitionRepository) + .startedTransition( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.PRIMARY_BOUNCER, + ) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt index a8bb2b0aca56..46d1ebe75899 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt @@ -31,6 +31,7 @@ import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues import com.android.systemui.dock.DockManager import com.android.systemui.dock.DockManagerFake +import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.flags.FakeFeatureFlags import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig @@ -54,6 +55,7 @@ import com.android.systemui.res.R import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker +import com.android.systemui.shade.cameraLauncher import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots import com.android.systemui.statusbar.policy.KeyguardStateController @@ -117,8 +119,8 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { BuiltInKeyguardQuickAffordanceKeys.HOME_CONTROLS, KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END + ":" + - BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET - ) + BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET, + ), ) repository = FakeKeyguardRepository() @@ -141,13 +143,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { context = context, userFileManager = mock<UserFileManager>().apply { - whenever( - getSharedPreferences( - anyString(), - anyInt(), - anyInt(), - ) - ) + whenever(getSharedPreferences(anyString(), anyInt(), anyInt())) .thenReturn(FakeSharedPreferences()) }, userTracker = userTracker, @@ -181,10 +177,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { featureFlags = FakeFeatureFlags() val withDeps = - KeyguardInteractorFactory.create( - featureFlags = featureFlags, - repository = repository, - ) + KeyguardInteractorFactory.create(featureFlags = featureFlags, repository = repository) underTest = KeyguardQuickAffordanceInteractor( keyguardInteractor = withDeps.keyguardInteractor, @@ -205,6 +198,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { appContext = context, sceneInteractor = { kosmos.sceneInteractor }, ) + kosmos.keyguardQuickAffordanceInteractor = underTest whenever(shadeInteractor.anyExpansion).thenReturn(MutableStateFlow(0f)) } @@ -241,9 +235,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { testScope.runTest { val configKey = BuiltInKeyguardQuickAffordanceKeys.QUICK_ACCESS_WALLET quickAccessWallet.setState( - KeyguardQuickAffordanceConfig.LockScreenState.Visible( - icon = ICON, - ) + KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON) ) val collectedValue = @@ -268,9 +260,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { whenever(devicePolicyManager.getKeyguardDisabledFeatures(null, userTracker.userId)) .thenReturn(DevicePolicyManager.KEYGUARD_DISABLE_FEATURES_ALL) quickAccessWallet.setState( - KeyguardQuickAffordanceConfig.LockScreenState.Visible( - icon = ICON, - ) + KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON) ) val collectedValue by @@ -287,9 +277,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { whenever(devicePolicyManager.getKeyguardDisabledFeatures(null, userTracker.userId)) .thenReturn(DevicePolicyManager.KEYGUARD_DISABLE_SHORTCUTS_ALL) quickAccessWallet.setState( - KeyguardQuickAffordanceConfig.LockScreenState.Visible( - icon = ICON, - ) + KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON) ) val collectedValue by @@ -305,9 +293,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { testScope.runTest { biometricSettingsRepository.setIsUserInLockdown(true) quickAccessWallet.setState( - KeyguardQuickAffordanceConfig.LockScreenState.Visible( - icon = ICON, - ) + KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON) ) val collectedValue by @@ -323,9 +309,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { testScope.runTest { repository.setIsDozing(true) homeControls.setState( - KeyguardQuickAffordanceConfig.LockScreenState.Visible( - icon = ICON, - ) + KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON) ) val collectedValue = @@ -340,9 +324,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { testScope.runTest { repository.setKeyguardShowing(false) homeControls.setState( - KeyguardQuickAffordanceConfig.LockScreenState.Visible( - icon = ICON, - ) + KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON) ) val collectedValue = @@ -446,7 +428,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { val collectedValue = collectLastValue( underTest.quickAffordanceAlwaysVisible( - KeyguardQuickAffordancePosition.BOTTOM_START, + KeyguardQuickAffordancePosition.BOTTOM_START ) ) assertThat(collectedValue()) @@ -487,10 +469,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { @Test fun select() = testScope.runTest { - overrideResource( - R.array.config_keyguardQuickAffordanceDefaults, - arrayOf<String>(), - ) + overrideResource(R.array.config_keyguardQuickAffordanceDefaults, arrayOf<String>()) homeControls.setState( KeyguardQuickAffordanceConfig.LockScreenState.Visible(icon = ICON) ) @@ -530,10 +509,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { activationState = ActivationState.NotSupported, ) ) - assertThat(endConfig()) - .isEqualTo( - KeyguardQuickAffordanceModel.Hidden, - ) + assertThat(endConfig()).isEqualTo(KeyguardQuickAffordanceModel.Hidden) assertThat(underTest.getSelections()) .isEqualTo( mapOf( @@ -543,7 +519,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { id = homeControls.key, name = homeControls.pickerName(), iconResourceId = homeControls.pickerIconResourceId, - ), + ) ), KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END to emptyList(), ) @@ -551,7 +527,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { underTest.select( KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, - quickAccessWallet.key + quickAccessWallet.key, ) assertThat(startConfig()) @@ -564,10 +540,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { activationState = ActivationState.NotSupported, ) ) - assertThat(endConfig()) - .isEqualTo( - KeyguardQuickAffordanceModel.Hidden, - ) + assertThat(endConfig()).isEqualTo(KeyguardQuickAffordanceModel.Hidden) assertThat(underTest.getSelections()) .isEqualTo( mapOf( @@ -577,7 +550,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { id = quickAccessWallet.key, name = quickAccessWallet.pickerName(), iconResourceId = quickAccessWallet.pickerIconResourceId, - ), + ) ), KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END to emptyList(), ) @@ -614,7 +587,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { id = quickAccessWallet.key, name = quickAccessWallet.pickerName(), iconResourceId = quickAccessWallet.pickerIconResourceId, - ), + ) ), KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END to listOf( @@ -622,7 +595,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { id = qrCodeScanner.key, name = qrCodeScanner.pickerName(), iconResourceId = qrCodeScanner.pickerIconResourceId, - ), + ) ), ) ) @@ -653,10 +626,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { underTest.select(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, quickAccessWallet.key) underTest.unselect(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START, homeControls.key) - assertThat(startConfig()) - .isEqualTo( - KeyguardQuickAffordanceModel.Hidden, - ) + assertThat(startConfig()).isEqualTo(KeyguardQuickAffordanceModel.Hidden) assertThat(endConfig()) .isEqualTo( KeyguardQuickAffordanceModel.Visible( @@ -677,24 +647,18 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { id = quickAccessWallet.key, name = quickAccessWallet.pickerName(), iconResourceId = quickAccessWallet.pickerIconResourceId, - ), + ) ), ) ) underTest.unselect( KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, - quickAccessWallet.key + quickAccessWallet.key, ) - assertThat(startConfig()) - .isEqualTo( - KeyguardQuickAffordanceModel.Hidden, - ) - assertThat(endConfig()) - .isEqualTo( - KeyguardQuickAffordanceModel.Hidden, - ) + assertThat(startConfig()).isEqualTo(KeyguardQuickAffordanceModel.Hidden) + assertThat(endConfig()).isEqualTo(KeyguardQuickAffordanceModel.Hidden) assertThat(underTest.getSelections()) .isEqualTo( mapOf<String, List<String>>( @@ -768,15 +732,12 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { id = quickAccessWallet.key, name = quickAccessWallet.pickerName(), iconResourceId = quickAccessWallet.pickerIconResourceId, - ), + ) ), ) ) - underTest.unselect( - KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, - null, - ) + underTest.unselect(KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_END, null) assertThat(underTest.getSelections()) .isEqualTo( @@ -787,15 +748,29 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { ) } + @EnableSceneContainer + @Test + fun updatesLaunchingAffordanceFromCameraLauncher() = + testScope.runTest { + val launchingAffordance by collectLastValue(underTest.launchingAffordance) + runCurrent() + + kosmos.cameraLauncher.setLaunchingAffordance(true) + runCurrent() + assertThat(launchingAffordance).isTrue() + + kosmos.cameraLauncher.setLaunchingAffordance(false) + runCurrent() + assertThat(launchingAffordance).isFalse() + } + companion object { private const val CONTENT_DESCRIPTION_RESOURCE_ID = 1337 private val ICON: Icon = Icon.Resource( res = CONTENT_DESCRIPTION_RESOURCE_ID, contentDescription = - ContentDescription.Resource( - res = CONTENT_DESCRIPTION_RESOURCE_ID, - ), + ContentDescription.Resource(res = CONTENT_DESCRIPTION_RESOURCE_ID), ) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt index 12eadfcd2539..b5e670c4bbcc 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelTest.kt @@ -36,6 +36,7 @@ import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.flags.parameterizeSceneContainerFlag import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository +import com.android.systemui.keyguard.domain.interactor.pulseExpansionInteractor import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionStep @@ -82,6 +83,7 @@ class KeyguardRootViewModelTest(flags: FlagsParameterization) : SysuiTestCase() private val communalRepository by lazy { kosmos.communalSceneRepository } private val screenOffAnimationController by lazy { kosmos.screenOffAnimationController } private val deviceEntryRepository by lazy { kosmos.fakeDeviceEntryRepository } + private val pulseExpansionInteractor by lazy { kosmos.pulseExpansionInteractor } private val notificationsKeyguardInteractor by lazy { kosmos.notificationsKeyguardInteractor } private val dozeParameters by lazy { kosmos.dozeParameters } private val shadeTestUtil by lazy { kosmos.shadeTestUtil } @@ -188,7 +190,7 @@ class KeyguardRootViewModelTest(flags: FlagsParameterization) : SysuiTestCase() testScope.runTest { val isVisible by collectLastValue(underTest.isNotifIconContainerVisible) runCurrent() - notificationsKeyguardInteractor.setPulseExpanding(true) + pulseExpansionInteractor.setPulseExpanding(true) deviceEntryRepository.setBypassEnabled(false) runCurrent() @@ -200,7 +202,7 @@ class KeyguardRootViewModelTest(flags: FlagsParameterization) : SysuiTestCase() testScope.runTest { val isVisible by collectLastValue(underTest.isNotifIconContainerVisible) runCurrent() - notificationsKeyguardInteractor.setPulseExpanding(false) + pulseExpansionInteractor.setPulseExpanding(false) deviceEntryRepository.setBypassEnabled(true) notificationsKeyguardInteractor.setNotificationsFullyHidden(true) runCurrent() @@ -219,7 +221,7 @@ class KeyguardRootViewModelTest(flags: FlagsParameterization) : SysuiTestCase() to = KeyguardState.DOZING, testScope, ) - notificationsKeyguardInteractor.setPulseExpanding(false) + pulseExpansionInteractor.setPulseExpanding(false) deviceEntryRepository.setBypassEnabled(false) whenever(dozeParameters.alwaysOn).thenReturn(false) notificationsKeyguardInteractor.setNotificationsFullyHidden(true) @@ -239,7 +241,7 @@ class KeyguardRootViewModelTest(flags: FlagsParameterization) : SysuiTestCase() to = KeyguardState.DOZING, testScope, ) - notificationsKeyguardInteractor.setPulseExpanding(false) + pulseExpansionInteractor.setPulseExpanding(false) deviceEntryRepository.setBypassEnabled(false) whenever(dozeParameters.alwaysOn).thenReturn(true) whenever(dozeParameters.displayNeedsBlanking).thenReturn(true) @@ -260,7 +262,7 @@ class KeyguardRootViewModelTest(flags: FlagsParameterization) : SysuiTestCase() to = KeyguardState.DOZING, testScope, ) - notificationsKeyguardInteractor.setPulseExpanding(false) + pulseExpansionInteractor.setPulseExpanding(false) deviceEntryRepository.setBypassEnabled(false) whenever(dozeParameters.alwaysOn).thenReturn(true) whenever(dozeParameters.displayNeedsBlanking).thenReturn(false) @@ -276,7 +278,7 @@ class KeyguardRootViewModelTest(flags: FlagsParameterization) : SysuiTestCase() testScope.runTest { val isVisible by collectLastValue(underTest.isNotifIconContainerVisible) runCurrent() - notificationsKeyguardInteractor.setPulseExpanding(false) + pulseExpansionInteractor.setPulseExpanding(false) deviceEntryRepository.setBypassEnabled(true) whenever(dozeParameters.alwaysOn).thenReturn(true) whenever(dozeParameters.displayNeedsBlanking).thenReturn(false) @@ -298,7 +300,7 @@ class KeyguardRootViewModelTest(flags: FlagsParameterization) : SysuiTestCase() testScope.runTest { val isVisible by collectLastValue(underTest.isNotifIconContainerVisible) runCurrent() - notificationsKeyguardInteractor.setPulseExpanding(false) + pulseExpansionInteractor.setPulseExpanding(false) deviceEntryRepository.setBypassEnabled(false) whenever(dozeParameters.alwaysOn).thenReturn(true) whenever(dozeParameters.displayNeedsBlanking).thenReturn(false) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/AbstractQSFragmentComposeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/AbstractQSFragmentComposeViewModelTest.kt index 2e2894d43b12..b5fc52f7a8d6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/AbstractQSFragmentComposeViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/AbstractQSFragmentComposeViewModelTest.kt @@ -24,12 +24,16 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.lifecycle.activateIn +import com.android.systemui.media.controls.domain.pipeline.legacyMediaDataManagerImpl +import com.android.systemui.media.controls.domain.pipeline.mediaDataManager +import com.android.systemui.qs.composefragment.dagger.usingMediaInComposeFragment import com.android.systemui.testKosmos import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.TestResult import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain import org.junit.After @@ -39,7 +43,7 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) @OptIn(ExperimentalCoroutinesApi::class) abstract class AbstractQSFragmentComposeViewModelTest : SysuiTestCase() { - protected val kosmos = testKosmos() + protected val kosmos = testKosmos().apply { mediaDataManager = legacyMediaDataManagerImpl } protected val lifecycleOwner = TestLifecycleOwner( @@ -62,11 +66,15 @@ abstract class AbstractQSFragmentComposeViewModelTest : SysuiTestCase() { } protected inline fun TestScope.testWithinLifecycle( - crossinline block: suspend TestScope.() -> TestResult + usingMedia: Boolean = true, + crossinline block: suspend TestScope.() -> TestResult, ): TestResult { return runTest { + kosmos.usingMediaInComposeFragment = usingMedia + lifecycleOwner.setCurrentState(Lifecycle.State.RESUMED) underTest.activateIn(kosmos.testScope) + runCurrent() block().also { lifecycleOwner.setCurrentState(Lifecycle.State.DESTROYED) } } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt index 3b00f86c8f6a..9fe9ed2bf47a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelTest.kt @@ -25,6 +25,15 @@ import androidx.test.filters.SmallTest import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope +import com.android.systemui.media.controls.domain.pipeline.legacyMediaDataManagerImpl +import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager +import com.android.systemui.media.controls.ui.controller.mediaCarouselController +import com.android.systemui.media.controls.ui.view.MediaHostState +import com.android.systemui.media.controls.ui.view.qqsMediaHost +import com.android.systemui.media.controls.ui.view.qsMediaHost +import com.android.systemui.qs.composefragment.viewmodel.MediaState.ACTIVE_MEDIA +import com.android.systemui.qs.composefragment.viewmodel.MediaState.ANY_MEDIA +import com.android.systemui.qs.composefragment.viewmodel.MediaState.NO_MEDIA import com.android.systemui.qs.fgsManagerController import com.android.systemui.qs.panels.domain.interactor.tileSquishinessInteractor import com.android.systemui.res.R @@ -35,9 +44,11 @@ import com.android.systemui.statusbar.disableflags.data.repository.fakeDisableFl import com.android.systemui.statusbar.sysuiStatusBarStateController import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidJUnit4::class) @@ -185,6 +196,92 @@ class QSFragmentComposeViewModelTest : AbstractQSFragmentComposeViewModelTest() } } + @Test + fun qqsMediaHost_initializedCorrectly() = + with(kosmos) { + testScope.testWithinLifecycle { + assertThat(underTest.qqsMediaHost.location) + .isEqualTo(MediaHierarchyManager.LOCATION_QQS) + assertThat(underTest.qqsMediaHost.expansion).isEqualTo(MediaHostState.EXPANDED) + assertThat(underTest.qqsMediaHost.showsOnlyActiveMedia).isTrue() + assertThat(underTest.qqsMediaHost.hostView).isNotNull() + } + } + + @Test + fun qsMediaHost_initializedCorrectly() = + with(kosmos) { + testScope.testWithinLifecycle { + assertThat(underTest.qsMediaHost.location) + .isEqualTo(MediaHierarchyManager.LOCATION_QS) + assertThat(underTest.qsMediaHost.expansion).isEqualTo(MediaHostState.EXPANDED) + assertThat(underTest.qsMediaHost.showsOnlyActiveMedia).isFalse() + assertThat(underTest.qsMediaHost.hostView).isNotNull() + } + } + + @Test + fun qqsMediaVisible_onlyWhenActiveMedia() = + with(kosmos) { + testScope.testWithinLifecycle { + whenever(mediaCarouselController.isLockedAndHidden()).thenReturn(false) + + assertThat(underTest.qqsMediaVisible).isEqualTo(underTest.qqsMediaHost.visible) + + setMediaState(NO_MEDIA) + assertThat(underTest.qqsMediaVisible).isFalse() + + setMediaState(ANY_MEDIA) + assertThat(underTest.qqsMediaVisible).isFalse() + + setMediaState(ACTIVE_MEDIA) + assertThat(underTest.qqsMediaVisible).isTrue() + } + } + + @Test + fun qsMediaVisible_onAnyMedia() = + with(kosmos) { + testScope.testWithinLifecycle { + whenever(mediaCarouselController.isLockedAndHidden()).thenReturn(false) + + assertThat(underTest.qsMediaVisible).isEqualTo(underTest.qsMediaHost.visible) + + setMediaState(NO_MEDIA) + assertThat(underTest.qsMediaVisible).isFalse() + + setMediaState(ANY_MEDIA) + assertThat(underTest.qsMediaVisible).isTrue() + + setMediaState(ACTIVE_MEDIA) + assertThat(underTest.qsMediaVisible).isTrue() + } + } + + @Test + fun notUsingMedia_mediaNotVisible() = + with(kosmos) { + testScope.testWithinLifecycle(usingMedia = false) { + setMediaState(ACTIVE_MEDIA) + + assertThat(underTest.qqsMediaVisible).isFalse() + assertThat(underTest.qsMediaVisible).isFalse() + } + } + + private fun TestScope.setMediaState(state: MediaState) { + with(kosmos) { + val activeMedia = state == ACTIVE_MEDIA + val anyMedia = state != NO_MEDIA + whenever(legacyMediaDataManagerImpl.hasActiveMediaOrRecommendation()) + .thenReturn(activeMedia) + whenever(legacyMediaDataManagerImpl.hasAnyMediaOrRecommendation()).thenReturn(anyMedia) + qqsMediaHost.updateViewVisibility() + qsMediaHost.updateViewVisibility() + } + runCurrent() + } + companion object { private const val QS_DISABLE_FLAG = StatusBarManager.DISABLE2_QUICK_SETTINGS @@ -195,3 +292,9 @@ class QSFragmentComposeViewModelTest : AbstractQSFragmentComposeViewModelTest() private const val epsilon = 0.001f } } + +private enum class MediaState { + ACTIVE_MEDIA, + ANY_MEDIA, + NO_MEDIA, +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModelTest.kt index 2c894f9aa20f..ab5a049c91f1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModelTest.kt @@ -20,21 +20,25 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository -import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testCase import com.android.systemui.kosmos.testScope +import com.android.systemui.lifecycle.activateIn import com.android.systemui.qs.panels.domain.interactor.qsPreferencesInteractor import com.android.systemui.qs.pipeline.domain.interactor.currentTilesInteractor import com.android.systemui.qs.pipeline.shared.TileSpec 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.TestScope +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) class QuickQuickSettingsViewModelTest : SysuiTestCase() { @@ -65,7 +69,8 @@ class QuickQuickSettingsViewModelTest : SysuiTestCase() { fakeConfigurationRepository.onConfigurationChange() } - private val underTest = kosmos.quickQuickSettingsViewModel + private val underTest = + kosmos.quickQuickSettingsViewModelFactory.create().apply { activateIn(kosmos.testScope) } @Before fun setUp() { @@ -77,17 +82,15 @@ class QuickQuickSettingsViewModelTest : SysuiTestCase() { with(kosmos) { testScope.runTest { setRows(2) - val columns by collectLastValue(underTest.columns) - val tileViewModels by collectLastValue(underTest.tileViewModels) - assertThat(columns).isEqualTo(4) + assertThat(underTest.columns).isEqualTo(4) // All tiles in 4 columns // [1] [2] [3 3] // [4] [5 5] // [6 6] [7] [8] // [9 9] - assertThat(tileViewModels!!.map { it.tile.spec }).isEqualTo(tiles.take(5)) + assertThat(underTest.tileViewModels.map { it.tile.spec }).isEqualTo(tiles.take(5)) } } @@ -96,10 +99,8 @@ class QuickQuickSettingsViewModelTest : SysuiTestCase() { with(kosmos) { testScope.runTest { setRows(2) - val columns by collectLastValue(underTest.columns) - val tileViewModels by collectLastValue(underTest.tileViewModels) - assertThat(columns).isEqualTo(4) + assertThat(underTest.columns).isEqualTo(4) // All tiles in 4 columns // [1] [2] [3 3] // [4] [5 5] @@ -107,9 +108,9 @@ class QuickQuickSettingsViewModelTest : SysuiTestCase() { // [9 9] setRows(3) - assertThat(tileViewModels!!.map { it.tile.spec }).isEqualTo(tiles.take(8)) + assertThat(underTest.tileViewModels.map { it.tile.spec }).isEqualTo(tiles.take(8)) setRows(1) - assertThat(tileViewModels!!.map { it.tile.spec }).isEqualTo(tiles.take(3)) + assertThat(underTest.tileViewModels.map { it.tile.spec }).isEqualTo(tiles.take(3)) } } @@ -118,10 +119,8 @@ class QuickQuickSettingsViewModelTest : SysuiTestCase() { with(kosmos) { testScope.runTest { setRows(2) - val columns by collectLastValue(underTest.columns) - val tileViewModels by collectLastValue(underTest.tileViewModels) - assertThat(columns).isEqualTo(4) + assertThat(underTest.columns).isEqualTo(4) // All tiles in 4 columns // [1] [2] [3 3] // [4] [5 5] @@ -130,8 +129,9 @@ class QuickQuickSettingsViewModelTest : SysuiTestCase() { // Remove tile small:4 currentTilesInteractor.removeTiles(setOf(tiles[3])) + runCurrent() - assertThat(tileViewModels!!.map { it.tile.spec }) + assertThat(underTest.tileViewModels.map { it.tile.spec }) .isEqualTo( listOf( "$PREFIX_SMALL:1", @@ -149,12 +149,15 @@ class QuickQuickSettingsViewModelTest : SysuiTestCase() { currentTilesInteractor.setTiles(tiles) } - private fun Kosmos.setRows(rows: Int) { - testCase.context.orCreateTestableResources.addOverride( - R.integer.quick_qs_paginated_grid_num_rows, - rows, - ) - fakeConfigurationRepository.onConfigurationChange() + private fun TestScope.setRows(rows: Int) { + with(kosmos) { + testCase.context.orCreateTestableResources.addOverride( + R.integer.quick_qs_paginated_grid_num_rows, + rows, + ) + fakeConfigurationRepository.onConfigurationChange() + } + runCurrent() } private companion object { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt index 9db201465cf2..319f1e577cd9 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt @@ -44,6 +44,8 @@ import com.android.systemui.flags.Flags import com.android.systemui.flags.fakeFeatureFlagsClassic import com.android.systemui.keyguard.ui.viewmodel.lockscreenUserActionsViewModel import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.runCurrent +import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testScope import com.android.systemui.lifecycle.activateIn import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest @@ -70,7 +72,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.launch import kotlinx.coroutines.test.runCurrent -import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -101,7 +102,6 @@ import org.mockito.Mockito.verify @EnableSceneContainer class SceneFrameworkIntegrationTest : SysuiTestCase() { private val kosmos = testKosmos() - private val testScope = kosmos.testScope private var bouncerSceneJob: Job? = null @Before @@ -137,151 +137,149 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { .isTrue() } - @Test - fun startsInLockscreenScene() = - testScope.runTest { kosmos.assertCurrentScene(Scenes.Lockscreen) } + @Test fun startsInLockscreenScene() = kosmos.runTest { assertCurrentScene(Scenes.Lockscreen) } @Test fun clickLockButtonAndEnterCorrectPin_unlocksDevice() = - testScope.runTest { - kosmos.emulateUserDrivenTransition(Scenes.Bouncer) + kosmos.runTest { + emulateUserDrivenTransition(Scenes.Bouncer) - kosmos.fakeSceneDataSource.pause() - kosmos.enterPin() - kosmos.emulatePendingTransitionProgress(expectedVisible = false) - kosmos.assertCurrentScene(Scenes.Gone) + fakeSceneDataSource.pause() + enterPin() + emulatePendingTransitionProgress(expectedVisible = false) + assertCurrentScene(Scenes.Gone) } @Test fun swipeUpOnLockscreen_enterCorrectPin_unlocksDevice() = - testScope.runTest { - val actions by collectLastValue(kosmos.lockscreenUserActionsViewModel.actions) + kosmos.runTest { + val actions by testScope.collectLastValue(kosmos.lockscreenUserActionsViewModel.actions) val upDestinationSceneKey = (actions?.get(Swipe.Up) as? UserActionResult.ChangeScene)?.toScene assertThat(upDestinationSceneKey).isEqualTo(Scenes.Bouncer) - kosmos.emulateUserDrivenTransition(to = upDestinationSceneKey) + emulateUserDrivenTransition(to = upDestinationSceneKey) - kosmos.fakeSceneDataSource.pause() - kosmos.enterPin() - kosmos.emulatePendingTransitionProgress(expectedVisible = false) - kosmos.assertCurrentScene(Scenes.Gone) + fakeSceneDataSource.pause() + enterPin() + emulatePendingTransitionProgress(expectedVisible = false) + assertCurrentScene(Scenes.Gone) } @Test fun swipeUpOnLockscreen_withAuthMethodSwipe_dismissesLockscreen() = - testScope.runTest { - kosmos.setAuthMethod(AuthenticationMethodModel.None, enableLockscreen = true) + kosmos.runTest { + setAuthMethod(AuthenticationMethodModel.None, enableLockscreen = true) - val actions by collectLastValue(kosmos.lockscreenUserActionsViewModel.actions) + val actions by testScope.collectLastValue(lockscreenUserActionsViewModel.actions) val upDestinationSceneKey = (actions?.get(Swipe.Up) as? UserActionResult.ChangeScene)?.toScene assertThat(upDestinationSceneKey).isEqualTo(Scenes.Gone) - kosmos.emulateUserDrivenTransition(to = upDestinationSceneKey) + emulateUserDrivenTransition(to = upDestinationSceneKey) } @Test fun swipeUpOnShadeScene_withAuthMethodSwipe_lockscreenNotDismissed_goesToLockscreen() = - testScope.runTest { - val actions by collectLastValue(kosmos.shadeUserActionsViewModel.actions) - kosmos.setAuthMethod(AuthenticationMethodModel.None, enableLockscreen = true) - kosmos.assertCurrentScene(Scenes.Lockscreen) + kosmos.runTest { + val actions by testScope.collectLastValue(shadeUserActionsViewModel.actions) + setAuthMethod(AuthenticationMethodModel.None, enableLockscreen = true) + assertCurrentScene(Scenes.Lockscreen) // Emulate a user swipe to the shade scene. - kosmos.emulateUserDrivenTransition(to = Scenes.Shade) - kosmos.assertCurrentScene(Scenes.Shade) + emulateUserDrivenTransition(to = Scenes.Shade) + assertCurrentScene(Scenes.Shade) val upDestinationSceneKey = (actions?.get(Swipe.Up) as? UserActionResult.ChangeScene)?.toScene assertThat(upDestinationSceneKey).isEqualTo(Scenes.Lockscreen) - kosmos.emulateUserDrivenTransition(to = Scenes.Lockscreen) + emulateUserDrivenTransition(to = Scenes.Lockscreen) } @Test fun swipeUpOnShadeScene_withAuthMethodSwipe_lockscreenDismissed_goesToGone() = - testScope.runTest { - val actions by collectLastValue(kosmos.shadeUserActionsViewModel.actions) - val canSwipeToEnter by collectLastValue(kosmos.deviceEntryInteractor.canSwipeToEnter) + kosmos.runTest { + val actions by testScope.collectLastValue(shadeUserActionsViewModel.actions) + val canSwipeToEnter by testScope.collectLastValue(deviceEntryInteractor.canSwipeToEnter) - kosmos.setAuthMethod(AuthenticationMethodModel.None, enableLockscreen = true) + setAuthMethod(AuthenticationMethodModel.None, enableLockscreen = true) assertThat(canSwipeToEnter).isTrue() - kosmos.assertCurrentScene(Scenes.Lockscreen) + assertCurrentScene(Scenes.Lockscreen) // Emulate a user swipe to dismiss the lockscreen. - kosmos.emulateUserDrivenTransition(to = Scenes.Gone) - kosmos.assertCurrentScene(Scenes.Gone) + emulateUserDrivenTransition(to = Scenes.Gone) + assertCurrentScene(Scenes.Gone) // Emulate a user swipe to the shade scene. - kosmos.emulateUserDrivenTransition(to = Scenes.Shade) - kosmos.assertCurrentScene(Scenes.Shade) + emulateUserDrivenTransition(to = Scenes.Shade) + assertCurrentScene(Scenes.Shade) val upDestinationSceneKey = (actions?.get(Swipe.Up) as? UserActionResult.ChangeScene)?.toScene assertThat(upDestinationSceneKey).isEqualTo(Scenes.Gone) - kosmos.emulateUserDrivenTransition(to = Scenes.Gone) + emulateUserDrivenTransition(to = Scenes.Gone) } @Test fun withAuthMethodNone_deviceWakeUp_skipsLockscreen() = - testScope.runTest { - kosmos.setAuthMethod(AuthenticationMethodModel.None, enableLockscreen = false) - kosmos.putDeviceToSleep() - kosmos.assertCurrentScene(Scenes.Lockscreen) + kosmos.runTest { + setAuthMethod(AuthenticationMethodModel.None, enableLockscreen = false) + putDeviceToSleep() + assertCurrentScene(Scenes.Lockscreen) - kosmos.wakeUpDevice() - kosmos.assertCurrentScene(Scenes.Gone) + wakeUpDevice() + assertCurrentScene(Scenes.Gone) } @Test fun withAuthMethodSwipe_deviceWakeUp_doesNotSkipLockscreen() = - testScope.runTest { - kosmos.setAuthMethod(AuthenticationMethodModel.None, enableLockscreen = true) - kosmos.putDeviceToSleep() - kosmos.assertCurrentScene(Scenes.Lockscreen) + kosmos.runTest { + setAuthMethod(AuthenticationMethodModel.None, enableLockscreen = true) + putDeviceToSleep() + assertCurrentScene(Scenes.Lockscreen) - kosmos.wakeUpDevice() - kosmos.assertCurrentScene(Scenes.Lockscreen) + wakeUpDevice() + assertCurrentScene(Scenes.Lockscreen) } @Test fun lockDeviceLocksDevice() = - testScope.runTest { - kosmos.unlockDevice() - kosmos.assertCurrentScene(Scenes.Gone) + kosmos.runTest { + unlockDevice() + assertCurrentScene(Scenes.Gone) - kosmos.lockDevice() - kosmos.assertCurrentScene(Scenes.Lockscreen) + lockDevice() + assertCurrentScene(Scenes.Lockscreen) } @Test fun deviceGoesToSleep_switchesToLockscreen() = - testScope.runTest { - kosmos.unlockDevice() - kosmos.assertCurrentScene(Scenes.Gone) + kosmos.runTest { + unlockDevice() + assertCurrentScene(Scenes.Gone) - kosmos.putDeviceToSleep() - kosmos.assertCurrentScene(Scenes.Lockscreen) + putDeviceToSleep() + assertCurrentScene(Scenes.Lockscreen) } @Test fun deviceGoesToSleep_wakeUp_unlock() = - testScope.runTest { - kosmos.unlockDevice() - kosmos.assertCurrentScene(Scenes.Gone) - kosmos.putDeviceToSleep() - kosmos.assertCurrentScene(Scenes.Lockscreen) - kosmos.wakeUpDevice() - kosmos.assertCurrentScene(Scenes.Lockscreen) - - kosmos.unlockDevice() - kosmos.assertCurrentScene(Scenes.Gone) + kosmos.runTest { + unlockDevice() + assertCurrentScene(Scenes.Gone) + putDeviceToSleep() + assertCurrentScene(Scenes.Lockscreen) + wakeUpDevice() + assertCurrentScene(Scenes.Lockscreen) + + unlockDevice() + assertCurrentScene(Scenes.Gone) } @Test fun swipeUpOnLockscreenWhileUnlocked_dismissesLockscreen() = - testScope.runTest { - kosmos.unlockDevice() - val actions by collectLastValue(kosmos.lockscreenUserActionsViewModel.actions) + kosmos.runTest { + unlockDevice() + val actions by testScope.collectLastValue(lockscreenUserActionsViewModel.actions) val upDestinationSceneKey = (actions?.get(Swipe.Up) as? UserActionResult.ChangeScene)?.toScene assertThat(upDestinationSceneKey).isEqualTo(Scenes.Gone) @@ -289,46 +287,46 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { @Test fun deviceGoesToSleep_withLockTimeout_staysOnLockscreen() = - testScope.runTest { - kosmos.unlockDevice() - kosmos.assertCurrentScene(Scenes.Gone) - kosmos.putDeviceToSleep() - kosmos.assertCurrentScene(Scenes.Lockscreen) + kosmos.runTest { + unlockDevice() + assertCurrentScene(Scenes.Gone) + putDeviceToSleep() + assertCurrentScene(Scenes.Lockscreen) // Pretend like the timeout elapsed and now lock the device. - kosmos.lockDevice() - kosmos.assertCurrentScene(Scenes.Lockscreen) + lockDevice() + assertCurrentScene(Scenes.Lockscreen) } @Test fun dismissingIme_whileOnPasswordBouncer_navigatesToLockscreen() = - testScope.runTest { - kosmos.setAuthMethod(AuthenticationMethodModel.Password) - val actions by collectLastValue(kosmos.lockscreenUserActionsViewModel.actions) + kosmos.runTest { + setAuthMethod(AuthenticationMethodModel.Password) + val actions by testScope.collectLastValue(lockscreenUserActionsViewModel.actions) val upDestinationSceneKey = (actions?.get(Swipe.Up) as? UserActionResult.ChangeScene)?.toScene assertThat(upDestinationSceneKey).isEqualTo(Scenes.Bouncer) - kosmos.emulateUserDrivenTransition(to = upDestinationSceneKey) + emulateUserDrivenTransition(to = upDestinationSceneKey) - kosmos.fakeSceneDataSource.pause() - kosmos.dismissIme() + fakeSceneDataSource.pause() + dismissIme() - kosmos.emulatePendingTransitionProgress() - kosmos.assertCurrentScene(Scenes.Lockscreen) + emulatePendingTransitionProgress() + assertCurrentScene(Scenes.Lockscreen) } @Test fun bouncerActionButtonClick_opensEmergencyServicesDialer() = - testScope.runTest { - kosmos.setAuthMethod(AuthenticationMethodModel.Password) - val actions by collectLastValue(kosmos.lockscreenUserActionsViewModel.actions) + kosmos.runTest { + setAuthMethod(AuthenticationMethodModel.Password) + val actions by testScope.collectLastValue(lockscreenUserActionsViewModel.actions) val upDestinationSceneKey = (actions?.get(Swipe.Up) as? UserActionResult.ChangeScene)?.toScene assertThat(upDestinationSceneKey).isEqualTo(Scenes.Bouncer) - kosmos.emulateUserDrivenTransition(to = upDestinationSceneKey) + emulateUserDrivenTransition(to = upDestinationSceneKey) val bouncerActionButton by - collectLastValue(kosmos.bouncerSceneContentViewModel.actionButton) + testScope.collectLastValue(bouncerSceneContentViewModel.actionButton) assertWithMessage("Bouncer action button not visible") .that(bouncerActionButton) .isNotNull() @@ -340,56 +338,56 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { @Test fun bouncerActionButtonClick_duringCall_returnsToCall() = - testScope.runTest { - kosmos.setAuthMethod(AuthenticationMethodModel.Password) - kosmos.startPhoneCall() - val actions by collectLastValue(kosmos.lockscreenUserActionsViewModel.actions) + kosmos.runTest { + setAuthMethod(AuthenticationMethodModel.Password) + startPhoneCall() + val actions by testScope.collectLastValue(lockscreenUserActionsViewModel.actions) val upDestinationSceneKey = (actions?.get(Swipe.Up) as? UserActionResult.ChangeScene)?.toScene assertThat(upDestinationSceneKey).isEqualTo(Scenes.Bouncer) - kosmos.emulateUserDrivenTransition(to = upDestinationSceneKey) + emulateUserDrivenTransition(to = upDestinationSceneKey) val bouncerActionButton by - collectLastValue(kosmos.bouncerSceneContentViewModel.actionButton) + testScope.collectLastValue(bouncerSceneContentViewModel.actionButton) assertWithMessage("Bouncer action button not visible during call") .that(bouncerActionButton) .isNotNull() kosmos.bouncerSceneContentViewModel.onActionButtonClicked(bouncerActionButton!!) runCurrent() - verify(kosmos.mockTelecomManager).showInCallScreen(any()) + verify(mockTelecomManager).showInCallScreen(any()) } @Test fun showBouncer_whenLockedSimIntroduced() = - testScope.runTest { - kosmos.setAuthMethod(AuthenticationMethodModel.None) - kosmos.introduceLockedSim() - kosmos.assertCurrentScene(Scenes.Bouncer) + kosmos.runTest { + setAuthMethod(AuthenticationMethodModel.None) + introduceLockedSim() + assertCurrentScene(Scenes.Bouncer) } @Test fun goesToGone_whenSimUnlocked_whileDeviceUnlocked() = - testScope.runTest { - kosmos.fakeSceneDataSource.pause() - kosmos.introduceLockedSim() - kosmos.emulatePendingTransitionProgress(expectedVisible = true) - kosmos.enterSimPin( + kosmos.runTest { + fakeSceneDataSource.pause() + introduceLockedSim() + emulatePendingTransitionProgress(expectedVisible = true) + enterSimPin( authMethodAfterSimUnlock = AuthenticationMethodModel.None, enableLockscreen = false, ) - kosmos.assertCurrentScene(Scenes.Gone) + assertCurrentScene(Scenes.Gone) } @Test fun showLockscreen_whenSimUnlocked_whileDeviceLocked() = - testScope.runTest { - kosmos.fakeSceneDataSource.pause() - kosmos.introduceLockedSim() - kosmos.emulatePendingTransitionProgress(expectedVisible = true) - kosmos.enterSimPin(authMethodAfterSimUnlock = AuthenticationMethodModel.Pin) - kosmos.assertCurrentScene(Scenes.Lockscreen) + kosmos.runTest { + fakeSceneDataSource.pause() + introduceLockedSim() + emulatePendingTransitionProgress(expectedVisible = true) + enterSimPin(authMethodAfterSimUnlock = AuthenticationMethodModel.Pin) + assertCurrentScene(Scenes.Lockscreen) } /** @@ -457,10 +455,10 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { */ private fun Kosmos.emulatePendingTransitionProgress(expectedVisible: Boolean = true) { assertWithMessage("The FakeSceneDataSource has to be paused for this to do anything.") - .that(kosmos.fakeSceneDataSource.isPaused) + .that(fakeSceneDataSource.isPaused) .isTrue() - val to = kosmos.fakeSceneDataSource.pendingScene ?: return + val to = fakeSceneDataSource.pendingScene ?: return val from = getCurrentSceneInUi() if (to == from) { @@ -489,7 +487,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { // End the transition and report the change. transitionState.value = ObservableTransitionState.Idle(to) - kosmos.fakeSceneDataSource.unpause(force = true) + fakeSceneDataSource.unpause(force = true) testScope.runCurrent() assertWithMessage("Visibility mismatch after scene transition from $from to $to!") @@ -523,7 +521,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { private fun Kosmos.emulateUserDrivenTransition(to: SceneKey?) { checkNotNull(to) - kosmos.fakeSceneDataSource.pause() + fakeSceneDataSource.pause() sceneInteractor.changeScene(to, "reason") emulatePendingTransitionProgress(expectedVisible = to != Scenes.Gone) @@ -634,7 +632,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { } /** Changes device wakefulness state from awake to asleep, going through intermediary states. */ - private suspend fun Kosmos.putDeviceToSleep() { + private fun Kosmos.putDeviceToSleep() { val wakefulnessModel = powerInteractor.detailedWakefulness.value assertWithMessage("Cannot put device to sleep as it's already asleep!") .that(wakefulnessModel.isAwake()) 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 499592051731..2c8f7cf47723 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 @@ -32,6 +32,7 @@ import com.android.compose.animation.scene.SceneKey import com.android.internal.logging.uiEventLoggerFake import com.android.internal.policy.IKeyguardDismissCallback import com.android.keyguard.AuthInteractionProperties +import com.android.keyguard.keyguardUpdateMonitor import com.android.systemui.Flags import com.android.systemui.SysuiTestCase import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository @@ -71,8 +72,6 @@ import com.android.systemui.keyguard.dismissCallbackRegistry import com.android.systemui.keyguard.domain.interactor.keyguardEnabledInteractor import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.domain.interactor.scenetransition.lockscreenSceneTransitionInteractor -import com.android.systemui.keyguard.shared.model.BiometricUnlockMode -import com.android.systemui.keyguard.shared.model.BiometricUnlockSource import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus @@ -128,6 +127,7 @@ import org.mockito.Mockito.never import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations +import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidJUnit4::class) @@ -159,7 +159,8 @@ class SceneContainerStartableTest : SysuiTestCase() { @Before fun setUp() { MockitoAnnotations.initMocks(this) - + whenever(kosmos.keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(anyBoolean())) + .thenReturn(true) underTest = kosmos.sceneContainerStartable } @@ -405,10 +406,7 @@ class SceneContainerStartableTest : SysuiTestCase() { assertThat(currentSceneKey).isEqualTo(Scenes.Bouncer) underTest.start() - kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( - SuccessFingerprintAuthenticationStatus(0, true) - ) - + updateFingerprintAuthStatus(isSuccess = true) assertThat(currentSceneKey).isEqualTo(Scenes.Gone) } @@ -430,10 +428,7 @@ class SceneContainerStartableTest : SysuiTestCase() { assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) underTest.start() - kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( - SuccessFingerprintAuthenticationStatus(0, true) - ) - + updateFingerprintAuthStatus(isSuccess = true) assertThat(currentSceneKey).isEqualTo(Scenes.Gone) assertThat(alternateBouncerVisible).isFalse() } @@ -464,9 +459,7 @@ class SceneContainerStartableTest : SysuiTestCase() { runCurrent() assertThat(currentSceneKey).isEqualTo(Scenes.QuickSettings) - kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( - SuccessFingerprintAuthenticationStatus(0, true) - ) + updateFingerprintAuthStatus(isSuccess = true) runCurrent() assertThat(currentSceneKey).isEqualTo(Scenes.QuickSettings) @@ -501,10 +494,7 @@ class SceneContainerStartableTest : SysuiTestCase() { assertThat(currentSceneKey).isEqualTo(Scenes.Bouncer) assertThat(backStack?.asIterable()?.last()).isEqualTo(Scenes.Lockscreen) - kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( - SuccessFingerprintAuthenticationStatus(0, true) - ) - + updateFingerprintAuthStatus(isSuccess = true) assertThat(currentSceneKey).isEqualTo(Scenes.QuickSettings) assertThat(backStack?.asIterable()?.last()).isEqualTo(Scenes.Gone) } @@ -535,10 +525,7 @@ class SceneContainerStartableTest : SysuiTestCase() { runCurrent() assertThat(currentSceneKey).isEqualTo(Scenes.Bouncer) - kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( - SuccessFingerprintAuthenticationStatus(0, true) - ) - + updateFingerprintAuthStatus(isSuccess = true) assertThat(currentSceneKey).isEqualTo(Scenes.Gone) } @@ -554,10 +541,7 @@ class SceneContainerStartableTest : SysuiTestCase() { assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) underTest.start() - kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( - SuccessFingerprintAuthenticationStatus(0, true) - ) - + updateFingerprintAuthStatus(isSuccess = true) assertThat(currentSceneKey).isEqualTo(Scenes.Gone) } @@ -592,9 +576,7 @@ class SceneContainerStartableTest : SysuiTestCase() { transitionStateFlowValue.value = ObservableTransitionState.Idle(Scenes.Shade) assertThat(currentSceneKey).isEqualTo(Scenes.Shade) - kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( - SuccessFingerprintAuthenticationStatus(0, true) - ) + updateFingerprintAuthStatus(isSuccess = true) runCurrent() assertThat(currentSceneKey).isEqualTo(Scenes.Shade) @@ -786,6 +768,7 @@ class SceneContainerStartableTest : SysuiTestCase() { @DisableFlags(Flags.FLAG_MSDL_FEEDBACK) fun playSuccessHaptics_onSuccessfulLockscreenAuth_udfps() = testScope.runTest { + whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true) val currentSceneKey by collectLastValue(sceneInteractor.currentScene) val playSuccessHaptic by collectLastValue(deviceEntryHapticsInteractor.playSuccessHaptic) @@ -795,24 +778,19 @@ class SceneContainerStartableTest : SysuiTestCase() { assertThat(kosmos.deviceEntryInteractor.isDeviceEntered.value).isFalse() underTest.start() - unlockWithFingerprintAuth() + // unlock with fingerprint + updateFingerprintAuthStatus(isSuccess = true) assertThat(playSuccessHaptic).isNotNull() - assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) - verify(vibratorHelper) - .vibrateAuthSuccess( - "SceneContainerStartable, $currentSceneKey device-entry::success" - ) + verify(vibratorHelper).vibrateAuthSuccess(anyString()) verify(vibratorHelper, never()).vibrateAuthError(anyString()) - - updateFingerprintAuthStatus(isSuccess = true) - assertThat(currentSceneKey).isEqualTo(Scenes.Gone) } @Test @EnableFlags(Flags.FLAG_MSDL_FEEDBACK) fun playSuccessMSDLHaptics_onSuccessfulLockscreenAuth_udfps() = testScope.runTest { + whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true) val currentSceneKey by collectLastValue(sceneInteractor.currentScene) val playSuccessHaptic by collectLastValue(deviceEntryHapticsInteractor.playSuccessHaptic) @@ -822,21 +800,19 @@ class SceneContainerStartableTest : SysuiTestCase() { assertThat(kosmos.deviceEntryInteractor.isDeviceEntered.value).isFalse() underTest.start() - unlockWithFingerprintAuth() + // unlock with fingerprint + updateFingerprintAuthStatus(isSuccess = true) assertThat(playSuccessHaptic).isNotNull() - assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.UNLOCK) assertThat(msdlPlayer.latestPropertiesPlayed).isEqualTo(authInteractionProperties) - - updateFingerprintAuthStatus(isSuccess = true) - assertThat(currentSceneKey).isEqualTo(Scenes.Gone) } @Test @DisableFlags(Flags.FLAG_MSDL_FEEDBACK) fun playSuccessHaptics_onSuccessfulLockscreenAuth_sfps() = testScope.runTest { + whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true) val currentSceneKey by collectLastValue(sceneInteractor.currentScene) val playSuccessHaptic by collectLastValue(deviceEntryHapticsInteractor.playSuccessHaptic) @@ -847,24 +823,19 @@ class SceneContainerStartableTest : SysuiTestCase() { underTest.start() allowHapticsOnSfps() - unlockWithFingerprintAuth() + // unlock with fingerprint + updateFingerprintAuthStatus(isSuccess = true) assertThat(playSuccessHaptic).isNotNull() - assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) - verify(vibratorHelper) - .vibrateAuthSuccess( - "SceneContainerStartable, $currentSceneKey device-entry::success" - ) + verify(vibratorHelper).vibrateAuthSuccess(anyString()) verify(vibratorHelper, never()).vibrateAuthError(anyString()) - - updateFingerprintAuthStatus(isSuccess = true) - assertThat(currentSceneKey).isEqualTo(Scenes.Gone) } @Test @EnableFlags(Flags.FLAG_MSDL_FEEDBACK) fun playSuccessMSDLHaptics_onSuccessfulLockscreenAuth_sfps() = testScope.runTest { + whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true) val currentSceneKey by collectLastValue(sceneInteractor.currentScene) val playSuccessHaptic by collectLastValue(deviceEntryHapticsInteractor.playSuccessHaptic) @@ -875,15 +846,12 @@ class SceneContainerStartableTest : SysuiTestCase() { underTest.start() allowHapticsOnSfps() - unlockWithFingerprintAuth() + // unlock with fingerprint + updateFingerprintAuthStatus(isSuccess = true) assertThat(playSuccessHaptic).isNotNull() - assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.UNLOCK) assertThat(msdlPlayer.latestPropertiesPlayed).isEqualTo(authInteractionProperties) - - updateFingerprintAuthStatus(isSuccess = true) - assertThat(currentSceneKey).isEqualTo(Scenes.Gone) } @Test @@ -902,8 +870,7 @@ class SceneContainerStartableTest : SysuiTestCase() { assertThat(playErrorHaptic).isNotNull() assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) - verify(vibratorHelper) - .vibrateAuthError("SceneContainerStartable, $currentSceneKey device-entry::error") + verify(vibratorHelper).vibrateAuthError(anyString()) verify(vibratorHelper, never()).vibrateAuthSuccess(anyString()) } @@ -943,8 +910,7 @@ class SceneContainerStartableTest : SysuiTestCase() { assertThat(playErrorHaptic).isNotNull() assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) - verify(vibratorHelper) - .vibrateAuthError("SceneContainerStartable, $currentSceneKey device-entry::error") + verify(vibratorHelper).vibrateAuthError(anyString()) verify(vibratorHelper, never()).vibrateAuthSuccess(anyString()) } @@ -972,6 +938,7 @@ class SceneContainerStartableTest : SysuiTestCase() { @DisableFlags(Flags.FLAG_MSDL_FEEDBACK) fun skipsSuccessHaptics_whenPowerButtonDown_sfps() = testScope.runTest { + whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true) val currentSceneKey by collectLastValue(sceneInteractor.currentScene) val playSuccessHaptic by collectLastValue(deviceEntryHapticsInteractor.playSuccessHaptic) @@ -982,24 +949,19 @@ class SceneContainerStartableTest : SysuiTestCase() { underTest.start() allowHapticsOnSfps(isPowerButtonDown = true) - unlockWithFingerprintAuth() + // unlock with fingerprint + updateFingerprintAuthStatus(isSuccess = true) assertThat(playSuccessHaptic).isNull() - assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) - verify(vibratorHelper, never()) - .vibrateAuthSuccess( - "SceneContainerStartable, $currentSceneKey device-entry::success" - ) + verify(vibratorHelper, never()).vibrateAuthSuccess(anyString()) verify(vibratorHelper, never()).vibrateAuthError(anyString()) - - updateFingerprintAuthStatus(isSuccess = true) - assertThat(currentSceneKey).isEqualTo(Scenes.Gone) } @Test @EnableFlags(Flags.FLAG_MSDL_FEEDBACK) fun skipsMSDLSuccessHaptics_whenPowerButtonDown_sfps() = testScope.runTest { + whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true) val currentSceneKey by collectLastValue(sceneInteractor.currentScene) val playSuccessHaptic by collectLastValue(deviceEntryHapticsInteractor.playSuccessHaptic) @@ -1010,21 +972,19 @@ class SceneContainerStartableTest : SysuiTestCase() { underTest.start() allowHapticsOnSfps(isPowerButtonDown = true) - unlockWithFingerprintAuth() + // unlock with fingerprint + updateFingerprintAuthStatus(isSuccess = true) assertThat(playSuccessHaptic).isNull() - assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) assertThat(msdlPlayer.latestTokenPlayed).isNull() assertThat(msdlPlayer.latestPropertiesPlayed).isNull() - - updateFingerprintAuthStatus(isSuccess = true) - assertThat(currentSceneKey).isEqualTo(Scenes.Gone) } @Test @DisableFlags(Flags.FLAG_MSDL_FEEDBACK) fun skipsSuccessHaptics_whenPowerButtonRecentlyPressed_sfps() = testScope.runTest { + whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true) val currentSceneKey by collectLastValue(sceneInteractor.currentScene) val playSuccessHaptic by collectLastValue(deviceEntryHapticsInteractor.playSuccessHaptic) @@ -1035,24 +995,19 @@ class SceneContainerStartableTest : SysuiTestCase() { underTest.start() allowHapticsOnSfps(lastPowerPress = 50) - unlockWithFingerprintAuth() + // unlock with fingerprint + updateFingerprintAuthStatus(isSuccess = true) assertThat(playSuccessHaptic).isNull() - assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) - verify(vibratorHelper, never()) - .vibrateAuthSuccess( - "SceneContainerStartable, $currentSceneKey device-entry::success" - ) + verify(vibratorHelper, never()).vibrateAuthSuccess(anyString()) verify(vibratorHelper, never()).vibrateAuthError(anyString()) - - updateFingerprintAuthStatus(isSuccess = true) - assertThat(currentSceneKey).isEqualTo(Scenes.Gone) } @Test @EnableFlags(Flags.FLAG_MSDL_FEEDBACK) fun skipsMSDLSuccessHaptics_whenPowerButtonRecentlyPressed_sfps() = testScope.runTest { + whenever(kosmos.keyguardUpdateMonitor.isDeviceInteractive).thenReturn(true) val currentSceneKey by collectLastValue(sceneInteractor.currentScene) val playSuccessHaptic by collectLastValue(deviceEntryHapticsInteractor.playSuccessHaptic) @@ -1063,15 +1018,12 @@ class SceneContainerStartableTest : SysuiTestCase() { underTest.start() allowHapticsOnSfps(lastPowerPress = 50) - unlockWithFingerprintAuth() + // unlock with fingerprint + updateFingerprintAuthStatus(isSuccess = true) assertThat(playSuccessHaptic).isNull() - assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) assertThat(msdlPlayer.latestTokenPlayed).isNull() assertThat(msdlPlayer.latestPropertiesPlayed).isNull() - - updateFingerprintAuthStatus(isSuccess = true) - assertThat(currentSceneKey).isEqualTo(Scenes.Gone) } @Test @@ -1090,9 +1042,7 @@ class SceneContainerStartableTest : SysuiTestCase() { updateFingerprintAuthStatus(isSuccess = false) assertThat(playErrorHaptic).isNull() - assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) - verify(vibratorHelper, never()) - .vibrateAuthError("SceneContainerStartable, $currentSceneKey device-entry::error") + verify(vibratorHelper, never()).vibrateAuthError(anyString()) verify(vibratorHelper, never()).vibrateAuthSuccess(anyString()) } @@ -1112,7 +1062,6 @@ class SceneContainerStartableTest : SysuiTestCase() { updateFingerprintAuthStatus(isSuccess = false) assertThat(playErrorHaptic).isNull() - assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) assertThat(msdlPlayer.latestTokenPlayed).isNull() assertThat(msdlPlayer.latestPropertiesPlayed).isNull() } @@ -1132,9 +1081,7 @@ class SceneContainerStartableTest : SysuiTestCase() { updateFaceAuthStatus(isSuccess = false) assertThat(playErrorHaptic).isNull() - assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) - verify(vibratorHelper, never()) - .vibrateAuthError("SceneContainerStartable, $currentSceneKey device-entry::error") + verify(vibratorHelper, never()).vibrateAuthError(anyString()) verify(vibratorHelper, never()).vibrateAuthSuccess(anyString()) } @@ -1153,7 +1100,6 @@ class SceneContainerStartableTest : SysuiTestCase() { updateFaceAuthStatus(isSuccess = false) assertThat(playErrorHaptic).isNull() - assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) assertThat(msdlPlayer.latestTokenPlayed).isNull() assertThat(msdlPlayer.latestPropertiesPlayed).isNull() } @@ -1176,9 +1122,7 @@ class SceneContainerStartableTest : SysuiTestCase() { ) .forEachIndexed { index, sceneKey -> if (sceneKey == Scenes.Gone) { - kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( - SuccessFingerprintAuthenticationStatus(0, true) - ) + updateFingerprintAuthStatus(isSuccess = true) runCurrent() } fakeSceneDataSource.pause() @@ -1334,9 +1278,7 @@ class SceneContainerStartableTest : SysuiTestCase() { assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen) underTest.start() - kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( - SuccessFingerprintAuthenticationStatus(0, true) - ) + updateFingerprintAuthStatus(isSuccess = true) runCurrent() powerInteractor.setAwakeForTest() runCurrent() @@ -1586,9 +1528,7 @@ class SceneContainerStartableTest : SysuiTestCase() { runCurrent() verify(falsingCollector).onBouncerShown() - kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( - SuccessFingerprintAuthenticationStatus(0, true) - ) + updateFingerprintAuthStatus(isSuccess = true) runCurrent() sceneInteractor.changeScene(Scenes.Gone, "reason") runCurrent() @@ -2350,9 +2290,7 @@ class SceneContainerStartableTest : SysuiTestCase() { val dismissCallback: IKeyguardDismissCallback = mock() kosmos.dismissCallbackRegistry.addCallback(dismissCallback) - kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( - SuccessFingerprintAuthenticationStatus(0, true) - ) + updateFingerprintAuthStatus(isSuccess = true) runCurrent() kosmos.fakeExecutor.runAllReady() @@ -2641,13 +2579,6 @@ class SceneContainerStartableTest : SysuiTestCase() { runCurrent() } - private fun unlockWithFingerprintAuth() { - kosmos.fakeKeyguardRepository.setBiometricUnlockSource( - BiometricUnlockSource.FINGERPRINT_SENSOR - ) - kosmos.fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockMode.UNLOCK_COLLAPSING) - } - private fun TestScope.setupBiometricAuth( hasSfps: Boolean = false, hasUdfps: Boolean = false, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt index b632a8aaad46..af895c82c975 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModelTest.kt @@ -21,6 +21,8 @@ package com.android.systemui.scene.ui.viewmodel import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.view.MotionEvent +import android.view.MotionEvent.ACTION_DOWN +import android.view.MotionEvent.ACTION_OUTSIDE import android.view.View import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -43,6 +45,7 @@ import com.android.systemui.shade.data.repository.fakeShadeRepository import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.shade.shared.flag.DualShade import com.android.systemui.shade.shared.model.ShadeMode +import com.android.systemui.statusbar.data.repository.fakeRemoteInputRepository import com.android.systemui.testKosmos import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever @@ -68,6 +71,7 @@ class SceneContainerViewModelTest : SysuiTestCase() { private val fakeSceneDataSource by lazy { kosmos.fakeSceneDataSource } private val fakeShadeRepository by lazy { kosmos.fakeShadeRepository } private val sceneContainerConfig by lazy { kosmos.sceneContainerConfig } + private val fakeRemoteInputRepository by lazy { kosmos.fakeRemoteInputRepository } private val falsingManager by lazy { kosmos.fakeFalsingManager } private val view = mock<View>() @@ -234,6 +238,35 @@ class SceneContainerViewModelTest : SysuiTestCase() { } @Test + fun userInputOnEmptySpace_insideEvent() = + testScope.runTest { + assertThat(fakeRemoteInputRepository.areRemoteInputsClosed).isFalse() + val insideMotionEvent = MotionEvent.obtain(0, 0, ACTION_DOWN, 0f, 0f, 0) + underTest.onEmptySpaceMotionEvent(insideMotionEvent) + assertThat(fakeRemoteInputRepository.areRemoteInputsClosed).isFalse() + } + + @Test + fun userInputOnEmptySpace_outsideEvent_remoteInputActive() = + testScope.runTest { + fakeRemoteInputRepository.isRemoteInputActive.value = true + assertThat(fakeRemoteInputRepository.areRemoteInputsClosed).isFalse() + val outsideMotionEvent = MotionEvent.obtain(0, 0, ACTION_OUTSIDE, 0f, 0f, 0) + underTest.onEmptySpaceMotionEvent(outsideMotionEvent) + assertThat(fakeRemoteInputRepository.areRemoteInputsClosed).isTrue() + } + + @Test + fun userInputOnEmptySpace_outsideEvent_remoteInputInactive() = + testScope.runTest { + fakeRemoteInputRepository.isRemoteInputActive.value = false + assertThat(fakeRemoteInputRepository.areRemoteInputsClosed).isFalse() + val outsideMotionEvent = MotionEvent.obtain(0, 0, ACTION_OUTSIDE, 0f, 0f, 0) + underTest.onEmptySpaceMotionEvent(outsideMotionEvent) + assertThat(fakeRemoteInputRepository.areRemoteInputsClosed).isFalse() + } + + @Test fun remoteUserInteraction_keepsContainerVisible() = testScope.runTest { sceneInteractor.setVisible(false, "reason") diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java index 0ff2a4a9b7a3..01c17bd6285c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java @@ -212,6 +212,7 @@ import org.junit.Rule; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; +import org.mockito.invocation.InvocationOnMock; import org.mockito.junit.MockitoJUnit; import org.mockito.junit.MockitoRule; import org.mockito.stubbing.Answer; @@ -515,7 +516,8 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { when(mView.findViewById(R.id.qs_frame)).thenReturn(mQsFrame); when(mView.findViewById(R.id.keyguard_status_view)) .thenReturn(mock(KeyguardStatusView.class)); - View rootView = mock(View.class); + ViewGroup rootView = mock(ViewGroup.class); + when(rootView.isVisibleToUser()).thenReturn(true); when(mView.getRootView()).thenReturn(rootView); when(rootView.findViewById(R.id.keyguard_status_view)) .thenReturn(mock(KeyguardStatusView.class)); @@ -617,7 +619,8 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { mScreenOffAnimationController, new NotificationWakeUpCoordinatorLogger(logcatLogBuffer()), notifsKeyguardInteractor, - mKosmos.getCommunalInteractor()); + mKosmos.getCommunalInteractor(), + mKosmos.getPulseExpansionInteractor()); mConfigurationController = new ConfigurationControllerImpl(mContext); PulseExpansionHandler expansionHandler = new PulseExpansionHandler( mContext, @@ -652,12 +655,21 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { ((Runnable) invocation.getArgument(0)).run(); return null; }).when(mNotificationShadeWindowController).batchApplyWindowLayoutParams(any()); + when(mNotificationShadeWindowController.getWindowRootView()).thenReturn(rootView); doAnswer(invocation -> { mLayoutChangeListener = invocation.getArgument(0); return null; }).when(mView).addOnLayoutChangeListener(any()); when(mView.getViewTreeObserver()).thenReturn(mViewTreeObserver); + doAnswer(new Answer<Void>() { + @Override + public Void answer(InvocationOnMock invocation) throws Throwable { + ViewTreeObserver.OnGlobalLayoutListener gll = invocation.getArgument(0); + gll.onGlobalLayout(); + return null; + } + }).when(mViewTreeObserver).addOnGlobalLayoutListener(any()); when(mView.getParent()).thenReturn(mViewParent); when(mQs.getHeader()).thenReturn(mQsHeader); when(mDownMotionEvent.getAction()).thenReturn(MotionEvent.ACTION_DOWN); @@ -911,7 +923,7 @@ public class NotificationPanelViewControllerBaseTest extends SysuiTestCase { } protected boolean onTouchEvent(MotionEvent ev) { - return mTouchHandler.onTouch(mView, ev); + return mNotificationPanelViewController.handleExternalTouch(ev); } protected void setDozing(boolean dozing, boolean dozingAlwaysOn) { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java index ec75972aecfe..550fcf78d857 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerTest.java @@ -364,6 +364,64 @@ public class NotificationPanelViewControllerTest extends NotificationPanelViewCo } @Test + @EnableFlags(com.android.systemui.Flags.FLAG_SHADE_EXPANDS_ON_STATUS_BAR_LONG_PRESS) + public void onStatusBarLongPress_shadeExpands() { + long downTime = 42L; + // Start touch session with down event + onTouchEvent(MotionEvent.obtain(downTime, downTime, MotionEvent.ACTION_DOWN, 1f, 1f, 0)); + // Status bar triggers long press expand + mNotificationPanelViewController.onStatusBarLongPress( + MotionEvent.obtain(downTime, downTime + 27L, MotionEvent.ACTION_MOVE, 1f, 1f, 0)); + assertThat(mNotificationPanelViewController.isExpanded()).isTrue(); + // Shade ignores the rest of the long press's touch session + assertThat(onTouchEvent( + MotionEvent.obtain(downTime, downTime + 42L, MotionEvent.ACTION_MOVE, 1f, 1f, + 0))).isFalse(); + + // Start new touch session + long downTime2 = downTime + 100L; + assertThat(onTouchEvent( + MotionEvent.obtain(downTime2, downTime2, MotionEvent.ACTION_DOWN, 1f, 1f, + 0))).isTrue(); + // Shade no longer ignoring touches + assertThat(onTouchEvent( + MotionEvent.obtain(downTime2, downTime2 + 2L, MotionEvent.ACTION_MOVE, 1f, 1f, + 0))).isTrue(); + } + + @Test + @EnableFlags(com.android.systemui.Flags.FLAG_SHADE_EXPANDS_ON_STATUS_BAR_LONG_PRESS) + public void onStatusBarLongPress_qsExpands() { + long downTime = 42L; + // Start with shade already expanded + mNotificationPanelViewController.setExpandedFraction(1F); + + // Start touch session with down event + onTouchEvent(MotionEvent.obtain(downTime, downTime, MotionEvent.ACTION_DOWN, 1f, 1f, 0)); + // Status bar triggers long press expand + mNotificationPanelViewController.onStatusBarLongPress( + MotionEvent.obtain(downTime, downTime + 27L, MotionEvent.ACTION_MOVE, 1f, 1f, 0)); + assertThat(mNotificationPanelViewController.isExpanded()).isTrue(); + // Shade expands to QS + verify(mQsController, atLeastOnce()).flingQs(0F, ShadeViewController.FLING_EXPAND); + // Shade ignores the rest of the long press's touch session + assertThat(onTouchEvent( + MotionEvent.obtain(downTime, downTime + 42L, MotionEvent.ACTION_MOVE, 1f, 1f, + 0))).isFalse(); + + // Start new touch session + long downTime2 = downTime + 100L; + assertThat(onTouchEvent( + MotionEvent.obtain(downTime2, downTime2, MotionEvent.ACTION_DOWN, 1f, 1f, + 0))).isTrue(); + // Shade no longer ignoring touches + assertThat(onTouchEvent( + MotionEvent.obtain(downTime2, downTime2 + 2L, MotionEvent.ACTION_MOVE, 1f, 1f, + 0))).isTrue(); + + } + + @Test @DisableFlags(com.android.systemui.Flags.FLAG_MIGRATE_CLOCKS_TO_BLUEPRINT) public void test_pulsing_onTouchEvent_noTracking() { // GIVEN device is pulsing diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt index 022825a48de3..f4a43a454a6f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/clocks/ClockRegistryTest.kt @@ -22,19 +22,20 @@ import android.graphics.drawable.Drawable import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.plugins.PluginLifecycleManager +import com.android.systemui.plugins.PluginListener +import com.android.systemui.plugins.PluginManager import com.android.systemui.plugins.clocks.ClockController +import com.android.systemui.plugins.clocks.ClockFontAxisSetting import com.android.systemui.plugins.clocks.ClockId import com.android.systemui.plugins.clocks.ClockMessageBuffers import com.android.systemui.plugins.clocks.ClockMetadata import com.android.systemui.plugins.clocks.ClockPickerConfig import com.android.systemui.plugins.clocks.ClockProviderPlugin import com.android.systemui.plugins.clocks.ClockSettings -import com.android.systemui.plugins.PluginLifecycleManager -import com.android.systemui.plugins.PluginListener -import com.android.systemui.plugins.PluginManager +import com.android.systemui.util.ThreadAssert import com.android.systemui.util.mockito.argumentCaptor import com.android.systemui.util.mockito.eq -import com.android.systemui.util.ThreadAssert import java.util.function.BiConsumer import junit.framework.Assert.assertEquals import junit.framework.Assert.fail @@ -42,6 +43,8 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestCoroutineScheduler import kotlinx.coroutines.test.TestScope +import org.json.JSONArray +import org.json.JSONObject import org.junit.Before import org.junit.Rule import org.junit.Test @@ -81,28 +84,32 @@ class ClockRegistryTest : SysuiTestCase() { return null!! } - private fun failPickerConfig(clockId: ClockId): ClockPickerConfig { - fail("Unexpected call to getClockPickerConfig: $clockId") + private fun failPickerConfig(settings: ClockSettings): ClockPickerConfig { + fail("Unexpected call to getClockPickerConfig: ${settings.clockId}") return null!! } } - private class FakeLifecycle( - private val tag: String, - private val plugin: ClockProviderPlugin?, - ) : PluginLifecycleManager<ClockProviderPlugin> { + private class FakeLifecycle(private val tag: String, private val plugin: ClockProviderPlugin?) : + PluginLifecycleManager<ClockProviderPlugin> { var onLoad: (() -> Unit)? = null var onUnload: (() -> Unit)? = null private var mIsLoaded: Boolean = true + override fun isLoaded() = mIsLoaded + override fun getPlugin(): ClockProviderPlugin? = if (isLoaded) plugin else null var mComponentName = ComponentName("Package[$tag]", "Class[$tag]") + override fun toString() = "Manager[$tag]" + override fun getPackage(): String = mComponentName.getPackageName() + override fun getComponentName(): ComponentName = mComponentName - override fun setLogFunc(func: BiConsumer<String, String>) { } + + override fun setLogFunc(func: BiConsumer<String, String>) {} override fun loadPlugin() { if (!mIsLoaded) { @@ -122,7 +129,7 @@ class ClockRegistryTest : SysuiTestCase() { private class FakeClockPlugin : ClockProviderPlugin { private val metadata = mutableListOf<ClockMetadata>() private val createCallbacks = mutableMapOf<ClockId, (ClockId) -> ClockController>() - private val pickerConfigs = mutableMapOf<ClockId, (ClockId) -> ClockPickerConfig>() + private val pickerConfigs = mutableMapOf<ClockId, (ClockSettings) -> ClockPickerConfig>() override fun getClocks() = metadata @@ -132,17 +139,17 @@ class ClockRegistryTest : SysuiTestCase() { ?: throw NotImplementedError("No callback for '$clockId'") } - override fun getClockPickerConfig(clockId: ClockId): ClockPickerConfig { - return pickerConfigs[clockId]?.invoke(clockId) - ?: throw NotImplementedError("No picker config for '$clockId'") + override fun getClockPickerConfig(settings: ClockSettings): ClockPickerConfig { + return pickerConfigs[settings.clockId]?.invoke(settings) + ?: throw NotImplementedError("No picker config for '${settings.clockId}'") } - override fun initialize(buffers: ClockMessageBuffers?) { } + override fun initialize(buffers: ClockMessageBuffers?) {} fun addClock( id: ClockId, create: (ClockId) -> ClockController = ::failFactory, - getPickerConfig: (ClockId) -> ClockPickerConfig = ::failPickerConfig + getPickerConfig: (ClockSettings) -> ClockPickerConfig = ::failPickerConfig, ): FakeClockPlugin { metadata.add(ClockMetadata(id)) createCallbacks[id] = create @@ -158,29 +165,32 @@ class ClockRegistryTest : SysuiTestCase() { scope = TestScope(dispatcher) pickerConfig = ClockPickerConfig("CLOCK_ID", "NAME", "DESC", mockThumbnail) - fakeDefaultProvider = FakeClockPlugin() - .addClock(DEFAULT_CLOCK_ID, { mockDefaultClock }, { pickerConfig }) + fakeDefaultProvider = + FakeClockPlugin().addClock(DEFAULT_CLOCK_ID, { mockDefaultClock }, { pickerConfig }) whenever(mockContext.contentResolver).thenReturn(mockContentResolver) val captor = argumentCaptor<PluginListener<ClockProviderPlugin>>() - registry = object : ClockRegistry( - mockContext, - mockPluginManager, - scope = scope.backgroundScope, - mainDispatcher = dispatcher, - bgDispatcher = dispatcher, - isEnabled = true, - handleAllUsers = true, - defaultClockProvider = fakeDefaultProvider, - keepAllLoaded = false, - subTag = "Test", - assert = mockThreadAssert, - ) { - override fun querySettings() { } - override fun applySettings(value: ClockSettings?) { - settings = value + registry = + object : + ClockRegistry( + mockContext, + mockPluginManager, + scope = scope.backgroundScope, + mainDispatcher = dispatcher, + bgDispatcher = dispatcher, + isEnabled = true, + handleAllUsers = true, + defaultClockProvider = fakeDefaultProvider, + keepAllLoaded = false, + subTag = "Test", + assert = mockThreadAssert, + ) { + override fun querySettings() {} + + override fun applySettings(value: ClockSettings?) { + settings = value + } } - } registry.registerListeners() verify(mockPluginManager) @@ -190,14 +200,10 @@ class ClockRegistryTest : SysuiTestCase() { @Test fun pluginRegistration_CorrectState() { - val plugin1 = FakeClockPlugin() - .addClock("clock_1") - .addClock("clock_2") + val plugin1 = FakeClockPlugin().addClock("clock_1").addClock("clock_2") val lifecycle1 = FakeLifecycle("1", plugin1) - val plugin2 = FakeClockPlugin() - .addClock("clock_3") - .addClock("clock_4") + val plugin2 = FakeClockPlugin().addClock("clock_3").addClock("clock_4") val lifecycle2 = FakeLifecycle("2", plugin2) pluginListener.onPluginLoaded(plugin1, mockContext, lifecycle1) @@ -210,8 +216,8 @@ class ClockRegistryTest : SysuiTestCase() { ClockMetadata("clock_1"), ClockMetadata("clock_2"), ClockMetadata("clock_3"), - ClockMetadata("clock_4") - ) + ClockMetadata("clock_4"), + ), ) } @@ -223,14 +229,13 @@ class ClockRegistryTest : SysuiTestCase() { @Test fun clockIdConflict_ErrorWithoutCrash_unloadDuplicate() { - val plugin1 = FakeClockPlugin() - .addClock("clock_1", { mockClock }, { pickerConfig }) - .addClock("clock_2", { mockClock }, { pickerConfig }) + val plugin1 = + FakeClockPlugin() + .addClock("clock_1", { mockClock }, { pickerConfig }) + .addClock("clock_2", { mockClock }, { pickerConfig }) val lifecycle1 = spy(FakeLifecycle("1", plugin1)) - val plugin2 = FakeClockPlugin() - .addClock("clock_1") - .addClock("clock_2") + val plugin2 = FakeClockPlugin().addClock("clock_1").addClock("clock_2") val lifecycle2 = spy(FakeLifecycle("2", plugin2)) pluginListener.onPluginLoaded(plugin1, mockContext, lifecycle1) @@ -241,8 +246,8 @@ class ClockRegistryTest : SysuiTestCase() { setOf( ClockMetadata(DEFAULT_CLOCK_ID), ClockMetadata("clock_1"), - ClockMetadata("clock_2") - ) + ClockMetadata("clock_2"), + ), ) assertEquals(registry.createExampleClock("clock_1"), mockClock) @@ -255,14 +260,10 @@ class ClockRegistryTest : SysuiTestCase() { @Test fun createCurrentClock_pluginConnected() { - val plugin1 = FakeClockPlugin() - .addClock("clock_1") - .addClock("clock_2") + val plugin1 = FakeClockPlugin().addClock("clock_1").addClock("clock_2") val lifecycle1 = spy(FakeLifecycle("1", plugin1)) - val plugin2 = FakeClockPlugin() - .addClock("clock_3", { mockClock }) - .addClock("clock_4") + val plugin2 = FakeClockPlugin().addClock("clock_3", { mockClock }).addClock("clock_4") val lifecycle2 = spy(FakeLifecycle("2", plugin2)) registry.applySettings(ClockSettings("clock_3", null)) @@ -275,14 +276,10 @@ class ClockRegistryTest : SysuiTestCase() { @Test fun activeClockId_changeAfterPluginConnected() { - val plugin1 = FakeClockPlugin() - .addClock("clock_1") - .addClock("clock_2") + val plugin1 = FakeClockPlugin().addClock("clock_1").addClock("clock_2") val lifecycle1 = spy(FakeLifecycle("1", plugin1)) - val plugin2 = FakeClockPlugin() - .addClock("clock_3", { mockClock }) - .addClock("clock_4") + val plugin2 = FakeClockPlugin().addClock("clock_3", { mockClock }).addClock("clock_4") val lifecycle2 = spy(FakeLifecycle("2", plugin2)) registry.applySettings(ClockSettings("clock_3", null)) @@ -296,14 +293,10 @@ class ClockRegistryTest : SysuiTestCase() { @Test fun createDefaultClock_pluginDisconnected() { - val plugin1 = FakeClockPlugin() - .addClock("clock_1") - .addClock("clock_2") + val plugin1 = FakeClockPlugin().addClock("clock_1").addClock("clock_2") val lifecycle1 = spy(FakeLifecycle("1", plugin1)) - val plugin2 = FakeClockPlugin() - .addClock("clock_3") - .addClock("clock_4") + val plugin2 = FakeClockPlugin().addClock("clock_3").addClock("clock_4") val lifecycle2 = spy(FakeLifecycle("2", plugin2)) registry.applySettings(ClockSettings("clock_3", null)) @@ -317,22 +310,25 @@ class ClockRegistryTest : SysuiTestCase() { @Test fun pluginRemoved_clockAndListChanged() { - val plugin1 = FakeClockPlugin() - .addClock("clock_1") - .addClock("clock_2") + val plugin1 = FakeClockPlugin().addClock("clock_1").addClock("clock_2") val lifecycle1 = spy(FakeLifecycle("1", plugin1)) - val plugin2 = FakeClockPlugin() - .addClock("clock_3", { mockClock }) - .addClock("clock_4") + val plugin2 = FakeClockPlugin().addClock("clock_3", { mockClock }).addClock("clock_4") val lifecycle2 = spy(FakeLifecycle("2", plugin2)) var changeCallCount = 0 var listChangeCallCount = 0 - registry.registerClockChangeListener(object : ClockRegistry.ClockChangeListener { - override fun onCurrentClockChanged() { changeCallCount++ } - override fun onAvailableClocksChanged() { listChangeCallCount++ } - }) + registry.registerClockChangeListener( + object : ClockRegistry.ClockChangeListener { + override fun onCurrentClockChanged() { + changeCallCount++ + } + + override fun onAvailableClocksChanged() { + listChangeCallCount++ + } + } + ) registry.applySettings(ClockSettings("clock_3", null)) scheduler.runCurrent() @@ -372,16 +368,24 @@ class ClockRegistryTest : SysuiTestCase() { @Test fun unknownPluginAttached_clockAndListUnchanged_loadRequested() { - val lifecycle = FakeLifecycle("", null).apply { - mComponentName = ComponentName("some.other.package", "SomeClass") - } + val lifecycle = + FakeLifecycle("", null).apply { + mComponentName = ComponentName("some.other.package", "SomeClass") + } var changeCallCount = 0 var listChangeCallCount = 0 - registry.registerClockChangeListener(object : ClockRegistry.ClockChangeListener { - override fun onCurrentClockChanged() { changeCallCount++ } - override fun onAvailableClocksChanged() { listChangeCallCount++ } - }) + registry.registerClockChangeListener( + object : ClockRegistry.ClockChangeListener { + override fun onCurrentClockChanged() { + changeCallCount++ + } + + override fun onAvailableClocksChanged() { + listChangeCallCount++ + } + } + ) assertEquals(true, pluginListener.onPluginAttached(lifecycle)) scheduler.runCurrent() @@ -391,22 +395,33 @@ class ClockRegistryTest : SysuiTestCase() { @Test fun knownPluginAttached_clockAndListChanged_loadedCurrent() { - val metroLifecycle = FakeLifecycle("Metro", null).apply { - mComponentName = ComponentName("com.android.systemui.clocks.metro", "Metro") - } - val bignumLifecycle = FakeLifecycle("BigNum", null).apply { - mComponentName = ComponentName("com.android.systemui.clocks.bignum", "BigNum") - } - val calligraphyLifecycle = FakeLifecycle("Calligraphy", null).apply { - mComponentName = ComponentName("com.android.systemui.clocks.calligraphy", "Calligraphy") - } + val metroLifecycle = + FakeLifecycle("Metro", null).apply { + mComponentName = ComponentName("com.android.systemui.clocks.metro", "Metro") + } + val bignumLifecycle = + FakeLifecycle("BigNum", null).apply { + mComponentName = ComponentName("com.android.systemui.clocks.bignum", "BigNum") + } + val calligraphyLifecycle = + FakeLifecycle("Calligraphy", null).apply { + mComponentName = + ComponentName("com.android.systemui.clocks.calligraphy", "Calligraphy") + } var changeCallCount = 0 var listChangeCallCount = 0 - registry.registerClockChangeListener(object : ClockRegistry.ClockChangeListener { - override fun onCurrentClockChanged() { changeCallCount++ } - override fun onAvailableClocksChanged() { listChangeCallCount++ } - }) + registry.registerClockChangeListener( + object : ClockRegistry.ClockChangeListener { + override fun onCurrentClockChanged() { + changeCallCount++ + } + + override fun onAvailableClocksChanged() { + listChangeCallCount++ + } + } + ) registry.applySettings(ClockSettings("DIGITAL_CLOCK_CALLIGRAPHY", null)) scheduler.runCurrent() @@ -466,67 +481,84 @@ class ClockRegistryTest : SysuiTestCase() { scheduler.runCurrent() // Verify all plugins were correctly loaded into the registry - assertEquals(registry.getClocks().toSet(), setOf( - ClockMetadata("DEFAULT"), - ClockMetadata("clock_2"), - ClockMetadata("clock_3"), - ClockMetadata("clock_4") - )) + assertEquals( + registry.getClocks().toSet(), + setOf( + ClockMetadata("DEFAULT"), + ClockMetadata("clock_2"), + ClockMetadata("clock_3"), + ClockMetadata("clock_4"), + ), + ) } @Test - fun jsonDeserialization_gotExpectedObject() { - val expected = ClockSettings("ID", null).apply { - metadata.put("appliedTimestamp", 500) - } - val actual = ClockSettings.deserialize("""{ - "clockId":"ID", - "metadata": { - "appliedTimestamp":500 - } - }""") + fun jsonDeserialization() { + val expected = ClockSettings("ID").apply { metadata.put("appliedTimestamp", 50) } + val json = JSONObject("""{"clockId":"ID", "metadata": { "appliedTimestamp":50 } }""") + val actual = ClockSettings.fromJson(json) assertEquals(expected, actual) } @Test - fun jsonDeserialization_noTimestamp_gotExpectedObject() { - val expected = ClockSettings("ID", null) - val actual = ClockSettings.deserialize("{\"clockId\":\"ID\"}") + fun jsonDeserialization_noTimestamp() { + val expected = ClockSettings("ID") + val actual = ClockSettings.fromJson(JSONObject("""{"clockId":"ID"}""")) assertEquals(expected, actual) } @Test - fun jsonDeserialization_nullTimestamp_gotExpectedObject() { - val expected = ClockSettings("ID", null) - val actual = ClockSettings.deserialize("""{ - "clockId":"ID", - "metadata":null - }""") + fun jsonDeserialization_nullTimestamp() { + val expected = ClockSettings("ID") + val actual = ClockSettings.fromJson(JSONObject("""{"clockId":"ID", "metadata":null}""")) assertEquals(expected, actual) } @Test fun jsonDeserialization_noId_deserializedEmpty() { - val expected = ClockSettings(null, null).apply { - metadata.put("appliedTimestamp", 500) - } - val actual = ClockSettings.deserialize("{\"metadata\":{\"appliedTimestamp\":500}}") + val expected = ClockSettings().apply { metadata.put("appliedTimestamp", 50) } + val actual = ClockSettings.fromJson(JSONObject("""{"metadata":{"appliedTimestamp":50}}""")) assertEquals(expected, actual) } @Test - fun jsonSerialization_gotExpectedString() { - val expected = "{\"clockId\":\"ID\",\"metadata\":{\"appliedTimestamp\":500}}" - val actual = ClockSettings.serialize(ClockSettings("ID", null).apply { - metadata.put("appliedTimestamp", 500) - }) + fun jsonDeserialization_fontAxes() { + val expected = ClockSettings(axes = listOf(ClockFontAxisSetting("KEY", 10f))) + val json = JSONObject("""{"axes":[{"key":"KEY","value":10}]}""") + val actual = ClockSettings.fromJson(json) assertEquals(expected, actual) } @Test - fun jsonSerialization_noTimestamp_gotExpectedString() { - val expected = "{\"clockId\":\"ID\",\"metadata\":{}}" - val actual = ClockSettings.serialize(ClockSettings("ID", null)) - assertEquals(expected, actual) + fun jsonSerialization() { + val expected = + JSONObject().apply { + put("clockId", "ID") + put("metadata", JSONObject().apply { put("appliedTimestamp", 50) }) + put("axes", JSONArray()) + } + val settings = ClockSettings("ID", null).apply { metadata.put("appliedTimestamp", 50) } + val actual = ClockSettings.toJson(settings) + assertEquals(expected.toString(), actual.toString()) + } + + @Test + fun jsonSerialization_noTimestamp() { + val expected = + JSONObject().apply { + put("clockId", "ID") + put("metadata", JSONObject()) + put("axes", JSONArray()) + } + val actual = ClockSettings.toJson(ClockSettings("ID", null)) + assertEquals(expected.toString(), actual.toString()) + } + + @Test + fun jsonSerialization_axisSettings() { + val settings = ClockSettings(axes = listOf(ClockFontAxisSetting("KEY", 10f))) + val actual = ClockSettings.toJson(settings) + val expected = JSONObject("""{"metadata":{},"axes":[{"key":"KEY","value":10}]}""") + assertEquals(expected.toString(), actual.toString()) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt index 7e86ff3e95ac..aa8b4f136683 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shared/clocks/DefaultClockProviderTest.kt @@ -89,7 +89,7 @@ class DefaultClockProviderTest : SysuiTestCase() { // All providers need to provide clocks & thumbnails for exposed clocks for (metadata in provider.getClocks()) { assertNotNull(provider.createClock(metadata.clockId)) - assertNotNull(provider.getClockPickerConfig(metadata.clockId)) + assertNotNull(provider.getClockPickerConfig(ClockSettings(metadata.clockId))) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/core/CommandQueueInitializerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/core/CommandQueueInitializerTest.kt index 2a196c6b979f..1b3f29a6f0c5 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/core/CommandQueueInitializerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/core/CommandQueueInitializerTest.kt @@ -32,7 +32,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.verify -@EnableFlags(StatusBarSimpleFragment.FLAG_NAME) +@EnableFlags(StatusBarConnectedDisplays.FLAG_NAME) @SmallTest @RunWith(AndroidJUnit4::class) class CommandQueueInitializerTest : SysuiTestCase() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/RemoteInputRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/RemoteInputRepositoryImplTest.kt index f7a8858a2741..3b720ef30db7 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/RemoteInputRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/RemoteInputRepositoryImplTest.kt @@ -50,7 +50,7 @@ class RemoteInputRepositoryImplTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) testScope = TestScope() - underTest = RemoteInputRepositoryImpl(remoteInputManager) + underTest = RemoteInputRepositoryImpl(testScope.backgroundScope, remoteInputManager) } @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/events/PrivacyDotViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/events/PrivacyDotViewControllerTest.kt index 16da3d22f4f7..4795a123617f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/events/PrivacyDotViewControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/events/PrivacyDotViewControllerTest.kt @@ -29,6 +29,10 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.res.R import com.android.systemui.statusbar.FakeStatusBarStateController +import com.android.systemui.statusbar.events.PrivacyDotCorner.BottomLeft +import com.android.systemui.statusbar.events.PrivacyDotCorner.BottomRight +import com.android.systemui.statusbar.events.PrivacyDotCorner.TopLeft +import com.android.systemui.statusbar.events.PrivacyDotCorner.TopRight import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider import com.android.systemui.statusbar.policy.FakeConfigurationController import com.android.systemui.util.concurrency.DelayableExecutor @@ -71,15 +75,15 @@ class PrivacyDotViewControllerTest : SysuiTestCase() { private fun createController() = PrivacyDotViewControllerImpl( - executor, - testScope.backgroundScope, - statusBarStateController, - configurationController, - contentInsetsProvider, - animationScheduler = mock<SystemStatusAnimationScheduler>(), - shadeInteractor = null, - ) - .also { it.setUiExecutor(executor) } + executor, + testScope.backgroundScope, + statusBarStateController, + configurationController, + contentInsetsProvider, + animationScheduler = mock<SystemStatusAnimationScheduler>(), + shadeInteractor = null, + uiExecutor = executor, + ) @Test fun topMargin_topLeftView_basedOnSeascapeArea() { @@ -215,7 +219,7 @@ class PrivacyDotViewControllerTest : SysuiTestCase() { val controller = createAndInitializeController() - assertThat(controller.currentViewState.cornerIndex).isEqualTo(TOP_RIGHT) + assertThat(controller.currentViewState.corner).isEqualTo(TopRight) assertThat(controller.currentViewState.designatedCorner).isEqualTo(topRightView) } @@ -225,7 +229,7 @@ class PrivacyDotViewControllerTest : SysuiTestCase() { val controller = createAndInitializeController() - assertThat(controller.currentViewState.cornerIndex).isEqualTo(BOTTOM_RIGHT) + assertThat(controller.currentViewState.corner).isEqualTo(BottomRight) assertThat(controller.currentViewState.designatedCorner).isEqualTo(bottomRightView) } @@ -235,7 +239,7 @@ class PrivacyDotViewControllerTest : SysuiTestCase() { val controller = createAndInitializeController() - assertThat(controller.currentViewState.cornerIndex).isEqualTo(TOP_LEFT) + assertThat(controller.currentViewState.corner).isEqualTo(TopLeft) assertThat(controller.currentViewState.designatedCorner).isEqualTo(topLeftView) } @@ -245,7 +249,7 @@ class PrivacyDotViewControllerTest : SysuiTestCase() { val controller = createAndInitializeController() - assertThat(controller.currentViewState.cornerIndex).isEqualTo(BOTTOM_LEFT) + assertThat(controller.currentViewState.corner).isEqualTo(BottomLeft) assertThat(controller.currentViewState.designatedCorner).isEqualTo(bottomLeftView) } @@ -256,7 +260,7 @@ class PrivacyDotViewControllerTest : SysuiTestCase() { enableRtl() val controller = createAndInitializeController() - assertThat(controller.currentViewState.cornerIndex).isEqualTo(TOP_LEFT) + assertThat(controller.currentViewState.corner).isEqualTo(TopLeft) assertThat(controller.currentViewState.designatedCorner).isEqualTo(topLeftView) } @@ -267,7 +271,7 @@ class PrivacyDotViewControllerTest : SysuiTestCase() { enableRtl() val controller = createAndInitializeController() - assertThat(controller.currentViewState.cornerIndex).isEqualTo(TOP_RIGHT) + assertThat(controller.currentViewState.corner).isEqualTo(TopRight) assertThat(controller.currentViewState.designatedCorner).isEqualTo(topRightView) } @@ -278,7 +282,7 @@ class PrivacyDotViewControllerTest : SysuiTestCase() { enableRtl() val controller = createAndInitializeController() - assertThat(controller.currentViewState.cornerIndex).isEqualTo(BOTTOM_LEFT) + assertThat(controller.currentViewState.corner).isEqualTo(BottomLeft) assertThat(controller.currentViewState.designatedCorner).isEqualTo(bottomLeftView) } @@ -289,7 +293,7 @@ class PrivacyDotViewControllerTest : SysuiTestCase() { enableRtl() val controller = createAndInitializeController() - assertThat(controller.currentViewState.cornerIndex).isEqualTo(BOTTOM_RIGHT) + assertThat(controller.currentViewState.corner).isEqualTo(BottomRight) assertThat(controller.currentViewState.designatedCorner).isEqualTo(bottomRightView) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationScrimNestedScrollConnectionTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationScrimNestedScrollConnectionTest.kt index 75479ad35725..60b95ad632e1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationScrimNestedScrollConnectionTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationScrimNestedScrollConnectionTest.kt @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.notification +import androidx.compose.foundation.gestures.FlingBehavior +import androidx.compose.foundation.gestures.ScrollScope import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.nestedscroll.NestedScrollSource.Companion.UserInput import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -35,6 +37,13 @@ class NotificationScrimNestedScrollConnectionTest : SysuiTestCase() { private var scrimOffset = 0f private var contentHeight = 0f private var isCurrentGestureOverscroll = false + private val customFlingBehavior = + object : FlingBehavior { + override suspend fun ScrollScope.performFling(initialVelocity: Float): Float { + scrollBy(initialVelocity) + return initialVelocity / 2f + } + } private val scrollConnection = NotificationScrimNestedScrollConnection( @@ -51,6 +60,7 @@ class NotificationScrimNestedScrollConnectionTest : SysuiTestCase() { wasStarted = true isStarted = false }, + flingBehavior = customFlingBehavior, ) @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt index 9f752a89b16d..3b5d358f7c2c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinatorTest.kt @@ -26,6 +26,7 @@ import com.android.systemui.communal.data.repository.communalSceneRepository import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.dump.DumpManager +import com.android.systemui.keyguard.domain.interactor.pulseExpansionInteractor import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testScope import com.android.systemui.log.logcatLogBuffer @@ -86,6 +87,7 @@ class NotificationWakeUpCoordinatorTest : SysuiTestCase() { private var bypassEnabled: Boolean = false private var statusBarState: Int = StatusBarState.KEYGUARD + private fun eased(dozeAmount: Float) = notificationWakeUpCoordinator.dozeAmountInterpolator.getInterpolation(dozeAmount) @@ -119,6 +121,7 @@ class NotificationWakeUpCoordinatorTest : SysuiTestCase() { logger, kosmos.notificationsKeyguardInteractor, kosmos.communalInteractor, + kosmos.pulseExpansionInteractor, ) statusBarStateCallback = withArgCaptor { verify(statusBarStateController).addCallback(capture()) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractorTest.kt index 133a114fed9d..dcc8ecd27b67 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractorTest.kt @@ -58,19 +58,4 @@ class NotificationsKeyguardInteractorTest : SysuiTestCase() { assertThat(notifsFullyHidden).isTrue() } - - @Test - fun isPulseExpanding_reflectsRepository() = - testComponent.runTest { - underTest.setPulseExpanding(false) - val isPulseExpanding by collectLastValue(underTest.isPulseExpanding) - runCurrent() - - assertThat(isPulseExpanding).isFalse() - - underTest.setPulseExpanding(true) - runCurrent() - - assertThat(isPulseExpanding).isTrue() - } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterViewTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterViewTest.java index a21ca9458b00..c9ca67e6af94 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterViewTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/footer/ui/view/FooterViewTest.java @@ -45,23 +45,25 @@ import androidx.test.filters.SmallTest; import com.android.systemui.SysuiTestCase; import com.android.systemui.res.R; import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor; +import com.android.systemui.statusbar.notification.footer.shared.NotifRedesignFooter; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import java.util.List; + import platform.test.runner.parameterized.ParameterizedAndroidJunit4; import platform.test.runner.parameterized.Parameters; -import java.util.List; - @SmallTest @RunWith(ParameterizedAndroidJunit4.class) public class FooterViewTest extends SysuiTestCase { @Parameters(name = "{0}") public static List<FlagsParameterization> getFlags() { - return FlagsParameterization.allCombinationsOf(FooterViewRefactor.FLAG_NAME); + return FlagsParameterization.progressionOf(FooterViewRefactor.FLAG_NAME, + NotifRedesignFooter.FLAG_NAME); } public FooterViewTest(FlagsParameterization flags) { @@ -74,8 +76,13 @@ public class FooterViewTest extends SysuiTestCase { @Before public void setUp() { - mView = (FooterView) LayoutInflater.from(mSpyContext).inflate( - R.layout.status_bar_notification_footer, null, false); + if (NotifRedesignFooter.isEnabled()) { + mView = (FooterView) LayoutInflater.from(mSpyContext).inflate( + R.layout.status_bar_notification_footer_redesign, null, false); + } else { + mView = (FooterView) LayoutInflater.from(mSpyContext).inflate( + R.layout.status_bar_notification_footer, null, false); + } mView.setAnimationDuration(0); } @@ -92,13 +99,14 @@ public class FooterViewTest extends SysuiTestCase { } @Test + @DisableFlags(NotifRedesignFooter.FLAG_NAME) public void setManageOnClick() { mView.setManageButtonClickListener(mock(View.OnClickListener.class)); assertTrue(mView.findViewById(R.id.manage_text).hasOnClickListeners()); } @Test - @DisableFlags(FooterViewRefactor.FLAG_NAME) + @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME}) public void setHistoryShown() { mView.showHistory(true); assertTrue(mView.isHistoryShown()); @@ -107,7 +115,7 @@ public class FooterViewTest extends SysuiTestCase { } @Test - @DisableFlags(FooterViewRefactor.FLAG_NAME) + @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME}) public void setHistoryNotShown() { mView.showHistory(false); assertFalse(mView.isHistoryShown()); @@ -133,6 +141,7 @@ public class FooterViewTest extends SysuiTestCase { @Test @EnableFlags(FooterViewRefactor.FLAG_NAME) + @DisableFlags(NotifRedesignFooter.FLAG_NAME) public void testSetManageOrHistoryButtonText_resourceOnlyFetchedOnce() { int resId = R.string.manage_notifications_history_text; mView.setManageOrHistoryButtonText(resId); @@ -151,7 +160,7 @@ public class FooterViewTest extends SysuiTestCase { } @Test - @DisableFlags(FooterViewRefactor.FLAG_NAME) + @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME}) public void testSetManageOrHistoryButtonText_expectsFlagEnabled() { clearInvocations(mSpyContext); int resId = R.string.manage_notifications_history_text; @@ -161,6 +170,7 @@ public class FooterViewTest extends SysuiTestCase { @Test @EnableFlags(FooterViewRefactor.FLAG_NAME) + @DisableFlags(NotifRedesignFooter.FLAG_NAME) public void testSetManageOrHistoryButtonDescription_resourceOnlyFetchedOnce() { int resId = R.string.manage_notifications_history_text; mView.setManageOrHistoryButtonDescription(resId); @@ -179,7 +189,7 @@ public class FooterViewTest extends SysuiTestCase { } @Test - @DisableFlags(FooterViewRefactor.FLAG_NAME) + @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME}) public void testSetManageOrHistoryButtonDescription_expectsFlagEnabled() { clearInvocations(mSpyContext); int resId = R.string.accessibility_clear_all; @@ -207,7 +217,7 @@ public class FooterViewTest extends SysuiTestCase { } @Test - @DisableFlags(FooterViewRefactor.FLAG_NAME) + @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME}) public void testSetClearAllButtonText_expectsFlagEnabled() { clearInvocations(mSpyContext); int resId = R.string.clear_all_notifications_text; @@ -235,7 +245,7 @@ public class FooterViewTest extends SysuiTestCase { } @Test - @DisableFlags(FooterViewRefactor.FLAG_NAME) + @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME}) public void testSetClearAllButtonDescription_expectsFlagEnabled() { clearInvocations(mSpyContext); int resId = R.string.accessibility_clear_all; @@ -263,7 +273,7 @@ public class FooterViewTest extends SysuiTestCase { } @Test - @DisableFlags(FooterViewRefactor.FLAG_NAME) + @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME}) public void testSetMessageString_expectsFlagEnabled() { clearInvocations(mSpyContext); int resId = R.string.unlock_to_see_notif_text; @@ -288,7 +298,7 @@ public class FooterViewTest extends SysuiTestCase { } @Test - @DisableFlags(FooterViewRefactor.FLAG_NAME) + @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME}) public void testSetMessageIcon_expectsFlagEnabled() { clearInvocations(mSpyContext); int resId = R.drawable.ic_friction_lock_closed; @@ -310,4 +320,3 @@ public class FooterViewTest extends SysuiTestCase { .isEqualTo(View.GONE); } } - diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowControllerTest.kt index c0057431f536..657e9df0029b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowControllerTest.kt @@ -58,6 +58,7 @@ import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.withArgCaptor import com.android.systemui.util.time.SystemClock import com.android.systemui.wmshell.BubblesManager +import com.google.android.msdl.domain.MSDLPlayer import java.util.Optional import junit.framework.Assert import org.junit.After @@ -110,6 +111,7 @@ class ExpandableNotificationRowControllerTest : SysuiTestCase() { private val dismissibilityProvider: NotificationDismissibilityProvider = mock() private val statusBarService: IStatusBarService = mock() private val uiEventLogger: UiEventLogger = mock() + private val msdlPlayer: MSDLPlayer = mock() private lateinit var controller: ExpandableNotificationRowController @Before @@ -150,7 +152,8 @@ class ExpandableNotificationRowControllerTest : SysuiTestCase() { dragController, dismissibilityProvider, statusBarService, - uiEventLogger + uiEventLogger, + msdlPlayer, ) whenever(view.childrenContainer).thenReturn(childrenContainer) @@ -268,14 +271,14 @@ class ExpandableNotificationRowControllerTest : SysuiTestCase() { controller.mSettingsListener.onSettingChanged( BUBBLES_SETTING_URI, view.entry.sbn.userId, - "1" + "1", ) verify(childView).setBubblesEnabledForUser(true) controller.mSettingsListener.onSettingChanged( BUBBLES_SETTING_URI, view.entry.sbn.userId, - "9" + "9", ) verify(childView).setBubblesEnabledForUser(false) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt index 2349c252369c..07935e47845e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/AmbientStateTest.kt @@ -16,10 +16,12 @@ package com.android.systemui.statusbar.notification.stack -import androidx.test.ext.junit.runners.AndroidJUnit4 +import android.platform.test.flag.junit.FlagsParameterization import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.dump.DumpManager +import com.android.systemui.flags.DisableSceneContainer +import com.android.systemui.flags.andSceneContainer import com.android.systemui.shade.transition.LargeScreenShadeInterpolator import com.android.systemui.statusbar.StatusBarState import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager @@ -30,12 +32,14 @@ import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters private const val MAX_PULSE_HEIGHT = 100000f -@RunWith(AndroidJUnit4::class) +@RunWith(ParameterizedAndroidJunit4::class) @SmallTest -class AmbientStateTest : SysuiTestCase() { +class AmbientStateTest(flags: FlagsParameterization) : SysuiTestCase() { private val dumpManager = mock<DumpManager>() private val sectionProvider = StackScrollAlgorithm.SectionProvider { _, _ -> false } @@ -46,6 +50,18 @@ class AmbientStateTest : SysuiTestCase() { private lateinit var sut: AmbientState + companion object { + @JvmStatic + @Parameters(name = "{0}") + fun getParams(): List<FlagsParameterization> { + return FlagsParameterization.allCombinationsOf().andSceneContainer() + } + } + + init { + mSetFlagsRule.setFlagsParameterization(flags) + } + @Before fun setUp() { sut = @@ -56,7 +72,7 @@ class AmbientStateTest : SysuiTestCase() { bypassController, statusBarKeyguardViewManager, largeScreenShadeInterpolator, - avalancheController + avalancheController, ) } @@ -97,6 +113,7 @@ class AmbientStateTest : SysuiTestCase() { assertThat(sut.pulseHeight).isEqualTo(expected) } + // endregion // region statusBarState @@ -119,6 +136,7 @@ class AmbientStateTest : SysuiTestCase() { assertThat(sut.isFlingRequiredAfterLockScreenSwipeUp).isTrue() } + // endregion // region hideAmount @@ -141,6 +159,7 @@ class AmbientStateTest : SysuiTestCase() { assertThat(sut.pulseHeight).isEqualTo(1f) } + // endregion // region dozeAmount @@ -173,6 +192,7 @@ class AmbientStateTest : SysuiTestCase() { assertThat(sut.pulseHeight).isEqualTo(1f) } + // endregion // region trackedHeadsUpRow @@ -189,10 +209,12 @@ class AmbientStateTest : SysuiTestCase() { assertThat(sut.trackedHeadsUpRow).isNull() } + // endregion // region isSwipingUp @Test + @DisableSceneContainer fun isSwipingUp_whenValueChangedToTrue_shouldRequireFling() { sut.isSwipingUp = false sut.isFlingRequiredAfterLockScreenSwipeUp = false @@ -203,6 +225,7 @@ class AmbientStateTest : SysuiTestCase() { } @Test + @DisableSceneContainer fun isSwipingUp_whenValueChangedToFalse_shouldRequireFling() { sut.isSwipingUp = true sut.isFlingRequiredAfterLockScreenSwipeUp = false @@ -211,10 +234,12 @@ class AmbientStateTest : SysuiTestCase() { assertThat(sut.isFlingRequiredAfterLockScreenSwipeUp).isTrue() } + // endregion // region isFlinging @Test + @DisableSceneContainer fun isFlinging_shouldNotNeedFling() { sut.arrangeFlinging(true) @@ -224,6 +249,7 @@ class AmbientStateTest : SysuiTestCase() { } @Test + @DisableSceneContainer fun isFlinging_whenNotOnLockScreen_shouldDoNothing() { sut.arrangeFlinging(true) sut.setStatusBarState(StatusBarState.SHADE) @@ -235,6 +261,7 @@ class AmbientStateTest : SysuiTestCase() { } @Test + @DisableSceneContainer fun isFlinging_whenValueChangedToTrue_shouldDoNothing() { sut.arrangeFlinging(false) @@ -242,10 +269,12 @@ class AmbientStateTest : SysuiTestCase() { assertThat(sut.isFlingRequiredAfterLockScreenSwipeUp).isTrue() } + // endregion // region scrollY @Test + @DisableSceneContainer fun scrollY_shouldSetValueGreaterThanZero() { sut.scrollY = 0 @@ -255,6 +284,7 @@ class AmbientStateTest : SysuiTestCase() { } @Test + @DisableSceneContainer fun scrollY_shouldNotSetValueLessThanZero() { sut.scrollY = 0 @@ -262,21 +292,24 @@ class AmbientStateTest : SysuiTestCase() { assertThat(sut.scrollY).isEqualTo(0) } + // endregion // region setOverScrollAmount + @Test + @DisableSceneContainer fun setOverScrollAmount_shouldSetValueOnTop() { - sut.setOverScrollAmount(/* amount = */ 10f, /* onTop = */ true) + sut.setOverScrollAmount(/* amount= */ 10f, /* onTop= */ true) - val resultOnTop = sut.getOverScrollAmount(/* top = */ true) - val resultOnBottom = sut.getOverScrollAmount(/* top = */ false) + val resultOnTop = sut.getOverScrollAmount(/* top= */ true) + val resultOnBottom = sut.getOverScrollAmount(/* top= */ false) assertThat(resultOnTop).isEqualTo(10f) assertThat(resultOnBottom).isEqualTo(0f) } fun setOverScrollAmount_shouldSetValueOnBottom() { - sut.setOverScrollAmount(/* amount = */ 10f, /* onTop = */ false) + sut.setOverScrollAmount(/* amount= */ 10f, /* onTop= */ false) val resultOnTop = sut.getOverScrollAmount(/* top */ true) val resultOnBottom = sut.getOverScrollAmount(/* top */ false) @@ -284,6 +317,7 @@ class AmbientStateTest : SysuiTestCase() { assertThat(resultOnTop).isEqualTo(0f) assertThat(resultOnBottom).isEqualTo(10f) } + // endregion // region IsPulseExpanding @@ -317,6 +351,7 @@ class AmbientStateTest : SysuiTestCase() { assertThat(sut.isPulseExpanding).isFalse() } + // endregion // region isOnKeyguard @@ -333,6 +368,7 @@ class AmbientStateTest : SysuiTestCase() { assertThat(sut.isOnKeyguard).isFalse() } + // endregion // region mIsClosing diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt index 1ef400710102..b2a485c48860 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/StackScrollAlgorithmTest.kt @@ -4,8 +4,8 @@ import android.annotation.DimenRes import android.content.pm.PackageManager import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.FlagsParameterization import android.widget.FrameLayout -import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.keyguard.BouncerPanelExpansionCalculator.aboutToShowBouncerProgress import com.android.systemui.Flags @@ -16,7 +16,9 @@ import com.android.systemui.flags.DisableSceneContainer import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.FeatureFlagsClassic +import com.android.systemui.flags.andSceneContainer import com.android.systemui.res.R +import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.transition.LargeScreenShadeInterpolator import com.android.systemui.statusbar.NotificationShelf import com.android.systemui.statusbar.StatusBarState @@ -47,10 +49,12 @@ import org.mockito.Mockito.eq import org.mockito.Mockito.mock import org.mockito.Mockito.verify import org.mockito.Mockito.`when` as whenever +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters @SmallTest -@RunWith(AndroidJUnit4::class) -class StackScrollAlgorithmTest : SysuiTestCase() { +@RunWith(ParameterizedAndroidJunit4::class) +class StackScrollAlgorithmTest(flags: FlagsParameterization) : SysuiTestCase() { @JvmField @Rule var expect: Expect = Expect.create() @@ -99,6 +103,18 @@ class StackScrollAlgorithmTest : SysuiTestCase() { private val bigGap = notifSectionDividerGap private val smallGap = px(R.dimen.notification_section_divider_height_lockscreen) + companion object { + @JvmStatic + @Parameters(name = "{0}") + fun getParams(): List<FlagsParameterization> { + return FlagsParameterization.allCombinationsOf().andSceneContainer() + } + } + + init { + mSetFlagsRule.setFlagsParameterization(flags) + } + @Before fun setUp() { Assume.assumeFalse(isTv()) @@ -614,7 +630,7 @@ class StackScrollAlgorithmTest : SysuiTestCase() { @Test fun resetViewStates_isOnKeyguard_viewBecomesTransparent() { - ambientState.setStatusBarState(StatusBarState.KEYGUARD) + ambientState.fakeShowingStackOnLockscreen() ambientState.hideAmount = 0.25f whenever(notificationRow.isHeadsUpState).thenReturn(true) @@ -676,7 +692,7 @@ class StackScrollAlgorithmTest : SysuiTestCase() { whenever(row1.isHeadsUpState).thenReturn(true) whenever(row2.isHeadsUpState).thenReturn(false) - ambientState.setStatusBarState(StatusBarState.KEYGUARD) + ambientState.fakeShowingStackOnLockscreen() ambientState.hideAmount = 0.25f ambientState.dozeAmount = 0.33f notificationShelf.viewState.hidden = true @@ -729,6 +745,7 @@ class StackScrollAlgorithmTest : SysuiTestCase() { } @Test + @DisableSceneContainer fun resetViewStates_noSpaceForFooter_footerHidden() { ambientState.isShadeExpanded = true ambientState.stackEndHeight = 0f // no space for the footer in the stack @@ -740,6 +757,20 @@ class StackScrollAlgorithmTest : SysuiTestCase() { } @Test + @EnableSceneContainer + fun resetViewStates_noSpaceForFooter_footerHidden_withSceneContainer() { + ambientState.isShadeExpanded = true + ambientState.stackTop = 0f + ambientState.stackCutoff = 100f + val footerView = mockFooterView(height = 200) // no space for the footer in the stack + hostView.addView(footerView) + + stackScrollAlgorithm.resetViewStates(ambientState, 0) + + assertThat((footerView.viewState as FooterViewState).hideContent).isTrue() + } + + @Test fun resetViewStates_clearAllInProgress_hasNonClearableRow_footerVisible() { whenever(notificationRow.canViewBeCleared()).thenReturn(false) ambientState.isClearAllInProgress = true @@ -1552,3 +1583,19 @@ private fun mockExpandableNotificationRow(): ExpandableNotificationRow { whenever(viewState).thenReturn(ExpandableViewState()) } } + +private fun mockFooterView(height: Int): FooterView { + return mock(FooterView::class.java).apply { + whenever(viewState).thenReturn(FooterViewState()) + whenever(intrinsicHeight).thenReturn(height) + } +} + +private fun AmbientState.fakeShowingStackOnLockscreen() { + if (SceneContainerFlag.isEnabled) { + isShowingStackOnLockscreen = true + lockscreenStackFadeInProgress = 1f // stack is fully opaque + } else { + setStatusBarState(StatusBarState.KEYGUARD) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt index d5a7c89e6c71..a940ed4d70f9 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.notification.stack.ui.viewmodel +import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization import androidx.test.filters.SmallTest @@ -66,10 +67,13 @@ import com.android.systemui.scene.data.repository.setTransition import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.mockLargeScreenHeaderHelper import com.android.systemui.shade.shadeTestUtil +import com.android.systemui.shade.shared.flag.DualShade import com.android.systemui.statusbar.notification.stack.domain.interactor.sharedNotificationContainerInteractor +import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel.HorizontalPosition import com.android.systemui.testKosmos import com.google.common.collect.Range import com.google.common.truth.Truth.assertThat +import kotlin.test.assertIs import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.flowOf @@ -165,6 +169,83 @@ class SharedNotificationContainerViewModelTest(flags: FlagsParameterization) : S } @Test + fun validateHorizontalPositionSingleShade() = + testScope.runTest { + overrideDimensionPixelSize(R.dimen.shade_panel_width, 200) + val dimens by collectLastValue(underTest.configurationBasedDimensions) + shadeTestUtil.setSplitShade(false) + + val horizontalPosition = checkNotNull(dimens).horizontalPosition + assertIs<HorizontalPosition.EdgeToEdge>(horizontalPosition) + } + + @Test + fun validateHorizontalPositionSplitShade() = + testScope.runTest { + overrideDimensionPixelSize(R.dimen.shade_panel_width, 200) + val dimens by collectLastValue(underTest.configurationBasedDimensions) + shadeTestUtil.setSplitShade(true) + + val horizontalPosition = checkNotNull(dimens).horizontalPosition + assertIs<HorizontalPosition.MiddleToEdge>(horizontalPosition) + assertThat(horizontalPosition.ratio).isEqualTo(0.5f) + } + + @Test + @EnableSceneContainer + @DisableFlags(DualShade.FLAG_NAME) + fun validateHorizontalPositionInSceneContainerSingleShade() = + testScope.runTest { + overrideDimensionPixelSize(R.dimen.shade_panel_width, 200) + val dimens by collectLastValue(underTest.configurationBasedDimensions) + shadeTestUtil.setSplitShade(false) + + val horizontalPosition = checkNotNull(dimens).horizontalPosition + assertIs<HorizontalPosition.EdgeToEdge>(horizontalPosition) + } + + @Test + @EnableSceneContainer + @DisableFlags(DualShade.FLAG_NAME) + fun validateHorizontalPositionInSceneContainerSplitShade() = + testScope.runTest { + overrideDimensionPixelSize(R.dimen.shade_panel_width, 200) + val dimens by collectLastValue(underTest.configurationBasedDimensions) + shadeTestUtil.setSplitShade(true) + + val horizontalPosition = checkNotNull(dimens).horizontalPosition + assertIs<HorizontalPosition.MiddleToEdge>(horizontalPosition) + assertThat(horizontalPosition.ratio).isEqualTo(0.5f) + } + + @Test + @EnableSceneContainer + @EnableFlags(DualShade.FLAG_NAME) + fun validateHorizontalPositionInDualShade_narrowLayout() = + testScope.runTest { + overrideDimensionPixelSize(R.dimen.shade_panel_width, 200) + val dimens by collectLastValue(underTest.configurationBasedDimensions) + shadeTestUtil.setSplitShade(false) + + val horizontalPosition = checkNotNull(dimens).horizontalPosition + assertIs<HorizontalPosition.EdgeToEdge>(horizontalPosition) + } + + @Test + @EnableSceneContainer + @EnableFlags(DualShade.FLAG_NAME) + fun validateHorizontalPositionInDualShade_wideLayout() = + testScope.runTest { + overrideDimensionPixelSize(R.dimen.shade_panel_width, 200) + val dimens by collectLastValue(underTest.configurationBasedDimensions) + shadeTestUtil.setSplitShade(true) + + val horizontalPosition = checkNotNull(dimens).horizontalPosition + assertIs<HorizontalPosition.FloatAtEnd>(horizontalPosition) + assertThat(horizontalPosition.width).isEqualTo(200) + } + + @Test fun validatePaddingTopInSplitShade_usesLargeHeaderHelper() = testScope.runTest { whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()).thenReturn(5) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java index 5052a008af68..198821f3bb5f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/DozeParametersTest.java @@ -44,6 +44,8 @@ import com.android.systemui.doze.AlwaysOnDisplayPolicy; import com.android.systemui.doze.DozeScreenState; import com.android.systemui.dump.DumpManager; import com.android.systemui.keyguard.domain.interactor.DozeInteractor; +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; +import com.android.systemui.keyguard.shared.model.KeyguardState; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.policy.BatteryController; @@ -59,6 +61,7 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.Answers; import org.mockito.ArgumentCaptor; import org.mockito.Captor; import org.mockito.Mock; @@ -88,6 +91,8 @@ public class DozeParametersTest extends SysuiTestCase { @Mock private ConfigurationController mConfigurationController; @Mock private UserTracker mUserTracker; @Mock private DozeInteractor mDozeInteractor; + @Mock(answer = Answers.RETURNS_DEEP_STUBS) + private KeyguardTransitionInteractor mKeyguardTransitionInteractor; @Captor private ArgumentCaptor<BatteryStateChangeCallback> mBatteryStateChangeCallback; /** @@ -134,6 +139,7 @@ public class DozeParametersTest extends SysuiTestCase { mStatusBarStateController, mUserTracker, mDozeInteractor, + mKeyguardTransitionInteractor, secureSettings ); @@ -286,6 +292,18 @@ public class DozeParametersTest extends SysuiTestCase { assertTrue(mDozeParameters.shouldControlScreenOff()); } + @Test + public void shouldDelayDisplayDozeTransition_True_WhenTransitioningToAod() { + setShouldControlUnlockedScreenOffForTest(false); + when(mScreenOffAnimationController.shouldDelayDisplayDozeTransition()).thenReturn(false); + when(mKeyguardTransitionInteractor.getTransitionState().getValue().getTo()) + .thenReturn(KeyguardState.LOCKSCREEN); + assertFalse(mDozeParameters.shouldDelayDisplayDozeTransition()); + + when(mKeyguardTransitionInteractor.getTransitionState().getValue().getTo()) + .thenReturn(KeyguardState.AOD); + assertTrue(mDozeParameters.shouldDelayDisplayDozeTransition()); + } @Test public void keyguardVisibility_changesControlScreenOffAnimation() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java index b2794d8f9b56..0eb620343e6a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java @@ -804,6 +804,13 @@ public class StatusBarKeyguardViewManagerTest extends SysuiTestCase { } @Test + public void onBackPressedResetsLeaveOnKeyguardHide() { + when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(true); + mStatusBarKeyguardViewManager.onBackPressed(); + verify(mStatusBarStateController).setLeaveOpenOnKeyguardHide(false); + } + + @Test public void testResetHideBouncerWhenShowingIsFalse_alternateBouncerHides() { // GIVEN the keyguard is showing reset(mAlternateBouncerInteractor); diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManagerTest.kt index 48c2cc7f11c4..a00858842742 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManagerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManagerTest.kt @@ -20,6 +20,8 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.compose.animation.scene.ObservableTransitionState import com.android.systemui.SysuiTestCase +import com.android.systemui.communal.data.repository.fakeCommunalSceneRepository +import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.flags.DisableSceneContainer import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.kosmos.testScope @@ -113,4 +115,21 @@ class StatusBarTouchableRegionManagerTest : SysuiTestCase() { assertThat(underTest.shouldMakeEntireScreenTouchable()).isFalse() } + + @Test + @DisableSceneContainer + fun entireScreenTouchable_communalVisible() = + testScope.runTest { + assertThat(underTest.shouldMakeEntireScreenTouchable()).isFalse() + + kosmos.fakeCommunalSceneRepository.snapToScene(CommunalScenes.Communal) + runCurrent() + + assertThat(underTest.shouldMakeEntireScreenTouchable()).isTrue() + + kosmos.fakeCommunalSceneRepository.snapToScene(CommunalScenes.Blank) + runCurrent() + + assertThat(underTest.shouldMakeEntireScreenTouchable()).isFalse() + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/domain/interactor/KeyguardBypassInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/domain/interactor/KeyguardBypassInteractorTest.kt new file mode 100644 index 000000000000..c90183df9847 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/domain/interactor/KeyguardBypassInteractorTest.kt @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.phone.domain.interactor + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.bouncer.data.repository.keyguardBouncerRepository +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.flags.EnableSceneContainer +import com.android.systemui.keyguard.data.repository.configureKeyguardBypass +import com.android.systemui.keyguard.domain.interactor.KeyguardBypassInteractor +import com.android.systemui.keyguard.domain.interactor.keyguardBypassInteractor +import com.android.systemui.keyguard.domain.interactor.keyguardQuickAffordanceInteractor +import com.android.systemui.keyguard.domain.interactor.pulseExpansionInteractor +import com.android.systemui.kosmos.testScope +import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.shade.shadeTestUtil +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +@EnableSceneContainer +class KeyguardBypassInteractorTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private lateinit var underTest: KeyguardBypassInteractor + + @Test + fun canBypassFalseWhenBypassAvailableFalse() = + testScope.runTest { + initializeDependenciesForCanBypass(skipIsBypassAvailableCheck = false) + val canBypass by collectLastValue(underTest.canBypass) + runCurrent() + assertThat(canBypass).isFalse() + } + + @Test + fun canBypassTrueOnPrimaryBouncerShowing() = + testScope.runTest { + initializeDependenciesForCanBypass(skipBouncerShowingCheck = false) + val canBypass by collectLastValue(underTest.canBypass) + runCurrent() + assertThat(canBypass).isTrue() + } + + @Test + fun canBypassTrueOnAlternateBouncerShowing() = + testScope.runTest { + initializeDependenciesForCanBypass(skipAlternateBouncerShowingCheck = false) + val canBypass by collectLastValue(underTest.canBypass) + runCurrent() + assertThat(canBypass).isTrue() + } + + @Test + fun canBypassFalseWhenNotOnLockscreenScene() = + testScope.runTest { + initializeDependenciesForCanBypass(skipOnLockscreenSceneCheck = false) + val canBypass by collectLastValue(underTest.canBypass) + val currentScene by collectLastValue(kosmos.sceneInteractor.currentScene) + runCurrent() + assertThat(currentScene).isNotEqualTo(Scenes.Lockscreen) + assertThat(canBypass).isFalse() + } + + @Test + fun canBypassFalseOnLaunchingAffordance() = + testScope.runTest { + initializeDependenciesForCanBypass(skipLaunchingAffordanceCheck = false) + val canBypass by collectLastValue(underTest.canBypass) + runCurrent() + assertThat(canBypass).isFalse() + } + + @Test + fun canBypassFalseOnPulseExpanding() = + testScope.runTest { + initializeDependenciesForCanBypass(skipPulseExpandingCheck = false) + val canBypass by collectLastValue(underTest.canBypass) + runCurrent() + assertThat(canBypass).isFalse() + } + + @Test + fun canBypassFalseOnQsExpanded() = + testScope.runTest { + initializeDependenciesForCanBypass(skipQsExpandedCheck = false) + val canBypass by collectLastValue(underTest.canBypass) + runCurrent() + assertThat(canBypass).isFalse() + } + + // Initializes all canBypass dependencies to opposite of value needed to return + private fun initializeDependenciesForCanBypass( + skipIsBypassAvailableCheck: Boolean = true, + skipBouncerShowingCheck: Boolean = true, + skipAlternateBouncerShowingCheck: Boolean = true, + skipOnLockscreenSceneCheck: Boolean = true, + skipLaunchingAffordanceCheck: Boolean = true, + skipPulseExpandingCheck: Boolean = true, + skipQsExpandedCheck: Boolean = true, + ) { + // !isBypassAvailable false + kosmos.configureKeyguardBypass(isBypassAvailable = skipIsBypassAvailableCheck) + underTest = kosmos.keyguardBypassInteractor + + // bouncerShowing false, !onLockscreenScene false + // !onLockscreenScene false + setScene( + bouncerShowing = !skipBouncerShowingCheck, + onLockscreenScene = skipOnLockscreenSceneCheck, + ) + // alternateBouncerShowing false + setAlternateBouncerShowing(!skipAlternateBouncerShowingCheck) + // launchingAffordance false + setLaunchingAffordance(!skipLaunchingAffordanceCheck) + // pulseExpanding false + setPulseExpanding(!skipPulseExpandingCheck) + // qsExpanding false + setQsExpanded(!skipQsExpandedCheck) + } + + private fun setAlternateBouncerShowing(alternateBouncerVisible: Boolean) { + kosmos.keyguardBouncerRepository.setAlternateVisible(alternateBouncerVisible) + } + + private fun setScene(bouncerShowing: Boolean, onLockscreenScene: Boolean) { + if (bouncerShowing) { + kosmos.sceneInteractor.changeScene(Scenes.Bouncer, "reason") + } else if (onLockscreenScene) { + kosmos.sceneInteractor.changeScene(Scenes.Lockscreen, "reason") + } else { + kosmos.sceneInteractor.changeScene(Scenes.Shade, "reason") + } + } + + private fun setLaunchingAffordance(launchingAffordance: Boolean) { + kosmos.keyguardQuickAffordanceInteractor.setLaunchingAffordance(launchingAffordance) + } + + private fun setPulseExpanding(pulseExpanding: Boolean) { + kosmos.pulseExpansionInteractor.setPulseExpanding(pulseExpanding) + } + + private fun setQsExpanded(qsExpanded: Boolean) { + if (qsExpanded) { + kosmos.shadeTestUtil.setQsExpansion(1f) + } else { + kosmos.shadeTestUtil.setQsExpansion(0f) + } + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/ConvenienceExtensionsKtTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/ConvenienceExtensionsKtTest.kt new file mode 100644 index 000000000000..2d57e2f01c86 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/ConvenienceExtensionsKtTest.kt @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.util + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class ConvenienceExtensionsKtTest : SysuiTestCase() { + + @Test + fun containsExactly_notDuplicatedElements_allSame_returnsTrue() { + val list = listOf(1, 2, 3) + + assertThat(list.containsExactly(2, 1, 3)).isTrue() + } + + @Test + fun containsExactly_duplicatedElements_allSame_returnsTrue() { + val list = listOf(1, 1, 2, 3, 3) + + assertThat(list.containsExactly(1, 1, 2, 3, 3)).isTrue() + } + + @Test + fun containsExactly_duplicatedElements_sameButNotDuplicated_returnsFalse() { + val list = listOf(1, 1, 2, 3, 3) + + assertThat(list.containsExactly(1, 2, 3)).isFalse() + } + + @Test + fun containsExactly_duplicatedElements_sameButNotSameAmount_returnsFalse() { + val list = listOf(1, 1, 2, 3, 3) + + assertThat(list.containsExactly(1, 2, 2, 3, 3)).isFalse() + } + + @Test + fun eachCountMap_returnsExpectedCount() { + val list = listOf(1, 3, 1, 3, 3, 3, 2) + + assertThat(list.eachCountMap()).isEqualTo(mapOf(1 to 2, 2 to 1, 3 to 4)) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/wakelock/WakeLockTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/wakelock/WakeLockTest.java index 207c35da1892..90aecfb62e91 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/wakelock/WakeLockTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/wakelock/WakeLockTest.java @@ -21,43 +21,23 @@ import static org.junit.Assert.assertTrue; import android.os.Build; import android.os.PowerManager; -import android.platform.test.flag.junit.FlagsParameterization; -import android.platform.test.flag.junit.SetFlagsRule; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; -import com.android.systemui.Flags; import com.android.systemui.SysuiTestCase; import org.junit.After; import org.junit.Assume; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; -import java.util.List; - -import platform.test.runner.parameterized.ParameterizedAndroidJunit4; -import platform.test.runner.parameterized.Parameters; - @SmallTest -@RunWith(ParameterizedAndroidJunit4.class) +@RunWith(AndroidJUnit4.class) public class WakeLockTest extends SysuiTestCase { - @Parameters(name = "{0}") - public static List<FlagsParameterization> getFlags() { - return FlagsParameterization.allCombinationsOf( - Flags.FLAG_DELAYED_WAKELOCK_RELEASE_ON_BACKGROUND_THREAD); - } - - @Rule public final SetFlagsRule mSetFlagsRule; - - public WakeLockTest(FlagsParameterization flags) { - mSetFlagsRule = new SetFlagsRule(SetFlagsRule.DefaultInitValueType.NULL_DEFAULT, flags); - } - private static final String WHY = "test"; WakeLock mWakeLock; PowerManager.WakeLock mInner; diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt index e264264d8c6c..7d55169e048a 100644 --- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt +++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/clocks/ClockProviderPlugin.kt @@ -28,6 +28,7 @@ import com.android.systemui.plugins.annotations.SimpleProperty import java.io.PrintWriter import java.util.Locale import java.util.TimeZone +import org.json.JSONArray import org.json.JSONObject /** Identifies a clock design */ @@ -61,7 +62,7 @@ interface ClockProvider { @ProtectedReturn("return new ClockPickerConfig(\"\", \"\", \"\", null);") /** Settings configuration parameters for the clock */ - fun getClockPickerConfig(id: ClockId): ClockPickerConfig + fun getClockPickerConfig(settings: ClockSettings): ClockPickerConfig } /** Interface for controlling an active clock */ @@ -203,17 +204,63 @@ interface ClockEvents { fun onZenDataChanged(data: ZenData) /** Update reactive axes for this clock */ - fun onReactiveAxesChanged(axes: List<ClockReactiveSetting>) + fun onFontAxesChanged(axes: List<ClockFontAxisSetting>) } /** Axis setting value for a clock */ -data class ClockReactiveSetting( - /** Axis key; matches ClockReactiveAxis.key */ +data class ClockFontAxisSetting( + /** Axis key; matches ClockFontAxis.key */ val key: String, /** Value to set this axis to */ val value: Float, -) +) { + companion object { + private val KEY_AXIS_KEY = "key" + private val KEY_AXIS_VALUE = "value" + + fun toJson(setting: ClockFontAxisSetting): JSONObject { + return JSONObject().apply { + put(KEY_AXIS_KEY, setting.key) + put(KEY_AXIS_VALUE, setting.value) + } + } + + fun toJson(settings: List<ClockFontAxisSetting>): JSONArray { + return JSONArray().apply { + for (axis in settings) { + put(toJson(axis)) + } + } + } + + fun fromJson(jsonObj: JSONObject): ClockFontAxisSetting { + return ClockFontAxisSetting( + key = jsonObj.getString(KEY_AXIS_KEY), + value = jsonObj.getDouble(KEY_AXIS_VALUE).toFloat(), + ) + } + + fun fromJson(jsonArray: JSONArray): List<ClockFontAxisSetting> { + val result = mutableListOf<ClockFontAxisSetting>() + for (i in 0..jsonArray.length() - 1) { + val obj = jsonArray.getJSONObject(i) + if (obj == null) continue + result.add(fromJson(obj)) + } + return result + } + + fun toFVar(settings: List<ClockFontAxisSetting>): String { + val sb = StringBuilder() + for (axis in settings) { + if (sb.length > 0) sb.append(", ") + sb.append("'${axis.key}' ${axis.value.toInt()}") + } + return sb.toString() + } + } +} /** Methods which trigger various clock animations */ @ProtectedInterface @@ -323,11 +370,11 @@ constructor( val isReactiveToTone: Boolean = true, /** Font axes that can be modified on this clock */ - val axes: List<ClockReactiveAxis> = listOf(), + val axes: List<ClockFontAxis> = listOf(), ) /** Represents an Axis that can be modified */ -data class ClockReactiveAxis( +data class ClockFontAxis( /** Axis key, not user renderable */ val key: String, @@ -348,15 +395,32 @@ data class ClockReactiveAxis( /** Description of the axis */ val description: String, -) +) { + fun toSetting() = ClockFontAxisSetting(key, currentValue) + + companion object { + fun merge( + fontAxes: List<ClockFontAxis>, + axisSettings: List<ClockFontAxisSetting>, + ): List<ClockFontAxis> { + val result = mutableListOf<ClockFontAxis>() + for (axis in fontAxes) { + val setting = axisSettings.firstOrNull { axis.key == it.key } + val output = setting?.let { axis.copy(currentValue = it.value) } ?: axis + result.add(output) + } + return result + } + } +} /** Axis user interaction modes */ enum class AxisType { - /** Boolean toggle. Swaps between minValue & maxValue */ - Toggle, + /** Continuous range between minValue & maxValue. */ + Float, - /** Continuous slider between minValue & maxValue */ - Slider, + /** Only minValue & maxValue are valid. No intermediate values between them are allowed. */ + Boolean, } /** Render configuration for the full clock. Modifies the way systemUI behaves with this clock. */ @@ -404,7 +468,7 @@ data class ClockFaceConfig( data class ClockSettings( val clockId: ClockId? = null, val seedColor: Int? = null, - val axes: List<ClockReactiveSetting>? = null, + val axes: List<ClockFontAxisSetting> = listOf(), ) { // Exclude metadata from equality checks var metadata: JSONObject = JSONObject() @@ -413,38 +477,24 @@ data class ClockSettings( private val KEY_CLOCK_ID = "clockId" private val KEY_SEED_COLOR = "seedColor" private val KEY_METADATA = "metadata" - - fun serialize(setting: ClockSettings?): String { - if (setting == null) { - return "" + private val KEY_AXIS_LIST = "axes" + + fun toJson(setting: ClockSettings): JSONObject { + return JSONObject().apply { + put(KEY_CLOCK_ID, setting.clockId) + put(KEY_SEED_COLOR, setting.seedColor) + put(KEY_METADATA, setting.metadata) + put(KEY_AXIS_LIST, ClockFontAxisSetting.toJson(setting.axes)) } - - // TODO(b/364673977): Serialize axes - - return JSONObject() - .put(KEY_CLOCK_ID, setting.clockId) - .put(KEY_SEED_COLOR, setting.seedColor) - .put(KEY_METADATA, setting.metadata) - .toString() } - fun deserialize(jsonStr: String?): ClockSettings? { - if (jsonStr.isNullOrEmpty()) { - return null + fun fromJson(json: JSONObject): ClockSettings { + val clockId = if (!json.isNull(KEY_CLOCK_ID)) json.getString(KEY_CLOCK_ID) else null + val seedColor = if (!json.isNull(KEY_SEED_COLOR)) json.getInt(KEY_SEED_COLOR) else null + val axisList = json.optJSONArray(KEY_AXIS_LIST)?.let(ClockFontAxisSetting::fromJson) + return ClockSettings(clockId, seedColor, axisList ?: listOf()).apply { + metadata = json.optJSONObject(KEY_METADATA) ?: JSONObject() } - - // TODO(b/364673977): Deserialize axes - - val json = JSONObject(jsonStr) - val result = - ClockSettings( - if (!json.isNull(KEY_CLOCK_ID)) json.getString(KEY_CLOCK_ID) else null, - if (!json.isNull(KEY_SEED_COLOR)) json.getInt(KEY_SEED_COLOR) else null, - ) - if (!json.isNull(KEY_METADATA)) { - result.metadata = json.getJSONObject(KEY_METADATA) - } - return result } } } diff --git a/packages/SystemUI/res/color/slider_active_track_color.xml b/packages/SystemUI/res/color/slider_active_track_color.xml new file mode 100644 index 000000000000..a5aa58dd6b51 --- /dev/null +++ b/packages/SystemUI/res/color/slider_active_track_color.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<selector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"> + <item android:color="?androidprv:attr/materialColorPrimary" android:state_enabled="true" /> + <item android:color="?androidprv:attr/materialColorSurfaceContainerHighest" /> +</selector>
\ No newline at end of file diff --git a/packages/SystemUI/res/color/slider_inactive_track_color.xml b/packages/SystemUI/res/color/slider_inactive_track_color.xml new file mode 100644 index 000000000000..89aef776c00e --- /dev/null +++ b/packages/SystemUI/res/color/slider_inactive_track_color.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<selector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"> + <item android:color="?androidprv:attr/materialColorSurfaceContainerHighest" android:state_enabled="true" /> + <item android:color="?androidprv:attr/materialColorPrimary" /> +</selector>
\ No newline at end of file diff --git a/packages/SystemUI/res/color/slider_thumb_color.xml b/packages/SystemUI/res/color/slider_thumb_color.xml new file mode 100644 index 000000000000..5206049edd41 --- /dev/null +++ b/packages/SystemUI/res/color/slider_thumb_color.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<selector xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"> + <item android:color="?androidprv:attr/materialColorSurfaceContainerHighest" android:state_enabled="false" /> + <item android:color="?androidprv:attr/materialColorPrimary" /> +</selector> diff --git a/packages/SystemUI/res/drawable/volume_dialog_background_small_radius.xml b/packages/SystemUI/res/drawable/volume_dialog_background_small_radius.xml new file mode 100644 index 000000000000..7d794966c480 --- /dev/null +++ b/packages/SystemUI/res/drawable/volume_dialog_background_small_radius.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:shape="rectangle"> + <corners android:radius="@dimen/volume_dialog_background_square_corner_radius" /> + <solid android:color="?androidprv:attr/materialColorSurface" /> +</shape>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/volume_drawer_selection_bg.xml b/packages/SystemUI/res/drawable/volume_drawer_selection_bg.xml index df8521a15f6d..131201e7a94f 100644 --- a/packages/SystemUI/res/drawable/volume_drawer_selection_bg.xml +++ b/packages/SystemUI/res/drawable/volume_drawer_selection_bg.xml @@ -17,8 +17,8 @@ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" android:paddingMode="stack" > <size - android:height="@dimen/volume_ringer_item_size" - android:width="@dimen/volume_ringer_item_size" /> + android:height="@dimen/volume_dialog_ringer_drawer_button_size" + android:width="@dimen/volume_dialog_ringer_drawer_button_size" /> <solid android:color="?androidprv:attr/materialColorPrimary" /> - <corners android:radius="360dp" /> + <corners android:radius="@dimen/volume_dialog_ringer_selected_button_background_radius" /> </shape>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/volume_ringer_item_bg.xml b/packages/SystemUI/res/drawable/volume_ringer_item_bg.xml index 7ddd57b7c34e..a8c9818b58a3 100644 --- a/packages/SystemUI/res/drawable/volume_ringer_item_bg.xml +++ b/packages/SystemUI/res/drawable/volume_ringer_item_bg.xml @@ -16,7 +16,7 @@ <shape xmlns:android="http://schemas.android.com/apk/res/android" xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" android:shape="rectangle" > - <size android:width="@dimen/volume_ringer_item_size" android:height="@dimen/volume_ringer_item_size"/> + <size android:width="@dimen/volume_dialog_ringer_drawer_button_size" android:height="@dimen/volume_dialog_ringer_drawer_button_size"/> <solid android:color="?androidprv:attr/materialColorSurfaceContainerHighest" /> - <corners android:radius="@dimen/volume_ringer_item_radius" /> + <corners android:radius="@dimen/volume_dialog_background_square_corner_radius" /> </shape>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/volume_dialog.xml b/packages/SystemUI/res/layout/volume_dialog.xml index 8b39e5eb341f..e90f055f2538 100644 --- a/packages/SystemUI/res/layout/volume_dialog.xml +++ b/packages/SystemUI/res/layout/volume_dialog.xml @@ -40,9 +40,9 @@ android:layout_height="wrap_content" android:background="@drawable/volume_dialog_background" android:divider="@drawable/volume_dialog_spacer" + android:paddingVertical="@dimen/volume_dialog_vertical_padding" android:gravity="center_horizontal" android:orientation="vertical" - android:paddingVertical="@dimen/volume_dialog_vertical_padding" android:showDividers="middle"> <include layout="@layout/volume_ringer_drawer" /> diff --git a/packages/SystemUI/res/layout/volume_dialog_slider.xml b/packages/SystemUI/res/layout/volume_dialog_slider.xml index 8acdd39faaa2..c1852b106544 100644 --- a/packages/SystemUI/res/layout/volume_dialog_slider.xml +++ b/packages/SystemUI/res/layout/volume_dialog_slider.xml @@ -18,10 +18,11 @@ android:layout_height="@dimen/volume_dialog_slider_height"> <com.google.android.material.slider.Slider + style="@style/SystemUI.Material3.Slider.Volume" android:id="@+id/volume_dialog_slider" android:layout_width="@dimen/volume_dialog_slider_height" android:layout_height="match_parent" android:layout_gravity="center" android:rotation="270" - android:theme="@style/Theme.MaterialComponents.DayNight" /> + android:theme="@style/Theme.Material3.Light" /> </FrameLayout> diff --git a/packages/SystemUI/res/layout/volume_ringer_button.xml b/packages/SystemUI/res/layout/volume_ringer_button.xml new file mode 100644 index 000000000000..dc6780aeae60 --- /dev/null +++ b/packages/SystemUI/res/layout/volume_ringer_button.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="wrap_content" + android:layout_height="wrap_content" > + + <ImageButton + android:id="@+id/volume_drawer_button" + android:layout_width="@dimen/volume_dialog_ringer_drawer_button_size" + android:layout_height="@dimen/volume_dialog_ringer_drawer_button_size" + android:padding="@dimen/volume_dialog_ringer_drawer_button_icon_radius" + android:layout_marginBottom="@dimen/volume_dialog_components_spacing" + android:contentDescription="@string/volume_ringer_mode" + android:gravity="center" + android:src="@drawable/volume_ringer_item_bg" + android:background="@drawable/volume_ringer_item_bg"/> + +</FrameLayout> diff --git a/packages/SystemUI/res/layout/volume_ringer_drawer.xml b/packages/SystemUI/res/layout/volume_ringer_drawer.xml index 94b55b18c64f..9b3af52e9704 100644 --- a/packages/SystemUI/res/layout/volume_ringer_drawer.xml +++ b/packages/SystemUI/res/layout/volume_ringer_drawer.xml @@ -20,9 +20,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center" - android:paddingLeft="@dimen/volume_dialog_ringer_container_padding" - android:paddingTop="@dimen/volume_dialog_ringer_container_padding" - android:paddingRight="@dimen/volume_dialog_ringer_container_padding" + android:paddingLeft="@dimen/volume_dialog_ringer_horizontal_padding" + android:paddingRight="@dimen/volume_dialog_ringer_horizontal_padding" android:layoutDirection="ltr" android:clipToPadding="false" android:clipChildren="false" @@ -31,7 +30,6 @@ <!-- Drawer view, invisible by default. --> <FrameLayout android:id="@+id/volume_drawer_container" - android:alpha="0.0" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical"> @@ -40,9 +38,8 @@ <FrameLayout android:id="@+id/volume_drawer_selection_background" android:alpha="0.0" - android:layout_width="@dimen/volume_ringer_item_size" - android:layout_marginBottom="8dp" - android:layout_height="@dimen/volume_ringer_item_size" + android:layout_width="@dimen/volume_dialog_ringer_drawer_button_size" + android:layout_height="@dimen/volume_dialog_ringer_drawer_button_size" android:layout_gravity="bottom|right" android:background="@drawable/volume_drawer_selection_bg" /> @@ -52,64 +49,7 @@ android:layout_height="wrap_content" android:orientation="vertical"> - <FrameLayout - android:id="@+id/volume_drawer_vibrate" - android:layout_width="@dimen/volume_ringer_item_size" - android:layout_height="@dimen/volume_ringer_item_size" - android:layout_marginBottom="8dp" - android:contentDescription="@string/volume_ringer_hint_vibrate" - android:gravity="center" - android:background="@drawable/volume_ringer_item_bg"> - - <ImageView - android:id="@+id/volume_drawer_vibrate_icon" - android:layout_width="@dimen/volume_ringer_icon_size" - android:layout_height="@dimen/volume_ringer_icon_size" - android:layout_gravity="center" - android:src="@drawable/ic_volume_ringer_vibrate" - android:tint="?androidprv:attr/materialColorOnSurface" /> - - </FrameLayout> - - <FrameLayout - android:id="@+id/volume_drawer_mute" - android:layout_width="@dimen/volume_ringer_item_size" - android:layout_height="@dimen/volume_ringer_item_size" - android:layout_marginBottom="8dp" - android:accessibilityTraversalAfter="@id/volume_drawer_vibrate" - android:contentDescription="@string/volume_ringer_hint_mute" - android:gravity="center" - android:background="@drawable/volume_ringer_item_bg"> - - <ImageView - android:id="@+id/volume_drawer_mute_icon" - android:layout_width="@dimen/volume_ringer_icon_size" - android:layout_height="@dimen/volume_ringer_icon_size" - android:layout_gravity="center" - android:src="@drawable/ic_speaker_mute" - android:tint="?androidprv:attr/materialColorOnSurface" /> - - </FrameLayout> - - <FrameLayout - android:id="@+id/volume_drawer_normal" - android:layout_width="@dimen/volume_ringer_item_size" - android:layout_height="@dimen/volume_ringer_item_size" - android:layout_marginBottom="8dp" - android:accessibilityTraversalAfter="@id/volume_drawer_mute" - android:contentDescription="@string/volume_ringer_hint_unmute" - android:gravity="center" - android:background="@drawable/volume_ringer_item_bg"> - - <ImageView - android:id="@+id/volume_drawer_normal_icon" - android:layout_width="@dimen/volume_ringer_icon_size" - android:layout_height="@dimen/volume_ringer_icon_size" - android:layout_gravity="center" - android:src="@drawable/ic_speaker_on" - android:tint="?androidprv:attr/materialColorOnSurface" /> - - </FrameLayout> + <!-- add ringer buttons here --> </LinearLayout> @@ -117,23 +57,16 @@ <!-- The current ringer selection. When the drawer is opened, this animates to the corresponding position in the drawer. When the drawer is closed, it animates back. --> - <FrameLayout - android:id="@+id/volume_new_ringer_active_icon_container" - android:layout_width="@dimen/volume_ringer_item_size" - android:layout_height="@dimen/volume_ringer_item_size" - android:layout_marginBottom="8dp" - android:layout_gravity="bottom|right" + <ImageButton + android:id="@+id/volume_new_ringer_active_button" + android:layout_width="@dimen/volume_dialog_ringer_drawer_button_size" + android:layout_height="@dimen/volume_dialog_ringer_drawer_button_size" + android:layout_marginBottom="@dimen/volume_dialog_components_spacing" + android:background="@drawable/volume_drawer_selection_bg" android:contentDescription="@string/volume_ringer_change" - android:background="@drawable/volume_drawer_selection_bg"> - - <ImageView - android:id="@+id/volume_new_ringer_active_icon" - android:layout_width="@dimen/volume_ringer_icon_size" - android:layout_height="@dimen/volume_ringer_icon_size" - android:layout_gravity="center" - android:tint="?androidprv:attr/materialColorOnPrimary" - android:src="@drawable/ic_volume_media" /> - - </FrameLayout> + android:gravity="center" + android:padding="10dp" + android:src="@drawable/ic_volume_media" + android:tint="?androidprv:attr/materialColorOnPrimary" /> </FrameLayout>
\ No newline at end of file diff --git a/packages/SystemUI/res/values-af/strings.xml b/packages/SystemUI/res/values-af/strings.xml index 9ec61063691a..c091cbf6c062 100644 --- a/packages/SystemUI/res/values-af/strings.xml +++ b/packages/SystemUI/res/values-af/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"Neem jou skerm op?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Neem een app op"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Neem hele skerm op"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"Wanneer jy jou hele skerm opneem, word enigiets wat op jou skerm wys, opgeneem. Wees dus versigtig met dinge soos wagwoorde, betalingbesonderhede, boodskappe, foto’s, en oudio en video."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"Wanneer jy ’n app opneem, word enigiets wat in daardie app gewys of gespeel word, opgeneem. Wees dus versigtig met dinge soos wagwoorde, betalingbesonderhede, boodskappe, foto’s, en oudio en video."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Neem skerm op"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"Huidige app"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Toeganklikheid"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Kortpadsleutels"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Soekkortpaaie"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"Geen soekresultate nie"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Vou ikoon in"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Vou ikoon uit"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"of"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Sleephandvatsel"</string> diff --git a/packages/SystemUI/res/values-am/strings.xml b/packages/SystemUI/res/values-am/strings.xml index 598e224c6a0e..af2971b2ec81 100644 --- a/packages/SystemUI/res/values-am/strings.xml +++ b/packages/SystemUI/res/values-am/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"ማያ ገፅዎን ይቀዳሉ?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"አንድ መተግበሪያ ቅዳ"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"መላው ማያ ገፅን ቅረጽ"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"መላው ማያ ገፅዎን በሚቀዱበት ጊዜ፣ በማያ ገፅዎ ላይ የሚታየው ማንኛውም ነገር ይቀዳል። ስለዚህ እንደ የይለፍ ቃላት፣ የክፍያ ዝርዝሮች፣ መልዕክቶች፣ ፎቶዎች እና ኦዲዮ እና ቪድዮ ላሉ ነገሮች ጥንቃቄ ያድርጉ።"</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"መተግበሪያን ሲቀዱ በዚያ መተግበሪያ ውስጥ የሚታይ ወይም የሚጫወት ማንኛውም ነገር ይቀዳል። ስለዚህ እንደ የይለፍ ቃላት፣ የክፍያ ዝርዝሮች፣ መልዕክቶች፣ ፎቶዎች እና ኦዲዮ እና ቪድዮ ላሉ ነገሮች ጥንቃቄ ያድርጉ።"</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"ማያ ገፅን ቅረጽ"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"የአሁን መተግበሪያ"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"ተደራሽነት"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"የቁልፍ ሰሌዳ አቋራጮች"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"የፍለጋ አቋራጮች"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"ምንም የፍለጋ ውጤቶች የሉም"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"መሰብሰቢያ አዶ"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"መዘርጊያ አዶ"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"ወይም"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"መያዣ ይጎትቱ"</string> diff --git a/packages/SystemUI/res/values-ar/strings.xml b/packages/SystemUI/res/values-ar/strings.xml index 342c8c2980d9..4ebac5a86ec1 100644 --- a/packages/SystemUI/res/values-ar/strings.xml +++ b/packages/SystemUI/res/values-ar/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"هل تريد تسجيل محتوى الشاشة؟"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"تسجيل محتوى تطبيق واحد"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"تسجيل محتوى الشاشة بالكامل"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"أثناء تسجيل محتوى الشاشة بالكامل، يتم تسجيل كل المحتوى المعروض على شاشتك. لذا يُرجى توخي الحذر بشأن المعلومات، مثل كلمات المرور وتفاصيل الدفع والرسائل والصور وملفات الصوت والفيديو."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"أثناء تسجيل محتوى تطبيق، يتم تسجيل أي محتوى يتم عرضه أو تشغيله في ذلك التطبيق. لذا يُرجى توخي الحذر بشأن المعلومات، مثل كلمات المرور وتفاصيل الدفع والرسائل والصور وملفات الصوت والفيديو."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"تسجيل محتوى الشاشة"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"التطبيق الحالي"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"تسهيل الاستخدام"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"اختصارات لوحة المفاتيح"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"اختصارات طلبات البحث"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"ما مِن نتائج بحث"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"رمز التصغير"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"رمز التوسيع"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"أو"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"مقبض السحب"</string> diff --git a/packages/SystemUI/res/values-as/strings.xml b/packages/SystemUI/res/values-as/strings.xml index b71db2e8528c..85517f3865d2 100644 --- a/packages/SystemUI/res/values-as/strings.xml +++ b/packages/SystemUI/res/values-as/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"আপোনাৰ স্ক্ৰীনখন ৰেকৰ্ড কৰিবনে?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"এটা এপ্ ৰেকৰ্ড কৰক"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"গোটেই স্ক্ৰীনখন ৰেকৰ্ড কৰক"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"আপুনি গোটেই স্ক্ৰীনখন ৰেকৰ্ডিং কৰিলে, আপোনাৰ স্ক্ৰীনখনত দেখুওৱা যিকোনো বস্তু ৰেকৰ্ড কৰা হয়। গতিকে, পাছৱৰ্ড, পৰিশোধৰ সবিশেষ, বাৰ্তা, ফট’ আৰু অডিঅ’ আৰু ভিডিঅ’ৰ ক্ষেত্ৰত সাৱধান হওক।"</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"আপুনি কোনো এপ্ ৰেকৰ্ড কৰিলে, সেই এপত দেখুওৱা বা প্লে’ কৰা যিকোনো বস্তু ৰেকৰ্ড কৰা হয়। গতিকে, পাছৱৰ্ড, পৰিশোধৰ সবিশেষ, বাৰ্তা, ফট’ আৰু অডিঅ’ আৰু ভিডিঅ’ৰ ক্ষেত্ৰত সাৱধান হওক।"</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"স্ক্ৰীনখন ৰেকৰ্ড কৰক"</string> @@ -581,10 +583,8 @@ <string name="clear_all_notifications_text" msgid="348312370303046130">"আটাইবোৰ মচক"</string> <string name="manage_notifications_text" msgid="6885645344647733116">"পৰিচালনা কৰক"</string> <string name="manage_notifications_history_text" msgid="57055985396576230">"ইতিহাস"</string> - <!-- no translation found for notification_settings_button_description (2441994740884163889) --> - <skip /> - <!-- no translation found for notification_history_button_description (1578657591405033383) --> - <skip /> + <string name="notification_settings_button_description" msgid="2441994740884163889">"জাননীৰ ছেটিং"</string> + <string name="notification_history_button_description" msgid="1578657591405033383">"জাননীৰ ইতিহাস"</string> <string name="notification_section_header_incoming" msgid="850925217908095197">"নতুন"</string> <string name="notification_section_header_gentle" msgid="6804099527336337197">"নীৰৱ"</string> <string name="notification_section_header_alerting" msgid="5581175033680477651">"জাননীসমূহ"</string> @@ -1413,9 +1413,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"বৰ্তমানৰ এপ্"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"সাধ্য সুবিধা"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"কীব’ৰ্ডৰ শ্বৰ্টকাট"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"সন্ধানৰ শ্বৰ্টকাট"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"সন্ধানৰ কোনো ফলাফল নাই"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"সংকোচন কৰাৰ চিহ্ন"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"বিস্তাৰ কৰাৰ চিহ্ন"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"অথবা"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"ড্ৰেগ হেণ্ডেল"</string> diff --git a/packages/SystemUI/res/values-az/strings.xml b/packages/SystemUI/res/values-az/strings.xml index d51c9aa3af3d..f08724a81449 100644 --- a/packages/SystemUI/res/values-az/strings.xml +++ b/packages/SystemUI/res/values-az/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"Ekran qeydə alınsın?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Bir tətbiqi qeydə alın"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Bütün ekranı qeydə alın"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"Bütün ekranı qeydə alarkən ekranda göstərilən bütün kontent qeydə alınır. Parol, ödəniş detalları, mesaj, foto, habelə audio və video kimi məlumatlarla bağlı diqqətli olun."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"Tətbiq qeydə aldıqda həmin tətbiqdə göstərilən və ya işə salınan bütün kontent qeydə alınır. Parol, ödəniş detalları, mesaj, foto, habelə audio və video kimi məlumatlarla bağlı diqqətli olun."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Ekranı qeydə alın"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"Cari tətbiq"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Xüsusi imkanlar"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Klaviatura qısayolları"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Axtarış qısayolları"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"Axtarış nəticəsi yoxdur"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"İkonanı yığcamlaşdırın"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"İkonanı genişləndirin"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"və ya"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Dəstəyi çəkin"</string> diff --git a/packages/SystemUI/res/values-b+sr+Latn/strings.xml b/packages/SystemUI/res/values-b+sr+Latn/strings.xml index 881495f0d3fe..f5096915e261 100644 --- a/packages/SystemUI/res/values-b+sr+Latn/strings.xml +++ b/packages/SystemUI/res/values-b+sr+Latn/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"Želite da snimite ekran?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Snimi jednu aplikaciju"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Snimi ceo ekran"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"Kada snimate ceo ekran, snima se sve što je na njemu. Zato pazite na lozinke, informacije o plaćanju, poruke, slike, audio i video sadržaj."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"Kada snimate aplikaciju, snima se sav sadržaj koji se prikazuje ili pušta u njoj. Zato pazite na lozinke, informacije o plaćanju, poruke, slike, audio i video sadržaj."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Snimi ekran"</string> @@ -581,10 +583,8 @@ <string name="clear_all_notifications_text" msgid="348312370303046130">"Obriši sve"</string> <string name="manage_notifications_text" msgid="6885645344647733116">"Upravljaj"</string> <string name="manage_notifications_history_text" msgid="57055985396576230">"Istorija"</string> - <!-- no translation found for notification_settings_button_description (2441994740884163889) --> - <skip /> - <!-- no translation found for notification_history_button_description (1578657591405033383) --> - <skip /> + <string name="notification_settings_button_description" msgid="2441994740884163889">"Podešavanja obaveštenja"</string> + <string name="notification_history_button_description" msgid="1578657591405033383">"Istorija obaveštenja"</string> <string name="notification_section_header_incoming" msgid="850925217908095197">"Novo"</string> <string name="notification_section_header_gentle" msgid="6804099527336337197">"Nečujno"</string> <string name="notification_section_header_alerting" msgid="5581175033680477651">"Obaveštenja"</string> @@ -1413,9 +1413,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"Aktuelna aplikacija"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Pristupačnost"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Tasterske prečice"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Prečice pretrage"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"Nema rezultata pretrage"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Ikona za skupljanje"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Ikona za proširivanje"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"ili"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Marker za prevlačenje"</string> diff --git a/packages/SystemUI/res/values-be/strings.xml b/packages/SystemUI/res/values-be/strings.xml index 16bb70cf62ca..bc0e2d189c02 100644 --- a/packages/SystemUI/res/values-be/strings.xml +++ b/packages/SystemUI/res/values-be/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"Запісаць экран?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Запісаць адну праграму"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Запісаць змесціва ўсяго экрана"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"Пры запісе ўсяго экрана запісваецца ўсё, што паказваецца на экране. Таму прадухіліце паказ пароляў, плацежных рэквізітаў, паведамленняў, фота, відэа і аўдыя."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"Пры запісе праграмы запісваецца ўсё, што паказваецца або прайграецца ў гэтай праграме. Таму прадухіліце паказ пароляў, плацежных рэквізітаў, паведамленняў, фота, відэа і аўдыя."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Запісаць экран"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"Бягучая праграма"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Спецыяльныя магчымасці"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Спалучэнні клавіш"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Пошук спалучэнняў клавіш"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"Няма вынікаў пошуку"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Значок \"Згарнуць\""</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Значок \"Разгарнуць\""</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"або"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Маркер перацягвання"</string> diff --git a/packages/SystemUI/res/values-bg/strings.xml b/packages/SystemUI/res/values-bg/strings.xml index 2b3a0b9383bf..2ee1f5f0f21e 100644 --- a/packages/SystemUI/res/values-bg/strings.xml +++ b/packages/SystemUI/res/values-bg/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"Да се записва ли екранът?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Записване на едно приложение"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Записване на целия екран"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"Когато записвате целия си екран, се записва всичко, което се показва на него. Затова бъдете внимателни с неща като пароли, подробности за начини на плащане, съобщения, снимки, аудио и видео."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"Когато записвате приложение, се записва всичко, което се показва или възпроизвежда в него. Затова бъдете внимателни с неща като пароли, подробности за начини на плащане, съобщения, снимки, аудио и видео."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Записване на екрана"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"Текущо приложение"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Достъпност"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Клавишни комбинации"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Търсете клавишни комбинации"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"Няма резултати от търсенето"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Икона за свиване"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Икона за разгъване"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"или"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Манипулатор за преместване с плъзгане"</string> diff --git a/packages/SystemUI/res/values-bn/strings.xml b/packages/SystemUI/res/values-bn/strings.xml index f39a644d5e88..c593210c4613 100644 --- a/packages/SystemUI/res/values-bn/strings.xml +++ b/packages/SystemUI/res/values-bn/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"আপনার স্ক্রিন রেকর্ড করবেন?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"একটি অ্যাপ রেকর্ড করুন"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"সম্পূর্ণ স্ক্রিন রেকর্ড করুন"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"আপনার সম্পূর্ণ স্ক্রিন রেকর্ড করার সময়, আপনার স্ক্রিনে দেখানো সব কিছু রেকর্ড করা হয়। তাই পাসওয়ার্ড, পেমেন্টের বিবরণ, মেসেজ, ফটো এবং অডিও ও ভিডিওর মতো বিষয়ের ক্ষেত্রে সতর্ক থাকুন।"</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"আপনি কোনও অ্যাপ রেকর্ড করার সময়, সেই অ্যাপে দেখানো বা চালানো সব কিছু রেকর্ড করা হয়। তাই পাসওয়ার্ড, পেমেন্টের বিবরণ, মেসেজ, ফটো এবং অডিও ও ভিডিওর মতো বিষয়ের ক্ষেত্রে সতর্ক থাকুন।"</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"স্ক্রিন রেকর্ড করুন"</string> @@ -581,10 +583,8 @@ <string name="clear_all_notifications_text" msgid="348312370303046130">"সব মুছে দিন"</string> <string name="manage_notifications_text" msgid="6885645344647733116">"ম্যানেজ করুন"</string> <string name="manage_notifications_history_text" msgid="57055985396576230">"ইতিহাস"</string> - <!-- no translation found for notification_settings_button_description (2441994740884163889) --> - <skip /> - <!-- no translation found for notification_history_button_description (1578657591405033383) --> - <skip /> + <string name="notification_settings_button_description" msgid="2441994740884163889">"বিজ্ঞপ্তি সেটিংস"</string> + <string name="notification_history_button_description" msgid="1578657591405033383">"বিজ্ঞপ্তির ইতিহাস"</string> <string name="notification_section_header_incoming" msgid="850925217908095197">"নতুন"</string> <string name="notification_section_header_gentle" msgid="6804099527336337197">"আওয়াজ করবে না"</string> <string name="notification_section_header_alerting" msgid="5581175033680477651">"বিজ্ঞপ্তি"</string> @@ -1413,9 +1413,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"বর্তমান অ্যাপ"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"অ্যাক্সেসিবিলিটি"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"কীবোর্ড শর্টকাট"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"সার্চ শর্টকাট"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"কোনও সার্চ ফলাফল নেই"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"আইকন আড়াল করুন"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"আইকন বড় করুন"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"অথবা"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"টেনে আনার হ্যান্ডেল"</string> diff --git a/packages/SystemUI/res/values-bs/strings.xml b/packages/SystemUI/res/values-bs/strings.xml index ec57c6abede1..38f42659245e 100644 --- a/packages/SystemUI/res/values-bs/strings.xml +++ b/packages/SystemUI/res/values-bs/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"Snimati ekran?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Snimaj jednu aplikaciju"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Snimaj cijeli ekran"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"Kada snimate cijeli ekran, snimat će se sve što se prikazuje na ekranu. Stoga budite oprezni s informacijama kao što su lozinke, podaci o plaćanju, poruke, fotografije, zvukovi i videozapisi."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"Kada snimate aplikaciju, snimat će se sve što se prikazuje ili reproducira u toj aplikaciji. Stoga budite oprezni s informacijama kao što su lozinke, podaci o plaćanju, poruke, fotografije, zvukovi i videozapisi."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Snimaj ekran"</string> @@ -581,10 +583,8 @@ <string name="clear_all_notifications_text" msgid="348312370303046130">"Obriši sve"</string> <string name="manage_notifications_text" msgid="6885645344647733116">"Upravljajte"</string> <string name="manage_notifications_history_text" msgid="57055985396576230">"Historija"</string> - <!-- no translation found for notification_settings_button_description (2441994740884163889) --> - <skip /> - <!-- no translation found for notification_history_button_description (1578657591405033383) --> - <skip /> + <string name="notification_settings_button_description" msgid="2441994740884163889">"Postavke obavještenja"</string> + <string name="notification_history_button_description" msgid="1578657591405033383">"Historija obavještenja"</string> <string name="notification_section_header_incoming" msgid="850925217908095197">"Novo"</string> <string name="notification_section_header_gentle" msgid="6804099527336337197">"Nečujno"</string> <string name="notification_section_header_alerting" msgid="5581175033680477651">"Obavještenja"</string> @@ -1413,9 +1413,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"Trenutna aplikacija"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Pristupačnost"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Prečice tastature"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Prečica pretraživanja"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"Nema rezultata pretraživanja"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Ikona sužavanja"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Ikona proširivanja"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"ili"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Ručica za prevlačenje"</string> diff --git a/packages/SystemUI/res/values-ca/strings.xml b/packages/SystemUI/res/values-ca/strings.xml index 9c9ba90a225d..bcaca5ae16e9 100644 --- a/packages/SystemUI/res/values-ca/strings.xml +++ b/packages/SystemUI/res/values-ca/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"Vols gravar la pantalla?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Grava una aplicació"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Grava tota la pantalla"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"Quan graves tota la pantalla, es grava tot el que es mostra en pantalla. Per aquest motiu, ves amb compte amb elements com les contrasenyes, les dades de pagament, els missatges, les fotos, i l\'àudio i el vídeo."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"Quan graves una aplicació, es grava tot el que es mostra o es reprodueix en aquesta aplicació. Per aquest motiu, ves amb compte amb les contrasenyes, les dades de pagament, els missatges, les fotos, i l\'àudio i el vídeo."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Grava la pantalla"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"Aplicació actual"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Accessibilitat"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Tecles de drecera"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Dreceres de cerca"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"No hi ha cap resultat de la cerca"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Replega la icona"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Desplega la icona"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"o"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Ansa per arrossegar"</string> diff --git a/packages/SystemUI/res/values-cs/strings.xml b/packages/SystemUI/res/values-cs/strings.xml index 95f369988c1e..76ae86deddd4 100644 --- a/packages/SystemUI/res/values-cs/strings.xml +++ b/packages/SystemUI/res/values-cs/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"Pořídit nahrávku obrazovky?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Nahrát jednu aplikaci"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Nahrát celou obrazovku"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"Při nahrávání celé obrazovky se zaznamenává veškerý obsah na obrazovce. Buďte proto opatrní, když jde o hesla, platební údaje, zprávy, fotografie, zvuk a video."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"Při nahrávání aplikace se zaznamenává všechno, co se v dané obrazovce zobrazuje nebo přehrává. Buďte proto opatrní, když jde o hesla, platební údaje, zprávy, fotografie, zvuk a video."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Nahrát obrazovku"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"Aktuální aplikace"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Přístupnost"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Klávesové zkratky"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Vyhledat zkratky"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"Žádné výsledky hledání"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Ikona sbalení"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Ikona rozbalení"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"nebo"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Úchyt pro přetažení"</string> diff --git a/packages/SystemUI/res/values-da/strings.xml b/packages/SystemUI/res/values-da/strings.xml index 8266799a081d..abcd4b8d45ce 100644 --- a/packages/SystemUI/res/values-da/strings.xml +++ b/packages/SystemUI/res/values-da/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"Vil du optage din skærm?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Optag én app"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Optag hele skærmen"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"Når du optager hele skærmen, bliver alt det, der vises på skærmen, optaget. Vær derfor forsigtig med ting såsom adgangskoder, betalingsoplysninger, beskeder, billeder, lyd og video."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"Når du optager en app, optages alt det, der vises eller afspilles i den pågældende app. Vær derfor forsigtig med ting såsom adgangskoder, betalingsoplysninger, beskeder, billeder, lyd og video."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Optag skærm"</string> @@ -581,10 +583,8 @@ <string name="clear_all_notifications_text" msgid="348312370303046130">"Ryd alle"</string> <string name="manage_notifications_text" msgid="6885645344647733116">"Administrer"</string> <string name="manage_notifications_history_text" msgid="57055985396576230">"Historik"</string> - <!-- no translation found for notification_settings_button_description (2441994740884163889) --> - <skip /> - <!-- no translation found for notification_history_button_description (1578657591405033383) --> - <skip /> + <string name="notification_settings_button_description" msgid="2441994740884163889">"Indstillinger for notifikationer"</string> + <string name="notification_history_button_description" msgid="1578657591405033383">"Notifikationshistorik"</string> <string name="notification_section_header_incoming" msgid="850925217908095197">"Nye"</string> <string name="notification_section_header_gentle" msgid="6804099527336337197">"Lydløs"</string> <string name="notification_section_header_alerting" msgid="5581175033680477651">"Notifikationer"</string> @@ -1413,9 +1413,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"Aktuel app"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Hjælpefunktioner"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Tastaturgenveje"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Genveje til søgning"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"Der er ingen søgeresultater"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Ikon for Skjul"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Ikon for Udvid"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"eller"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Håndtag"</string> diff --git a/packages/SystemUI/res/values-de/strings.xml b/packages/SystemUI/res/values-de/strings.xml index a4bc364be58b..9799a932bcd9 100644 --- a/packages/SystemUI/res/values-de/strings.xml +++ b/packages/SystemUI/res/values-de/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"Bildschirm aufnehmen?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Einzelne App aufnehmen"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Gesamten Bildschirm aufnehmen"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"Wenn du den gesamten Bildschirm aufnimmst, ist in der Aufnahme alles zu sehen, was auf dem Bildschirm angezeigt wird. Sei also vorsichtig mit Informationen wie Passwörtern, Zahlungsdetails, Nachrichten, Fotos sowie Audio- und Videoinhalten."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"Wenn du eine App aufnimmst, ist in der Aufnahme alles zu sehen, was in dieser App angezeigt oder abgespielt wird. Sei also vorsichtig mit Informationen wie Passwörtern, Zahlungsdetails, Nachrichten, Fotos sowie Audio- und Videoinhalten."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Bildschirm aufnehmen"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"Aktuelle App"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Bedienungshilfen"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Tastenkürzel"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Tastenkürzel suchen"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"Keine Suchergebnisse"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Symbol „Minimieren“"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Symbol „Maximieren“"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"oder"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Ziehpunkt"</string> diff --git a/packages/SystemUI/res/values-el/strings.xml b/packages/SystemUI/res/values-el/strings.xml index 278a8ea7cfeb..7200a91b618e 100644 --- a/packages/SystemUI/res/values-el/strings.xml +++ b/packages/SystemUI/res/values-el/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"Να γίνει εγγραφή της οθόνης σας;"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Εγγραφή μίας εφαρμογής"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Εγγραφή ολόκληρης της οθόνης"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"Όταν κάνετε εγγραφή ολόκληρης της οθόνη σας, καταγράφεται οτιδήποτε εμφανίζεται σε αυτήν. Επομένως, να είστε προσεκτικοί με τους κωδικούς πρόσβασης, τα στοιχεία πληρωμής, τα μηνύματα, τις φωτογραφίες, τον ήχο και το βίντεο."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"Όταν κάνετε εγγραφή μιας εφαρμογής, καταγράφεται οτιδήποτε εμφανίζεται ή αναπαράγεται στη συγκεκριμένη εφαρμογή. Επομένως, να είστε προσεκτικοί με τους κωδικούς πρόσβασης, τα στοιχεία πληρωμής, τα μηνύματα, τις φωτογραφίες, τον ήχο και το βίντεο."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Εγγραφή οθόνης"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"Τρέχουσα εφαρμογή"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Προσβασιμότητα"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Συντομεύσεις πληκτρολογίου"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Συντομεύσεις αναζήτησης"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"Κανένα αποτέλεσμα αναζήτησης"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Εικονίδιο σύμπτυξης"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Εικονίδιο ανάπτυξης"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"ή"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Λαβή μεταφοράς"</string> diff --git a/packages/SystemUI/res/values-en-rAU/strings.xml b/packages/SystemUI/res/values-en-rAU/strings.xml index 2fbd60a11faf..1ecf4f17cac3 100644 --- a/packages/SystemUI/res/values-en-rAU/strings.xml +++ b/packages/SystemUI/res/values-en-rAU/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"Record your screen?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Record one app"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Record entire screen"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"When you\'re recording your entire screen, anything displayed on your screen is recorded. So, be careful with things like passwords, payment details, messages, photos, audio and video."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"When you\'re recording an app, anything displayed or played in that app is recorded. So, be careful with things like passwords, payment details, messages, photos, audio and video."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Record screen"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"Current app"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Accessibility"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Keyboard shortcuts"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Search shortcuts"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"No search results"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Collapse icon"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Expand icon"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"or"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Drag handle"</string> diff --git a/packages/SystemUI/res/values-en-rCA/strings.xml b/packages/SystemUI/res/values-en-rCA/strings.xml index 23475fab120c..f775513ca418 100644 --- a/packages/SystemUI/res/values-en-rCA/strings.xml +++ b/packages/SystemUI/res/values-en-rCA/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"Record your screen?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Record one app"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Record entire screen"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"When you’re recording your entire screen, anything shown on your screen is recorded. So be careful with things like passwords, payment details, messages, photos, and audio and video."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"When you’re recording an app, anything shown or played in that app is recorded. So be careful with things like passwords, payment details, messages, photos, and audio and video."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Record screen"</string> @@ -136,17 +138,14 @@ <string name="screenrecord_stop_dialog_message_specific_app" msgid="5995770227684523244">"You\'re currently recording <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> <string name="screenrecord_stop_dialog_button" msgid="2883812564938194350">"Stop recording"</string> <string name="share_to_app_chip_accessibility_label" msgid="4210256229976947065">"Sharing screen"</string> - <!-- no translation found for share_to_app_chip_accessibility_label_generic (5517431657924536133) --> - <skip /> + <string name="share_to_app_chip_accessibility_label_generic" msgid="5517431657924536133">"Sharing content"</string> <string name="share_to_app_stop_dialog_title" msgid="9212915050910250438">"Stop sharing screen?"</string> - <!-- no translation found for share_to_app_stop_dialog_title_generic (9079161538135843648) --> - <skip /> + <string name="share_to_app_stop_dialog_title_generic" msgid="9079161538135843648">"Stop sharing?"</string> <string name="share_to_app_stop_dialog_message_entire_screen_with_host_app" msgid="522823522115375414">"You\'re currently sharing your entire screen with <xliff:g id="HOST_APP_NAME">%1$s</xliff:g>"</string> <string name="share_to_app_stop_dialog_message_entire_screen" msgid="5090115386271179270">"You\'re currently sharing your entire screen with an app"</string> <string name="share_to_app_stop_dialog_message_single_app_specific" msgid="5923772039347985172">"You\'re currently sharing <xliff:g id="APP_BEING_SHARED_NAME">%1$s</xliff:g>"</string> <string name="share_to_app_stop_dialog_message_single_app_generic" msgid="6681016774654578261">"You\'re currently sharing an app"</string> - <!-- no translation found for share_to_app_stop_dialog_message_generic (7622174291691249392) --> - <skip /> + <string name="share_to_app_stop_dialog_message_generic" msgid="7622174291691249392">"You\'re currently sharing with an app"</string> <string name="share_to_app_stop_dialog_button" msgid="6334056916284230217">"Stop sharing"</string> <string name="cast_screen_to_other_device_chip_accessibility_label" msgid="4687917476203009885">"Casting screen"</string> <string name="cast_to_other_device_stop_dialog_title" msgid="7836517190930357326">"Stop casting?"</string> @@ -521,10 +520,8 @@ <string name="communal_widget_picker_title" msgid="1953369090475731663">"Lock screen widgets"</string> <string name="communal_widget_picker_description" msgid="490515450110487871">"Anyone can view widgets on your lock screen, even if your tablet\'s locked."</string> <string name="accessibility_action_label_unselect_widget" msgid="1041811747619468698">"unselect widget"</string> - <!-- no translation found for accessibility_action_label_shrink_widget (8259511040536438771) --> - <skip /> - <!-- no translation found for accessibility_action_label_expand_widget (9190524260912211759) --> - <skip /> + <string name="accessibility_action_label_shrink_widget" msgid="8259511040536438771">"Decrease height"</string> + <string name="accessibility_action_label_expand_widget" msgid="9190524260912211759">"Increase height"</string> <string name="communal_widgets_disclaimer_title" msgid="1150954395585308868">"Lock screen widgets"</string> <string name="communal_widgets_disclaimer_text" msgid="1423545475160506349">"To open an app using a widget, you’ll need to verify it’s you. Also, keep in mind that anyone can view them, even when your tablet’s locked. Some widgets may not have been intended for your lock screen and may be unsafe to add here."</string> <string name="communal_widgets_disclaimer_button" msgid="4423059765740780753">"Got it"</string> @@ -1411,9 +1408,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"Current App"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Accessibility"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Keyboard shortcuts"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Search shortcuts"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"No search results"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Collapse icon"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Expand icon"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"or"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Drag handle"</string> diff --git a/packages/SystemUI/res/values-en-rGB/strings.xml b/packages/SystemUI/res/values-en-rGB/strings.xml index 2fbd60a11faf..1ecf4f17cac3 100644 --- a/packages/SystemUI/res/values-en-rGB/strings.xml +++ b/packages/SystemUI/res/values-en-rGB/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"Record your screen?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Record one app"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Record entire screen"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"When you\'re recording your entire screen, anything displayed on your screen is recorded. So, be careful with things like passwords, payment details, messages, photos, audio and video."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"When you\'re recording an app, anything displayed or played in that app is recorded. So, be careful with things like passwords, payment details, messages, photos, audio and video."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Record screen"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"Current app"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Accessibility"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Keyboard shortcuts"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Search shortcuts"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"No search results"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Collapse icon"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Expand icon"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"or"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Drag handle"</string> diff --git a/packages/SystemUI/res/values-en-rIN/strings.xml b/packages/SystemUI/res/values-en-rIN/strings.xml index 2fbd60a11faf..1ecf4f17cac3 100644 --- a/packages/SystemUI/res/values-en-rIN/strings.xml +++ b/packages/SystemUI/res/values-en-rIN/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"Record your screen?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Record one app"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Record entire screen"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"When you\'re recording your entire screen, anything displayed on your screen is recorded. So, be careful with things like passwords, payment details, messages, photos, audio and video."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"When you\'re recording an app, anything displayed or played in that app is recorded. So, be careful with things like passwords, payment details, messages, photos, audio and video."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Record screen"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"Current app"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Accessibility"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Keyboard shortcuts"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Search shortcuts"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"No search results"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Collapse icon"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Expand icon"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"or"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Drag handle"</string> diff --git a/packages/SystemUI/res/values-es-rUS/strings.xml b/packages/SystemUI/res/values-es-rUS/strings.xml index 9ea154020d73..77c2a639f903 100644 --- a/packages/SystemUI/res/values-es-rUS/strings.xml +++ b/packages/SystemUI/res/values-es-rUS/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"¿Quieres grabar la pantalla?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Grabar una app"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Grabar toda la pantalla"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"Cuando grabes toda la pantalla, se grabará todo lo que se muestre en ella. Por lo tanto, debes tener cuidado con contraseñas, detalles de pagos, mensajes, fotos, audios y videos."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"Cuando grabes una app, se registrará todo lo que se muestre o reproduzca en ella. Por lo tanto, debes tener cuidado con contraseñas, detalles de pagos, mensajes, fotos, audios y videos."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Grabar pantalla"</string> @@ -581,10 +583,8 @@ <string name="clear_all_notifications_text" msgid="348312370303046130">"Cerrar todo"</string> <string name="manage_notifications_text" msgid="6885645344647733116">"Administrar"</string> <string name="manage_notifications_history_text" msgid="57055985396576230">"Historial"</string> - <!-- no translation found for notification_settings_button_description (2441994740884163889) --> - <skip /> - <!-- no translation found for notification_history_button_description (1578657591405033383) --> - <skip /> + <string name="notification_settings_button_description" msgid="2441994740884163889">"Configuración de notificaciones"</string> + <string name="notification_history_button_description" msgid="1578657591405033383">"Historial de notificaciones"</string> <string name="notification_section_header_incoming" msgid="850925217908095197">"Nuevo"</string> <string name="notification_section_header_gentle" msgid="6804099527336337197">"Silenciadas"</string> <string name="notification_section_header_alerting" msgid="5581175033680477651">"Notificaciones"</string> @@ -1413,9 +1413,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"App actual"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Accesibilidad"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Combinaciones de teclas"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Buscar combinaciones de teclas"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"La búsqueda no arrojó resultados"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Ícono de contraer"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Ícono de expandir"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"o"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Controlador de arrastre"</string> diff --git a/packages/SystemUI/res/values-es/strings.xml b/packages/SystemUI/res/values-es/strings.xml index 16ffd44a044c..81b1562ed533 100644 --- a/packages/SystemUI/res/values-es/strings.xml +++ b/packages/SystemUI/res/values-es/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"¿Grabar la pantalla?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Grabar una aplicación"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Grabar toda la pantalla"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"Cuando grabas toda la pantalla, se graba todo lo que se muestre en ella. Debes tener cuidado con elementos como contraseñas, detalles de pagos, mensajes, fotos, audio y vídeo."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"Cuando grabas una aplicación, se graba todo lo que se muestre o reproduzca en ella. Debes tener cuidado con elementos como contraseñas, detalles de pagos, mensajes, fotos, audio y vídeo."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Grabar pantalla"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"Aplicación en uso"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Accesibilidad"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Combinaciones de teclas"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Atajos de búsqueda"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"No hay resultados de búsqueda"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Icono de contraer"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Icono de desplegar"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"o"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Controlador de arrastre"</string> diff --git a/packages/SystemUI/res/values-et/strings.xml b/packages/SystemUI/res/values-et/strings.xml index 5c9664b84d66..56039da5acc8 100644 --- a/packages/SystemUI/res/values-et/strings.xml +++ b/packages/SystemUI/res/values-et/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"Kas salvestada ekraanikuvast video?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Ühe rakenduse salvestamine"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Kogu ekraanikuva salvestamine"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"Kui salvestate kogu ekraani, salvestatakse kõik ekraanil kuvatud andmed. Seega olge ettevaatlik selliste andmetega nagu paroolid, makseteave, sõnumid, fotod ning heli ja video."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"Kui salvestate rakendust, salvestatakse kõik, mida selles rakenduses näidatakse või esitatakse. Seega olge ettevaatlik selliste andmetega nagu paroolid, makseteave, sõnumid, fotod ning heli ja video."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Ekraanikuva jäädvustamine"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"Praegune rakendus"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Juurdepääsetavus"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Klaviatuuri otseteed"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Otsingu otseteed"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"Otsingutulemused puuduvad"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Ahendamisikoon"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Laiendamisikoon"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"või"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Lohistamispide"</string> diff --git a/packages/SystemUI/res/values-eu/strings.xml b/packages/SystemUI/res/values-eu/strings.xml index 31348b61484f..426d1d754c94 100644 --- a/packages/SystemUI/res/values-eu/strings.xml +++ b/packages/SystemUI/res/values-eu/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"Pantaila grabatu nahi duzu?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Grabatu aplikazio bat"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Grabatu pantaila osoa"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"Pantaila osoa grabatzen ari zarenean, pantailan agertzen den guztia grabatzen da. Beraz, kontuz ibili pasahitzekin, ordainketen xehetasunekin, mezuekin, argazkiekin, audioekin eta bideoekin, besteak beste."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"Aplikazio bat grabatzen ari zarenean, aplikazio horretan agertzen den edo bertan erreproduzitzen ari den guztia grabatzen da. Beraz, kontuz ibili pasahitzekin, ordainketen xehetasunekin, mezuekin, argazkiekin, audioekin eta bideoekin, besteak beste."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Grabatu pantaila"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"Oraingo aplikazioa"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Erabilerraztasuna"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Lasterbideak"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Bilatu lasterbideak"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"Ez dago bilaketa-emaitzarik"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Tolesteko ikonoa"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Zabaltzeko ikonoa"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"edo"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Arrastatzeko kontrol-puntua"</string> diff --git a/packages/SystemUI/res/values-fa/strings.xml b/packages/SystemUI/res/values-fa/strings.xml index bc89200599cb..b0d23aa81898 100644 --- a/packages/SystemUI/res/values-fa/strings.xml +++ b/packages/SystemUI/res/values-fa/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"صفحهنمایش ضبط شود؟"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"ضبط یک برنامه"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"ضبط کل صفحهنمایش"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"وقتی کل صفحهنمایش را ضبط میکنید، هر چیزی که در صفحهنمایش نشان داده شود ضبط خواهد شد. درنتیجه مراقب چیزهایی مثل گذرواژهها، جزئیات پرداخت، پیامها، عکسها، و صدا و تصویر باشید."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"وقتی برنامهای را ضبط میکنید، هر چیزی که در آن برنامه نشان داده شود یا پخش شود ضبط خواهد شد. درنتیجه مراقب چیزهایی مثل گذرواژهها، جزئیات پرداخت، پیامها، عکسها، و صدا و تصویر باشید."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"ضبط صفحهنمایش"</string> @@ -581,10 +583,8 @@ <string name="clear_all_notifications_text" msgid="348312370303046130">"پاک کردن همه موارد"</string> <string name="manage_notifications_text" msgid="6885645344647733116">"مدیریت"</string> <string name="manage_notifications_history_text" msgid="57055985396576230">"سابقه"</string> - <!-- no translation found for notification_settings_button_description (2441994740884163889) --> - <skip /> - <!-- no translation found for notification_history_button_description (1578657591405033383) --> - <skip /> + <string name="notification_settings_button_description" msgid="2441994740884163889">"تنظیمات اعلان"</string> + <string name="notification_history_button_description" msgid="1578657591405033383">"سابقه اعلان"</string> <string name="notification_section_header_incoming" msgid="850925217908095197">"جدید"</string> <string name="notification_section_header_gentle" msgid="6804099527336337197">"بیصدا"</string> <string name="notification_section_header_alerting" msgid="5581175033680477651">"اعلانها"</string> @@ -1413,9 +1413,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"برنامه فعلی"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"دسترسپذیری"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"میانبرهای صفحهکلید"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"جستجوی میانبرها"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"نتیجهای برای جستجو پیدا نشد"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"نماد جمع کردن"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"نماد ازهم بازکردن"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"یا"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"دستگیره کشاندن"</string> diff --git a/packages/SystemUI/res/values-fi/strings.xml b/packages/SystemUI/res/values-fi/strings.xml index a40cbaecfaa5..690228f9d333 100644 --- a/packages/SystemUI/res/values-fi/strings.xml +++ b/packages/SystemUI/res/values-fi/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"Tallennetaanko näytön toimintaa?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Tallenna yhdestä sovelluksesta"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Tallenna koko näyttö"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"Kun tallennat koko näyttöä, kaikki näytöllä näkyvä sisältö tallennetaan. Ole siis varovainen, kun lisäät salasanoja, maksutietoja, viestejä, kuvia, audiota tai videoita."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"Kun tallennat sovellusta, kaikki sovelluksessa näkyvä tai toistettu sisältö tallennetaan. Ole siis varovainen, kun lisäät salasanoja, maksutietoja, viestejä, kuvia, audiota tai videoita."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Tallenna näyttö"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"Nykyinen sovellus"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Saavutettavuus"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Pikanäppäimet"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Pikahaut"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"Ei hakutuloksia"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Tiivistyskuvake"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Laajennuskuvake"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"tai"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Vetokahva"</string> diff --git a/packages/SystemUI/res/values-fr-rCA/strings.xml b/packages/SystemUI/res/values-fr-rCA/strings.xml index 288be348f670..5ea58ce1e574 100644 --- a/packages/SystemUI/res/values-fr-rCA/strings.xml +++ b/packages/SystemUI/res/values-fr-rCA/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"Enregistrer votre écran?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Enregistrer une appli"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Enregistrer l\'écran entier"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"Lorsque vous enregistrez l\'intégralité de votre écran, tout ce qui s\'affiche sur votre écran est enregistré. Par conséquent, soyez prudent avec les mots de passe, les détails du mode de paiement, les messages, les photos et les contenus audio et vidéo."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"Lorsque vous enregistrez une appli, tout ce qui est affiché ou lu dans cette appli est enregistré. Par conséquent, soyez prudent avec les mots de passe, les détails du mode de paiement, les messages, les photos et les contenus audio et vidéo."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Enregistrer l\'écran"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"Appli actuelle"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Accessibilité"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Raccourcis-clavier"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Recherchez des raccourcis"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"Aucun résultat de recherche"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Icône Réduire"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Icône Développer"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"ou"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Poignée de déplacement"</string> diff --git a/packages/SystemUI/res/values-fr/strings.xml b/packages/SystemUI/res/values-fr/strings.xml index 9d9f3f99708c..2be31b11c1f5 100644 --- a/packages/SystemUI/res/values-fr/strings.xml +++ b/packages/SystemUI/res/values-fr/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"Enregistrer l\'écran ?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Enregistrer une appli"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Enregistrer tout l\'écran"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"Lorsque vous enregistrez l\'intégralité de votre écran, tout ce qui s\'y affiche est enregistré. Faites donc attention aux éléments tels que les mots de passe, les détails du mode de paiement, les messages, les photos, et les contenus audio et vidéo."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"Lorsque vous enregistrez une appli, tout ce qui est affiché ou lu dans celle-ci est enregistré. Faites donc attention aux éléments tels que les mots de passe, détails de mode de paiement, messages, photos et contenus audio et vidéo."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Enregistrer l\'écran"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"Appli actuelle"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Accessibilité"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Raccourcis clavier"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Raccourcis de recherche"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"Aucun résultat de recherche"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Icône Réduire"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Icône Développer"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"ou"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Poignée de déplacement"</string> diff --git a/packages/SystemUI/res/values-gl/strings.xml b/packages/SystemUI/res/values-gl/strings.xml index 1430bc2a1131..2bd0b30988e7 100644 --- a/packages/SystemUI/res/values-gl/strings.xml +++ b/packages/SystemUI/res/values-gl/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"Queres gravar a túa pantalla?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Gravar unha aplicación"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Gravar pantalla completa"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"Cando gravas a pantalla completa, recóllese todo o que se mostra nela. Recomendámosche que teñas coidado con determinada información, como os contrasinais, os detalles de pago, as mensaxes e as fotos, así como co contido de audio e de vídeo."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"Cando gravas unha aplicación, recóllese todo o que se mostra ou reproduce nela. Recomendámosche que teñas coidado con determinada información, como os contrasinais, os detalles de pago, as mensaxes e as fotos, así como co contido de audio e de vídeo."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Gravar pantalla"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"Aplicación actual"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Accesibilidade"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Atallos de teclado"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Atallos de busca"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"Non hai resultados de busca"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Icona de contraer"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Icona de despregar"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"ou"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Controlador de arrastre"</string> diff --git a/packages/SystemUI/res/values-gu/strings.xml b/packages/SystemUI/res/values-gu/strings.xml index bc2739f3a9dc..becca8f8b37f 100644 --- a/packages/SystemUI/res/values-gu/strings.xml +++ b/packages/SystemUI/res/values-gu/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"તમારી સ્ક્રીન રેકોર્ડ કરીએ?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"એક ઍપ રેકોર્ડ કરો"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"પૂર્ણ સ્ક્રીન રેકોર્ડ કરો"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"જ્યારે તમે તમારી પૂર્ણ સ્ક્રીન રેકોર્ડ કરી રહ્યાં હો, ત્યારે તમારી સ્ક્રીન પર બતાવવામાં આવતી હોય તેવી બધી વસ્તુ રેકોર્ડ કરવામાં આવે છે. તેથી પાસવર્ડ, ચુકવણીની વિગતો, મેસેજ, ફોટા અને ડિવાઇસ પર વાગી રહેલા ઑડિયો તથા વીડિયો જેવી બાબતોને લઈને સાવચેત રહો."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"જ્યારે તમે કોઈ ઍપને રેકોર્ડ કરી રહ્યાં હો, ત્યારે એ ઍપમાં બતાવવામાં કે ચલાવવામાં આવતી હોય તેવી બધી વસ્તુ રેકોર્ડ કરવામાં આવે છે. તેથી પાસવર્ડ, ચુકવણીની વિગતો, મેસેજ, ફોટા અને ડિવાઇસ પર વાગી રહેલા ઑડિયો તથા વીડિયો જેવી બાબતોને લઈને સાવચેત રહો."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"સ્ક્રીન રેકોર્ડ કરો"</string> @@ -581,10 +583,8 @@ <string name="clear_all_notifications_text" msgid="348312370303046130">"બધુ સાફ કરો"</string> <string name="manage_notifications_text" msgid="6885645344647733116">"મેનેજ કરો"</string> <string name="manage_notifications_history_text" msgid="57055985396576230">"ઇતિહાસ"</string> - <!-- no translation found for notification_settings_button_description (2441994740884163889) --> - <skip /> - <!-- no translation found for notification_history_button_description (1578657591405033383) --> - <skip /> + <string name="notification_settings_button_description" msgid="2441994740884163889">"નોટિફિકેશનના સેટિંગ"</string> + <string name="notification_history_button_description" msgid="1578657591405033383">"નોટિફિકેશનનો ઇતિહાસ"</string> <string name="notification_section_header_incoming" msgid="850925217908095197">"નવા"</string> <string name="notification_section_header_gentle" msgid="6804099527336337197">"સાઇલન્ટ"</string> <string name="notification_section_header_alerting" msgid="5581175033680477651">"નોટિફિકેશન"</string> @@ -1413,9 +1413,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"હાલની ઍપ"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"ઍક્સેસિબિલિટી"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"કીબોર્ડ શૉર્ટકટ"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"શૉર્ટકટ શોધો"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"કોઈ શોધ પરિણામો નથી"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"\'નાનું કરો\'નું આઇકન"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"\'મોટું કરો\'નું આઇકન"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"અથવા"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"ઑબ્જેક્ટ ખેંચવાનું હૅન્ડલ"</string> diff --git a/packages/SystemUI/res/values-hi/strings.xml b/packages/SystemUI/res/values-hi/strings.xml index f9a1fa5f84ad..a6fc05e2c7e0 100644 --- a/packages/SystemUI/res/values-hi/strings.xml +++ b/packages/SystemUI/res/values-hi/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"क्या आपको स्क्रीन रिकॉर्ड करनी है?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"एक ऐप्लिकेशन की रिकॉर्डिंग करें"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"पूरी स्क्रीन रिकॉर्ड करें"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"पूरी स्क्रीन रिकॉर्ड करते समय, स्क्रीन पर दिखने वाली हर चीज़ रिकॉर्ड की जाती है. इसलिए पासवर्ड, पेमेंट के तरीके की जानकारी, मैसेज, डिवाइस पर चल रहे ऑडियो और वीडियो, और फ़ोटो जैसी चीज़ों को लेकर सावधानी बरतें."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"किसी ऐप्लिकेशन को रिकॉर्ड करने के दौरान, उस पर दिख रहा कॉन्टेंट या चल रहा मीडिया दूसरी स्क्रीन पर भी रिकॉर्ड होता है. इसलिए, रिकॉर्ड करते समय पासवर्ड, पेमेंट के तरीके की जानकारी, मैसेज, फ़ोटो, ऑडियो, और वीडियो को लेकर सावधानी बरतें."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"स्क्रीन रिकॉर्ड करें"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"मौजूदा ऐप्लिकेशन"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"सुलभता"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"कीबोर्ड शॉर्टकट"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"सर्च शॉर्टकट"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"खोज का कोई नतीजा नहीं मिला"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"छोटा करने का आइकॉन"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"बड़ा करने का आइकॉन"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"या"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"खींचकर छोड़ने वाला हैंडल"</string> diff --git a/packages/SystemUI/res/values-hr/strings.xml b/packages/SystemUI/res/values-hr/strings.xml index 4a17fdc37b8e..cb7a1931be4d 100644 --- a/packages/SystemUI/res/values-hr/strings.xml +++ b/packages/SystemUI/res/values-hr/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"Želite li snimati zaslon?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Snimanje jedne aplikacije"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Snimanje cijelog zaslona"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"Kad snimate cijeli zaslon, snima se sve što se prikazuje na zaslonu. Stoga pazite na stvari kao što su zaporke, podaci o plaćanju, poruke, fotografije te audio i videozapisi."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"Kad snimate aplikaciju, snima se sve što se prikazuje ili reproducira u toj aplikaciji. Stoga pazite na stvari kao što su zaporke, podaci o plaćanju, poruke, fotografije te audio i videozapisi."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Snimanje zaslona"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"Trenutačna aplikacija"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Pristupačnost"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Tipkovni prečaci"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Prečaci za pretraživanje"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"Nema rezultata pretraživanja"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Ikona za sažimanje"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Ikona za proširivanje"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"ili"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Marker za povlačenje"</string> diff --git a/packages/SystemUI/res/values-hu/strings.xml b/packages/SystemUI/res/values-hu/strings.xml index ff5339ff5488..b09947fc2600 100644 --- a/packages/SystemUI/res/values-hu/strings.xml +++ b/packages/SystemUI/res/values-hu/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"Rögzíti a képernyőt?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Egyetlen alkalmazás rögzítése"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Teljes képernyő rögzítése"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"A teljes képernyő rögzítése esetén a képernyőn megjelenő minden tartalom rögzítésre kerül. Ezért legyen elővigyázatos a jelszavakkal, a fizetési adatokkal, az üzenetekkel, a fotókkal, valamint a hang- és videófelvételekkel."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"Alkalmazás rögzítésekor az adott alkalmazásban megjelenített vagy lejátszott minden tartalom rögzítésre kerül. Ezért legyen elővigyázatos a jelszavakkal, a fizetési adatokkal, az üzenetekkel, a fotókkal, valamint a hang- és videófelvételekkel."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Képernyő rögzítése"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"Jelenlegi alkalmazás"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Kisegítő lehetőségek"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Billentyűparancsok"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Billentyűparancsok keresése"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"Nincsenek keresési találatok"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Összecsukás ikon"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Kibontás ikon"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"vagy"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Fogópont"</string> diff --git a/packages/SystemUI/res/values-hy/strings.xml b/packages/SystemUI/res/values-hy/strings.xml index 8fb13478da16..174b52622fd2 100644 --- a/packages/SystemUI/res/values-hy/strings.xml +++ b/packages/SystemUI/res/values-hy/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"Տեսագրե՞լ ձեր էկրանը"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Տեսագրել մեկ հավելված"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Տեսագրել ամբողջ էկրանը"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"Երբ դուք տեսագրում եք ամբողջ էկրանը, էկրանին ցուցադրվող ամեն ինչ տեսագրվում է։ Ուստի ուշադիր եղեք այնպիսի բաների հետ, ինչպիսիք են գաղտնաբառերը, վճարային տվյալները, հաղորդագրությունները, լուսանկարները, աուդիո և վիդեո բովանդակությունը։"</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"Երբ դուք որևէ հավելված եք տեսագրում, հավելվածում ցուցադրվող կամ նվագարկվող ամեն ինչ տեսագրվում է։ Ուստի ուշադիր եղեք այնպիսի բաների հետ, ինչպիսիք են գաղտնաբառերը, վճարային տվյալները, հաղորդագրությունները, լուսանկարները, աուդիո և վիդեո բովանդակությունը։"</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Տեսագրել էկրանը"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"Այս հավելվածը"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Հատուկ գործառույթներ"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Ստեղնային դյուրանցումներ"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Դյուրանցումների որոնում"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"Որոնման արդյունքներ չկան"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Ծալել պատկերակը"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Ծավալել պատկերակը"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"կամ"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Տեղափոխման նշիչ"</string> diff --git a/packages/SystemUI/res/values-in/strings.xml b/packages/SystemUI/res/values-in/strings.xml index 0b316befc259..e706b272458a 100644 --- a/packages/SystemUI/res/values-in/strings.xml +++ b/packages/SystemUI/res/values-in/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"Rekam layar Anda?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Rekam satu aplikasi"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Rekam seluruh layar"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"Saat Anda merekam seluruh layar, semua hal yang ditampilkan di layar akan direkam. Jadi, berhati-hatilah saat memasukkan sandi, detail pembayaran, pesan, foto, audio, dan video."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"Jika Anda merekam aplikasi, semua hal yang ditampilkan atau diputar di aplikasi tersebut akan direkam. Jadi, berhati-hatilah saat memasukkan sandi, detail pembayaran, pesan, foto, audio, dan video."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Rekam layar"</string> @@ -409,7 +411,7 @@ <string name="quick_settings_onehanded_label" msgid="2416537930246274991">"Mode satu tangan"</string> <string name="quick_settings_hearing_devices_label" msgid="7277170419679404129">"Alat bantu dengar"</string> <string name="quick_settings_hearing_devices_connected" msgid="6519069502397037781">"Aktif"</string> - <string name="quick_settings_hearing_devices_disconnected" msgid="8907061223998176187">"Terputus"</string> + <string name="quick_settings_hearing_devices_disconnected" msgid="8907061223998176187">"Tidak terhubung"</string> <string name="quick_settings_hearing_devices_dialog_title" msgid="9004774017688484981">"Alat bantu dengar"</string> <string name="quick_settings_pair_hearing_devices" msgid="5987105102207447322">"Sambungkan perangkat baru"</string> <string name="accessibility_hearing_device_pair_new_device" msgid="8440082580186130090">"Klik untuk menyambungkan perangkat baru"</string> @@ -1190,7 +1192,7 @@ <string name="media_output_dialog_group" msgid="5571251347877452212">"Grup"</string> <string name="media_output_dialog_single_device" msgid="3102758980643351058">"1 perangkat dipilih"</string> <string name="media_output_dialog_multiple_devices" msgid="1093771040315422350">"<xliff:g id="COUNT">%1$d</xliff:g> perangkat dipilih"</string> - <string name="media_output_dialog_disconnected" msgid="7090512852817111185">"(terputus)"</string> + <string name="media_output_dialog_disconnected" msgid="7090512852817111185">"(tidak terhubung)"</string> <string name="media_output_dialog_connect_failed" msgid="3080972621975339387">"Tidak dapat beralih. Ketuk untuk mencoba lagi."</string> <string name="media_output_dialog_pairing_new" msgid="5098212763195577270">"Hubungkan perangkat"</string> <string name="media_output_dialog_launch_app_text" msgid="1527413319632586259">"Buka aplikasi untuk mentransmisikan sesi ini."</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"Aplikasi Saat Ini"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Aksesibilitas"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Pintasan keyboard"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Pintasan penelusuran"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"Tidak ada hasil penelusuran"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Ikon ciutkan"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Ikon luaskan"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"atau"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Handel geser"</string> diff --git a/packages/SystemUI/res/values-is/strings.xml b/packages/SystemUI/res/values-is/strings.xml index 700ae589073c..267291069366 100644 --- a/packages/SystemUI/res/values-is/strings.xml +++ b/packages/SystemUI/res/values-is/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"Viltu taka upp skjáinn?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Taka upp eitt forrit"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Taka upp allan skjáinn"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"Þegar þú tekur upp allan skjáinn verður allt sem er sýnilegt á skjánum tekið upp. Passaðu því upp á aðgangsorð, greiðsluupplýsingar, skilaboð, myndir, hljóð og vídeó."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"Þegar þú tekur upp forrit verður allt sem er sýnilegt eða spilað í forritinu tekið upp. Passaðu því upp á aðgangsorð, greiðsluupplýsingar, skilaboð, myndir, hljóð og vídeó."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Taka upp skjá"</string> @@ -581,10 +583,8 @@ <string name="clear_all_notifications_text" msgid="348312370303046130">"Hreinsa allt"</string> <string name="manage_notifications_text" msgid="6885645344647733116">"Stjórna"</string> <string name="manage_notifications_history_text" msgid="57055985396576230">"Ferill"</string> - <!-- no translation found for notification_settings_button_description (2441994740884163889) --> - <skip /> - <!-- no translation found for notification_history_button_description (1578657591405033383) --> - <skip /> + <string name="notification_settings_button_description" msgid="2441994740884163889">"Tilkynningastillingar"</string> + <string name="notification_history_button_description" msgid="1578657591405033383">"Tilkynningaferill"</string> <string name="notification_section_header_incoming" msgid="850925217908095197">"Nýtt"</string> <string name="notification_section_header_gentle" msgid="6804099527336337197">"Hljóðlaust"</string> <string name="notification_section_header_alerting" msgid="5581175033680477651">"Tilkynningar"</string> @@ -1413,9 +1413,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"Núverandi forrit"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Aðgengi"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Flýtilyklar"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Leitarflýtileiðir"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"Engar leitarniðurstöður"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Minnka tákn"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Stækka tákn"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"eða"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Dragkló"</string> diff --git a/packages/SystemUI/res/values-it/strings.xml b/packages/SystemUI/res/values-it/strings.xml index e7734fa4a38d..fd56c5b69f5a 100644 --- a/packages/SystemUI/res/values-it/strings.xml +++ b/packages/SystemUI/res/values-it/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"Registrare lo schermo?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Registra un\'app"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Registra l\'intero schermo"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"Quando registri l\'intero schermo, tutto ciò che viene mostrato sullo schermo viene registrato. Presta quindi attenzione a password, dati di pagamento, messaggi, foto, audio e video."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"Quando registri un\'app, tutto ciò che viene mostrato o riprodotto al suo interno viene registrato. Presta quindi attenzione a password, dati di pagamento, messaggi, foto, audio e video."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Registra lo schermo"</string> @@ -136,17 +138,14 @@ <string name="screenrecord_stop_dialog_message_specific_app" msgid="5995770227684523244">"Al momento stai registrando <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> <string name="screenrecord_stop_dialog_button" msgid="2883812564938194350">"Interrompi registrazione"</string> <string name="share_to_app_chip_accessibility_label" msgid="4210256229976947065">"Condivisione dello schermo in corso"</string> - <!-- no translation found for share_to_app_chip_accessibility_label_generic (5517431657924536133) --> - <skip /> + <string name="share_to_app_chip_accessibility_label_generic" msgid="5517431657924536133">"Condivisione di contenuti in corso…"</string> <string name="share_to_app_stop_dialog_title" msgid="9212915050910250438">"Vuoi interrompere la condivisione dello schermo?"</string> - <!-- no translation found for share_to_app_stop_dialog_title_generic (9079161538135843648) --> - <skip /> + <string name="share_to_app_stop_dialog_title_generic" msgid="9079161538135843648">"Interrompere la condivisione?"</string> <string name="share_to_app_stop_dialog_message_entire_screen_with_host_app" msgid="522823522115375414">"Al momento stai condividendo l\'intero schermo con <xliff:g id="HOST_APP_NAME">%1$s</xliff:g>"</string> <string name="share_to_app_stop_dialog_message_entire_screen" msgid="5090115386271179270">"Al momento stai condividendo l\'intero schermo con un\'app"</string> <string name="share_to_app_stop_dialog_message_single_app_specific" msgid="5923772039347985172">"Al momento stai condividendo <xliff:g id="APP_BEING_SHARED_NAME">%1$s</xliff:g>"</string> <string name="share_to_app_stop_dialog_message_single_app_generic" msgid="6681016774654578261">"Al momento stai condividendo un\'app"</string> - <!-- no translation found for share_to_app_stop_dialog_message_generic (7622174291691249392) --> - <skip /> + <string name="share_to_app_stop_dialog_message_generic" msgid="7622174291691249392">"Al momento stai condividendo contenuti con un\'app"</string> <string name="share_to_app_stop_dialog_button" msgid="6334056916284230217">"Interrompi condivisione"</string> <string name="cast_screen_to_other_device_chip_accessibility_label" msgid="4687917476203009885">"Trasmissione dello schermo in corso…"</string> <string name="cast_to_other_device_stop_dialog_title" msgid="7836517190930357326">"Vuoi interrompere la trasmissione?"</string> @@ -521,10 +520,8 @@ <string name="communal_widget_picker_title" msgid="1953369090475731663">"Widget della schermata di blocco"</string> <string name="communal_widget_picker_description" msgid="490515450110487871">"Chiunque può visualizzare i widget sulla tua schermata di blocco, anche se il tablet è bloccato."</string> <string name="accessibility_action_label_unselect_widget" msgid="1041811747619468698">"deseleziona widget"</string> - <!-- no translation found for accessibility_action_label_shrink_widget (8259511040536438771) --> - <skip /> - <!-- no translation found for accessibility_action_label_expand_widget (9190524260912211759) --> - <skip /> + <string name="accessibility_action_label_shrink_widget" msgid="8259511040536438771">"Riduci altezza"</string> + <string name="accessibility_action_label_expand_widget" msgid="9190524260912211759">"Aumenta altezza"</string> <string name="communal_widgets_disclaimer_title" msgid="1150954395585308868">"Widget della schermata di blocco"</string> <string name="communal_widgets_disclaimer_text" msgid="1423545475160506349">"Per aprire un\'app utilizzando un widget, dovrai verificare la tua identità. Inoltre tieni presente che chiunque può vederlo, anche quando il tablet è bloccato. Alcuni widget potrebbero non essere stati progettati per la schermata di blocco e potrebbe non essere sicuro aggiungerli qui."</string> <string name="communal_widgets_disclaimer_button" msgid="4423059765740780753">"Ok"</string> @@ -1413,9 +1410,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"App corrente"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Accessibilità"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Scorciatoie da tastiera"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Scorciatoie per la ricerca"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"Nessun risultato di ricerca"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Icona Comprimi"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Icona Espandi"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"oppure"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Punto di trascinamento"</string> diff --git a/packages/SystemUI/res/values-iw/strings.xml b/packages/SystemUI/res/values-iw/strings.xml index 553eaae83da9..384c680b116f 100644 --- a/packages/SystemUI/res/values-iw/strings.xml +++ b/packages/SystemUI/res/values-iw/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"להקליט את המסך?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"הקלטה של אפליקציה אחת"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"הקלטה של כל המסך"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"כשמקליטים את כל המסך, כל מה שמופיע במסך מוקלט. מומלץ להיזהר עם סיסמאות, פרטי תשלום, הודעות, תמונות, אודיו וסרטונים."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"כשמקליטים אפליקציה, כל מה שרואים או מפעילים בה מוקלט. מומלץ להיזהר עם סיסמאות, פרטי תשלום, הודעות, תמונות, אודיו וסרטונים."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"הקלטת המסך"</string> @@ -581,10 +583,8 @@ <string name="clear_all_notifications_text" msgid="348312370303046130">"ניקוי הכול"</string> <string name="manage_notifications_text" msgid="6885645344647733116">"ניהול"</string> <string name="manage_notifications_history_text" msgid="57055985396576230">"היסטוריה"</string> - <!-- no translation found for notification_settings_button_description (2441994740884163889) --> - <skip /> - <!-- no translation found for notification_history_button_description (1578657591405033383) --> - <skip /> + <string name="notification_settings_button_description" msgid="2441994740884163889">"הגדרות של התראות"</string> + <string name="notification_history_button_description" msgid="1578657591405033383">"היסטוריית ההתראות"</string> <string name="notification_section_header_incoming" msgid="850925217908095197">"התראות חדשות"</string> <string name="notification_section_header_gentle" msgid="6804099527336337197">"מצב שקט"</string> <string name="notification_section_header_alerting" msgid="5581175033680477651">"התראות"</string> @@ -1413,9 +1413,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"האפליקציה הנוכחית"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"נגישות"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"מקשי קיצור"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"קיצורי דרך לחיפוש"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"אין תוצאות חיפוש"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"סמל הכיווץ"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"סמל ההרחבה"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"או"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"נקודת האחיזה לגרירה"</string> diff --git a/packages/SystemUI/res/values-ja/strings.xml b/packages/SystemUI/res/values-ja/strings.xml index a6fffee06695..a0a3b2bdd403 100644 --- a/packages/SystemUI/res/values-ja/strings.xml +++ b/packages/SystemUI/res/values-ja/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"画面を録画しますか?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"1 つのアプリを録画"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"画面全体を録画"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"画面全体を録画すると、画面に表示されるものがすべて録画されます。パスワード、お支払いの詳細、メッセージ、写真、音声、動画などの情報にご注意ください。"</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"アプリを録画すると、そのアプリで表示または再生される内容がすべて録画されます。パスワード、お支払いの詳細、メッセージ、写真、音声、動画などの情報にご注意ください。"</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"画面を録画"</string> @@ -1411,9 +1413,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"現在のアプリ"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"ユーザー補助"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"キーボード ショートカット"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"検索ショートカット"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"検索結果がありません"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"閉じるアイコン"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"開くアイコン"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"または"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"ドラッグ ハンドル"</string> diff --git a/packages/SystemUI/res/values-ka/strings.xml b/packages/SystemUI/res/values-ka/strings.xml index 9608253c1e1e..471957e044cb 100644 --- a/packages/SystemUI/res/values-ka/strings.xml +++ b/packages/SystemUI/res/values-ka/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"გსურთ თქვენი ეკრანის ჩაწერა?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"ერთი აპის ჩაწერა"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"მთლიანი ეკრანის ჩაწერა"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"მთლიანი ეკრანის ჩაწერისას ჩაიწერება ყველაფერი, რაც თქვენს ეკრანზე გამოჩნდება. ამიტომ სიფრთხილე გამოიჩინეთ ისეთ ინფორმაციასთან, როგორიცაა პაროლები, გადახდის დეტალები, შეტყობინებები, ფოტოები, აუდიო და ვიდეო."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"აპის ჩაწერისას ჩაიწერება ყველაფერი, რაც ამ აპში გამოჩნდება ან დაიკვრება. ამიტომ სიფრთხილე გამოიჩინეთ ისეთ ინფორმაციასთან, როგორიცაა პაროლები, გადახდის დეტალები, შეტყობინებები, ფოტოები, აუდიო და ვიდეო."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"ეკრანის ჩაწერა"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"მიმდინარე აპი"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"მისაწვდომობა"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"კლავიატურის მალსახმობები"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"ძიების მალსახმობები"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"ძიების შედეგები არ არის"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"ხატულის ჩაკეცვა"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"ხატულის გაფართოება"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"ან"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"სახელური ჩავლებისთვის"</string> diff --git a/packages/SystemUI/res/values-kk/strings.xml b/packages/SystemUI/res/values-kk/strings.xml index 0456fa8164fc..3dbfd86b3f38 100644 --- a/packages/SystemUI/res/values-kk/strings.xml +++ b/packages/SystemUI/res/values-kk/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"Қолданба экранын жазасыз ба?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Бір қолданба экранын жазу"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Бүкіл экранды жазу"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"Бүкіл экранды жазған кезде, онда көрінетін барлық нәрсе жазылады. Сондықтан құпия сөздерді, төлем туралы мәліметті, хабарларды немесе басқа құпия ақпаратты енгізген кезде сақ болыңыз."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"Қолданбаны жазған кезде, онда көрінетін не ойнатылатын барлық нәрсе жазылады. Сондықтан құпия сөздерді, төлем туралы мәліметті, хабарларды немесе басқа құпия ақпаратты енгізген кезде сақ болыңыз."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Экранды жазу"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"Қолданыстағы қолданба"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Арнайы мүмкіндіктер"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Перне тіркесімдері"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Іздеу жылдам пәрмендері"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"Іздеу нәтижелері жоқ."</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Жию белгішесі"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Жаю белгішесі"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"немесе"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Сүйрейтін тетік"</string> diff --git a/packages/SystemUI/res/values-km/strings.xml b/packages/SystemUI/res/values-km/strings.xml index 3c33531717bc..b2c8977ab7ce 100644 --- a/packages/SystemUI/res/values-km/strings.xml +++ b/packages/SystemUI/res/values-km/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"ថតអេក្រង់របស់អ្នកឬ?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"ថតកម្មវិធីទោល"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"ថតអេក្រង់ទាំងមូល"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"នៅពេលអ្នកកំពុងថតអេក្រង់ទាំងមូលរបស់អ្នក អ្វីគ្រប់យ៉ាងដែលបង្ហាញនៅលើអេក្រង់របស់អ្នកត្រូវបានថត។ ដូច្នេះ សូមប្រុងប្រយ័ត្នចំពោះអ្វីៗដូចជា ពាក្យសម្ងាត់ ព័ត៌មានលម្អិតអំពីការទូទាត់ប្រាក់ សារ រូបថត ព្រមទាំងសំឡេង និងវីដេអូ។"</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"នៅពេលអ្នកកំពុងថតកម្មវិធីណាមួយ អ្វីគ្រប់យ៉ាងដែលបង្ហាញ ឬចាក់នៅក្នុងកម្មវិធីនោះត្រូវបានថត។ ដូច្នេះ សូមប្រុងប្រយ័ត្នចំពោះអ្វីៗដូចជា ពាក្យសម្ងាត់ ព័ត៌មានលម្អិតអំពីការទូទាត់ប្រាក់ សារ រូបថត ព្រមទាំងសំឡេង និងវីដេអូ។"</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"ថតអេក្រង់"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"កម្មវិធីបច្ចុប្បន្ន"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"ភាពងាយស្រួល"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"ផ្លូវកាត់ក្ដារចុច"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"ផ្លូវកាត់ការស្វែងរក"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"គ្មានលទ្ធផលស្វែងរកទេ"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"រូបតំណាង \"បង្រួម\""</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"រូបតំណាង \"ពង្រីក\""</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"ឬ"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"ដងអូស"</string> diff --git a/packages/SystemUI/res/values-kn/strings.xml b/packages/SystemUI/res/values-kn/strings.xml index dca2e4b4a59e..20c367696858 100644 --- a/packages/SystemUI/res/values-kn/strings.xml +++ b/packages/SystemUI/res/values-kn/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"ನಿಮ್ಮ ಸ್ಕ್ರೀನ್ ಅನ್ನು ರೆಕಾರ್ಡ್ ಮಾಡಬೇಕೇ?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"ಒಂದು ಆ್ಯಪ್ ಅನ್ನು ರೆಕಾರ್ಡ್ ಮಾಡಿ"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"ಸಂಪೂರ್ಣ ಸ್ಕ್ರೀನ್ ಅನ್ನು ರೆಕಾರ್ಡ್ ಮಾಡಿ"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"ನಿಮ್ಮ ಸಂಪೂರ್ಣ ಸ್ಕ್ರೀನ್ ಅನ್ನು ನೀವು ರೆಕಾರ್ಡ್ ಮಾಡುತ್ತಿರುವಾಗ, ನಿಮ್ಮ ಸ್ಕ್ರೀನ್ ಮೇಲೆ ಗೋಚರಿಸುವ ಎಲ್ಲವನ್ನೂ ರೆಕಾರ್ಡ್ ಮಾಡಲಾಗುತ್ತದೆ. ಆದ್ದರಿಂದ ಪಾಸ್ವರ್ಡ್ಗಳು, ಪಾವತಿ ವಿವರಗಳು, ಸಂದೇಶಗಳು, ಫೋಟೋಗಳು ಮತ್ತು ಆಡಿಯೋ ಮತ್ತು ವೀಡಿಯೊದಂತಹ ವಿಷಯಗಳ ಬಗ್ಗೆ ಜಾಗರೂಕರಾಗಿರಿ."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"ನೀವು ಆ್ಯಪ್ ಅನ್ನು ರೆಕಾರ್ಡ್ ಮಾಡುವಾಗ, ಆ ಆ್ಯಪ್ನಲ್ಲಿ ತೋರಿಸಿರುವ ಅಥವಾ ಪ್ಲೇ ಮಾಡಿದ ಎಲ್ಲವನ್ನೂ ರೆಕಾರ್ಡ್ ಮಾಡಲಾಗುತ್ತದೆ. ಆದ್ದರಿಂದ ಪಾಸ್ವರ್ಡ್ಗಳು, ಪಾವತಿ ವಿವರಗಳು, ಸಂದೇಶಗಳು, ಫೋಟೋಗಳು ಮತ್ತು ಆಡಿಯೋ ಮತ್ತು ವೀಡಿಯೊದಂತಹ ವಿಷಯಗಳ ಬಗ್ಗೆ ಜಾಗರೂಕರಾಗಿರಿ."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"ಸ್ಕ್ರೀನ್ ರೆಕಾರ್ಡ್ ಮಾಡಿ"</string> @@ -581,10 +583,8 @@ <string name="clear_all_notifications_text" msgid="348312370303046130">"ಎಲ್ಲವನ್ನೂ ತೆರವುಗೊಳಿಸಿ"</string> <string name="manage_notifications_text" msgid="6885645344647733116">"ನಿರ್ವಹಿಸಿ"</string> <string name="manage_notifications_history_text" msgid="57055985396576230">"ಇತಿಹಾಸ"</string> - <!-- no translation found for notification_settings_button_description (2441994740884163889) --> - <skip /> - <!-- no translation found for notification_history_button_description (1578657591405033383) --> - <skip /> + <string name="notification_settings_button_description" msgid="2441994740884163889">"ನೋಟಿಫಿಕೇಶನ್ ಸೆಟ್ಟಿಂಗ್ಗಳು"</string> + <string name="notification_history_button_description" msgid="1578657591405033383">"ನೋಟಿಫಿಕೇಶನ್ ಇತಿಹಾಸ"</string> <string name="notification_section_header_incoming" msgid="850925217908095197">"ಹೊಸತು"</string> <string name="notification_section_header_gentle" msgid="6804099527336337197">"ನಿಶ್ಶಬ್ದ"</string> <string name="notification_section_header_alerting" msgid="5581175033680477651">"ಅಧಿಸೂಚನೆಗಳು"</string> @@ -1413,9 +1413,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"ಪ್ರಸ್ತುತ ಆ್ಯಪ್"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"ಆ್ಯಕ್ಸೆಸಿಬಿಲಿಟಿ"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"ಕೀಬೋರ್ಡ್ ಶಾರ್ಟ್ಕಟ್ಗಳು"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"ಹುಡುಕಾಟದ ಶಾರ್ಟ್ಕಟ್ಗಳು"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"ಯಾವುದೇ ಹುಡುಕಾಟ ಫಲಿತಾಂಶಗಳಿಲ್ಲ"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"ಕುಗ್ಗಿಸುವ ಐಕಾನ್"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"ವಿಸ್ತೃತಗೊಳಿಸುವ ಐಕಾನ್"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"ಅಥವಾ"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"ಡ್ರ್ಯಾಗ್ ಹ್ಯಾಂಡಲ್"</string> diff --git a/packages/SystemUI/res/values-ko/strings.xml b/packages/SystemUI/res/values-ko/strings.xml index 8674dc3ad863..35dc245643ae 100644 --- a/packages/SystemUI/res/values-ko/strings.xml +++ b/packages/SystemUI/res/values-ko/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"화면을 녹화하시겠습니까?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"단일 앱 녹화"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"전체 화면 녹화"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"전체 화면을 녹화하면 화면에 표시되는 모든 항목이 녹화됩니다. 따라서 비밀번호, 결제 세부정보, 메시지, 사진, 오디오 및 동영상 등이 노출되지 않도록 주의하세요."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"앱을 녹화하면 앱에 표시되거나 앱에서 재생되는 모든 항목이 녹화됩니다. 따라서 비밀번호, 결제 세부정보, 메시지, 사진, 오디오 및 동영상 등이 노출되지 않도록 주의하세요."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"화면 녹화"</string> @@ -581,10 +583,8 @@ <string name="clear_all_notifications_text" msgid="348312370303046130">"모두 지우기"</string> <string name="manage_notifications_text" msgid="6885645344647733116">"관리"</string> <string name="manage_notifications_history_text" msgid="57055985396576230">"기록"</string> - <!-- no translation found for notification_settings_button_description (2441994740884163889) --> - <skip /> - <!-- no translation found for notification_history_button_description (1578657591405033383) --> - <skip /> + <string name="notification_settings_button_description" msgid="2441994740884163889">"알림 설정"</string> + <string name="notification_history_button_description" msgid="1578657591405033383">"알림 기록"</string> <string name="notification_section_header_incoming" msgid="850925217908095197">"새 알림"</string> <string name="notification_section_header_gentle" msgid="6804099527336337197">"무음"</string> <string name="notification_section_header_alerting" msgid="5581175033680477651">"알림"</string> @@ -1413,15 +1413,21 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"현재 앱"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"접근성"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"단축키"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"검색 바로가기"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"검색 결과 없음"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"접기 아이콘"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"확장 아이콘"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"또는"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"드래그 핸들"</string> <string name="shortcut_helper_keyboard_settings_buttons_label" msgid="6720967595915985259">"키보드 설정"</string> <string name="launch_keyboard_tutorial_notification_title" msgid="8849933155160522519">"키보드를 사용하여 이동"</string> - <string name="launch_keyboard_tutorial_notification_content" msgid="2880339951512757918">"단축키 알아보기"</string> + <string name="launch_keyboard_tutorial_notification_content" msgid="2880339951512757918">"단축키에 관해 알아보세요."</string> <string name="launch_touchpad_tutorial_notification_title" msgid="2243780062772196901">"터치패드를 사용하여 이동"</string> <string name="launch_touchpad_tutorial_notification_content" msgid="7931085031240753226">"터치패드 동작 알아보기"</string> <string name="launch_keyboard_touchpad_tutorial_notification_title" msgid="1940023776496198762">"키보드와 터치패드를 사용하여 이동"</string> diff --git a/packages/SystemUI/res/values-ky/strings.xml b/packages/SystemUI/res/values-ky/strings.xml index 363342a94793..72e86cc4a703 100644 --- a/packages/SystemUI/res/values-ky/strings.xml +++ b/packages/SystemUI/res/values-ky/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"Экранды жаздырасызбы?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Бир колдонмону жаздыруу"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Бүтүндөй экранды жаздыруу"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"Бүтүндөй экранды жаздырганда, андагы нерселердин баары видеого түшүп калат. Андыктан этият болуп, сырсөздөр, төлөм ыкмалары, билдирүүлөр, сүрөттөр, аудио жана видео материалдар сыяктуу купуя нерселерди көрсөтүп албаңыз."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"Колдонмону жаздырганда ал колдонмодо көрсөтүлүп же ойнотулуп жаткан нерселер жаздырылат. Андыктан сырсөздөрдү, төлөмдүн чоо-жайын, билдирүүлөрдү, сүрөттөрдү, аудио жана видеону көрсөтүп албаңыз."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Экранды жаздыруу"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"Учурдагы колдонмо"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Атайын мүмкүнчүлүктөр"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Ыкчам баскычтар"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Ыкчам баскычтарды издөө"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"Эч нерсе табылган жок"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Жыйыштыруу сүрөтчөсү"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Жайып көрсөтүү сүрөтчөсү"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"же"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Cүйрөө маркери"</string> diff --git a/packages/SystemUI/res/values-lo/strings.xml b/packages/SystemUI/res/values-lo/strings.xml index 54c2c6baf6a7..f6fe3cce059a 100644 --- a/packages/SystemUI/res/values-lo/strings.xml +++ b/packages/SystemUI/res/values-lo/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"ບັນທຶກໜ້າຈໍຂອງທ່ານບໍ?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"ບັນທຶກແອັບດຽວ"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"ບັນທຶກໝົດໜ້າຈໍ"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"ເມື່ອທ່ານບັນທຶກໝົດໜ້າຈໍຂອງທ່ານ, ລະບົບຈະບັນທຶກທຸກສິ່ງທີ່ສະແດງຢູ່ໜ້າຈໍຂອງທ່ານ. ດັ່ງນັ້ນ, ໃຫ້ລະມັດລະວັງສິ່ງຕ່າງໆ ເຊັ່ນ: ລະຫັດຜ່ານ, ລາຍລະອຽດການຈ່າຍເງິນ, ຂໍ້ຄວາມ, ຮູບພາບ, ພ້ອມທັງສຽງ ແລະ ວິດີໂອ."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"ເມື່ອທ່ານບັນທຶກແອັບ, ລະບົບຈະບັນທຶກທຸກສິ່ງທີ່ສະແດງ ຫຼື ຫຼິ້ນຢູ່ໃນແອັບນັ້ນ. ດັ່ງນັ້ນ, ໃຫ້ລະມັດລະວັງສິ່ງຕ່າງໆ ເຊັ່ນ: ລະຫັດຜ່ານ, ລາຍລະອຽດການຈ່າຍເງິນ, ຂໍ້ຄວາມ, ຮູບພາບ, ພ້ອມທັງສຽງ ແລະ ວິດີໂອ."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"ບັນທຶກໜ້າຈໍ"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"ແອັບປັດຈຸບັນ"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"ການຊ່ວຍເຂົ້າເຖິງ"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"ຄີລັດ"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"ທາງລັດການຊອກຫາ"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"ບໍ່ມີຜົນການຊອກຫາ"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"ໄອຄອນຫຍໍ້ລົງ"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"ໄອຄອນຂະຫຍາຍ"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"ຫຼື"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"ບ່ອນຈັບລາກ"</string> diff --git a/packages/SystemUI/res/values-lt/strings.xml b/packages/SystemUI/res/values-lt/strings.xml index dca26a41edbf..70a0063d7627 100644 --- a/packages/SystemUI/res/values-lt/strings.xml +++ b/packages/SystemUI/res/values-lt/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"Įrašyti ekraną?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Įrašyti vieną programą"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Įrašyti visą ekraną"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"Kai įrašote visą ekraną, įrašomas visas ekrane rodomas turinys. Todėl būkite atsargūs naudodami slaptažodžius, išsamią mokėjimo metodo informaciją, pranešimus, nuotraukas ir garso bei vaizdo įrašus."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"Kai įrašote programą, įrašomas visas toje programoje rodomas ar leidžiamas turinys. Todėl būkite atsargūs naudodami slaptažodžius, išsamią mokėjimo metodo informaciją, pranešimus, nuotraukas ir garso bei vaizdo įrašus."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Įrašyti ekraną"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"Esama programa"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Pritaikomumas"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Spartieji klavišai"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Paieškos šaukiniai"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"Nėra jokių paieškos rezultatų"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Sutraukimo piktograma"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Išskleidimo piktograma"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"arba"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Vilkimo rankenėlė"</string> diff --git a/packages/SystemUI/res/values-lv/strings.xml b/packages/SystemUI/res/values-lv/strings.xml index c5ba0ad80493..360afad1c6fa 100644 --- a/packages/SystemUI/res/values-lv/strings.xml +++ b/packages/SystemUI/res/values-lv/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"Vai ierakstīt ekrānu?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Ierakstīt vienu lietotni"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Ierakstīt visu ekrānu"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"Ierakstot visu ekrānu, viss, kas redzams ekrānā, tiek ierakstīts. Tāpēc piesardzīgi apejieties ar parolēm, maksājumu informāciju, ziņojumiem, fotoattēliem un audio un video saturu."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"Ierakstot lietotni, tiek ierakstīts viss attiecīgajā lietotnē rādītais vai atskaņotais. Tāpēc piesardzīgi apejieties ar parolēm, maksājumu informāciju, ziņojumiem, fotoattēliem un audio un video saturu."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Ierakstīt ekrānu"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"Pašreizējā lietotne"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Pieejamība"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Īsinājumtaustiņi"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Meklēšanas saīsnes"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"Nav meklēšanas rezultātu"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Sakļaušanas ikona"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Izvēršanas ikona"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"vai"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Vilkšanas turis"</string> diff --git a/packages/SystemUI/res/values-mk/strings.xml b/packages/SystemUI/res/values-mk/strings.xml index 65e958e8d765..ebd62a9568e0 100644 --- a/packages/SystemUI/res/values-mk/strings.xml +++ b/packages/SystemUI/res/values-mk/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"Да се снима екранот?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Снимање на една апликација"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Снимање на целиот екран"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"Додека го снимате целиот екран, сѐ што е прикажано на екранот се снима. Затоа, бидете внимателни со лозинките, деталите за плаќање, пораките, фотографиите и аудиото и видеото."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"Додека снимате апликација, може да се сними сѐ што се прикажува или пушта во таа апликација. Затоа, бидете внимателни со лозинките, деталите за плаќање, пораките, фотографиите и аудиото и видеото."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Снимај го екранот"</string> @@ -581,10 +583,8 @@ <string name="clear_all_notifications_text" msgid="348312370303046130">"Избриши сѐ"</string> <string name="manage_notifications_text" msgid="6885645344647733116">"Управувајте"</string> <string name="manage_notifications_history_text" msgid="57055985396576230">"Историја"</string> - <!-- no translation found for notification_settings_button_description (2441994740884163889) --> - <skip /> - <!-- no translation found for notification_history_button_description (1578657591405033383) --> - <skip /> + <string name="notification_settings_button_description" msgid="2441994740884163889">"Поставки за известувања"</string> + <string name="notification_history_button_description" msgid="1578657591405033383">"Историја на известувања"</string> <string name="notification_section_header_incoming" msgid="850925217908095197">"Нов"</string> <string name="notification_section_header_gentle" msgid="6804099527336337197">"Безгласно"</string> <string name="notification_section_header_alerting" msgid="5581175033680477651">"Известувања"</string> @@ -1413,9 +1413,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"Тековна апликација"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Пристапност"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Кратенки од тастатура"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Кратенки за пребарување"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"Нема резултати од пребарување"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Икона за собирање"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Икона за проширување"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"или"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Рачка за влечење"</string> diff --git a/packages/SystemUI/res/values-ml/strings.xml b/packages/SystemUI/res/values-ml/strings.xml index e10ecff4b318..6152fce2e02b 100644 --- a/packages/SystemUI/res/values-ml/strings.xml +++ b/packages/SystemUI/res/values-ml/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"നിങ്ങളുടെ സ്ക്രീൻ റെക്കോർഡ് ചെയ്യണോ?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"ഒരു ആപ്പ് റെക്കോർഡ് ചെയ്യുക"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"സ്ക്രീൻ പൂർണ്ണമായി റെക്കോർഡ് ചെയ്യുക"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"നിങ്ങളുടെ സ്ക്രീൻ പൂർണ്ണമായി റെക്കോർഡ് ചെയ്യുമ്പോൾ, സ്ക്രീനിൽ ദൃശ്യമാകുന്ന എല്ലാം റെക്കോർഡ് ചെയ്യപ്പെടും. അതിനാൽ പാസ്വേഡുകൾ, പേയ്മെന്റ് വിശദാംശങ്ങൾ, സന്ദേശങ്ങൾ, ഫോട്ടോകൾ, ഓഡിയോ, വീഡിയോ എന്നിവ പോലുള്ള കാര്യങ്ങൾ നൽകുമ്പോൾ സൂക്ഷിക്കുക."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"നിങ്ങളുടെ ആപ്പ് റെക്കോർഡ് ചെയ്യുമ്പോൾ, ആ ആപ്പിൽ കാണിക്കുന്നതോ പ്ലേ ചെയ്യുന്നതോ ആയ എല്ലാ കാര്യങ്ങളും റെക്കോർഡ് ചെയ്യപ്പെടും. അതിനാൽ പാസ്വേഡുകൾ, പേയ്മെന്റ് വിശദാംശങ്ങൾ, സന്ദേശങ്ങൾ, ഫോട്ടോകൾ, ഓഡിയോ, വീഡിയോ എന്നിവ പോലുള്ള കാര്യങ്ങൾ നൽകുമ്പോൾ സൂക്ഷിക്കുക."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"സ്ക്രീൻ റെക്കോർഡ് ചെയ്യുക"</string> @@ -581,10 +583,8 @@ <string name="clear_all_notifications_text" msgid="348312370303046130">"എല്ലാം മായ്ക്കുക"</string> <string name="manage_notifications_text" msgid="6885645344647733116">"മാനേജ് ചെയ്യുക"</string> <string name="manage_notifications_history_text" msgid="57055985396576230">"ചരിത്രം"</string> - <!-- no translation found for notification_settings_button_description (2441994740884163889) --> - <skip /> - <!-- no translation found for notification_history_button_description (1578657591405033383) --> - <skip /> + <string name="notification_settings_button_description" msgid="2441994740884163889">"അറിയിപ്പ് ക്രമീകരണം"</string> + <string name="notification_history_button_description" msgid="1578657591405033383">"അറിയിപ്പ് ചരിത്രം"</string> <string name="notification_section_header_incoming" msgid="850925217908095197">"പുതിയത്"</string> <string name="notification_section_header_gentle" msgid="6804099527336337197">"നിശബ്ദം"</string> <string name="notification_section_header_alerting" msgid="5581175033680477651">"അറിയിപ്പുകൾ"</string> @@ -1413,9 +1413,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"നിലവിലെ ആപ്പ്"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"ഉപയോഗസഹായി"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"കീബോഡ് കുറുക്കുവഴികൾ"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"തിരയൽ കുറുക്കുവഴികൾ"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"തിരയൽ ഫലങ്ങളൊന്നുമില്ല"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"ചുരുക്കൽ ഐക്കൺ"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"വികസിപ്പിക്കൽ ഐക്കൺ"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"അല്ലെങ്കിൽ"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"വലിച്ചിടുന്നതിനുള്ള ഹാൻഡിൽ"</string> diff --git a/packages/SystemUI/res/values-mn/strings.xml b/packages/SystemUI/res/values-mn/strings.xml index cb5f054cccf5..aafc8c610c55 100644 --- a/packages/SystemUI/res/values-mn/strings.xml +++ b/packages/SystemUI/res/values-mn/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"Дэлгэцээ бичих үү?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Нэг аппыг бичих"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Бүтэн дэлгэцийг бичих"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"Таныг бүтэн дэлгэцээ бичиж байхад дэлгэц дээр тань харуулж буй аливаа зүйлийг бичдэг. Тиймээс нууц үг, төлбөрийн дэлгэрэнгүй, мессеж, зураг, аудио, видео зэрэг зүйлд болгоомжтой хандаарай."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"Таныг апп бичиж байхад тухайн аппад харуулж эсвэл тоглуулж буй аливаа зүйлийг бичдэг. Тиймээс нууц үг, төлбөрийн дэлгэрэнгүй, мессеж, зураг, аудио, видео зэрэг зүйлд болгоомжтой хандаарай."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Дэлгэцийг бичих"</string> @@ -581,10 +583,8 @@ <string name="clear_all_notifications_text" msgid="348312370303046130">"Бүгдийг арилгах"</string> <string name="manage_notifications_text" msgid="6885645344647733116">"Удирдах"</string> <string name="manage_notifications_history_text" msgid="57055985396576230">"Түүх"</string> - <!-- no translation found for notification_settings_button_description (2441994740884163889) --> - <skip /> - <!-- no translation found for notification_history_button_description (1578657591405033383) --> - <skip /> + <string name="notification_settings_button_description" msgid="2441994740884163889">"Мэдэгдлийн тохиргоо"</string> + <string name="notification_history_button_description" msgid="1578657591405033383">"Мэдэгдлийн түүх"</string> <string name="notification_section_header_incoming" msgid="850925217908095197">"Шинэ"</string> <string name="notification_section_header_gentle" msgid="6804099527336337197">"Чимээгүй"</string> <string name="notification_section_header_alerting" msgid="5581175033680477651">"Мэдэгдлүүд"</string> @@ -1413,9 +1413,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"Одоогийн апп"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Хандалт"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Товчлуурын шууд холбоос"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Товчлолууд хайх"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"Ямар ч хайлтын илэрц байхгүй"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Хураах дүрс тэмдэг"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Дэлгэх дүрс тэмдэг"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"эсвэл"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Чирэх бариул"</string> diff --git a/packages/SystemUI/res/values-mr/strings.xml b/packages/SystemUI/res/values-mr/strings.xml index 8aa0afaaa207..fbbb1670b286 100644 --- a/packages/SystemUI/res/values-mr/strings.xml +++ b/packages/SystemUI/res/values-mr/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"तुमची स्क्रीन रेकॉर्ड करायची आहे का?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"एक अॅप रेकॉर्ड करा"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"पूर्ण स्क्रीन रेकॉर्ड करा"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"तुम्ही तुमची पूर्ण स्क्रीन रेकॉर्ड करता, तेव्हा तुमच्या स्क्रीनवर दाखवलेली कोणतीही गोष्टी रेकॉर्ड केली जाते. त्यामुळे पासवर्ड, पेमेंट तपशील, मेसेज, फोटो आणि ऑडिओ व व्हिडिओ यांसारख्या गोष्टींबाबत सावधगिरी बाळगा."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"तुम्ही अॅप रेकॉर्ड करता, तेव्हा त्या अॅपमध्ये दाखवलेली किंवा प्ले केलेली कोणतीही गोष्ट रेकॉर्ड केली जाते. त्यामुळे पासवर्ड, पेमेंट तपशील, मेसेज, फोटो आणि ऑडिओ व व्हिडिओ यांसारख्या गोष्टींबाबत सावधगिरी बाळगा."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"स्क्रीन रेकॉर्ड करा"</string> @@ -581,10 +583,8 @@ <string name="clear_all_notifications_text" msgid="348312370303046130">"सर्व साफ करा"</string> <string name="manage_notifications_text" msgid="6885645344647733116">"व्यवस्थापित करा"</string> <string name="manage_notifications_history_text" msgid="57055985396576230">"इतिहास"</string> - <!-- no translation found for notification_settings_button_description (2441994740884163889) --> - <skip /> - <!-- no translation found for notification_history_button_description (1578657591405033383) --> - <skip /> + <string name="notification_settings_button_description" msgid="2441994740884163889">"नोटिफिकेशन सेटिंग्ज"</string> + <string name="notification_history_button_description" msgid="1578657591405033383">"नोटिफिकेशन इतिहास"</string> <string name="notification_section_header_incoming" msgid="850925217908095197">"नवीन"</string> <string name="notification_section_header_gentle" msgid="6804099527336337197">"सायलंट"</string> <string name="notification_section_header_alerting" msgid="5581175033680477651">"सूचना"</string> @@ -1413,9 +1413,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"सध्याचे अॅप"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"अॅक्सेसिबिलिटी"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"कीबोर्ड शॉर्टकट"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"शोधण्यासाठी शॉर्टकट"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"कोणतेही शोध परिणाम नाहीत"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"कोलॅप्स करा आयकन"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"विस्तार करा आयकन"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"किंवा"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"ड्रॅग हॅंडल"</string> diff --git a/packages/SystemUI/res/values-ms/strings.xml b/packages/SystemUI/res/values-ms/strings.xml index d0071f4d73d7..087c8a664076 100644 --- a/packages/SystemUI/res/values-ms/strings.xml +++ b/packages/SystemUI/res/values-ms/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"Rakam skrin anda?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Rakam satu apl"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Rakam seluruh skrin"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"Apabila anda merakam seluruh skrin anda, apa-apa sahaja yang dipaparkan pada skrin anda akan dirakam. Oleh hal yang demikian, berhati-hati dengan perkara seperti kata laluan, butiran pembayaran, mesej, foto dan audio serta video."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"Apabila anda merakam apl, apa-apa sahaja yang dipaparkan atau dimainkan dalam apl tersebut akan dirakam. Oleh hal yang demikian, berhati-hati dengan perkara seperti kata laluan, butiran pembayaran, mesej, foto dan audio serta video."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Rakam skrin"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"Apl Semasa"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Kebolehaksesan"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Pintasan papan kekunci"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Pintasan carian"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"Tiada hasil carian"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Kuncupkan ikon"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Kembangkan ikon"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"atau"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Pemegang seret"</string> diff --git a/packages/SystemUI/res/values-my/strings.xml b/packages/SystemUI/res/values-my/strings.xml index fd4c1478f914..d456bcae7085 100644 --- a/packages/SystemUI/res/values-my/strings.xml +++ b/packages/SystemUI/res/values-my/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"ဖန်သားပြင်ကို ရိုက်ကူးမလား။"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"အက်ပ်တစ်ခုကို ရိုက်ကူးရန်"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"ဖန်သားပြင်တစ်ခုလုံးကို ရိုက်ကူးရန်"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"သင့်ဖန်သားပြင်တစ်ခုလုံး ရိုက်ကူးနေချိန်တွင် ဖန်သားပြင်တွင် ပြထားသည့် အရာအားလုံးကို ရိုက်ကူးသည်။ စကားဝှက်၊ ငွေပေးချေမှု အချက်အလက်၊ မက်ဆေ့ဂျ်၊ ဓာတ်ပုံ၊ အသံနှင့် ဗီဒီယိုကဲ့သို့ အရာများကို ဂရုစိုက်ပါ။"</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"အက်ပ်ကို ရိုက်ကူးနေချိန်တွင် ယင်းအက်ပ်တွင် ပြထားသော (သို့) ဖွင့်ထားသော အရာအားလုံးကို ရိုက်ကူးသည်။ စကားဝှက်၊ ငွေပေးချေမှု အချက်အလက်၊ မက်ဆေ့ဂျ်၊ ဓာတ်ပုံ၊ အသံနှင့် ဗီဒီယိုကဲ့သို့ အရာများကို ဂရုစိုက်ပါ။"</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"ဖန်သားပြင်ကို ရိုက်ကူးရန်"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"လက်ရှိအက်ပ်"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"အများသုံးနိုင်မှု"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"လက်ကွက်ဖြတ်လမ်းများ"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"ရှာဖွေစာလုံး ဖြတ်လမ်း"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"ရှာဖွေမှုရလဒ် မရှိပါ"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"လျှော့ပြရန် သင်္ကေတ"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"ပိုပြရန် သင်္ကေတ"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"သို့မဟုတ်"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"ဖိဆွဲအထိန်း"</string> diff --git a/packages/SystemUI/res/values-nb/strings.xml b/packages/SystemUI/res/values-nb/strings.xml index 1c30ca7a4f4c..1fbb35b0905e 100644 --- a/packages/SystemUI/res/values-nb/strings.xml +++ b/packages/SystemUI/res/values-nb/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"Vil du ta opp skjermen?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Ta opp én app"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Ta opp hele skjermen"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"Når du tar opp hele skjermen, blir alt som vises på skjermen, tatt opp. Derfor bør du være forsiktig med for eksempel passord, betalingsopplysninger, meldinger, bilder, lyd og video."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"Når du tar opp en app, blir alt som vises eller spilles av i appen, tatt opp. Derfor bør du være forsiktig med for eksempel passord, betalingsopplysninger, meldinger, bilder, lyd og video."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Ta opp skjermen"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"Aktiv app"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Tilgjengelighet"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Hurtigtaster"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Snarveier til søk"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"Ingen søkeresultater"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Skjul-ikon"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Vis-ikon"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"eller"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Håndtak"</string> diff --git a/packages/SystemUI/res/values-ne/strings.xml b/packages/SystemUI/res/values-ne/strings.xml index f7ac880de720..381118ac4d6c 100644 --- a/packages/SystemUI/res/values-ne/strings.xml +++ b/packages/SystemUI/res/values-ne/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"तपाईंको स्क्रिन रेकर्ड गर्ने हो?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"एउटा एप रेकर्ड गर्नुहोस्"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"पूरै स्क्रिन रेकर्ड गर्नुहोस्"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"तपाईंले आफ्नो पूरै स्क्रिन रेकर्ड गरिरहेका बेला तपाईंको स्क्रिनमा देखाइने सबै सामग्री रेकर्ड गरिन्छ। त्यसैले पासवर्ड, भुक्तानीसम्बन्धी विवरण, म्यासेज, फोटो र अडियो तथा भिडियो जस्ता कुरा हेर्दा वा प्ले गर्दा सावधानी अपनाउनुहोला।"</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"तपाईंले यो एप रेकर्ड गरिरहेका बेला यो एपमा देखाइने वा प्ले गरिने सबै सामग्री रेकर्ड गरिन्छ। त्यसैले पासवर्ड, भुक्तानीसम्बन्धी विवरण, म्यासेज, फोटो र अडियो तथा भिडियो जस्ता कुरा हेर्दा वा प्ले गर्दा सावधानी अपनाउनुहोला।"</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"स्क्रिन रेकर्ड गर्नुहोस्"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"हालको एप"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"सर्वसुलभता"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"किबोर्डका सर्टकटहरू"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"खोजका सर्टकटहरू"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"कुनै पनि खोज परिणाम भेटिएन"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"\"कोल्याप्स गर्नुहोस्\" आइकन"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"\"एक्स्पान्ड गर्नुहोस्\" आइकन"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"वा"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"ड्र्याग ह्यान्डल"</string> diff --git a/packages/SystemUI/res/values-nl/strings.xml b/packages/SystemUI/res/values-nl/strings.xml index 4cea60528e2a..8ff59ee93c98 100644 --- a/packages/SystemUI/res/values-nl/strings.xml +++ b/packages/SystemUI/res/values-nl/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"Je scherm opnemen?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Eén app opnemen"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Hele scherm opnemen"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"Als je je hele scherm opneemt, wordt alles opgenomen wat op je scherm wordt getoond. Wees daarom voorzichtig met bijvoorbeeld wachtwoorden, betalingsgegevens, berichten, foto\'s, en audio en video."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"Als je een app opneemt, wordt alles opgenomen wat wordt getoond of afgespeeld in die app. Wees daarom voorzichtig met bijvoorbeeld wachtwoorden, betalingsgegevens, berichten, foto\'s, en audio en video."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Scherm opnemen"</string> @@ -581,10 +583,8 @@ <string name="clear_all_notifications_text" msgid="348312370303046130">"Alles wissen"</string> <string name="manage_notifications_text" msgid="6885645344647733116">"Beheren"</string> <string name="manage_notifications_history_text" msgid="57055985396576230">"Geschiedenis"</string> - <!-- no translation found for notification_settings_button_description (2441994740884163889) --> - <skip /> - <!-- no translation found for notification_history_button_description (1578657591405033383) --> - <skip /> + <string name="notification_settings_button_description" msgid="2441994740884163889">"Instellingen voor meldingen"</string> + <string name="notification_history_button_description" msgid="1578657591405033383">"Meldingsgeschiedenis"</string> <string name="notification_section_header_incoming" msgid="850925217908095197">"Nieuw"</string> <string name="notification_section_header_gentle" msgid="6804099527336337197">"Stil"</string> <string name="notification_section_header_alerting" msgid="5581175033680477651">"Meldingen"</string> @@ -1413,9 +1413,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"Huidige app"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Toegankelijkheid"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Sneltoetsen"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Snelkoppelingen voor zoekopdrachten"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"Geen zoekresultaten"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Icoon voor samenvouwen"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Icoon voor uitvouwen"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"of"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Handgreep voor slepen"</string> diff --git a/packages/SystemUI/res/values-or/strings.xml b/packages/SystemUI/res/values-or/strings.xml index bbbea612f7f8..60864409ee7f 100644 --- a/packages/SystemUI/res/values-or/strings.xml +++ b/packages/SystemUI/res/values-or/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"ଆପଣଙ୍କ ସ୍କ୍ରିନକୁ ରେକର୍ଡ କରିବେ?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"ଗୋଟିଏ ଆପ ରେକର୍ଡ କରନ୍ତୁ"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"ସମ୍ପୂର୍ଣ୍ଣ ସ୍କ୍ରିନ ରେକର୍ଡ କରନ୍ତୁ"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"ଆପଣ ଆପଣଙ୍କର ସମ୍ପୂର୍ଣ୍ଣ ସ୍କ୍ରିନ ରେକର୍ଡ କରିବା ସମୟରେ, ଆପଣଙ୍କ ସ୍କ୍ରିନରେ ଦେଖାଯାଉଥିବା ସବୁକିଛି ରେକର୍ଡ ହୋଇଥାଏ। ତେଣୁ ପାସୱାର୍ଡ, ପେମେଣ୍ଟ ବିବରଣୀ, ମେସେଜ, ଫଟୋ ଏବଂ ଅଡିଓ ଓ ଭିଡିଓ ପରି ବିଷୟଗୁଡ଼ିକ ପ୍ରତି ସତର୍କ ରୁହନ୍ତୁ।"</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"ଆପଣ ଏକ ଆପ ରେକର୍ଡ କରିବା ସମୟରେ, ସେହି ଆପରେ ଦେଖାଯାଉଥିବା କିମ୍ବା ପ୍ଲେ ହେଉଥିବା ସବୁକିଛି ରେକର୍ଡ ହୋଇଥାଏ। ତେଣୁ ପାସୱାର୍ଡ, ପେମେଣ୍ଟ ବିବରଣୀ, ମେସେଜ, ଫଟୋ ଏବଂ ଅଡିଓ ଓ ଭିଡିଓ ପରି ବିଷୟଗୁଡ଼ିକ ପ୍ରତି ସତର୍କ ରୁହନ୍ତୁ।"</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"ସ୍କ୍ରିନ ରେକର୍ଡ କରନ୍ତୁ"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"ବର୍ତ୍ତମାନର ଆପ"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"ଆକ୍ସେସିବିଲିଟୀ"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"କୀବୋର୍ଡ ସର୍ଟକଟ"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"ସର୍ଚ୍ଚ ସର୍ଟକଟ"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"କୌଣସି ସର୍ଚ୍ଚ ଫଳାଫଳ ନାହିଁ"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"ଆଇକନକୁ ସଙ୍କୁଚିତ କରନ୍ତୁ"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"ଆଇକନକୁ ବିସ୍ତାର କରନ୍ତୁ"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"କିମ୍ବା"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"ଡ୍ରାଗ ହେଣ୍ଡେଲ"</string> diff --git a/packages/SystemUI/res/values-pa/strings.xml b/packages/SystemUI/res/values-pa/strings.xml index 0170a71db268..b99986a5e955 100644 --- a/packages/SystemUI/res/values-pa/strings.xml +++ b/packages/SystemUI/res/values-pa/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"ਕੀ ਤੁਹਾਡੀ ਸਕ੍ਰੀਨ ਨੂੰ ਰਿਕਾਰਡ ਕਰਨਾ ਹੈ?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"ਇੱਕ ਐਪ ਨੂੰ ਰਿਕਾਰਡ ਕਰੋ"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"ਪੂਰੀ ਸਕ੍ਰੀਨ ਨੂੰ ਰਿਕਾਰਡ ਕਰੋ"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"ਜਦੋਂ ਤੁਸੀਂ ਆਪਣੀ ਪੂਰੀ ਸਕ੍ਰੀਨ ਨੂੰ ਰਿਕਾਰਡ ਕਰ ਰਹੇ ਹੁੰਦੇ ਹੋ, ਤਾਂ ਤੁਹਾਡੀ ਸਕ੍ਰੀਨ \'ਤੇ ਦਿਖਾਈ ਜਾ ਰਹੀ ਹਰ ਚੀਜ਼ ਨੂੰ ਰਿਕਾਰਡ ਕੀਤਾ ਜਾਂਦਾ ਹੈ। ਇਸ ਲਈ ਪਾਸਵਰਡਾਂ, ਭੁਗਤਾਨ ਵੇਰਵਿਆਂ, ਸੁਨੇਹਿਆਂ, ਫ਼ੋਟੋਆਂ ਅਤੇ ਨਾਲ ਹੀ ਆਡੀਓ ਅਤੇ ਵੀਡੀਓ ਵਰਗੀਆਂ ਚੀਜ਼ਾਂ ਵਾਸਤੇ ਸਾਵਧਾਨ ਰਹੋ।"</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"ਜਦੋਂ ਤੁਸੀਂ ਕਿਸੇ ਐਪ ਨੂੰ ਰਿਕਾਰਡ ਕਰ ਰਹੇ ਹੁੰਦੇ ਹੋ, ਤਾਂ ਉਸ ਐਪ ਵਿੱਚ ਦਿਖਾਈ ਜਾਂ ਚਲਾਈ ਜਾ ਰਹੀ ਹਰ ਚੀਜ਼ ਨੂੰ ਰਿਕਾਰਡ ਕੀਤਾ ਜਾਂਦਾ ਹੈ। ਇਸ ਲਈ ਪਾਸਵਰਡਾਂ, ਭੁਗਤਾਨ ਵੇਰਵਿਆਂ, ਸੁਨੇਹਿਆਂ, ਫ਼ੋਟੋਆਂ ਅਤੇ ਆਡੀਓ ਅਤੇ ਵੀਡੀਓ ਵਰਗੀਆਂ ਚੀਜ਼ਾਂ ਵਾਸਤੇ ਸਾਵਧਾਨ ਰਹੋ।"</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"ਸਕ੍ਰੀਨ ਰਿਕਾਰਡ ਕਰੋ"</string> @@ -581,10 +583,8 @@ <string name="clear_all_notifications_text" msgid="348312370303046130">"ਸਭ ਕਲੀਅਰ ਕਰੋ"</string> <string name="manage_notifications_text" msgid="6885645344647733116">"ਪ੍ਰਬੰਧਨ ਕਰੋ"</string> <string name="manage_notifications_history_text" msgid="57055985396576230">"ਇਤਿਹਾਸ"</string> - <!-- no translation found for notification_settings_button_description (2441994740884163889) --> - <skip /> - <!-- no translation found for notification_history_button_description (1578657591405033383) --> - <skip /> + <string name="notification_settings_button_description" msgid="2441994740884163889">"ਸੂਚਨਾ ਸੈਟਿੰਗਾਂ"</string> + <string name="notification_history_button_description" msgid="1578657591405033383">"ਸੂਚਨਾ ਇਤਿਹਾਸ"</string> <string name="notification_section_header_incoming" msgid="850925217908095197">"ਨਵੀਆਂ"</string> <string name="notification_section_header_gentle" msgid="6804099527336337197">"ਸ਼ਾਂਤ"</string> <string name="notification_section_header_alerting" msgid="5581175033680477651">"ਸੂਚਨਾਵਾਂ"</string> @@ -1413,9 +1413,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"ਮੌਜੂਦਾ ਐਪ"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"ਪਹੁੰਚਯੋਗਤਾ"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"ਕੀ-ਬੋਰਡ ਸ਼ਾਰਟਕੱਟ"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"ਖੋਜ ਸੰਬੰਧੀ ਸ਼ਾਰਟਕੱਟ"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"ਕੋਈ ਖੋਜ ਨਤੀਜਾ ਨਹੀਂ ਮਿਲਿਆ"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"ਪ੍ਰਤੀਕ ਨੂੰ ਸਮੇਟੋ"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"ਪ੍ਰਤੀਕ ਦਾ ਵਿਸਤਾਰ ਕਰੋ"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"ਜਾਂ"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"ਘਸੀਟਣ ਵਾਲਾ ਹੈਂਡਲ"</string> diff --git a/packages/SystemUI/res/values-pl/strings.xml b/packages/SystemUI/res/values-pl/strings.xml index 33a743f625d3..88177ac56bb7 100644 --- a/packages/SystemUI/res/values-pl/strings.xml +++ b/packages/SystemUI/res/values-pl/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"Nagrywać ekran?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Nagrywaj jedną aplikację"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Nagrywaj cały ekran"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"Kiedy nagrywasz cały ekran, nagrane zostanie wszystko, co jest na nim widoczne. Dlatego uważaj na hasła, dane do płatności, wiadomości, zdjęcia, nagrania audio czy filmy."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"Kiedy nagrywasz aplikację, wszystko, co jest w niej wyświetlane lub odtwarzane, zostaje nagrane. Dlatego zachowaj ostrożność w zakresie haseł, danych do płatności, wiadomości, zdjęć, audio i filmów."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Nagrywaj ekran"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"Bieżąca aplikacja"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Ułatwienia dostępu"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Skróty klawiszowe"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Skróty do wyszukiwania"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"Brak wyników wyszukiwania"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Ikona zwijania"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Ikona rozwijania"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"lub"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Uchwyt do przeciągania"</string> diff --git a/packages/SystemUI/res/values-pt-rBR/strings.xml b/packages/SystemUI/res/values-pt-rBR/strings.xml index 8041760ae14c..e67c16856e58 100644 --- a/packages/SystemUI/res/values-pt-rBR/strings.xml +++ b/packages/SystemUI/res/values-pt-rBR/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"Gravar a tela?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Gravar um app"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Gravar a tela toda"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"Quando você grava a tela toda, tudo o que aparece nela é registrado. Portanto, tenha cuidado com senhas, detalhes de pagamento, mensagens, fotos, áudios e vídeos."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"Quando você grava um app, todas as informações visíveis ou abertas nele ficam registradas. Portanto, tenha cuidado com senhas, detalhes de pagamento, mensagens, fotos, áudios e vídeos."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Gravar a tela"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"App atual"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Acessibilidade"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Atalhos do teclado"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Atalhos de pesquisa"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"Nenhum resultado de pesquisa"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Ícone \"Fechar\""</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Ícone \"Abrir\""</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"ou"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Alça de arrastar"</string> diff --git a/packages/SystemUI/res/values-pt-rPT/strings.xml b/packages/SystemUI/res/values-pt-rPT/strings.xml index f0009986b232..30e16f06f00f 100644 --- a/packages/SystemUI/res/values-pt-rPT/strings.xml +++ b/packages/SystemUI/res/values-pt-rPT/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"Gravar o ecrã?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Gravar uma app"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Gravar o ecrã inteiro"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"Quando está a gravar o ecrã inteiro, tudo o que é apresentado no ecrã é gravado. Por isso, tenha cuidado com, por exemplo, palavras-passe, detalhes de pagamento, mensagens, fotos, áudio e vídeo."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"Quando está a gravar uma app, tudo o que é apresentado ou reproduzido nessa app é gravado. Por isso, tenha cuidado com, por exemplo, palavras-passe, detalhes de pagamento, mensagens, fotos, áudio e vídeo."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Gravar ecrã"</string> @@ -136,17 +138,14 @@ <string name="screenrecord_stop_dialog_message_specific_app" msgid="5995770227684523244">"Neste momento, está a gravar a app <xliff:g id="APP_NAME">%1$s</xliff:g>"</string> <string name="screenrecord_stop_dialog_button" msgid="2883812564938194350">"Parar gravação"</string> <string name="share_to_app_chip_accessibility_label" msgid="4210256229976947065">"A partilhar o ecrã"</string> - <!-- no translation found for share_to_app_chip_accessibility_label_generic (5517431657924536133) --> - <skip /> + <string name="share_to_app_chip_accessibility_label_generic" msgid="5517431657924536133">"A partilhar conteúdo"</string> <string name="share_to_app_stop_dialog_title" msgid="9212915050910250438">"Parar a partilha do ecrã?"</string> - <!-- no translation found for share_to_app_stop_dialog_title_generic (9079161538135843648) --> - <skip /> + <string name="share_to_app_stop_dialog_title_generic" msgid="9079161538135843648">"Parar a partilha?"</string> <string name="share_to_app_stop_dialog_message_entire_screen_with_host_app" msgid="522823522115375414">"Neste momento, está a partilhar todo o seu ecrã com a app <xliff:g id="HOST_APP_NAME">%1$s</xliff:g>"</string> <string name="share_to_app_stop_dialog_message_entire_screen" msgid="5090115386271179270">"Neste momento, está a partilhar todo o seu ecrã com uma app"</string> <string name="share_to_app_stop_dialog_message_single_app_specific" msgid="5923772039347985172">"Neste momento, está a partilhar a app <xliff:g id="APP_BEING_SHARED_NAME">%1$s</xliff:g>"</string> <string name="share_to_app_stop_dialog_message_single_app_generic" msgid="6681016774654578261">"Neste momento, está a partilhar uma app"</string> - <!-- no translation found for share_to_app_stop_dialog_message_generic (7622174291691249392) --> - <skip /> + <string name="share_to_app_stop_dialog_message_generic" msgid="7622174291691249392">"Neste momento, está a partilhar com uma app"</string> <string name="share_to_app_stop_dialog_button" msgid="6334056916284230217">"Parar partilha"</string> <string name="cast_screen_to_other_device_chip_accessibility_label" msgid="4687917476203009885">"A transmitir o ecrã"</string> <string name="cast_to_other_device_stop_dialog_title" msgid="7836517190930357326">"Parar a transmissão?"</string> @@ -521,10 +520,8 @@ <string name="communal_widget_picker_title" msgid="1953369090475731663">"Widgets do ecrã de bloqueio"</string> <string name="communal_widget_picker_description" msgid="490515450110487871">"Todos podem pode ver widgets no ecrã de bloqueio, mesmo com o tablet bloqueado."</string> <string name="accessibility_action_label_unselect_widget" msgid="1041811747619468698">"desmarcar widget"</string> - <!-- no translation found for accessibility_action_label_shrink_widget (8259511040536438771) --> - <skip /> - <!-- no translation found for accessibility_action_label_expand_widget (9190524260912211759) --> - <skip /> + <string name="accessibility_action_label_shrink_widget" msgid="8259511040536438771">"Diminuir altura"</string> + <string name="accessibility_action_label_expand_widget" msgid="9190524260912211759">"Aumentar altura"</string> <string name="communal_widgets_disclaimer_title" msgid="1150954395585308868">"Widgets do ecrã de bloqueio"</string> <string name="communal_widgets_disclaimer_text" msgid="1423545475160506349">"Para abrir uma app através de um widget, vai ter de validar a sua identidade. Além disso, tenha em atenção que qualquer pessoa pode ver os widgets, mesmo quando o tablet estiver bloqueado. Alguns widgets podem não se destinar ao ecrã de bloqueio e pode ser inseguro adicioná-los aqui."</string> <string name="communal_widgets_disclaimer_button" msgid="4423059765740780753">"OK"</string> @@ -1413,9 +1410,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"App atual"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Acessibilidade"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Atalhos de teclado"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Atalhos de pesquisa"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"Nenhum resultado da pesquisa"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Ícone de reduzir"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Ícone de expandir"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"ou"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Indicador para arrastar"</string> diff --git a/packages/SystemUI/res/values-pt/strings.xml b/packages/SystemUI/res/values-pt/strings.xml index 8041760ae14c..e67c16856e58 100644 --- a/packages/SystemUI/res/values-pt/strings.xml +++ b/packages/SystemUI/res/values-pt/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"Gravar a tela?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Gravar um app"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Gravar a tela toda"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"Quando você grava a tela toda, tudo o que aparece nela é registrado. Portanto, tenha cuidado com senhas, detalhes de pagamento, mensagens, fotos, áudios e vídeos."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"Quando você grava um app, todas as informações visíveis ou abertas nele ficam registradas. Portanto, tenha cuidado com senhas, detalhes de pagamento, mensagens, fotos, áudios e vídeos."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Gravar a tela"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"App atual"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Acessibilidade"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Atalhos do teclado"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Atalhos de pesquisa"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"Nenhum resultado de pesquisa"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Ícone \"Fechar\""</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Ícone \"Abrir\""</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"ou"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Alça de arrastar"</string> diff --git a/packages/SystemUI/res/values-ro/strings.xml b/packages/SystemUI/res/values-ro/strings.xml index 57a2a0c2da63..de7f8dac8dd1 100644 --- a/packages/SystemUI/res/values-ro/strings.xml +++ b/packages/SystemUI/res/values-ro/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"Înregistrezi ecranul?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Înregistrează o aplicație"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Înregistrează tot ecranul"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"Când înregistrezi întregul ecran, se înregistrează tot ce apare pe ecran. Prin urmare, ai grijă cu parolele, detaliile de plată, mesajele, fotografiile și conținutul audio și video."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"Când înregistrezi o aplicație, se înregistrează tot ce se afișează sau se redă în aplicație. Prin urmare, ai grijă cu parolele, detaliile de plată, mesajele, fotografiile și conținutul audio și video."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Înregistrează ecranul"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"Aplicația actuală"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Accesibilitate"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Comenzi rapide de la tastatură"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Comenzi directe de căutare"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"Niciun rezultat al căutării"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Pictograma de restrângere"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Pictograma de extindere"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"sau"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Ghidaj de tragere"</string> diff --git a/packages/SystemUI/res/values-ru/strings.xml b/packages/SystemUI/res/values-ru/strings.xml index 507f70926620..a007685d4af3 100644 --- a/packages/SystemUI/res/values-ru/strings.xml +++ b/packages/SystemUI/res/values-ru/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"Начать запись экрана?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Записывать одно приложение"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Записывать весь экран"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"Во время записи всего экрана все данные и действия, которые на нем показываются, попадают на видео. Поэтому будьте осторожны с паролями, сведениями о способах оплаты, сообщениями, фотографиями, аудио- и видеозаписями."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"Во время записи приложения все данные и действия, которые показываются в его окне, попадают на видео. Поэтому будьте осторожны с паролями, сведениями о способах оплаты, сообщениями, фотографиями, аудио- и видеозаписями."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Запись экрана"</string> @@ -581,10 +583,8 @@ <string name="clear_all_notifications_text" msgid="348312370303046130">"Очистить все"</string> <string name="manage_notifications_text" msgid="6885645344647733116">"Настроить"</string> <string name="manage_notifications_history_text" msgid="57055985396576230">"История"</string> - <!-- no translation found for notification_settings_button_description (2441994740884163889) --> - <skip /> - <!-- no translation found for notification_history_button_description (1578657591405033383) --> - <skip /> + <string name="notification_settings_button_description" msgid="2441994740884163889">"Настройки уведомлений"</string> + <string name="notification_history_button_description" msgid="1578657591405033383">"История уведомлений"</string> <string name="notification_section_header_incoming" msgid="850925217908095197">"Новое"</string> <string name="notification_section_header_gentle" msgid="6804099527336337197">"Без звука"</string> <string name="notification_section_header_alerting" msgid="5581175033680477651">"Уведомления"</string> @@ -1413,9 +1413,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"Это приложение"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Специальные возможности"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Быстрые клавиши"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Найти быстрые клавиши"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"Ничего не найдено"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Значок \"Свернуть\""</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Значок \"Развернуть\""</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"или"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Маркер перемещения"</string> diff --git a/packages/SystemUI/res/values-si/strings.xml b/packages/SystemUI/res/values-si/strings.xml index fb9be30202db..ae9c4d0cf711 100644 --- a/packages/SystemUI/res/values-si/strings.xml +++ b/packages/SystemUI/res/values-si/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"ඔබේ තිරය පටිගත කරන්න ද?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"එක් යෙදුමක් පටිගත කරන්න"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"සම්පූර්ණ තිරය පටිගත කරන්න"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"ඔබ ඔබේ සම්පූර්ණ තිරය පටිගත කරන විට, ඔබේ තිරයේ පෙන්වන ඕනෑම දෙයක් වාර්තා වේ. ඒ නිසා මුරපද, ගෙවීම් විස්තර, පණිවුඩ, ඡායාරූප, සහ ශ්රව්ය සහ දෘශ්ය වැනි දේවල් පිළිබඳ ප්රවේශම් වන්න."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"ඔබ යෙදුමක් පටිගත කරන විට, එම යෙදුමේ පෙන්වන හෝ වාදනය කරන ඕනෑම දෙයක් වාර්තා වේ. ඒ නිසා මුරපද, ගෙවීම් විස්තර, පණිවුඩ, ඡායාරූප, සහ ශ්රව්ය සහ දෘශ්ය වැනි දේවල් පිළිබඳ ප්රවේශම් වන්න."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"තිරය පටිගත කරන්න"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"වත්මන් යෙදුම"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"ප්රවේශ්යතාව"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"යතුරු පුවරු කෙටි මං"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"කෙටි මං සොයන්න"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"සෙවීම් ප්රතිඵල නැත"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"හැකුළුම් නිරූපකය"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"දිගහැරීම් නිරූපකය"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"හෝ"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"ඇදීම් හැඬලය"</string> diff --git a/packages/SystemUI/res/values-sk/strings.xml b/packages/SystemUI/res/values-sk/strings.xml index 42a2613cf329..c2235667f08b 100644 --- a/packages/SystemUI/res/values-sk/strings.xml +++ b/packages/SystemUI/res/values-sk/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"Chcete nahrávať obrazovku?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Nahrávať jednu aplikáciu"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Nahrávať celú obrazovku"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"Pri nahrávaní celej obrazovky sa zaznamená všetko, čo sa na nej zobrazuje. Preto venujte pozornosť položkám, ako sú heslá, platobné údaje, správy, fotky a zvuk či video."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"Pri nahrávaní aplikácie sa zaznamená všetko, čo sa v nej zobrazuje alebo prehráva. Preto venujte pozornosť položkám, ako sú heslá, platobné údaje, správy, fotky a zvuk či video."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Nahrávať obrazovku"</string> @@ -581,10 +583,8 @@ <string name="clear_all_notifications_text" msgid="348312370303046130">"Vymazať všetko"</string> <string name="manage_notifications_text" msgid="6885645344647733116">"Spravovať"</string> <string name="manage_notifications_history_text" msgid="57055985396576230">"História"</string> - <!-- no translation found for notification_settings_button_description (2441994740884163889) --> - <skip /> - <!-- no translation found for notification_history_button_description (1578657591405033383) --> - <skip /> + <string name="notification_settings_button_description" msgid="2441994740884163889">"Nastavenia upozornení"</string> + <string name="notification_history_button_description" msgid="1578657591405033383">"História upozornení"</string> <string name="notification_section_header_incoming" msgid="850925217908095197">"Nové"</string> <string name="notification_section_header_gentle" msgid="6804099527336337197">"Tichý"</string> <string name="notification_section_header_alerting" msgid="5581175033680477651">"Upozornenia"</string> @@ -1413,9 +1413,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"Aktuálna aplikácia"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Dostupnosť"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Klávesové skratky"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Vyhľadávacie odkazy"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"Žiadne výsledky vyhľadávania"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Ikona zbalenia"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Ikona rozbalenia"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"alebo"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Presúvadlo"</string> diff --git a/packages/SystemUI/res/values-sl/strings.xml b/packages/SystemUI/res/values-sl/strings.xml index 4ca7b244e9a7..15f1b3ccca7d 100644 --- a/packages/SystemUI/res/values-sl/strings.xml +++ b/packages/SystemUI/res/values-sl/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"Želite posneti zaslon?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Snemanje ene aplikacije"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Snemanje celotnega zaslona"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"Pri snemanju celotnega zaslona se posname vse, kar je prikazano na zaslonu. Zato bodite previdni z gesli, podatki za plačilo, sporočili, fotografijami ter z zvokom in videom."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"Pri snemanju aplikacije se posname vse, kar je prikazano ali predvajano v tej aplikaciji. Zato bodite previdni z gesli, podatki za plačilo, sporočili, fotografijami ter z zvokom in videom."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Snemanje zaslona"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"Trenutna aplikacija"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Dostopnost"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Bližnjične tipke"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Bližnjice za iskanje"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"Ni rezultatov iskanja"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Ikona za strnitev"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Ikona za razširitev"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"ali"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Ročica za vlečenje"</string> diff --git a/packages/SystemUI/res/values-sq/strings.xml b/packages/SystemUI/res/values-sq/strings.xml index ddea921e900d..7f74487ac2b0 100644 --- a/packages/SystemUI/res/values-sq/strings.xml +++ b/packages/SystemUI/res/values-sq/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"Të regjistrohet ekrani?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Regjistro një aplikacion"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Regjistro të gjithë ekranin"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"Kur regjistron të gjithë ekranin, regjistrohet çdo gjë e shfaqur në ekranin tënd. Prandaj, ki kujdes me gjërat si fjalëkalimet, detajet e pagesave, mesazhet, fotografitë, si dhe audion dhe videon."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"Kur regjistron një aplikacion, regjistrohet çdo gjë që shfaqet ose luhet në atë aplikacion. Prandaj, ki kujdes me gjërat si fjalëkalimet, detajet e pagesave, mesazhet, fotografitë, si dhe audion dhe videon."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Regjistro ekranin"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"Aplikacioni aktual"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Qasshmëria"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Shkurtoret e tastierës"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Kërko për shkurtoret"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"Asnjë rezultat kërkimi"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Ikona e palosjes"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Ikona e zgjerimit"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"ose"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Doreza e zvarritjes"</string> diff --git a/packages/SystemUI/res/values-sr/strings.xml b/packages/SystemUI/res/values-sr/strings.xml index 5936cd780e95..a1952d428f10 100644 --- a/packages/SystemUI/res/values-sr/strings.xml +++ b/packages/SystemUI/res/values-sr/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"Желите да снимите екран?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Сними једну апликацију"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Сними цео екран"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"Када снимате цео екран, снима се све што је на њему. Зато пазите на лозинке, информације о плаћању, поруке, слике, аудио и видео садржај."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"Када снимате апликацију, снима се сав садржај који се приказује или пушта у њој. Зато пазите на лозинке, информације о плаћању, поруке, слике, аудио и видео садржај."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Сними екран"</string> @@ -581,10 +583,8 @@ <string name="clear_all_notifications_text" msgid="348312370303046130">"Обриши све"</string> <string name="manage_notifications_text" msgid="6885645344647733116">"Управљај"</string> <string name="manage_notifications_history_text" msgid="57055985396576230">"Историја"</string> - <!-- no translation found for notification_settings_button_description (2441994740884163889) --> - <skip /> - <!-- no translation found for notification_history_button_description (1578657591405033383) --> - <skip /> + <string name="notification_settings_button_description" msgid="2441994740884163889">"Подешавања обавештења"</string> + <string name="notification_history_button_description" msgid="1578657591405033383">"Историја обавештења"</string> <string name="notification_section_header_incoming" msgid="850925217908095197">"Ново"</string> <string name="notification_section_header_gentle" msgid="6804099527336337197">"Нечујно"</string> <string name="notification_section_header_alerting" msgid="5581175033680477651">"Обавештења"</string> @@ -1413,9 +1413,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"Актуелна апликација"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Приступачност"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Тастерске пречице"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Пречице претраге"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"Нема резултата претраге"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Икона за скупљање"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Икона за проширивање"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"или"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Маркер за превлачење"</string> diff --git a/packages/SystemUI/res/values-sv/strings.xml b/packages/SystemUI/res/values-sv/strings.xml index 45e150a4574b..b898eafc7f03 100644 --- a/packages/SystemUI/res/values-sv/strings.xml +++ b/packages/SystemUI/res/values-sv/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"Vill du spela in det som visas på skärmen?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Spela in en app"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Spela in hela skärmen"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"När du spelar in hela skärmen spelas allt som visas på skärmen in. Var försiktig med sådant som lösenord, betalningsuppgifter, meddelanden, foton, ljud och video."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"När du spelar in en app spelas allt som visas eller spelas upp i appen in. Var försiktig med sådant som lösenord, betalningsuppgifter, meddelanden, foton, ljud och video."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Spela in skärmen"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"Aktuell app"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Tillgänglighet"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Kortkommandon"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Sökgenvägar"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"Inga sökresultat"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Ikonen Komprimera"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Ikonen Utöka"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"eller"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Handtag"</string> diff --git a/packages/SystemUI/res/values-sw/strings.xml b/packages/SystemUI/res/values-sw/strings.xml index 86ea7e492007..8731337c88f0 100644 --- a/packages/SystemUI/res/values-sw/strings.xml +++ b/packages/SystemUI/res/values-sw/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"Ungependa kurekodi skrini yako?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Rekodi programu moja"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Rekodi skrini nzima"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"Unaporekodi skrini yako nzima, chochote kinachoonyeshwa kwenye skrini yako kitarekodiwa. Kwa hivyo kuwa mwangalifu na vitu kama vile manenosiri, maelezo ya malipo, ujumbe, picha, sauti na video."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"Unaporekodi programu, chochote kinachoonyeshwa au kuchezwa kwenye programu hiyo kitarekodiwa. Kwa hivyo kuwa mwangalifu na vitu kama vile manenosiri, maelezo ya malipo, ujumbe, picha, sauti na video."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Rekodi skrini"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"Programu Inayotumika Sasa"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Ufikivu"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Mikato ya kibodi"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Njia mkato za kutafutia"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"Hamna matokeo ya utafutaji"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Kunja aikoni"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Panua aikoni"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"au"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Aikoni ya buruta"</string> diff --git a/packages/SystemUI/res/values-sw600dp/dimens.xml b/packages/SystemUI/res/values-sw600dp/dimens.xml index 4007bb1fcb98..393631e3364b 100644 --- a/packages/SystemUI/res/values-sw600dp/dimens.xml +++ b/packages/SystemUI/res/values-sw600dp/dimens.xml @@ -76,6 +76,14 @@ <dimen name="large_dialog_width">472dp</dimen> <dimen name="large_screen_shade_header_height">42dp</dimen> + + <!-- + The horizontal distance between the shade overlay panel (both notifications and quick settings) + and the edge of the screen. On Compact screens in portrait orientation (< w600dp) this is + ignored in the shade layout, which takes up the full screen width without margins. + --> + <dimen name="shade_panel_margin_horizontal">24dp</dimen> + <!-- start padding is smaller to account for status icon margins coming from drawable itself --> <dimen name="hover_system_icons_container_padding_start">3dp</dimen> <dimen name="hover_system_icons_container_padding_end">4dp</dimen> diff --git a/packages/SystemUI/res/values-sw720dp-h1000dp/dimens.xml b/packages/SystemUI/res/values-sw720dp-h1000dp/dimens.xml index ca62d286f4ee..f38f42df2b8a 100644 --- a/packages/SystemUI/res/values-sw720dp-h1000dp/dimens.xml +++ b/packages/SystemUI/res/values-sw720dp-h1000dp/dimens.xml @@ -23,4 +23,7 @@ <dimen name="keyguard_clock_top_margin">80dp</dimen> <dimen name="bouncer_user_switcher_view_mode_user_switcher_bottom_margin">155dp</dimen> <dimen name="bouncer_user_switcher_view_mode_view_flipper_bottom_margin">85dp</dimen> + + <!-- The width of the shade overlay panel (both notifications and quick settings). --> + <dimen name="shade_panel_width">474dp</dimen> </resources> diff --git a/packages/SystemUI/res/values-sw800dp/dimens.xml b/packages/SystemUI/res/values-sw800dp/dimens.xml index 0d82217456e4..8ae3a2ee3cc5 100644 --- a/packages/SystemUI/res/values-sw800dp/dimens.xml +++ b/packages/SystemUI/res/values-sw800dp/dimens.xml @@ -21,4 +21,7 @@ <!-- Biometric Auth pattern view size, better to align keyguard_security_width --> <dimen name="biometric_auth_pattern_view_size">348dp</dimen> + + <!-- The width of the shade overlay panel (both notifications and quick settings). --> + <dimen name="shade_panel_width">392dp</dimen> </resources> diff --git a/packages/SystemUI/res/values-ta/strings.xml b/packages/SystemUI/res/values-ta/strings.xml index 93b18646a831..b75a2186ed64 100644 --- a/packages/SystemUI/res/values-ta/strings.xml +++ b/packages/SystemUI/res/values-ta/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"உங்கள் திரையை ரெக்கார்டு செய்யவா?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"ஓர் ஆப்ஸை ரெக்கார்டு செய்தல்"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"முழுத் திரையை ரெக்கார்டு செய்தல்"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"முழுத் திரையை நீங்கள் ரெக்கார்டு செய்யும்போது அதில் காட்டப்படும் அனைத்தும் ரெக்கார்டு செய்யப்படும். எனவே கடவுச்சொற்கள், பேமெண்ட் விவரங்கள், மெசேஜ்கள், படங்கள், ஆடியோ, வீடியோ போன்றவை குறித்துக் கவனத்துடன் இருங்கள்."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"ஓர் ஆப்ஸை ரெக்கார்டு செய்யும்போது அதில் காட்டப்படும் அல்லது பிளே செய்யப்படும் அனைத்தும் ரெக்கார்டு செய்யப்படும். எனவே கடவுச்சொற்கள், பேமெண்ட் விவரங்கள், மெசேஜ்கள், படங்கள், ஆடியோ, வீடியோ போன்றவை குறித்துக் கவனத்துடன் இருங்கள்."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"திரையை ரெக்கார்டு செய்"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"தற்போதைய ஆப்ஸ்"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"மாற்றுத்திறன் வசதி"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"கீபோர்டு ஷார்ட்கட்கள்"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"தேடல் ஷார்ட்கட்கள்"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"தேடல் முடிவுகள் இல்லை"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"சுருக்குவதற்கான ஐகான்"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"விரிவாக்குவதற்கான ஐகான்"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"அல்லது"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"இழுப்பதற்கான ஹேண்டில்"</string> diff --git a/packages/SystemUI/res/values-te/strings.xml b/packages/SystemUI/res/values-te/strings.xml index 4a976513a8da..d66c6560fd7c 100644 --- a/packages/SystemUI/res/values-te/strings.xml +++ b/packages/SystemUI/res/values-te/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"మీ స్క్రీన్ను రికార్డ్ చేయాలా?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"ఒక యాప్ను రికార్డ్ చేయండి"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"ఫుల్ స్క్రీన్ను రికార్డ్ చేయండి"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"మీ ఫుల్ స్క్రీన్ను మీరు రికార్డ్ చేసేటప్పుడు, మీ స్క్రీన్పై కనిపించేవన్నీ రికార్డ్ అవుతాయి. కాబట్టి పాస్వర్డ్లు, పేమెంట్ వివరాలు, మెసేజ్లు, ఫోటోలు, ఆడియో, ఇంకా వీడియో వంటి విషయాల్లో జాగ్రత్త వహించండి."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"మీరు యాప్ను రికార్డ్ చేసేటప్పుడు, సంబంధిత యాప్లో కనిపించేవన్నీ లేదా ప్లే అయ్యేవన్నీ రికార్డ్ అవుతాయి. కాబట్టి పాస్వర్డ్లు, పేమెంట్ వివరాలు, మెసేజ్లు, ఫోటోలు, ఆడియో, ఇంకా వీడియో వంటి విషయాల్లో జాగ్రత్త వహించండి."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"స్క్రీన్ను రికార్డ్ చేయండి"</string> @@ -581,10 +583,8 @@ <string name="clear_all_notifications_text" msgid="348312370303046130">"అన్నీ క్లియర్ చేయండి"</string> <string name="manage_notifications_text" msgid="6885645344647733116">"మేనేజ్ చేయండి"</string> <string name="manage_notifications_history_text" msgid="57055985396576230">"హిస్టరీ"</string> - <!-- no translation found for notification_settings_button_description (2441994740884163889) --> - <skip /> - <!-- no translation found for notification_history_button_description (1578657591405033383) --> - <skip /> + <string name="notification_settings_button_description" msgid="2441994740884163889">"నోటిఫికేషన్ సెట్టింగ్లు"</string> + <string name="notification_history_button_description" msgid="1578657591405033383">"నోటిఫికేషన్ హిస్టరీ"</string> <string name="notification_section_header_incoming" msgid="850925217908095197">"కొత్తవి"</string> <string name="notification_section_header_gentle" msgid="6804099527336337197">"నిశ్శబ్దం"</string> <string name="notification_section_header_alerting" msgid="5581175033680477651">"నోటిఫికేషన్లు"</string> @@ -1413,9 +1413,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"ప్రస్తుత యాప్"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"యాక్సెసిబిలిటీ"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"కీబోర్డ్ షార్ట్కట్లు"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"సెర్చ్ షార్ట్కట్లు"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"సెర్చ్ ఫలితాలు ఏవీ లేవు"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"కుదించండి చిహ్నం"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"విస్తరించండి చిహ్నం"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"లేదా"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"లాగే హ్యాండిల్"</string> diff --git a/packages/SystemUI/res/values-th/strings.xml b/packages/SystemUI/res/values-th/strings.xml index ac027c753d12..b7d1a32fdeba 100644 --- a/packages/SystemUI/res/values-th/strings.xml +++ b/packages/SystemUI/res/values-th/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"บันทึกหน้าจอไหม"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"บันทึกแอปเดียว"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"บันทึกทั้งหน้าจอ"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"ขณะบันทึกทั้งหน้าจอ ระบบจะบันทึกทุกสิ่งที่แสดงอยู่บนหน้าจอ ดังนั้นโปรดระวังสิ่งต่างๆ อย่างเช่นรหัสผ่าน รายละเอียดการชำระเงิน ข้อความ รูปภาพ รวมถึงเสียงและวิดีโอ"</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"ขณะบันทึกแอป ระบบจะบันทึกทุกสิ่งที่แสดงหรือเล่นอยู่ในแอปดังกล่าว ดังนั้นโปรดระวังสิ่งต่างๆ อย่างเช่นรหัสผ่าน รายละเอียดการชำระเงิน ข้อความ รูปภาพ รวมถึงเสียงและวิดีโอ"</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"บันทึกหน้าจอ"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"แอปปัจจุบัน"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"การช่วยเหลือพิเศษ"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"แป้นพิมพ์ลัด"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"ค้นหาแป้นพิมพ์ลัด"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"ไม่พบผลการค้นหา"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"ไอคอนยุบ"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"ไอคอนขยาย"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"หรือ"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"แฮนเดิลการลาก"</string> diff --git a/packages/SystemUI/res/values-tl/strings.xml b/packages/SystemUI/res/values-tl/strings.xml index 09a0e0400310..a30a2f2f45f6 100644 --- a/packages/SystemUI/res/values-tl/strings.xml +++ b/packages/SystemUI/res/values-tl/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"I-record ang iyong screen?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Mag-record ng isang app"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"I-record ang buong screen"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"Kapag nire-record mo ang iyong buong screen, nire-record ang anumang ipinapakita sa screen mo. Kaya mag-ingat sa mga bagay-bagay tulad ng mga password, detalye ng pagbabayad, mensahe, larawan, at audio at video."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"Kapag nagre-record ka ng app, nire-record ang anumang ipinapakita o pine-play sa app na iyon. Kaya mag-ingat sa mga bagay-bagay tulad ng mga password, detalye ng pagbabayad, mensahe, larawan, at audio at video."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"I-record ang screen"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"Kasalukuyang App"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Accessibility"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Mga keyboard shortcut"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Mga shortcut ng paghahanap"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"Walang resulta ng paghahanap"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"I-collapse ang icon"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"I-expand ang icon"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"o"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Handle sa pag-drag"</string> diff --git a/packages/SystemUI/res/values-tr/strings.xml b/packages/SystemUI/res/values-tr/strings.xml index ae9cd186520e..c79dfcf8f162 100644 --- a/packages/SystemUI/res/values-tr/strings.xml +++ b/packages/SystemUI/res/values-tr/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"Ekranınız kaydedilsin mi?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Bir uygulamayı kaydet"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Tüm ekranı kaydedin"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"Tüm ekranınızı kaydettiğinizde ekranınızda gösterilen her şey kaydedilir. Bu nedenle şifre, ödeme ayrıntıları, mesaj, fotoğraf, ses ve video gibi öğeler konusunda dikkatli olun."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"Bir uygulamayı kaydettiğinizde o uygulamada gösterilen veya oynatılan her şey kaydedilir. Bu nedenle şifre, ödeme ayrıntıları, mesaj, fotoğraf, ses ve video gibi öğeler konusunda dikkatli olun."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Ekranı kaydet"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"Mevcut Uygulama"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Erişilebilirlik"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Klavye kısayolları"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Arama kısayolları"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"Arama sonucu yok"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Daralt simgesi"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Genişlet simgesi"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"veya"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Sürükleme tutamacı"</string> diff --git a/packages/SystemUI/res/values-uk/strings.xml b/packages/SystemUI/res/values-uk/strings.xml index 820d072f152e..2808922e166c 100644 --- a/packages/SystemUI/res/values-uk/strings.xml +++ b/packages/SystemUI/res/values-uk/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"Записати відео з екрана?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Записувати один додаток"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Записувати весь екран"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"Коли ви записуєте вміст усього екрана, на відео потрапляє все, що на ньому відображається. Тому будьте уважні з паролями, повідомленнями, фотографіями, аудіо, відео, платіжною інформацією тощо."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"Коли ви записуєте додаток, на відео потрапляє все, що відображається або відтворюється в ньому. Тому будьте уважні з паролями, повідомленнями, фотографіями, аудіо, відео, платіжною інформацією тощо."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Записувати вміст екрана"</string> @@ -1413,15 +1415,21 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"Поточний додаток"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Доступність"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Комбінації клавіш"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Комбінації клавіш для пошуку"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"Нічого не знайдено"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Значок згортання"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Значок розгортання"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"або"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Маркер переміщення"</string> <string name="shortcut_helper_keyboard_settings_buttons_label" msgid="6720967595915985259">"Налаштування клавіатури"</string> <string name="launch_keyboard_tutorial_notification_title" msgid="8849933155160522519">"Навігація за допомогою клавіатури"</string> - <string name="launch_keyboard_tutorial_notification_content" msgid="2880339951512757918">"Комбінації клавіш: докладніше"</string> + <string name="launch_keyboard_tutorial_notification_content" msgid="2880339951512757918">"Дізнайтеся більше про комбінації клавіш"</string> <string name="launch_touchpad_tutorial_notification_title" msgid="2243780062772196901">"Навігація за допомогою сенсорної панелі"</string> <string name="launch_touchpad_tutorial_notification_content" msgid="7931085031240753226">"Жести для сенсорної панелі: докладніше"</string> <string name="launch_keyboard_touchpad_tutorial_notification_title" msgid="1940023776496198762">"Навігація за допомогою клавіатури й сенсорної панелі"</string> diff --git a/packages/SystemUI/res/values-ur/strings.xml b/packages/SystemUI/res/values-ur/strings.xml index b98abbda3f7b..b72464cad145 100644 --- a/packages/SystemUI/res/values-ur/strings.xml +++ b/packages/SystemUI/res/values-ur/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"آپ کی اسکرین ریکارڈ کریں؟"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"ایک ایپ ریکارڈ کریں"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"پوری اسکرین کو ریکارڈ کریں"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"جب آپ اپنی پوری اسکرین کو ریکارڈ کر رہے ہوتے ہیں تو آپ کی اسکرین پر دکھائی گئی ہر چیز ریکارڈ کی جاتی ہے۔ لہذا، پاس ورڈز، ادائیگی کی تفصیلات، پیغامات، تصاویر، ساتھ ہی آڈیو اور ویڈیو جیسی چیزوں کے سلسلے میں محتاط رہیں۔"</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"جب آپ کسی ایپ کو ریکارڈ کر رہے ہوتے ہیں تو اس ایپ میں دکھائی گئی یا چلائی گئی ہر چیز ریکارڈ کی جاتی ہے۔ لہذا، پاس ورڈز، ادائیگی کی تفصیلات، پیغامات، تصاویر، ساتھ ہی آڈیو اور ویڈیو جیسی چیزوں کے سلسلے میں محتاط رہیں۔"</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"اسکرین ریکارڈ کریں"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"موجودہ ایپ"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"ایکسیسبیلٹی"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"کی بورڈ شارٹ کٹس"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"تلاش کے شارٹ کٹس"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"تلاش کا کوئی نتیجہ نہیں ہے"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"آئیکن سکیڑیں"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"آئیکن پھیلائیں"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"یا"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"گھسیٹنے کا ہینڈل"</string> diff --git a/packages/SystemUI/res/values-uz/strings.xml b/packages/SystemUI/res/values-uz/strings.xml index 3309772d5fc8..7f6275c5a1d2 100644 --- a/packages/SystemUI/res/values-uz/strings.xml +++ b/packages/SystemUI/res/values-uz/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"Ekran yozib olinsinmi?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Bitta ilovani yozib olish"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Butun ekranni yozib olish"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"Butun ekranni yozib olishda ekranda koʻrsatilgan barcha axborotlar yozib olinadi. Shu sababli parollar, toʻlov tafsilotlari, xabarlar, suratlar, audio va video chiqmasligi uchun ehtiyot boʻling."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"Ilovani yozib olishda ilova koʻrsatilgan yoki ijro etilgan barcha axborotlar yozib olinadi. Shu sababli parollar, toʻlov tafsilotlari, xabarlar, suratlar, audio va video chiqmasligi uchun ehtiyot boʻling."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Ekranni yozib olish"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"Joriy ilova"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Qulayliklar"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Tezkor tugmalar"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Tezkor tugmalar qidiruvi"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"Hech narsa topilmadi"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Yigʻish belgisi"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Yoyish belgisi"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"yoki"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Surish dastagi"</string> diff --git a/packages/SystemUI/res/values-vi/strings.xml b/packages/SystemUI/res/values-vi/strings.xml index f404a2bf455b..388ebb804828 100644 --- a/packages/SystemUI/res/values-vi/strings.xml +++ b/packages/SystemUI/res/values-vi/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"Ghi màn hình?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Ghi một ứng dụng"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Ghi toàn màn hình"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"Khi bạn ghi toàn màn hình, mọi nội dung trên màn hình của bạn đều được ghi. Vì vậy, hãy thận trọng để không làm lộ thông tin như mật khẩu, thông tin thanh toán, tin nhắn, ảnh, âm thanh và video."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"Khi bạn ghi một ứng dụng, mọi nội dung xuất hiện hoặc phát trong ứng dụng đó sẽ đều được ghi. Vì vậy, hãy thận trọng để không làm lộ thông tin như mật khẩu, thông tin thanh toán, tin nhắn, ảnh, âm thanh và video."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Ghi màn hình"</string> @@ -581,10 +583,8 @@ <string name="clear_all_notifications_text" msgid="348312370303046130">"Xóa tất cả"</string> <string name="manage_notifications_text" msgid="6885645344647733116">"Quản lý"</string> <string name="manage_notifications_history_text" msgid="57055985396576230">"Lịch sử"</string> - <!-- no translation found for notification_settings_button_description (2441994740884163889) --> - <skip /> - <!-- no translation found for notification_history_button_description (1578657591405033383) --> - <skip /> + <string name="notification_settings_button_description" msgid="2441994740884163889">"Cài đặt thông báo"</string> + <string name="notification_history_button_description" msgid="1578657591405033383">"Nhật ký thông báo"</string> <string name="notification_section_header_incoming" msgid="850925217908095197">"Mới"</string> <string name="notification_section_header_gentle" msgid="6804099527336337197">"Im lặng"</string> <string name="notification_section_header_alerting" msgid="5581175033680477651">"Thông báo"</string> @@ -1413,9 +1413,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"Ứng dụng hiện tại"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Hỗ trợ tiếp cận"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Phím tắt"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Lối tắt tìm kiếm"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"Không có kết quả tìm kiếm nào"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Biểu tượng Thu gọn"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Biểu tượng Mở rộng"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"hoặc"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Nút kéo"</string> diff --git a/packages/SystemUI/res/values-w1000dp/dimens.xml b/packages/SystemUI/res/values-w1000dp/dimens.xml new file mode 100644 index 000000000000..b3f7acd37daa --- /dev/null +++ b/packages/SystemUI/res/values-w1000dp/dimens.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<resources> + <!-- The width of the shade overlay panel (both notifications and quick settings). --> + <dimen name="shade_panel_width">474dp</dimen> +</resources> diff --git a/packages/SystemUI/res/values-zh-rCN/strings.xml b/packages/SystemUI/res/values-zh-rCN/strings.xml index 24c9f40f05a7..b463900a331d 100644 --- a/packages/SystemUI/res/values-zh-rCN/strings.xml +++ b/packages/SystemUI/res/values-zh-rCN/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"要录制屏幕吗?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"录制单个应用"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"录制整个屏幕"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"录制整个屏幕时,屏幕上显示的所有内容均会被录制。因此,请务必小心操作,谨防泄露密码、付款信息、消息、照片、音频、视频等。"</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"录制单个应用时,该应用中显示或播放的所有内容均会被录制。因此,请务必小心操作,谨防泄露密码、付款信息、消息、照片、音频、视频等。"</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"录制屏幕"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"当前应用"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"无障碍功能"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"键盘快捷键"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"搜索快捷键"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"无搜索结果"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"收起图标"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"展开图标"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"或"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"拖动手柄"</string> diff --git a/packages/SystemUI/res/values-zh-rHK/strings.xml b/packages/SystemUI/res/values-zh-rHK/strings.xml index e7ebe5ccf697..91185912af9d 100644 --- a/packages/SystemUI/res/values-zh-rHK/strings.xml +++ b/packages/SystemUI/res/values-zh-rHK/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"要錄影螢幕畫面嗎?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"錄影一個應用程式"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"錄影整個螢幕畫面"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"當你錄影整個螢幕畫面時,系統會錄影螢幕畫面上顯示的任何內容。因此,請謹慎處理密碼、付款資料、訊息、相片、音訊和影片等。"</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"當你錄影應用程式時,系統會錄影該應用程式中顯示或播放的任何內容。因此,請謹慎處理密碼、付款資料、訊息、相片、音訊和影片等。"</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"錄影螢幕畫面"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"目前的應用程式"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"無障礙功能"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"鍵盤快速鍵"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"搜尋快速鍵"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"沒有相符的搜尋結果"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"收合圖示"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"展開圖示"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"或"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"拖曳控點"</string> diff --git a/packages/SystemUI/res/values-zh-rTW/strings.xml b/packages/SystemUI/res/values-zh-rTW/strings.xml index 0f298dc314c6..2189cbfdf278 100644 --- a/packages/SystemUI/res/values-zh-rTW/strings.xml +++ b/packages/SystemUI/res/values-zh-rTW/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"要錄製畫面嗎?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"錄製單一應用程式"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"錄製整個畫面"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"錄製整個畫面時,系統會錄下畫面上的所有內容。因此,請謹慎處理密碼、付款資料、訊息、相片和影音內容等資訊。"</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"當你錄製應用程式畫面時,系統會錄下該應用程式顯示或播放的所有內容。因此,請謹慎處理密碼、付款資料、訊息、相片和影音內容等資訊。"</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"錄製畫面"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"目前的應用程式"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"無障礙"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"鍵盤快速鍵"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"搜尋快速鍵"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"找不到相符的搜尋結果"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"收合圖示"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"展開圖示"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"或"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"拖曳控點"</string> diff --git a/packages/SystemUI/res/values-zu/strings.xml b/packages/SystemUI/res/values-zu/strings.xml index 444ca041f31b..05b5040faf85 100644 --- a/packages/SystemUI/res/values-zu/strings.xml +++ b/packages/SystemUI/res/values-zu/strings.xml @@ -112,6 +112,8 @@ <string name="screenrecord_permission_dialog_title" msgid="7415261783188749730">"Rekhoda isikrini sakho?"</string> <string name="screenrecord_permission_dialog_option_text_single_app" msgid="1996450687814647583">"Rekhoda i-app eyodwa"</string> <string name="screenrecord_permission_dialog_option_text_entire_screen" msgid="2794896384693120020">"Rekhoda sonke isikrini"</string> + <!-- no translation found for screenrecord_permission_dialog_option_text_entire_screen_for_display (3754611651558838691) --> + <skip /> <string name="screenrecord_permission_dialog_warning_entire_screen" msgid="1321758636709366068">"Uma urekhoda sonke isikrini sakho, noma yini evela esikrinini iyarekhodwa. Ngakho-ke qaphela ngezinto ezifana namaphasiwedi, imininingwane yenkokhelo, imilayezo, izithombe, nomsindo nevidiyo."</string> <string name="screenrecord_permission_dialog_warning_single_app" msgid="3738199712880063924">"Uma urekhoda i-app, noma yini evezwa noma edlala kuleyo app iyarekhodwa. Ngakho-ke qaphela ngezinto ezifana namaphasiwedi, imininingwane yenkokhelo, imilayezo, izithombe, nomsindo nevidiyo."</string> <string name="screenrecord_permission_dialog_continue_entire_screen" msgid="5557974446773486600">"Rekhoda isikrini"</string> @@ -1413,9 +1415,15 @@ <string name="shortcut_helper_category_current_app_shortcuts" msgid="4017840565974573628">"I-App yamanje"</string> <string name="shortcut_helper_category_a11y" msgid="6314444792641773464">"Ukufinyeleleka"</string> <string name="shortcut_helper_title" msgid="8567500639300970049">"Izinqamuleli zekhibhodi"</string> + <!-- no translation found for shortcut_helper_customize_mode_title (1467657117101096033) --> + <skip /> <string name="shortcut_helper_search_placeholder" msgid="5488547526269871819">"Sesha izinqamuleli"</string> <string name="shortcut_helper_no_search_results" msgid="8554756497996692160">"Ayikho imiphumela yosesho"</string> <string name="shortcut_helper_content_description_collapse_icon" msgid="8028015738431664954">"Goqa isithonjana"</string> + <!-- no translation found for shortcut_helper_customize_button_text (3124983502748069338) --> + <skip /> + <!-- no translation found for shortcut_helper_done_button_text (7249905942125386191) --> + <skip /> <string name="shortcut_helper_content_description_expand_icon" msgid="1084435697860417390">"Nweba isithonjana"</string> <string name="shortcut_helper_key_combinations_or_separator" msgid="7082902112102125540">"noma"</string> <string name="shortcut_helper_content_description_drag_handle" msgid="5092426406009848110">"Hudula isibambi"</string> diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index f96a0b95945d..d4a52c3aeafb 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -338,6 +338,9 @@ <!-- Whether to show the full screen user switcher. --> <bool name="config_enableFullscreenUserSwitcher">false</bool> + <!-- Whether to go to the launcher when unlocking via an occluding app --> + <bool name="config_goToHomeFromOccludedApps">false</bool> + <!-- Determines whether the shell features all run on another thread. --> <bool name="config_enableShellMainThread">true</bool> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index c2d942ff5851..7fa287944956 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -577,6 +577,19 @@ <dimen name="notification_panel_margin_horizontal">0dp</dimen> + <!-- + The width of the shade overlay panel (both notifications and quick settings). On Compact screens + in portrait orientation (< w600dp) this is ignored, and the shade layout takes up the full + screen width. + --> + <dimen name="shade_panel_width">412dp</dimen> + + <!-- + The horizontal distance between the shade overlay panel (both notifications and quick settings) + and the edge of the screen. This is zero only on Compact screens (< sw600dp). + --> + <dimen name="shade_panel_margin_horizontal">0dp</dimen> + <dimen name="brightness_mirror_height">48dp</dimen> <dimen name="volume_dialog_panel_transparent_padding_right">8dp</dimen> @@ -2051,7 +2064,7 @@ <!-- Volume start --> <dimen name="volume_dialog_background_corner_radius">30dp</dimen> <dimen name="volume_dialog_width">60dp</dimen> - <dimen name="volume_dialog_vertical_padding">6dp</dimen> + <dimen name="volume_dialog_vertical_padding">10dp</dimen> <dimen name="volume_dialog_components_spacing">8dp</dimen> <dimen name="volume_dialog_floating_sliders_spacing">8dp</dimen> <dimen name="volume_dialog_floating_sliders_vertical_padding">10dp</dimen> @@ -2065,10 +2078,10 @@ <dimen name="volume_panel_slice_vertical_padding">8dp</dimen> <dimen name="volume_panel_slice_horizontal_padding">24dp</dimen> - <dimen name="volume_dialog_ringer_container_padding">10dp</dimen> - <dimen name="volume_ringer_item_size">40dp</dimen> - <dimen name="volume_ringer_icon_size">20dp</dimen> - <dimen name="volume_ringer_item_radius">12dp</dimen> - <dimen name="volume_dialog_ringer_item_margin_bottom">8dp</dimen> + <dimen name="volume_dialog_ringer_horizontal_padding">10dp</dimen> + <dimen name="volume_dialog_ringer_drawer_button_size">40dp</dimen> + <dimen name="volume_dialog_ringer_drawer_button_icon_radius">10dp</dimen> + <dimen name="volume_dialog_background_square_corner_radius">12dp</dimen> + <dimen name="volume_dialog_ringer_selected_button_background_radius">20dp</dimen> <!-- Volume end --> </resources> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 96021631415f..1766cdf8c804 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -1797,6 +1797,7 @@ <string name="volume_panel_spatial_audio_tracking">Head Tracking</string> <string name="volume_ringer_change">Tap to change ringer mode</string> + <string name="volume_ringer_mode">ringer mode</string> <!-- Hint for accessibility. For example: double tap to mute [CHAR_LIMIT=NONE] --> <string name="volume_ringer_hint_mute">mute</string> @@ -3930,10 +3931,10 @@ </string> <!-- Title for the Reset Tiles dialog in QS Edit mode. [CHAR LIMIT=NONE] --> <string name="qs_edit_mode_reset_dialog_title"> - Reset tiles + Reset all tiles? </string> <!-- Content of the Reset Tiles dialog in QS Edit mode. [CHAR LIMIT=NONE] --> <string name="qs_edit_mode_reset_dialog_content"> - Reset tiles to their original order and sizes? + All Quick Settings tiles will reset to the device’s original settings </string> </resources> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index ab45b9f1b5bc..7d071cd4a04c 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -557,6 +557,20 @@ <item name="android:showWhenLocked">true</item> </style> + <style name="SystemUI.Material3.Slider.Volume"> + <item name="trackHeight">40dp</item> + <item name="thumbHeight">52dp</item> + </style> + + <style name="SystemUI.Material3.Slider" parent="@style/Widget.Material3.Slider"> + <item name="labelStyle">@style/Widget.Material3.Slider.Label</item> + <item name="thumbColor">@color/slider_thumb_color</item> + <item name="tickColorActive">@color/slider_inactive_track_color</item> + <item name="tickColorInactive">@color/slider_active_track_color</item> + <item name="trackColorActive">@color/slider_active_track_color</item> + <item name="trackColorInactive">@color/slider_inactive_track_color</item> + </style> + <style name="Theme.SystemUI.DayNightDialog" parent="@android:style/Theme.DeviceDefault.Light.Dialog"/> <style name="Theme.SystemUI.Dialog" parent="@style/Theme.SystemUI.DayNightDialog"> diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java index 9b5d5b6eadca..46e45aaf8a8a 100644 --- a/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java +++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorations.java @@ -92,7 +92,6 @@ import com.android.systemui.statusbar.commandline.CommandRegistry; import com.android.systemui.statusbar.events.PrivacyDotViewController; import com.android.systemui.statusbar.policy.ConfigurationController; import com.android.systemui.util.concurrency.DelayableExecutor; -import com.android.systemui.util.concurrency.ThreadFactory; import com.android.systemui.util.kotlin.JavaAdapter; import com.android.systemui.util.settings.SecureSettings; @@ -147,7 +146,6 @@ public class ScreenDecorations implements private CameraAvailabilityListener mCameraListener; private final UserTracker mUserTracker; private final PrivacyDotViewController mDotViewController; - private final ThreadFactory mThreadFactory; private final DecorProviderFactory mDotFactory; private final FaceScanningProviderFactory mFaceScanningFactory; private final CameraProtectionLoader mCameraProtectionLoader; @@ -172,7 +170,6 @@ public class ScreenDecorations implements private ViewCaptureAwareWindowManager mWindowManager; private int mRotation; private UserSettingObserver mColorInversionSetting; - @Nullable private DelayableExecutor mExecutor; private Handler mHandler; boolean mPendingConfigChange; @@ -327,27 +324,28 @@ public class ScreenDecorations implements } @Inject - public ScreenDecorations(Context context, + public ScreenDecorations( + Context context, SecureSettings secureSettings, CommandRegistry commandRegistry, UserTracker userTracker, DisplayTracker displayTracker, PrivacyDotViewController dotViewController, - ThreadFactory threadFactory, PrivacyDotDecorProviderFactory dotFactory, FaceScanningProviderFactory faceScanningFactory, ScreenDecorationsLogger logger, FacePropertyRepository facePropertyRepository, JavaAdapter javaAdapter, CameraProtectionLoader cameraProtectionLoader, - ViewCaptureAwareWindowManager viewCaptureAwareWindowManager) { + ViewCaptureAwareWindowManager viewCaptureAwareWindowManager, + @ScreenDecorationsThread Handler handler, + @ScreenDecorationsThread DelayableExecutor executor) { mContext = context; mSecureSettings = secureSettings; mCommandRegistry = commandRegistry; mUserTracker = userTracker; mDisplayTracker = displayTracker; mDotViewController = dotViewController; - mThreadFactory = threadFactory; mDotFactory = dotFactory; mFaceScanningFactory = faceScanningFactory; mCameraProtectionLoader = cameraProtectionLoader; @@ -356,6 +354,8 @@ public class ScreenDecorations implements mFacePropertyRepository = facePropertyRepository; mJavaAdapter = javaAdapter; mWindowManager = viewCaptureAwareWindowManager; + mHandler = handler; + mExecutor = executor; } private final ScreenDecorCommand.Callback mScreenDecorCommandCallback = (cmd, pw) -> { @@ -403,10 +403,7 @@ public class ScreenDecorations implements Log.i(TAG, "ScreenDecorations is disabled"); return; } - mHandler = mThreadFactory.buildHandlerOnNewThread("ScreenDecorations"); - mExecutor = mThreadFactory.buildDelayableExecutorOnHandler(mHandler); mExecutor.execute(this::startOnScreenDecorationsThread); - mDotViewController.setUiExecutor(mExecutor); mCommandRegistry.registerCommand(ScreenDecorCommand.SCREEN_DECOR_CMD_NAME, () -> new ScreenDecorCommand(mScreenDecorCommandCallback)); } diff --git a/packages/SystemUI/src/com/android/systemui/ScreenDecorationsModule.kt b/packages/SystemUI/src/com/android/systemui/ScreenDecorationsModule.kt index 6fc50fb1f460..6786a71e7d4e 100644 --- a/packages/SystemUI/src/com/android/systemui/ScreenDecorationsModule.kt +++ b/packages/SystemUI/src/com/android/systemui/ScreenDecorationsModule.kt @@ -17,16 +17,23 @@ package com.android.systemui import android.content.Context +import android.os.Handler import com.android.systemui.dagger.SysUISingleton import com.android.systemui.decor.FaceScanningProviderFactory import com.android.systemui.decor.FaceScanningProviderFactoryImpl import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener +import com.android.systemui.util.concurrency.DelayableExecutor +import com.android.systemui.util.concurrency.ThreadFactory import dagger.Binds import dagger.Module import dagger.Provides import dagger.multibindings.ClassKey import dagger.multibindings.IntoMap import dagger.multibindings.IntoSet +import java.util.concurrent.Executor +import javax.inject.Qualifier + +@Qualifier annotation class ScreenDecorationsThread @Module interface ScreenDecorationsModule { @@ -41,6 +48,12 @@ interface ScreenDecorationsModule { @IntoSet fun bindScreenDecorationsConfigListener(impl: ScreenDecorations): ConfigurationListener + @Binds + @ScreenDecorationsThread + fun screenDecorationsExecutor( + @ScreenDecorationsThread delayableExecutor: DelayableExecutor + ): Executor + companion object { @Provides @SysUISingleton @@ -50,5 +63,22 @@ interface ScreenDecorationsModule { ): FaceScanningProviderFactory { return creator.create(context) } + + @Provides + @SysUISingleton + @ScreenDecorationsThread + fun screenDecorationsHandler(threadFactory: ThreadFactory): Handler { + return threadFactory.buildHandlerOnNewThread("ScreenDecorations") + } + + @Provides + @SysUISingleton + @ScreenDecorationsThread + fun screenDecorationsDelayableExecutor( + @ScreenDecorationsThread handler: Handler, + threadFactory: ThreadFactory, + ): DelayableExecutor { + return threadFactory.buildDelayableExecutorOnHandler(handler) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java index 69ab976c5301..12b5fc01845c 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java @@ -33,7 +33,6 @@ import android.graphics.PixelFormat; import android.hardware.biometrics.BiometricAuthenticator.Modality; import android.hardware.biometrics.BiometricConstants; import android.hardware.biometrics.BiometricManager.Authenticators; -import android.hardware.biometrics.Flags; import android.hardware.biometrics.PromptInfo; import android.hardware.face.FaceSensorPropertiesInternal; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; @@ -323,7 +322,7 @@ public class AuthContainerView extends LinearLayout final boolean isLandscape = mContext.getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE; mPromptSelectorInteractorProvider = promptSelectorInteractorProvider; - mPromptSelectorInteractorProvider.get().setPrompt(mConfig.mPromptInfo, mEffectiveUserId, + mPromptSelectorInteractorProvider.get().setPrompt(mConfig.mPromptInfo, mConfig.mUserId, getRequestId(), biometricModalities, mConfig.mOperationId, mConfig.mOpPackageName, false /*onSwitchToCredential*/, isLandscape); @@ -676,10 +675,8 @@ public class AuthContainerView extends LinearLayout final Runnable endActionRunnable = () -> { setVisibility(View.INVISIBLE); - if (Flags.customBiometricPrompt()) { // TODO(b/288175645): resetPrompt calls should be lifecycle aware mPromptSelectorInteractorProvider.get().resetPrompt(getRequestId()); - } removeWindowIfAttached(); }; diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt index 49973708487b..b07006887011 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/CredentialInteractor.kt @@ -3,6 +3,7 @@ package com.android.systemui.biometrics.domain.interactor import android.app.admin.DevicePolicyManager import android.app.admin.DevicePolicyResources import android.content.Context +import android.hardware.biometrics.Flags import android.os.UserManager import com.android.internal.widget.LockPatternUtils import com.android.internal.widget.LockscreenCredential @@ -71,13 +72,22 @@ constructor( // Request LockSettingsService to return the Gatekeeper Password in the // VerifyCredentialResponse so that we can request a Gatekeeper HAT with the // Gatekeeper Password and operationId. - val effectiveUserId = request.userInfo.deviceCredentialOwnerId + var effectiveUserId = request.userInfo.userIdForPasswordEntry val response = - lockPatternUtils.verifyCredential( - credential, - effectiveUserId, - LockPatternUtils.VERIFY_FLAG_REQUEST_GK_PW_HANDLE - ) + if (Flags.privateSpaceBp() && effectiveUserId != request.userInfo.userId) { + effectiveUserId = request.userInfo.userId + lockPatternUtils.verifyTiedProfileChallenge( + credential, + request.userInfo.userId, + LockPatternUtils.VERIFY_FLAG_REQUEST_GK_PW_HANDLE, + ) + } else { + lockPatternUtils.verifyCredential( + credential, + effectiveUserId, + LockPatternUtils.VERIFY_FLAG_REQUEST_GK_PW_HANDLE, + ) + } if (response.isMatched) { lockPatternUtils.userPresent(effectiveUserId) @@ -91,7 +101,7 @@ constructor( lockPatternUtils.verifyGatekeeperPasswordHandle( pwHandle, request.operationInfo.gatekeeperChallenge, - effectiveUserId + effectiveUserId, ) val hat = gkResponse.gatekeeperHAT lockPatternUtils.removeGatekeeperPasswordHandle(pwHandle) @@ -108,7 +118,7 @@ constructor( CredentialStatus.Fail.Throttled( applicationContext.getString( R.string.biometric_dialog_credential_too_many_attempts, - remaining / 1000 + remaining / 1000, ) ) ) @@ -129,10 +139,10 @@ constructor( applicationContext.getString( R.string.biometric_dialog_credential_attempts_before_wipe, numAttempts, - maxAttempts + maxAttempts, ), remainingAttempts, - fetchFinalAttemptMessageOrNull(request, remainingAttempts) + fetchFinalAttemptMessageOrNull(request, remainingAttempts), ) ) } @@ -150,9 +160,9 @@ constructor( devicePolicyManager, userManager.getUserTypeForWipe( devicePolicyManager, - request.userInfo.deviceCredentialOwnerId + request.userInfo.deviceCredentialOwnerId, ), - remainingAttempts + remainingAttempts, ) } else { null @@ -205,7 +215,7 @@ private fun Context.getLastAttemptBeforeWipeMessage( } private fun Context.getLastAttemptBeforeWipeDeviceMessage( - request: BiometricPromptRequest.Credential, + request: BiometricPromptRequest.Credential ): String { val id = when (request) { @@ -249,7 +259,7 @@ private fun Context.getLastAttemptBeforeWipeProfileMessage( } private fun Context.getLastAttemptBeforeWipeUserMessage( - request: BiometricPromptRequest.Credential, + request: BiometricPromptRequest.Credential ): String { val resId = when (request) { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt index 6da5e42c12d9..008fb26424e2 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt @@ -16,7 +16,6 @@ package com.android.systemui.biometrics.domain.interactor -import android.hardware.biometrics.Flags import android.hardware.biometrics.PromptInfo import com.android.internal.widget.LockPatternUtils import com.android.systemui.biometrics.Utils @@ -104,6 +103,7 @@ class PromptSelectorInteractorImpl constructor( fingerprintPropertyRepository: FingerprintPropertyRepository, private val displayStateInteractor: DisplayStateInteractor, + private val credentialInteractor: CredentialInteractor, private val promptRepository: PromptRepository, private val lockPatternUtils: LockPatternUtils, ) : PromptSelectorInteractor { @@ -177,7 +177,7 @@ constructor( override fun setPrompt( promptInfo: PromptInfo, - effectiveUserId: Int, + userId: Int, requestId: Long, modalities: BiometricModalities, challenge: Long, @@ -185,10 +185,10 @@ constructor( onSwitchToCredential: Boolean, isLandscape: Boolean, ) { + val effectiveUserId = credentialInteractor.getCredentialOwnerOrSelfId(userId) val hasCredentialViewShown = promptKind.value.isCredential() val showBpForCredential = - Flags.customBiometricPrompt() && - !Utils.isBiometricAllowed(promptInfo) && + !Utils.isBiometricAllowed(promptInfo) && isDeviceCredentialAllowed(promptInfo) && promptInfo.contentView != null && !promptInfo.isContentViewMoreOptionsButtonUsed @@ -211,10 +211,7 @@ constructor( PromptKind.Biometric.PaneType.ONE_PANE_NO_SENSOR_LANDSCAPE else -> PromptKind.Biometric.PaneType.TWO_PANE_LANDSCAPE } - PromptKind.Biometric( - modalities, - paneType = paneType, - ) + PromptKind.Biometric(modalities, paneType = paneType) } else { PromptKind.Biometric(modalities) } @@ -224,7 +221,7 @@ constructor( promptRepository.setPrompt( promptInfo = promptInfo, - userId = effectiveUserId, + userId = userId, requestId = requestId, gatekeeperChallenge = challenge, kind = kind, diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt index 2df5f16698b4..db4b0f2522ed 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt @@ -22,7 +22,6 @@ import android.content.Context import android.hardware.biometrics.BiometricAuthenticator import android.hardware.biometrics.BiometricConstants import android.hardware.biometrics.BiometricPrompt -import android.hardware.biometrics.Flags import android.hardware.face.FaceManager import android.util.Log import android.view.MotionEvent @@ -93,7 +92,7 @@ object BiometricViewBinder { val attributes = view.context.obtainStyledAttributes( R.style.TextAppearance_AuthCredential_Indicator, - intArrayOf(android.R.attr.textColor) + intArrayOf(android.R.attr.textColor), ) val textColorHint = attributes.getColor(0, 0) attributes.recycle() @@ -130,13 +129,13 @@ object BiometricViewBinder { object : AccessibilityDelegateCompat() { override fun onInitializeAccessibilityNodeInfo( host: View, - info: AccessibilityNodeInfoCompat + info: AccessibilityNodeInfoCompat, ) { super.onInitializeAccessibilityNodeInfo(host, info) info.addAction( AccessibilityActionCompat( AccessibilityNodeInfoCompat.ACTION_CLICK, - view.context.getString(R.string.biometric_dialog_cancel_authentication) + view.context.getString(R.string.biometric_dialog_cancel_authentication), ) ) } @@ -189,13 +188,11 @@ object BiometricViewBinder { subtitleView.text = viewModel.subtitle.first() descriptionView.text = viewModel.description.first() - if (Flags.customBiometricPrompt()) { - BiometricCustomizedViewBinder.bind( - customizedViewContainer, - viewModel.contentView.first(), - legacyCallback - ) - } + BiometricCustomizedViewBinder.bind( + customizedViewContainer, + viewModel.contentView.first(), + legacyCallback, + ) // set button listeners negativeButton.setOnClickListener { legacyCallback.onButtonNegative() } @@ -233,10 +230,7 @@ object BiometricViewBinder { lifecycleScope.launch { viewModel.hideSensorIcon.collect { showWithoutIcon -> if (!showWithoutIcon) { - PromptIconViewBinder.bind( - iconView, - viewModel, - ) + PromptIconViewBinder.bind(iconView, viewModel) } } } @@ -421,7 +415,7 @@ object BiometricViewBinder { launch { viewModel.onAnnounceAccessibilityHint( event, - accessibilityManager.isTouchExplorationEnabled + accessibilityManager.isTouchExplorationEnabled, ) } false @@ -444,10 +438,7 @@ object BiometricViewBinder { haptics.flag, ) } else { - vibratorHelper.performHapticFeedback( - view, - haptics.constant, - ) + vibratorHelper.performHapticFeedback(view, haptics.constant) } } is PromptViewModel.HapticsToPlay.MSDL -> { @@ -561,14 +552,12 @@ class Spaghetti( viewModel.showAuthenticated( modality = authenticatedModality, dismissAfterDelay = 500, - helpMessage = if (msgId != null) applicationContext.getString(msgId) else "" + helpMessage = if (msgId != null) applicationContext.getString(msgId) else "", ) } } - private fun getHelpForSuccessfulAuthentication( - authenticatedModality: BiometricModality, - ): Int? { + private fun getHelpForSuccessfulAuthentication(authenticatedModality: BiometricModality): Int? { // for coex, show a message when face succeeds after fingerprint has also started if (authenticatedModality != BiometricModality.Face) { return null diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt index 3ea91f05827c..39543e78f784 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt @@ -1,6 +1,5 @@ package com.android.systemui.biometrics.ui.binder -import android.hardware.biometrics.Flags import android.view.View import android.view.ViewGroup import android.widget.Button @@ -67,7 +66,7 @@ object CredentialViewBinder { updateForContentDimensions( containerWidth, containerHeight, - 0 // animateDurationMs + 0, // animateDurationMs ) } } @@ -81,13 +80,11 @@ object CredentialViewBinder { subtitleView.textOrHide = header.subtitle descriptionView.textOrHide = header.description - if (Flags.customBiometricPrompt()) { - BiometricCustomizedViewBinder.bind( - customizedViewContainer, - header.contentView, - legacyCallback - ) - } + BiometricCustomizedViewBinder.bind( + customizedViewContainer, + header.contentView, + legacyCallback, + ) iconView?.setImageDrawable(header.icon) diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt index 761c3da77a4d..0c5c723a4b2f 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt @@ -2,7 +2,6 @@ package com.android.systemui.biometrics.ui.viewmodel import android.content.Context import android.graphics.drawable.Drawable -import android.hardware.biometrics.Flags.customBiometricPrompt import android.hardware.biometrics.PromptContentView import android.text.InputType import com.android.internal.widget.LockPatternView @@ -36,21 +35,17 @@ constructor( val header: Flow<CredentialHeaderViewModel> = combine( credentialInteractor.prompt.filterIsInstance<BiometricPromptRequest.Credential>(), - credentialInteractor.showTitleOnly + credentialInteractor.showTitleOnly, ) { request, showTitleOnly -> - val flagEnabled = customBiometricPrompt() - val showTitleOnlyForCredential = showTitleOnly && flagEnabled BiometricPromptHeaderViewModelImpl( request, user = request.userInfo, title = request.title, - subtitle = if (showTitleOnlyForCredential) "" else request.subtitle, - contentView = - if (flagEnabled && !showTitleOnlyForCredential) request.contentView else null, - description = - if (flagEnabled && request.contentView != null) "" else request.description, + subtitle = if (showTitleOnly) "" else request.subtitle, + contentView = if (!showTitleOnly) request.contentView else null, + description = if (request.contentView != null) "" else request.description, icon = applicationContext.asLockIcon(request.userInfo.deviceCredentialOwnerId), - showEmergencyCallButton = request.showEmergencyCallButton + showEmergencyCallButton = request.showEmergencyCallButton, ) } @@ -125,7 +120,7 @@ constructor( /** Check a pattern and update [validatedAttestation] or [remainingAttempts]. */ suspend fun checkCredential( pattern: List<LockPatternView.Cell>, - header: CredentialHeaderViewModel + header: CredentialHeaderViewModel, ) = checkCredential(credentialInteractor.checkCredential(header.asRequest(), pattern = pattern)) private suspend fun checkCredential(result: CredentialStatus) { diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt index 0ac9405d0d8c..cbf783d66d42 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt @@ -27,7 +27,6 @@ import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable import android.hardware.biometrics.BiometricFingerprintConstants import android.hardware.biometrics.BiometricPrompt -import android.hardware.biometrics.Flags.customBiometricPrompt import android.hardware.biometrics.PromptContentView import android.os.UserHandle import android.text.TextPaint @@ -145,13 +144,13 @@ constructor( rotatedBounds, params.naturalDisplayWidth, params.naturalDisplayHeight, - rotation.ordinal + rotation.ordinal, ) Rect( rotatedBounds.left, rotatedBounds.top, params.logicalDisplayWidth - rotatedBounds.right, - params.logicalDisplayHeight - rotatedBounds.bottom + params.logicalDisplayHeight - rotatedBounds.bottom, ) } .distinctUntilChanged() @@ -263,7 +262,7 @@ constructor( promptKind, displayStateInteractor.isLargeScreen, displayStateInteractor.currentRotation, - modalities + modalities, ) { forceLarge, promptKind, isLargeScreen, rotation, modalities -> when { forceLarge || @@ -351,7 +350,7 @@ constructor( 0, 0, landscapeMediumHorizontalPadding, - landscapeMediumBottomPadding + landscapeMediumBottomPadding, ) } PromptPosition.Left -> @@ -365,7 +364,7 @@ constructor( landscapeMediumHorizontalPadding, 0, 0, - landscapeMediumBottomPadding + landscapeMediumBottomPadding, ) } PromptPosition.Top -> @@ -474,7 +473,7 @@ constructor( promptSelectorInteractor.prompt .map { when { - !(customBiometricPrompt()) || it == null -> Pair(null, "") + it == null -> Pair(null, "") else -> context.getUserBadgedLogoInfo(it, iconProvider, activityTaskManager) } } @@ -490,9 +489,7 @@ constructor( /** Custom content view for the prompt. */ val contentView: Flow<PromptContentView?> = - promptSelectorInteractor.prompt - .map { if (customBiometricPrompt()) it?.contentView else null } - .distinctUntilChanged() + promptSelectorInteractor.prompt.map { it?.contentView }.distinctUntilChanged() private val originalDescription = promptSelectorInteractor.prompt.map { it?.description ?: "" }.distinctUntilChanged() @@ -521,7 +518,7 @@ constructor( val attributes = context.obtainStyledAttributes( R.style.TextAppearance_AuthCredential_Title, - intArrayOf(android.R.attr.textSize) + intArrayOf(android.R.attr.textSize), ) val paint = TextPaint() paint.textSize = attributes.getDimensionPixelSize(0, 0).toFloat() @@ -566,7 +563,7 @@ constructor( private fun getHorizontalPadding( size: PromptSize, modalities: BiometricModalities, - hasOnlyOneLineTitle: Boolean + hasOnlyOneLineTitle: Boolean, ) = if (size.isSmall) { -smallHorizontalGuidelinePadding @@ -582,21 +579,13 @@ constructor( /** If the indicator (help, error) message should be shown. */ val isIndicatorMessageVisible: Flow<Boolean> = - combine( - size, - position, - message, - ) { size, _, message -> + combine(size, position, message) { size, _, message -> size.isMedium && message.message.isNotBlank() } /** If the auth is pending confirmation and the confirm button should be shown. */ val isConfirmButtonVisible: Flow<Boolean> = - combine( - size, - position, - isPendingConfirmation, - ) { size, _, isPendingConfirmation -> + combine(size, position, isPendingConfirmation) { size, _, isPendingConfirmation -> size.isNotSmall && isPendingConfirmation } @@ -606,24 +595,22 @@ constructor( /** If the negative button should be shown. */ val isNegativeButtonVisible: Flow<Boolean> = - combine( + combine(size, position, isAuthenticated, promptSelectorInteractor.isCredentialAllowed) { size, - position, - isAuthenticated, - promptSelectorInteractor.isCredentialAllowed, - ) { size, _, authState, credentialAllowed -> + _, + authState, + credentialAllowed -> size.isNotSmall && authState.isNotAuthenticated && !credentialAllowed } /** If the cancel button should be shown (. */ val isCancelButtonVisible: Flow<Boolean> = - combine( + combine(size, position, isAuthenticated, isNegativeButtonVisible, isConfirmButtonVisible) { size, - position, - isAuthenticated, - isNegativeButtonVisible, - isConfirmButtonVisible, - ) { size, _, authState, showNegativeButton, showConfirmButton -> + _, + authState, + showNegativeButton, + showConfirmButton -> size.isNotSmall && authState.isAuthenticated && !showNegativeButton && showConfirmButton } @@ -633,33 +620,28 @@ constructor( * fingerprint sensor. */ val canTryAgainNow: Flow<Boolean> = - combine( - _canTryAgainNow, + combine(_canTryAgainNow, size, position, isAuthenticated, isRetrySupported) { + readyToTryAgain, size, - position, - isAuthenticated, - isRetrySupported, - ) { readyToTryAgain, size, _, authState, supportsRetry -> + _, + authState, + supportsRetry -> readyToTryAgain && size.isNotSmall && supportsRetry && authState.isNotAuthenticated } /** If the try again button show be shown (only the button, see [canTryAgainNow]). */ val isTryAgainButtonVisible: Flow<Boolean> = - combine( - canTryAgainNow, - modalities, - ) { tryAgainIsPossible, modalities -> + combine(canTryAgainNow, modalities) { tryAgainIsPossible, modalities -> tryAgainIsPossible && modalities.hasFaceOnly } /** If the credential fallback button show be shown. */ val isCredentialButtonVisible: Flow<Boolean> = - combine( + combine(size, position, isAuthenticated, promptSelectorInteractor.isCredentialAllowed) { size, - position, - isAuthenticated, - promptSelectorInteractor.isCredentialAllowed, - ) { size, _, authState, credentialAllowed -> + _, + authState, + credentialAllowed -> size.isMedium && authState.isNotAuthenticated && credentialAllowed } @@ -759,10 +741,7 @@ constructor( * * Ignored if the user has already authenticated. */ - suspend fun showTemporaryHelp( - message: String, - messageAfterHelp: String = "", - ) = coroutineScope { + suspend fun showTemporaryHelp(message: String, messageAfterHelp: String = "") = coroutineScope { if (_isAuthenticated.value.isAuthenticated) { return@coroutineScope } @@ -910,13 +889,13 @@ constructor( udfpsUtils.getTouchInNativeCoordinates( event.getPointerId(0), event, - udfpsOverlayParams.value + udfpsOverlayParams.value, ) if ( !udfpsUtils.isWithinSensorArea( event.getPointerId(0), event, - udfpsOverlayParams.value + udfpsOverlayParams.value, ) ) { _accessibilityHint.emit( @@ -925,7 +904,7 @@ constructor( context, scaledTouch.x, scaledTouch.y, - udfpsOverlayParams.value + udfpsOverlayParams.value, ) ) } @@ -948,10 +927,7 @@ constructor( if (msdlFeedback()) { HapticsToPlay.MSDL(MSDLToken.UNLOCK, authInteractionProperties) } else { - HapticsToPlay.HapticConstant( - HapticFeedbackConstants.BIOMETRIC_CONFIRM, - flag = null, - ) + HapticsToPlay.HapticConstant(HapticFeedbackConstants.BIOMETRIC_CONFIRM, flag = null) } _hapticsToPlay.value = haptics } @@ -961,10 +937,7 @@ constructor( if (msdlFeedback()) { HapticsToPlay.MSDL(MSDLToken.FAILURE, authInteractionProperties) } else { - HapticsToPlay.HapticConstant( - HapticFeedbackConstants.BIOMETRIC_REJECT, - flag = null, - ) + HapticsToPlay.HapticConstant(HapticFeedbackConstants.BIOMETRIC_REJECT, flag = null) } _hapticsToPlay.value = haptics } @@ -1006,7 +979,7 @@ constructor( private fun Context.getUserBadgedLogoInfo( prompt: BiometricPromptRequest.Biometric, iconProvider: IconProvider, - activityTaskManager: ActivityTaskManager + activityTaskManager: ActivityTaskManager, ): Pair<Drawable?, String> { var icon: Drawable? = if (prompt.logoBitmap != null) BitmapDrawable(resources, prompt.logoBitmap) else null @@ -1045,7 +1018,7 @@ private fun Context.getUserBadgedLogoInfo( packageManager .getUserBadgedLabel( packageManager.getApplicationLabel(appInfo), - UserHandle.of(userId) + UserHandle.of(userId), ) .toString() } @@ -1070,7 +1043,7 @@ private fun BiometricPromptRequest.Biometric.getComponentNameForLogo( private fun BiometricPromptRequest.Biometric.getApplicationInfo( context: Context, - componentNameForLogo: ComponentName? + componentNameForLogo: ComponentName?, ): ApplicationInfo? { val packageName = when { @@ -1088,7 +1061,7 @@ private fun BiometricPromptRequest.Biometric.getApplicationInfo( try { context.packageManager.getApplicationInfo( packageName, - PackageManager.MATCH_DISABLED_COMPONENTS or PackageManager.MATCH_ANY_USER + PackageManager.MATCH_DISABLED_COMPONENTS or PackageManager.MATCH_ANY_USER, ) } catch (e: PackageManager.NameNotFoundException) { Log.w(PromptViewModel.TAG, "Cannot find application info for $opPackageName", e) diff --git a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt index a33e0ac0b33a..44dd34a89303 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalModule.kt @@ -32,6 +32,8 @@ import com.android.systemui.communal.domain.interactor.CommunalSceneTransitionIn import com.android.systemui.communal.shared.log.CommunalMetricsLogger import com.android.systemui.communal.shared.log.CommunalStatsLogProxyImpl import com.android.systemui.communal.shared.model.CommunalScenes +import com.android.systemui.communal.shared.model.GlanceableHubMultiUserHelper +import com.android.systemui.communal.shared.model.GlanceableHubMultiUserHelperImpl import com.android.systemui.communal.util.CommunalColors import com.android.systemui.communal.util.CommunalColorsImpl import com.android.systemui.communal.widgets.CommunalWidgetModule @@ -65,6 +67,7 @@ import kotlinx.coroutines.CoroutineScope CommunalSettingsRepositoryModule::class, CommunalSmartspaceRepositoryModule::class, CommunalStartableModule::class, + GlanceableHubWidgetManagerModule::class, ] ) interface CommunalModule { @@ -91,6 +94,11 @@ interface CommunalModule { impl: CommunalSceneTransitionInteractor ): CoreStartable + @Binds + fun bindGlanceableHubMultiUserHelper( + impl: GlanceableHubMultiUserHelperImpl + ): GlanceableHubMultiUserHelper + companion object { const val LOGGABLE_PREFIXES = "loggable_prefixes" const val LAUNCHER_PACKAGE = "launcher_package" @@ -106,19 +114,14 @@ interface CommunalModule { sceneKeys = listOf(CommunalScenes.Blank, CommunalScenes.Communal), initialSceneKey = CommunalScenes.Blank, navigationDistances = - mapOf( - CommunalScenes.Blank to 0, - CommunalScenes.Communal to 1, - ), + mapOf(CommunalScenes.Blank to 0, CommunalScenes.Communal to 1), ) return SceneDataSourceDelegator(applicationScope, config) } @Provides @SysUISingleton - fun providesCommunalBackupUtils( - @Application context: Context, - ): CommunalBackupUtils { + fun providesCommunalBackupUtils(@Application context: Context): CommunalBackupUtils { return CommunalBackupUtils(context) } diff --git a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalStartableModule.kt b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalStartableModule.kt index 6cbf5405f46b..2d19b026489c 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalStartableModule.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/dagger/CommunalStartableModule.kt @@ -24,6 +24,7 @@ import com.android.systemui.communal.CommunalOngoingContentStartable import com.android.systemui.communal.CommunalSceneStartable import com.android.systemui.communal.log.CommunalLoggerStartable import com.android.systemui.communal.widgets.CommunalAppWidgetHostStartable +import com.android.systemui.dagger.qualifiers.PerUser import dagger.Binds import dagger.Module import dagger.multibindings.ClassKey @@ -48,6 +49,7 @@ interface CommunalStartableModule { @Binds @IntoMap + @PerUser @ClassKey(CommunalAppWidgetHostStartable::class) fun bindCommunalAppWidgetHostStartable(impl: CommunalAppWidgetHostStartable): CoreStartable diff --git a/packages/SystemUI/src/com/android/systemui/communal/dagger/GlanceableHubWidgetManagerModule.kt b/packages/SystemUI/src/com/android/systemui/communal/dagger/GlanceableHubWidgetManagerModule.kt new file mode 100644 index 000000000000..8c0758398cb1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/dagger/GlanceableHubWidgetManagerModule.kt @@ -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.systemui.communal.dagger + +import com.android.server.servicewatcher.ServiceWatcher.ServiceSupplier +import com.android.systemui.communal.widgets.GlanceableHubWidgetManagerServiceInfo +import com.android.systemui.communal.widgets.GlanceableHubWidgetManagerServiceSupplier +import com.android.systemui.communal.widgets.GlanceableHubWidgetManagerServiceWatcherFactoryImpl +import com.android.systemui.communal.widgets.ServiceWatcherFactory +import dagger.Binds +import dagger.Module + +@Module +interface GlanceableHubWidgetManagerModule { + @Binds + fun bindServiceSupplier( + impl: GlanceableHubWidgetManagerServiceSupplier + ): ServiceSupplier<GlanceableHubWidgetManagerServiceInfo?> + + @Binds + fun bindServiceWatcherFactory( + impl: GlanceableHubWidgetManagerServiceWatcherFactoryImpl + ): ServiceWatcherFactory<GlanceableHubWidgetManagerServiceInfo?> +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalDatabase.kt b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalDatabase.kt index 17f4f0c83d6f..e72088f37fa7 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalDatabase.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalDatabase.kt @@ -17,6 +17,7 @@ package com.android.systemui.communal.data.db import android.content.Context +import android.os.Process import android.util.Log import androidx.annotation.VisibleForTesting import androidx.room.Database @@ -24,6 +25,7 @@ import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase +import com.android.systemui.communal.shared.model.GlanceableHubMultiUserHelperImpl import com.android.systemui.res.R @Database(entities = [CommunalWidgetItem::class, CommunalItemRank::class], version = 4) @@ -44,6 +46,11 @@ abstract class CommunalDatabase : RoomDatabase() { * new instance is created. */ fun getInstance(context: Context, callback: Callback? = null): CommunalDatabase { + with(GlanceableHubMultiUserHelperImpl(Process.myUserHandle())) { + // Assert that the database is never accessed from a headless system user. + assertNotInHeadlessSystemUser() + } + if (instance == null) { instance = Room.databaseBuilder( diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt index 258480e7fef5..3d40aa75b488 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/db/CommunalWidgetDao.kt @@ -25,6 +25,7 @@ import androidx.room.RoomDatabase import androidx.room.Transaction import androidx.room.Update import androidx.sqlite.db.SupportSQLiteDatabase +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.communal.nano.CommunalHubState import com.android.systemui.communal.shared.model.CommunalContentSize import com.android.systemui.communal.widgets.CommunalWidgetHost @@ -39,7 +40,6 @@ import javax.inject.Named import javax.inject.Provider import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow -import com.android.app.tracing.coroutines.launchTraced as launch /** * Callback that will be invoked when the Room database is created. Then the database will be @@ -224,9 +224,9 @@ interface CommunalWidgetDao { ): Long { val widgets = getWidgetsNow() - // If rank is not specified, rank it last by finding the current maximum rank and increment - // by 1. If the new widget is the first widget, set the rank to 0. - val newRank = rank ?: widgets.keys.maxOfOrNull { it.rank + 1 } ?: 0 + // If rank is not specified (null or less than 0), rank it last by finding the current + // maximum rank and increment by 1. If the new widget is the first widget, set rank to 0. + val newRank = rank?.takeIf { it >= 0 } ?: widgets.keys.maxOfOrNull { it.rank + 1 } ?: 0 // Shift widgets after [rank], unless widget is added at the end. if (rank != null) { diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt index e164587ce02d..29569f8b7df5 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepository.kt @@ -21,6 +21,7 @@ import android.appwidget.AppWidgetProviderInfo import android.content.ComponentName import android.os.UserHandle import android.os.UserManager +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.Flags.communalWidgetResizing import com.android.systemui.common.data.repository.PackageChangeRepository import com.android.systemui.common.shared.model.PackageInstallSession @@ -51,11 +52,10 @@ import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import com.android.app.tracing.coroutines.launchTraced as launch /** Encapsulates the state of widgets for communal mode. */ interface CommunalWidgetRepository { - /** A flow of information about active communal widgets stored in database. */ + /** A flow of the list of Glanceable Hub widgets ordered by rank. */ val communalWidgets: Flow<List<CommunalWidgetContentModel>> /** @@ -106,8 +106,12 @@ interface CommunalWidgetRepository { fun resizeWidget(appWidgetId: Int, spanY: Int, widgetIdToRankMap: Map<Int, Int>) } +/** + * The local implementation of the [CommunalWidgetRepository] that should be injected in a + * foreground user process. + */ @SysUISingleton -class CommunalWidgetRepositoryImpl +class CommunalWidgetRepositoryLocalImpl @Inject constructor( private val appWidgetHost: CommunalAppWidgetHost, @@ -123,7 +127,7 @@ constructor( private val defaultWidgetPopulation: DefaultWidgetPopulation, ) : CommunalWidgetRepository { companion object { - const val TAG = "CommunalWidgetRepository" + const val TAG = "CommunalWidgetRepositoryLocalImpl" } private val logger = Logger(logBuffer, TAG) diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryModule.kt index b502fb11d7ac..824edcb26809 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryModule.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryModule.kt @@ -17,11 +17,24 @@ package com.android.systemui.communal.data.repository -import dagger.Binds +import com.android.systemui.communal.shared.model.GlanceableHubMultiUserHelper +import dagger.Lazy import dagger.Module +import dagger.Provides @Module interface CommunalWidgetRepositoryModule { - @Binds - fun communalWidgetRepository(impl: CommunalWidgetRepositoryImpl): CommunalWidgetRepository + companion object { + @Provides + fun provideCommunalWidgetRepository( + localImpl: Lazy<CommunalWidgetRepositoryLocalImpl>, + remoteImpl: Lazy<CommunalWidgetRepositoryRemoteImpl>, + helper: GlanceableHubMultiUserHelper, + ): CommunalWidgetRepository { + // Provide an implementation based on the current user. + return if (helper.glanceableHubHsumFlagEnabled && helper.isInHeadlessSystemUser()) + remoteImpl.get() + else localImpl.get() + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryRemoteImpl.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryRemoteImpl.kt new file mode 100644 index 000000000000..6682186c94f5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryRemoteImpl.kt @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.data.repository + +import android.content.ComponentName +import android.os.UserHandle +import com.android.systemui.communal.shared.model.CommunalWidgetContentModel +import com.android.systemui.communal.shared.model.GlanceableHubMultiUserHelper +import com.android.systemui.communal.widgets.GlanceableHubWidgetManager +import com.android.systemui.communal.widgets.WidgetConfigurator +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch + +/** + * The remote implementation of the [CommunalWidgetRepository] that should be injected in a headless + * system user process. This implementation receives widget data from and routes requests to the + * remote service in the foreground user. + */ +@SysUISingleton +class CommunalWidgetRepositoryRemoteImpl +@Inject +constructor( + @Background private val bgScope: CoroutineScope, + private val glanceableHubWidgetManager: GlanceableHubWidgetManager, + glanceableHubMultiUserHelper: GlanceableHubMultiUserHelper, +) : CommunalWidgetRepository { + + init { + // This is the implementation for the headless system user. For the foreground user + // implementation see [CommunalWidgetRepositoryLocalImpl]. + glanceableHubMultiUserHelper.assertInHeadlessSystemUser() + } + + override val communalWidgets: Flow<List<CommunalWidgetContentModel>> = + glanceableHubWidgetManager.widgets + + override fun addWidget( + provider: ComponentName, + user: UserHandle, + rank: Int?, + configurator: WidgetConfigurator?, + ) { + bgScope.launch { glanceableHubWidgetManager.addWidget(provider, user, rank, configurator) } + } + + override fun deleteWidget(widgetId: Int) { + bgScope.launch { glanceableHubWidgetManager.deleteWidget(widgetId) } + } + + override fun updateWidgetOrder(widgetIdToRankMap: Map<Int, Int>) { + bgScope.launch { glanceableHubWidgetManager.updateWidgetOrder(widgetIdToRankMap) } + } + + override fun resizeWidget(appWidgetId: Int, spanY: Int, widgetIdToRankMap: Map<Int, Int>) { + bgScope.launch { + glanceableHubWidgetManager.resizeWidget(appWidgetId, spanY, widgetIdToRankMap) + } + } + + override fun restoreWidgets(oldToNewWidgetIdMap: Map<Int, Int>) { + throw IllegalStateException("Restore widgets should be performed on a foreground user") + } + + override fun abortRestoreWidgets() { + throw IllegalStateException("Restore widgets should be performed on a foreground user") + } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt index dc248059b3d4..602fe307a1fe 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt @@ -40,7 +40,6 @@ import com.android.systemui.communal.shared.model.CommunalContentSize.THIRD import com.android.systemui.communal.shared.model.CommunalScenes import com.android.systemui.communal.shared.model.CommunalWidgetContentModel import com.android.systemui.communal.shared.model.EditModeState -import com.android.systemui.communal.widgets.CommunalAppWidgetHost import com.android.systemui.communal.widgets.EditWidgetsActivityStarter import com.android.systemui.communal.widgets.WidgetConfigurator import com.android.systemui.dagger.SysUISingleton @@ -108,7 +107,6 @@ constructor( keyguardInteractor: KeyguardInteractor, keyguardTransitionInteractor: KeyguardTransitionInteractor, communalSettingsInteractor: CommunalSettingsInteractor, - private val appWidgetHost: CommunalAppWidgetHost, private val editWidgetsActivityStarter: EditWidgetsActivityStarter, private val userTracker: UserTracker, private val activityStarter: ActivityStarter, @@ -451,7 +449,6 @@ constructor( appWidgetId = widget.appWidgetId, rank = widget.rank, providerInfo = widget.providerInfo, - appWidgetHost = appWidgetHost, inQuietMode = isQuietModeEnabled(widget.providerInfo.profile), size = CommunalContentSize.toSize(widget.spanY), ) diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt index 4c2c094a4aea..30f580e472e8 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/model/CommunalContentModel.kt @@ -23,7 +23,6 @@ import android.content.pm.ApplicationInfo import android.graphics.Bitmap import android.widget.RemoteViews import com.android.systemui.communal.shared.model.CommunalContentSize -import com.android.systemui.communal.widgets.CommunalAppWidgetHost import java.util.UUID /** Encapsulates data for a communal content. */ @@ -60,7 +59,6 @@ sealed interface CommunalContentModel { override val appWidgetId: Int, override val rank: Int, val providerInfo: AppWidgetProviderInfo, - val appWidgetHost: CommunalAppWidgetHost, val inQuietMode: Boolean, override val size: CommunalContentSize, ) : WidgetContent { diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalContentSize.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalContentSize.kt index 572794daaca6..cf80b7d62fd5 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalContentSize.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalContentSize.kt @@ -36,7 +36,7 @@ enum class CommunalContentSize(val span: Int) { /** Converts from span to communal content size. */ fun toSize(span: Int): CommunalContentSize { return entries.find { it.span == span } - ?: throw Exception("Invalid span for communal content size") + ?: throw IllegalArgumentException("$span is not a valid span size") } } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.aidl b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.aidl new file mode 100644 index 000000000000..a215698eaeba --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.shared.model; + +parcelable CommunalWidgetContentModel; diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.kt index 0c9ea789f3d1..f23347d083d6 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/shared/model/CommunalWidgetContentModel.kt @@ -19,21 +19,60 @@ package com.android.systemui.communal.shared.model import android.appwidget.AppWidgetProviderInfo import android.content.ComponentName import android.graphics.Bitmap +import android.os.Parcel +import android.os.Parcelable import android.os.UserHandle /** Encapsulates data for a communal widget. */ -sealed interface CommunalWidgetContentModel { +sealed interface CommunalWidgetContentModel : Parcelable { val appWidgetId: Int val rank: Int val spanY: Int + // Used for distinguishing subtypes when reading from a parcel. + val type: Int + /** Widget is ready to display */ data class Available( override val appWidgetId: Int, val providerInfo: AppWidgetProviderInfo, override val rank: Int, override val spanY: Int, - ) : CommunalWidgetContentModel + ) : CommunalWidgetContentModel { + + override val type = TYPE_AVAILABLE + + constructor( + parcel: Parcel + ) : this( + parcel.readInt(), + requireNotNull(parcel.readTypedObject(AppWidgetProviderInfo.CREATOR)), + parcel.readInt(), + parcel.readInt(), + ) + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeInt(type) + parcel.writeInt(appWidgetId) + parcel.writeTypedObject(providerInfo, flags) + parcel.writeInt(rank) + parcel.writeInt(spanY) + } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Parcelable.Creator<Available> { + override fun createFromParcel(parcel: Parcel): Available { + return Available(parcel) + } + + override fun newArray(size: Int): Array<Available?> { + return arrayOfNulls(size) + } + } + } /** Widget is pending installation */ data class Pending( @@ -43,5 +82,61 @@ sealed interface CommunalWidgetContentModel { val icon: Bitmap?, val user: UserHandle, override val spanY: Int, - ) : CommunalWidgetContentModel + ) : CommunalWidgetContentModel { + + override val type = TYPE_PENDING + + constructor( + parcel: Parcel + ) : this( + parcel.readInt(), + parcel.readInt(), + requireNotNull(parcel.readTypedObject(ComponentName.CREATOR)), + parcel.readTypedObject(Bitmap.CREATOR), + requireNotNull(parcel.readTypedObject(UserHandle.CREATOR)), + parcel.readInt(), + ) + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeInt(type) + parcel.writeInt(appWidgetId) + parcel.writeInt(rank) + parcel.writeTypedObject(componentName, flags) + parcel.writeTypedObject(icon, flags) + parcel.writeTypedObject(user, flags) + parcel.writeInt(spanY) + } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Parcelable.Creator<Pending> { + override fun createFromParcel(parcel: Parcel): Pending { + return Pending(parcel) + } + + override fun newArray(size: Int): Array<Pending?> { + return arrayOfNulls(size) + } + } + } + + // Used for distinguishing subtypes when reading from a parcel. + companion object CREATOR : Parcelable.Creator<CommunalWidgetContentModel> { + private const val TYPE_AVAILABLE = 0 + private const val TYPE_PENDING = 1 + + override fun createFromParcel(parcel: Parcel): CommunalWidgetContentModel { + return when (val type = parcel.readInt()) { + TYPE_AVAILABLE -> Available(parcel) + TYPE_PENDING -> Pending(parcel) + else -> throw IllegalArgumentException("Unknown type: $type") + } + } + + override fun newArray(size: Int): Array<CommunalWidgetContentModel?> { + return arrayOfNulls(size) + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/shared/model/GlanceableHubMultiUserHelper.kt b/packages/SystemUI/src/com/android/systemui/communal/shared/model/GlanceableHubMultiUserHelper.kt new file mode 100644 index 000000000000..ef6a9ce6752b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/shared/model/GlanceableHubMultiUserHelper.kt @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.shared.model + +import android.os.UserHandle +import android.os.UserManager +import com.android.systemui.Flags.secondaryUserWidgetHost +import com.android.systemui.dagger.SysUISingleton +import javax.inject.Inject + +/** Helper for multi-user / HSUM related functionality for the Glanceable Hub. */ +interface GlanceableHubMultiUserHelper { + /** Whether the Glanceable Hub in HSUM flag is enabled. */ + val glanceableHubHsumFlagEnabled: Boolean + + /** Whether the device is in headless system user mode. */ + fun isHeadlessSystemUserMode(): Boolean + + /** Whether the current process is running in the headless system user. */ + fun isInHeadlessSystemUser(): Boolean + + /** + * Asserts that the current process is running in the headless system user. + * + * Only throws an exception if [glanceableHubHsumFlagEnabled] is true. + */ + @Throws(IllegalStateException::class) fun assertInHeadlessSystemUser() + + /** + * Asserts that the current process is NOT running in the headless system user. + * + * Only throws an exception if [glanceableHubHsumFlagEnabled] is true. + */ + @Throws(IllegalStateException::class) fun assertNotInHeadlessSystemUser() +} + +@SysUISingleton +class GlanceableHubMultiUserHelperImpl @Inject constructor(private val userHandle: UserHandle) : + GlanceableHubMultiUserHelper { + + override val glanceableHubHsumFlagEnabled: Boolean = secondaryUserWidgetHost() + + override fun isHeadlessSystemUserMode(): Boolean = UserManager.isHeadlessSystemUserMode() + + override fun isInHeadlessSystemUser(): Boolean { + return isHeadlessSystemUserMode() && userHandle.isSystem + } + + override fun assertInHeadlessSystemUser() { + if (glanceableHubHsumFlagEnabled) { + check(isInHeadlessSystemUser()) + } + } + + override fun assertNotInHeadlessSystemUser() { + if (glanceableHubHsumFlagEnabled) { + check(!isInHeadlessSystemUser()) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModel.kt index bde5d0f87a66..113375fc257e 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/ResizeableItemFrameViewModel.kt @@ -76,10 +76,10 @@ class ResizeableItemFrameViewModel : ExclusiveActivatable() { floor(spans.toDouble() / resizeMultiple).toInt() * resizeMultiple val maxSpans: Int - get() = roundDownToMultiple(getSpansForPx(maxHeightPx)) + get() = roundDownToMultiple(getSpansForPx(maxHeightPx)).coerceAtLeast(currentSpan) val minSpans: Int - get() = roundDownToMultiple(getSpansForPx(minHeightPx)) + get() = roundDownToMultiple(getSpansForPx(minHeightPx)).coerceAtMost(currentSpan) } /** Check if widget can expanded based on current drag states */ diff --git a/packages/SystemUI/src/com/android/systemui/communal/util/WidgetViewFactory.kt b/packages/SystemUI/src/com/android/systemui/communal/util/WidgetViewFactory.kt index d5d3a927181b..cc6007b400f7 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/util/WidgetViewFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/util/WidgetViewFactory.kt @@ -21,11 +21,14 @@ import android.os.Bundle import android.util.SizeF import com.android.app.tracing.coroutines.withContextTraced as withContext import com.android.systemui.communal.domain.model.CommunalContentModel +import com.android.systemui.communal.shared.model.GlanceableHubMultiUserHelper import com.android.systemui.communal.widgets.AppWidgetHostListenerDelegate import com.android.systemui.communal.widgets.CommunalAppWidgetHost import com.android.systemui.communal.widgets.CommunalAppWidgetHostView +import com.android.systemui.communal.widgets.GlanceableHubWidgetManager import com.android.systemui.communal.widgets.WidgetInteractionHandler import com.android.systemui.dagger.qualifiers.UiBackground +import dagger.Lazy import java.util.concurrent.Executor import javax.inject.Inject import kotlin.coroutines.CoroutineContext @@ -36,9 +39,11 @@ class WidgetViewFactory constructor( @UiBackground private val uiBgContext: CoroutineContext, @UiBackground private val uiBgExecutor: Executor, - private val appWidgetHost: CommunalAppWidgetHost, + private val appWidgetHostLazy: Lazy<CommunalAppWidgetHost>, private val interactionHandler: WidgetInteractionHandler, private val listenerFactory: AppWidgetHostListenerDelegate.Factory, + private val glanceableHubWidgetManagerLazy: Lazy<GlanceableHubWidgetManager>, + private val multiUserHelper: GlanceableHubMultiUserHelper, ) { suspend fun createWidget( context: Context, @@ -51,9 +56,25 @@ constructor( setExecutor(uiBgExecutor) setAppWidget(model.appWidgetId, model.providerInfo) } - // Instead of setting the view as the listener directly, we wrap the view in a delegate - // which ensures the callbacks always get called on the main thread. - appWidgetHost.setListener(model.appWidgetId, listenerFactory.create(view)) + + if ( + multiUserHelper.glanceableHubHsumFlagEnabled && + multiUserHelper.isInHeadlessSystemUser() + ) { + // If the widget view is created in the headless system user, the widget host lives + // remotely in the foreground user, and therefore the host listener needs to be + // registered through the widget manager. + with(glanceableHubWidgetManagerLazy.get()) { + setAppWidgetHostListener(model.appWidgetId, listenerFactory.create(view)) + } + } else { + // Instead of setting the view as the listener directly, we wrap the view in a + // delegate which ensures the callbacks always get called on the main thread. + with(appWidgetHostLazy.get()) { + setListener(model.appWidgetId, listenerFactory.create(view)) + } + } + if (size != null) { view.updateAppWidgetSize( /* newOptions = */ Bundle(), diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt index 4f9ed2f7dbf8..70e7947e1b70 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt @@ -18,6 +18,8 @@ package com.android.systemui.communal.widgets import android.appwidget.AppWidgetHost import android.content.Context +import com.android.app.tracing.coroutines.launchTraced as launch +import com.android.systemui.communal.shared.model.GlanceableHubMultiUserHelper import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.Logger import javax.annotation.concurrent.GuardedBy @@ -25,7 +27,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.asSharedFlow -import com.android.app.tracing.coroutines.launchTraced as launch /** Communal app widget host that creates a [CommunalAppWidgetHostView]. */ class CommunalAppWidgetHost( @@ -33,7 +34,14 @@ class CommunalAppWidgetHost( private val backgroundScope: CoroutineScope, hostId: Int, logBuffer: LogBuffer, + glanceableHubMultiUserHelper: GlanceableHubMultiUserHelper, ) : AppWidgetHost(context, hostId) { + + init { + // The app widget host should never be accessed from a headless system user. + glanceableHubMultiUserHelper.assertNotInHeadlessSystemUser() + } + private val logger = Logger(logBuffer, TAG) private val _appWidgetIdToRemove = MutableSharedFlow<Int>() diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt index 301da51c8082..dec7ba3a3eb1 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt @@ -18,55 +18,118 @@ package com.android.systemui.communal.widgets import com.android.systemui.CoreStartable import com.android.systemui.communal.domain.interactor.CommunalInteractor +import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor import com.android.systemui.communal.shared.model.CommunalWidgetContentModel +import com.android.systemui.communal.shared.model.GlanceableHubMultiUserHelper import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.settings.UserTracker +import com.android.systemui.util.kotlin.BooleanFlowOperators.allOf import com.android.systemui.util.kotlin.BooleanFlowOperators.anyOf +import com.android.systemui.util.kotlin.BooleanFlowOperators.not import com.android.systemui.util.kotlin.pairwise import com.android.systemui.util.kotlin.sample +import dagger.Lazy import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.dropWhile import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.withContext +// Started per user, so make sure injections are lazy to avoid instantiating unnecessary +// dependencies. @SysUISingleton class CommunalAppWidgetHostStartable @Inject constructor( - private val appWidgetHost: CommunalAppWidgetHost, - private val communalWidgetHost: CommunalWidgetHost, - private val communalInteractor: CommunalInteractor, - private val userTracker: UserTracker, + private val appWidgetHostLazy: Lazy<CommunalAppWidgetHost>, + private val communalWidgetHostLazy: Lazy<CommunalWidgetHost>, + private val communalInteractorLazy: Lazy<CommunalInteractor>, + private val communalSettingsInteractorLazy: Lazy<CommunalSettingsInteractor>, + private val keyguardInteractorLazy: Lazy<KeyguardInteractor>, + private val userTrackerLazy: Lazy<UserTracker>, @Background private val bgScope: CoroutineScope, - @Main private val uiDispatcher: CoroutineDispatcher + @Main private val uiDispatcher: CoroutineDispatcher, + private val glanceableHubWidgetManagerLazy: Lazy<GlanceableHubWidgetManager>, + private val glanceableHubMultiUserHelper: GlanceableHubMultiUserHelper, ) : CoreStartable { + private val appWidgetHost by lazy { appWidgetHostLazy.get() } + private val communalWidgetHost by lazy { communalWidgetHostLazy.get() } + private val communalInteractor by lazy { communalInteractorLazy.get() } + private val communalSettingsInteractor by lazy { communalSettingsInteractorLazy.get() } + private val keyguardInteractor by lazy { keyguardInteractorLazy.get() } + private val userTracker by lazy { userTrackerLazy.get() } + private val glanceableHubWidgetManager by lazy { glanceableHubWidgetManagerLazy.get() } + override fun start() { - anyOf(communalInteractor.isCommunalAvailable, communalInteractor.editModeOpen) - // Only trigger updates on state changes, ignoring the initial false value. - .pairwise(false) - .filter { (previous, new) -> previous != new } - .onEach { (_, shouldListen) -> updateAppWidgetHostActive(shouldListen) } - .sample(communalInteractor.communalWidgets, ::Pair) - .onEach { (withPrev, widgets) -> - val (_, isActive) = withPrev - // The validation is performed once the hub becomes active. - if (isActive) { - validateWidgetsAndDeleteOrphaned(widgets) + if ( + glanceableHubMultiUserHelper.glanceableHubHsumFlagEnabled && + glanceableHubMultiUserHelper.isInHeadlessSystemUser() + ) { + onStartInHeadlessSystemUser() + } else { + onStartInForegroundUser() + } + } + + private fun onStartInForegroundUser() { + // Make the host active when communal becomes available, and delete widgets whose user has + // been removed from the system. + // Skipped in HSUM, because lifecycle of the host is controlled by the + // [GlanceableHubWidgetManagerService], and widgets are stored per user so no more orphaned + // widgets. + if ( + !glanceableHubMultiUserHelper.glanceableHubHsumFlagEnabled || + !glanceableHubMultiUserHelper.isHeadlessSystemUserMode() + ) { + anyOf(communalInteractor.isCommunalAvailable, communalInteractor.editModeOpen) + // Only trigger updates on state changes, ignoring the initial false value. + .pairwise(false) + .filter { (previous, new) -> previous != new } + .onEach { (_, shouldListen) -> updateAppWidgetHostActive(shouldListen) } + .sample(communalInteractor.communalWidgets, ::Pair) + .onEach { (withPrev, widgets) -> + val (_, isActive) = withPrev + // The validation is performed once the hub becomes active. + if (isActive) { + validateWidgetsAndDeleteOrphaned(widgets) + } } - } - .launchIn(bgScope) + .launchIn(bgScope) + } appWidgetHost.appWidgetIdToRemove .onEach { appWidgetId -> communalInteractor.deleteWidget(id = appWidgetId) } .launchIn(bgScope) } + private fun onStartInHeadlessSystemUser() { + // Connection to the widget manager service in the foreground user should stay open as long + // as the Glanceable Hub is available. + allOf( + communalSettingsInteractor.isCommunalEnabled, + not(keyguardInteractor.isEncryptedOrLockdown), + ) + .distinctUntilChanged() + // Drop the initial false + .dropWhile { !it } + .onEach { shouldRegister -> + if (shouldRegister) { + glanceableHubWidgetManager.register() + } else { + glanceableHubWidgetManager.unregister() + } + } + .launchIn(bgScope) + } + private suspend fun updateAppWidgetHostActive(active: Boolean) = // Always ensure this is called on the main/ui thread. withContext(uiDispatcher) { diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetHost.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetHost.kt index 0b8d9775675f..8c745f59e918 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetHost.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetHost.kt @@ -26,6 +26,8 @@ import android.os.Bundle import android.os.UserHandle import android.widget.RemoteViews import androidx.annotation.WorkerThread +import com.android.app.tracing.coroutines.launchTraced as launch +import com.android.systemui.communal.shared.model.GlanceableHubMultiUserHelper import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.Logger @@ -38,7 +40,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import com.android.app.tracing.coroutines.launchTraced as launch /** * Widget host that interacts with AppWidget service and host to bind and provide info for widgets @@ -52,7 +53,14 @@ constructor( private val appWidgetHost: CommunalAppWidgetHost, private val selectedUserInteractor: SelectedUserInteractor, @CommunalLog logBuffer: LogBuffer, + glanceableHubMultiUserHelper: GlanceableHubMultiUserHelper, ) : CommunalAppWidgetHost.Observer { + + init { + // The communal widget host should never be accessed from a headless system user. + glanceableHubMultiUserHelper.assertNotInHeadlessSystemUser() + } + companion object { private const val TAG = "CommunalWidgetHost" @@ -96,7 +104,7 @@ constructor( bindWidget( widgetId = id, user = user ?: UserHandle(selectedUserInteractor.getSelectedUserId()), - provider = provider + provider = provider, ) ) { logger.d("Successfully bound the widget $provider") diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetModule.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetModule.kt index f4962085000c..a235e7ae2b6a 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetModule.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalWidgetModule.kt @@ -20,6 +20,7 @@ package com.android.systemui.communal.widgets import android.appwidget.AppWidgetManager import android.content.Context import android.content.res.Resources +import com.android.systemui.communal.shared.model.GlanceableHubMultiUserHelper import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background @@ -46,8 +47,15 @@ interface CommunalWidgetModule { @Application context: Context, @Background backgroundScope: CoroutineScope, @CommunalLog logBuffer: LogBuffer, + glanceableHubMultiUserHelper: GlanceableHubMultiUserHelper, ): CommunalAppWidgetHost { - return CommunalAppWidgetHost(context, backgroundScope, APP_WIDGET_HOST_ID, logBuffer) + return CommunalAppWidgetHost( + context, + backgroundScope, + APP_WIDGET_HOST_ID, + logBuffer, + glanceableHubMultiUserHelper, + ) } @SysUISingleton @@ -58,6 +66,7 @@ interface CommunalWidgetModule { appWidgetHost: CommunalAppWidgetHost, selectedUserInteractor: SelectedUserInteractor, @CommunalLog logBuffer: LogBuffer, + glanceableHubMultiUserHelper: GlanceableHubMultiUserHelper, ): CommunalWidgetHost { return CommunalWidgetHost( applicationScope, @@ -65,6 +74,7 @@ interface CommunalWidgetModule { appWidgetHost, selectedUserInteractor, logBuffer, + glanceableHubMultiUserHelper, ) } diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/GlanceableHubWidgetManager.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/GlanceableHubWidgetManager.kt new file mode 100644 index 000000000000..202edf71fa11 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/GlanceableHubWidgetManager.kt @@ -0,0 +1,171 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.widgets + +import android.appwidget.AppWidgetHost.AppWidgetHostListener +import android.appwidget.AppWidgetProviderInfo +import android.content.ComponentName +import android.os.IBinder +import android.os.UserHandle +import android.widget.RemoteViews +import com.android.server.servicewatcher.ServiceWatcher +import com.android.server.servicewatcher.ServiceWatcher.ServiceListener +import com.android.systemui.communal.shared.model.CommunalWidgetContentModel +import com.android.systemui.communal.shared.model.GlanceableHubMultiUserHelper +import com.android.systemui.communal.widgets.IGlanceableHubWidgetManagerService.IAppWidgetHostListener +import com.android.systemui.communal.widgets.IGlanceableHubWidgetManagerService.IGlanceableHubWidgetsListener +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.core.Logger +import com.android.systemui.log.dagger.CommunalLog +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow +import javax.inject.Inject +import kotlinx.coroutines.channels.awaitClose + +/** + * Manages updates to Glanceable Hub widgets and requests to edit them from the headless system + * user. + * + * It communicates with the remote [GlanceableHubWidgetManagerService] which runs in the foreground + * user, and abstracts the IPC details from the rest of the system. + */ +@SysUISingleton +class GlanceableHubWidgetManager +@Inject +constructor( + glanceableHubMultiUserHelper: GlanceableHubMultiUserHelper, + @CommunalLog logBuffer: LogBuffer, + serviceWatcherFactory: ServiceWatcherFactory<GlanceableHubWidgetManagerServiceInfo?>, +) : ServiceListener<GlanceableHubWidgetManagerServiceInfo?> { + + init { + // The manager should only be used in the headless system user. + glanceableHubMultiUserHelper.assertInHeadlessSystemUser() + } + + private val logger = Logger(logBuffer, TAG) + + private val serviceWatcher by lazy { serviceWatcherFactory.create(this) } + + val widgets = conflatedCallbackFlow { + val callback = + object : IGlanceableHubWidgetsListener.Stub() { + override fun onWidgetsUpdated(widgets: List<CommunalWidgetContentModel>?) { + trySend(widgets ?: emptyList()) + } + } + runOnService { service -> service.addWidgetsListener(callback) } + awaitClose { runOnService { service -> service.removeWidgetsListener(callback) } } + } + + fun register() { + serviceWatcher.register() + } + + fun unregister() { + serviceWatcher.unregister() + } + + override fun onBind(binder: IBinder?, serviceInfo: GlanceableHubWidgetManagerServiceInfo?) { + logger.i("Service bound") + } + + override fun onUnbind() { + logger.i("Service unbound") + } + + /** Requests the foreground user to set a [AppWidgetHostListener] for the given app widget. */ + fun setAppWidgetHostListener(appWidgetId: Int, listener: AppWidgetHostListener) = + runOnService { service -> + service.setAppWidgetHostListener(appWidgetId, createIAppWidgetHostListener(listener)) + } + + /** Requests the foreground user to add a widget. */ + fun addWidget( + provider: ComponentName, + user: UserHandle, + rank: Int?, + configurator: WidgetConfigurator?, + ) = runOnService { service -> + // TODO(b/375036327): Add support for widget configuration + service.addWidget(provider, user, rank ?: -1) + } + + /** Requests the foreground user to delete a widget. */ + fun deleteWidget(appWidgetId: Int) = runOnService { service -> + service.deleteWidget(appWidgetId) + } + + /** Requests the foreground user to update widget order. */ + fun updateWidgetOrder(widgetIdToRankMap: Map<Int, Int>) = runOnService { service -> + service.updateWidgetOrder( + widgetIdToRankMap.keys.toIntArray(), + widgetIdToRankMap.values.toIntArray(), + ) + } + + /** Requests the foreground user to resize a widget. */ + fun resizeWidget(appWidgetId: Int, spanY: Int, widgetIdToRankMap: Map<Int, Int>) = + runOnService { service -> + service.resizeWidget( + appWidgetId, + spanY, + widgetIdToRankMap.keys.toIntArray(), + widgetIdToRankMap.values.toIntArray(), + ) + } + + private fun runOnService(block: (IGlanceableHubWidgetManagerService) -> Unit) { + serviceWatcher.runOnBinder( + object : ServiceWatcher.BinderOperation { + override fun run(binder: IBinder?) { + block(IGlanceableHubWidgetManagerService.Stub.asInterface(binder)) + } + + override fun onError(t: Throwable?) { + // TODO(b/375236794): handle failure in case service is unbound + } + } + ) + } + + private fun createIAppWidgetHostListener( + listener: AppWidgetHostListener + ): IAppWidgetHostListener { + return object : IAppWidgetHostListener.Stub() { + override fun onUpdateProviderInfo(appWidget: AppWidgetProviderInfo?) { + listener.onUpdateProviderInfo(appWidget) + } + + override fun updateAppWidget(views: RemoteViews?) { + listener.updateAppWidget(views) + } + + override fun updateAppWidgetDeferred(packageName: String?, appWidgetId: Int) { + listener.updateAppWidgetDeferred(packageName, appWidgetId) + } + + override fun onViewDataChanged(viewId: Int) { + listener.onViewDataChanged(viewId) + } + } + } + + companion object { + private const val TAG = "GlanceableHubWidgetManager" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/GlanceableHubWidgetManagerService.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/GlanceableHubWidgetManagerService.kt new file mode 100644 index 000000000000..4d042fc277de --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/GlanceableHubWidgetManagerService.kt @@ -0,0 +1,305 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.widgets + +import android.appwidget.AppWidgetHost.AppWidgetHostListener +import android.appwidget.AppWidgetProviderInfo +import android.content.ComponentName +import android.content.Intent +import android.os.IBinder +import android.os.RemoteCallbackList +import android.os.UserHandle +import android.widget.RemoteViews +import androidx.lifecycle.LifecycleService +import androidx.lifecycle.lifecycleScope +import com.android.systemui.communal.data.repository.CommunalWidgetRepository +import com.android.systemui.communal.shared.model.GlanceableHubMultiUserHelper +import com.android.systemui.communal.widgets.IGlanceableHubWidgetManagerService.IAppWidgetHostListener +import com.android.systemui.communal.widgets.IGlanceableHubWidgetManagerService.IGlanceableHubWidgetsListener +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.core.Logger +import com.android.systemui.log.dagger.CommunalLog +import javax.inject.Inject +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach + +/** + * Service for the [GlanceableHubWidgetManager], which runs in a foreground user in Headless System + * User Mode (HSUM), manages widgets as the user who owns them, and communicates back to the + * headless system user, where these widgets are rendered. + */ +class GlanceableHubWidgetManagerService +@Inject +constructor( + private val widgetRepository: CommunalWidgetRepository, + private val appWidgetHost: CommunalAppWidgetHost, + private val communalWidgetHost: CommunalWidgetHost, + glanceableHubMultiUserHelper: GlanceableHubMultiUserHelper, + @CommunalLog logBuffer: LogBuffer, +) : LifecycleService() { + + init { + // The service should only run in a foreground user. + glanceableHubMultiUserHelper.assertNotInHeadlessSystemUser() + } + + private val logger = Logger(logBuffer, TAG) + private val widgetListenersRegistry = WidgetListenerRegistry() + + override fun onCreate() { + super.onCreate() + + logger.i("Service created") + + communalWidgetHost.startObservingHost() + appWidgetHost.startListening() + } + + override fun onDestroy() { + super.onDestroy() + + logger.i("Service destroyed") + + appWidgetHost.stopListening() + communalWidgetHost.stopObservingHost() + + // Cancel all widget listener jobs and unregister listeners + widgetListenersRegistry.kill() + } + + override fun onBind(intent: Intent): IBinder? { + super.onBind(intent) + return WidgetManagerServiceBinder().asBinder() + } + + private fun addWidgetsListenerInternal(listener: IGlanceableHubWidgetsListener?) { + if (listener == null) { + throw IllegalStateException("Listener cannot be null") + } + + if (!listener.asBinder().isBinderAlive) { + throw IllegalStateException("Listener binder is dead") + } + + val job = + widgetRepository.communalWidgets + .onEach { widgets -> listener.onWidgetsUpdated(widgets) } + .launchIn(lifecycleScope) + widgetListenersRegistry.register(listener, job) + } + + private fun removeWidgetsListenerInternal(listener: IGlanceableHubWidgetsListener?) { + if (listener == null) { + throw IllegalStateException("Listener cannot be null") + } + + widgetListenersRegistry.unregister(listener) + } + + private fun setAppWidgetHostListenerInternal( + appWidgetId: Int, + listener: IAppWidgetHostListener?, + ) { + if (listener == null) { + throw IllegalStateException("Listener cannot be null") + } + + appWidgetHost.setListener(appWidgetId, createListener(listener)) + } + + private fun addWidgetInternal(provider: ComponentName?, user: UserHandle?, rank: Int) { + if (provider == null) { + throw IllegalStateException("Provider cannot be null") + } + + if (user == null) { + throw IllegalStateException("User cannot be null") + } + + // TODO(b/375036327): Add support for widget configuration + widgetRepository.addWidget(provider, user, rank, configurator = null) + } + + private fun deleteWidgetInternal(appWidgetId: Int) { + widgetRepository.deleteWidget(appWidgetId) + } + + private fun updateWidgetOrderInternal(appWidgetIds: IntArray?, ranks: IntArray?) { + if (appWidgetIds == null || ranks == null) { + throw IllegalStateException("appWidgetIds and ranks cannot be null") + } + + if (appWidgetIds.size != ranks.size) { + throw IllegalStateException("appWidgetIds and ranks must be the same size") + } + + widgetRepository.updateWidgetOrder(appWidgetIds.zip(ranks).toMap()) + } + + private fun resizeWidgetInternal( + appWidgetId: Int, + spanY: Int, + appWidgetIds: IntArray?, + ranks: IntArray?, + ) { + if (appWidgetIds == null || ranks == null) { + throw IllegalStateException("appWidgetIds and ranks cannot be null") + } + + if (appWidgetIds.size != ranks.size) { + throw IllegalStateException("appWidgetIds and ranks must be the same size") + } + + widgetRepository.resizeWidget(appWidgetId, spanY, appWidgetIds.zip(ranks).toMap()) + } + + private fun createListener(listener: IAppWidgetHostListener): AppWidgetHostListener { + return object : AppWidgetHostListener { + override fun onUpdateProviderInfo(appWidget: AppWidgetProviderInfo?) { + listener.onUpdateProviderInfo(appWidget) + } + + override fun updateAppWidget(views: RemoteViews?) { + listener.updateAppWidget(views) + } + + override fun updateAppWidgetDeferred(packageName: String?, appWidgetId: Int) { + listener.updateAppWidgetDeferred(packageName, appWidgetId) + } + + override fun onViewDataChanged(viewId: Int) { + listener.onViewDataChanged(viewId) + } + } + } + + private inner class WidgetManagerServiceBinder : IGlanceableHubWidgetManagerService.Stub() { + override fun addWidgetsListener(listener: IGlanceableHubWidgetsListener?) { + val iden = clearCallingIdentity() + + try { + addWidgetsListenerInternal(listener) + } finally { + restoreCallingIdentity(iden) + } + } + + override fun removeWidgetsListener(listener: IGlanceableHubWidgetsListener?) { + val iden = clearCallingIdentity() + + try { + removeWidgetsListenerInternal(listener) + } finally { + restoreCallingIdentity(iden) + } + } + + override fun setAppWidgetHostListener(appWidgetId: Int, listener: IAppWidgetHostListener?) { + val iden = clearCallingIdentity() + + try { + setAppWidgetHostListenerInternal(appWidgetId, listener) + } finally { + restoreCallingIdentity(iden) + } + } + + override fun addWidget(provider: ComponentName?, user: UserHandle?, rank: Int) { + val iden = clearCallingIdentity() + + try { + addWidgetInternal(provider, user, rank) + } finally { + restoreCallingIdentity(iden) + } + } + + override fun deleteWidget(appWidgetId: Int) { + val iden = clearCallingIdentity() + + try { + deleteWidgetInternal(appWidgetId) + } finally { + restoreCallingIdentity(iden) + } + } + + override fun updateWidgetOrder(appWidgetIds: IntArray?, ranks: IntArray?) { + val iden = clearCallingIdentity() + + try { + updateWidgetOrderInternal(appWidgetIds, ranks) + } finally { + restoreCallingIdentity(iden) + } + } + + override fun resizeWidget( + appWidgetId: Int, + spanY: Int, + appWidgetIds: IntArray?, + ranks: IntArray?, + ) { + val iden = clearCallingIdentity() + + try { + resizeWidgetInternal(appWidgetId, spanY, appWidgetIds, ranks) + } finally { + restoreCallingIdentity(iden) + } + } + } + + /** + * Registry of widget listener binders, which handles canceling the job associated with a + * listener when it is unregistered, or when the binder is dead. + */ + private class WidgetListenerRegistry : RemoteCallbackList<IGlanceableHubWidgetsListener>() { + private val jobs = mutableMapOf<IGlanceableHubWidgetsListener, Job>() + + fun register(listener: IGlanceableHubWidgetsListener, job: Job) { + if (register(listener)) { + synchronized(jobs) { jobs[listener] = job } + } else { + job.cancel() + } + } + + override fun unregister(listener: IGlanceableHubWidgetsListener?): Boolean { + synchronized(jobs) { jobs.remove(listener)?.cancel() } + return super.unregister(listener) + } + + override fun onCallbackDied(listener: IGlanceableHubWidgetsListener?) { + synchronized(jobs) { jobs.remove(listener)?.cancel() } + super.onCallbackDied(listener) + } + + override fun kill() { + synchronized(jobs) { + jobs.values.forEach { it.cancel() } + jobs.clear() + } + super.kill() + } + } + + companion object { + private const val TAG = "GlanceableHubWidgetManagerService" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/GlanceableHubWidgetManagerServiceInfo.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/GlanceableHubWidgetManagerServiceInfo.kt new file mode 100644 index 000000000000..d52761164a38 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/GlanceableHubWidgetManagerServiceInfo.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.communal.widgets + +import android.content.ComponentName +import android.content.Context +import android.content.Context.BIND_AUTO_CREATE +import android.os.UserHandle +import com.android.server.servicewatcher.ServiceWatcher + +/** + * Information about the [GlanceableHubWidgetManagerServiceInfo] used for binding with the + * [ServiceWatcher]. + */ +class GlanceableHubWidgetManagerServiceInfo(context: Context, userHandle: UserHandle) : + ServiceWatcher.BoundServiceInfo( + /* action= */ null, + UserHandle.getUid(userHandle.identifier, UserHandle.getCallingAppId()), + ComponentName(context.packageName, GlanceableHubWidgetManagerService::class.java.name), + BIND_AUTO_CREATE, + ) diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/GlanceableHubWidgetManagerServiceSupplier.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/GlanceableHubWidgetManagerServiceSupplier.kt new file mode 100644 index 000000000000..ed77e6f5d7ea --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/GlanceableHubWidgetManagerServiceSupplier.kt @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.widgets + +import android.content.Context +import com.android.server.servicewatcher.ServiceWatcher +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.settings.UserTracker +import java.util.concurrent.Executor +import javax.inject.Inject + +/** + * Supplies details about the [GlanceableHubWidgetManagerService] that the [ServiceWatcher] should + * bind to. Currently the service should only be bound if the current user is the main user. + */ +@SysUISingleton +class GlanceableHubWidgetManagerServiceSupplier +@Inject +constructor( + @Application private val context: Context, + @Background private val bgExecutor: Executor, + private val userTracker: UserTracker, +) : ServiceWatcher.ServiceSupplier<GlanceableHubWidgetManagerServiceInfo?>, UserTracker.Callback { + + private var userAboutToSwitch = false + private var listener: ServiceWatcher.ServiceChangedListener? = null + + override fun getServiceInfo(): GlanceableHubWidgetManagerServiceInfo? { + return GlanceableHubWidgetManagerServiceInfo(context, userTracker.userHandle) + } + + override fun hasMatchingService(): Boolean { + // The service becomes unavailable immediately before a user switching is about to happen + // so that it is disconnected before the user process is terminated. + // It is also only available if the current user is the main user. + return !userAboutToSwitch && userTracker.userInfo.isMain + } + + override fun register(listener: ServiceWatcher.ServiceChangedListener?) { + this.listener = listener + userTracker.addCallback(this, bgExecutor) + } + + override fun unregister() { + listener = null + userTracker.removeCallback(this) + } + + override fun onBeforeUserSwitching(newUser: Int) { + userAboutToSwitch = true + listener?.onServiceChanged() + } + + override fun onUserChanged(newUser: Int, userContext: Context) { + userAboutToSwitch = false + listener?.onServiceChanged() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/IGlanceableHubWidgetManagerService.aidl b/packages/SystemUI/src/com/android/systemui/communal/widgets/IGlanceableHubWidgetManagerService.aidl new file mode 100644 index 000000000000..e556472c78a7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/IGlanceableHubWidgetManagerService.aidl @@ -0,0 +1,51 @@ +package com.android.systemui.communal.widgets; + +import android.appwidget.AppWidgetProviderInfo; +import android.content.ComponentName; +import android.os.UserHandle; +import android.widget.RemoteViews; +import com.android.systemui.communal.shared.model.CommunalWidgetContentModel; +import java.util.List; + +// Interface for the [GlanceableHubWidgetManagerService], which runs in a foreground user in HSUM +// and communicates with the headless system user. +interface IGlanceableHubWidgetManagerService { + + // Adds a listener for updates of Glanceable Hub widgets. + oneway void addWidgetsListener(in IGlanceableHubWidgetsListener listener); + + // Removes a listener for updates of Glanceable Hub widgets. + oneway void removeWidgetsListener(in IGlanceableHubWidgetsListener listener); + + // Sets a listener for updates on a specific widget. + oneway void setAppWidgetHostListener(int appWidgetId, in IAppWidgetHostListener listener); + + // Requests to add a widget in the Glanceable Hub. + oneway void addWidget(in ComponentName provider, in UserHandle user, int rank); + + // Requests to delete a widget from the Glanceable Hub. + oneway void deleteWidget(int appWidgetId); + + // Requests to update the order of widgets in the Glanceable Hub. + oneway void updateWidgetOrder(in int[] appWidgetIds, in int[] ranks); + + // Requests to resize a widget in the Glanceable Hub. + oneway void resizeWidget(int appWidgetId, int spanY, in int[] appWidgetIds, in int[] ranks); + + // Listener for Glanceable Hub widget updates + oneway interface IGlanceableHubWidgetsListener { + // Called when widgets have updated. + void onWidgetsUpdated(in List<CommunalWidgetContentModel> widgets); + } + + // Mirrors [AppWidgetHost#AppWidgetHostListener]. + oneway interface IAppWidgetHostListener { + void onUpdateProviderInfo(in @nullable AppWidgetProviderInfo appWidget); + + void updateAppWidget(in @nullable RemoteViews views); + + void updateAppWidgetDeferred(in String packageName, int appWidgetId); + + void onViewDataChanged(int viewId); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/ServiceWatcherFactory.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/ServiceWatcherFactory.kt new file mode 100644 index 000000000000..f79cc87b19cb --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/ServiceWatcherFactory.kt @@ -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.systemui.communal.widgets + +import android.content.Context +import android.os.Handler +import com.android.server.servicewatcher.ServiceWatcher +import com.android.server.servicewatcher.ServiceWatcher.BoundServiceInfo +import com.android.server.servicewatcher.ServiceWatcher.ServiceSupplier +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import javax.inject.Inject + +/** Factory for creating a [ServiceWatcher]. */ +interface ServiceWatcherFactory<TBoundService : BoundServiceInfo?> { + fun create(listener: ServiceWatcher.ServiceListener<TBoundService?>): ServiceWatcher +} + +/** Implementation of the [ServiceWatcherFactory] for the [GlanceableHubWidgetManagerService]. */ +@SysUISingleton +class GlanceableHubWidgetManagerServiceWatcherFactoryImpl +@Inject +constructor( + @Application private val context: Context, + @Background private val handler: Handler, + private val supplier: ServiceSupplier<GlanceableHubWidgetManagerServiceInfo?>, +) : ServiceWatcherFactory<GlanceableHubWidgetManagerServiceInfo?> { + + override fun create( + listener: ServiceWatcher.ServiceListener<GlanceableHubWidgetManagerServiceInfo?> + ): ServiceWatcher { + return ServiceWatcher.create( + context, + handler, + GlanceableHubWidgetManagerService::class.java.simpleName, + supplier, + listener, + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetConfigurationController.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetConfigurationController.kt index 3e68479e717a..d157cd7acb76 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetConfigurationController.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetConfigurationController.kt @@ -21,8 +21,10 @@ import android.app.ActivityOptions import android.content.ActivityNotFoundException import android.window.SplashScreen import androidx.activity.ComponentActivity +import com.android.systemui.communal.shared.model.GlanceableHubMultiUserHelper import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.util.nullableAtomicReference +import dagger.Lazy import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -38,8 +40,9 @@ class WidgetConfigurationController @AssistedInject constructor( @Assisted private val activity: ComponentActivity, - private val appWidgetHost: CommunalAppWidgetHost, - @Background private val bgDispatcher: CoroutineDispatcher + private val appWidgetHostLazy: Lazy<CommunalAppWidgetHost>, + @Background private val bgDispatcher: CoroutineDispatcher, + private val glanceableHubMultiUserHelper: GlanceableHubMultiUserHelper, ) : WidgetConfigurator { @AssistedFactory fun interface Factory { @@ -62,13 +65,21 @@ constructor( } try { - appWidgetHost.startAppWidgetConfigureActivityForResult( - activity, - appWidgetId, - 0, - REQUEST_CODE, - options.toBundle() - ) + // TODO(b/375036327): Add support for widget configuration + if ( + !glanceableHubMultiUserHelper.glanceableHubHsumFlagEnabled || + !glanceableHubMultiUserHelper.isInHeadlessSystemUser() + ) { + with(appWidgetHostLazy.get()) { + startAppWidgetConfigureActivityForResult( + activity, + appWidgetId, + 0, + REQUEST_CODE, + options.toBundle(), + ) + } + } } catch (e: ActivityNotFoundException) { setConfigurationResult(Activity.RESULT_CANCELED) } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/DefaultServiceBinder.java b/packages/SystemUI/src/com/android/systemui/dagger/DefaultServiceBinder.java index 904d5898fcf8..8f01775bc969 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/DefaultServiceBinder.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/DefaultServiceBinder.java @@ -19,6 +19,7 @@ package com.android.systemui.dagger; import android.app.Service; import com.android.systemui.SystemUIService; +import com.android.systemui.communal.widgets.GlanceableHubWidgetManagerService; import com.android.systemui.doze.DozeService; import com.android.systemui.dreams.DreamOverlayService; import com.android.systemui.dump.SystemUIAuxiliaryDumpService; @@ -93,4 +94,10 @@ public abstract class DefaultServiceBinder { @ClassKey(IssueRecordingService.class) public abstract Service bindIssueRecordingService(IssueRecordingService service); + /** Inject into GlanceableHubWidgetManagerService */ + @Binds + @IntoMap + @ClassKey(GlanceableHubWidgetManagerService.class) + public abstract Service bindGlanceableHubWidgetManagerService( + GlanceableHubWidgetManagerService service); } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java index bf93469f6c70..609b7330b600 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSystemUIModule.java @@ -72,6 +72,7 @@ import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.SysuiStatusBarStateController; import com.android.systemui.statusbar.dagger.CentralSurfacesModule; import com.android.systemui.statusbar.dagger.StartCentralSurfacesModule; +import com.android.systemui.statusbar.notification.dagger.ReferenceNotificationsModule; import com.android.systemui.statusbar.phone.CentralSurfaces; import com.android.systemui.statusbar.phone.DozeServiceHost; import com.android.systemui.statusbar.phone.HeadsUpModule; @@ -145,6 +146,7 @@ import javax.inject.Named; QSModule.class, RearDisplayModule.class, RecentsModule.class, + ReferenceNotificationsModule.class, ReferenceScreenshotModule.class, RotationLockModule.class, RotationLockNewModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt index 6fb6236a4ed3..4cdf28670eab 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt @@ -29,7 +29,7 @@ import com.android.systemui.controls.dagger.StartControlsStartableModule import com.android.systemui.dagger.qualifiers.PerUser import com.android.systemui.dreams.AssistantAttentionMonitor import com.android.systemui.dreams.DreamMonitor -import com.android.systemui.dreams.homecontrols.HomeControlsDreamStartable +import com.android.systemui.dreams.homecontrols.system.HomeControlsDreamStartable import com.android.systemui.globalactions.GlobalActionsComponent import com.android.systemui.haptics.msdl.MSDLCoreStartable import com.android.systemui.keyboard.KeyboardUI diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index 59c8f06a40ad..cb649f28f95b 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -70,6 +70,9 @@ import com.android.systemui.haptics.msdl.dagger.MSDLModule; import com.android.systemui.inputmethod.InputMethodModule; import com.android.systemui.keyboard.KeyboardModule; import com.android.systemui.keyevent.data.repository.KeyEventRepositoryModule; +import com.android.systemui.keyguard.data.quickaffordance.KeyguardDataQuickAffordanceModule; +import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancesMetricsLogger; +import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancesMetricsLoggerImpl; import com.android.systemui.keyguard.ui.composable.LockscreenContent; import com.android.systemui.log.dagger.LogModule; import com.android.systemui.log.dagger.MonitorLog; @@ -227,6 +230,7 @@ import javax.inject.Named; InputMethodModule.class, KeyEventRepositoryModule.class, KeyboardModule.class, + KeyguardDataQuickAffordanceModule.class, LetterboxModule.class, LogModule.class, MediaProjectionActivitiesModule.class, @@ -428,6 +432,11 @@ public abstract class SystemUIModule { sysuiUiBgExecutor)); } + @Provides + static KeyguardQuickAffordancesMetricsLogger providesKeyguardQuickAffordancesMetricsLogger() { + return new KeyguardQuickAffordancesMetricsLoggerImpl(); + } + @Binds abstract FgsManagerController bindFgsManagerController(FgsManagerControllerImpl impl); diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt index 782bce494d11..41a59a959771 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt @@ -19,10 +19,12 @@ import com.android.keyguard.logging.BiometricUnlockLogger import com.android.systemui.biometrics.data.repository.FingerprintPropertyRepository import com.android.systemui.biometrics.shared.model.FingerprintSensorType import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dump.DumpManager import com.android.systemui.keyevent.domain.interactor.KeyEventInteractor import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.power.shared.model.WakeSleepReason +import com.android.systemui.util.kotlin.FlowDumperImpl import com.android.systemui.util.kotlin.sample import com.android.systemui.util.time.SystemClock import javax.inject.Inject @@ -46,16 +48,17 @@ import kotlinx.coroutines.flow.onStart class DeviceEntryHapticsInteractor @Inject constructor( - deviceEntrySourceInteractor: DeviceEntrySourceInteractor, - deviceEntryFingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor, + biometricSettingsRepository: BiometricSettingsRepository, deviceEntryBiometricAuthInteractor: DeviceEntryBiometricAuthInteractor, + deviceEntryFingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor, + deviceEntrySourceInteractor: DeviceEntrySourceInteractor, fingerprintPropertyRepository: FingerprintPropertyRepository, - biometricSettingsRepository: BiometricSettingsRepository, keyEventInteractor: KeyEventInteractor, + private val logger: BiometricUnlockLogger, powerInteractor: PowerInteractor, private val systemClock: SystemClock, - private val logger: BiometricUnlockLogger, -) { + dumpManager: DumpManager, +) : FlowDumperImpl(dumpManager) { private val powerButtonSideFpsEnrolled = combineTransform( fingerprintPropertyRepository.sensorType, @@ -86,7 +89,7 @@ constructor( powerButtonSideFpsEnrolled, powerButtonDown, lastPowerButtonWakeup, - ::Triple + ::Triple, ) ) .filter { (sideFpsEnrolled, powerButtonDown, lastPowerButtonWakeup) -> @@ -100,14 +103,19 @@ constructor( } allowHaptic } - .map {} // map to Unit + // map to Unit + .map {} + .dumpWhileCollecting("playSuccessHaptic") private val playErrorHapticForBiometricFailure: Flow<Unit> = merge( deviceEntryFingerprintAuthInteractor.fingerprintFailure, deviceEntryBiometricAuthInteractor.faceOnlyFaceFailure, ) - .map {} // map to Unit + // map to Unit + .map {} + .dumpWhileCollecting("playErrorHapticForBiometricFailure") + val playErrorHaptic: Flow<Unit> = playErrorHapticForBiometricFailure .sample(combine(powerButtonSideFpsEnrolled, powerButtonDown, ::Pair)) @@ -118,7 +126,9 @@ constructor( } allowHaptic } - .map {} // map to Unit + // map to Unit + .map {} + .dumpWhileCollecting("playErrorHaptic") private val recentPowerButtonPressThresholdMs = 400L } diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntrySourceInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntrySourceInteractor.kt index 0b9336fec946..b2d4405ee6be 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntrySourceInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntrySourceInteractor.kt @@ -16,18 +16,49 @@ package com.android.systemui.deviceentry.domain.interactor +import androidx.annotation.VisibleForTesting +import com.android.keyguard.KeyguardUpdateMonitor +import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor +import com.android.systemui.biometrics.AuthController +import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.deviceentry.shared.model.SuccessFaceAuthenticationStatus +import com.android.systemui.dump.DumpManager +import com.android.systemui.keyguard.domain.interactor.KeyguardBypassInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.shared.model.BiometricUnlockMode +import com.android.systemui.keyguard.shared.model.BiometricUnlockModel import com.android.systemui.keyguard.shared.model.BiometricUnlockSource +import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus +import com.android.systemui.scene.domain.interactor.SceneContainerOcclusionInteractor +import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.scene.shared.flag.SceneContainerFlag +import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_DISMISS_BOUNCER +import com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_NONE +import com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_ONLY_WAKE +import com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_SHOW_BOUNCER +import com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_UNLOCK_COLLAPSING +import com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK +import com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK_FROM_DREAM +import com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK_PULSING +import com.android.systemui.statusbar.phone.BiometricUnlockController.WakeAndUnlockMode +import com.android.systemui.statusbar.phone.DozeScrimController +import com.android.systemui.util.kotlin.FlowDumperImpl +import com.android.systemui.util.kotlin.Utils.Companion.sampleFilter +import com.android.systemui.util.kotlin.combine import com.android.systemui.util.kotlin.sample import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge /** * Hosts application business logic related to the source of the user entering the device. Note: The @@ -42,13 +73,184 @@ import kotlinx.coroutines.flow.map class DeviceEntrySourceInteractor @Inject constructor( + authenticationInteractor: AuthenticationInteractor, + authController: AuthController, + alternateBouncerInteractor: AlternateBouncerInteractor, + deviceEntryFaceAuthInteractor: DeviceEntryFaceAuthInteractor, + deviceEntryFingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor, + dozeScrimController: DozeScrimController, + keyguardBypassInteractor: KeyguardBypassInteractor, + keyguardUpdateMonitor: KeyguardUpdateMonitor, keyguardInteractor: KeyguardInteractor, -) { + sceneContainerOcclusionInteractor: SceneContainerOcclusionInteractor, + sceneInteractor: SceneInteractor, + dumpManager: DumpManager, +) : FlowDumperImpl(dumpManager) { + private val isShowingBouncerScene: Flow<Boolean> = + sceneInteractor.transitionState + .map { transitionState -> + transitionState.isIdle(Scenes.Bouncer) || + transitionState.isTransitioning(null, Scenes.Bouncer) + } + .distinctUntilChanged() + .dumpWhileCollecting("isShowingBouncerScene") + + private val isUnlockedWithStrongFaceUnlock = + deviceEntryFaceAuthInteractor.authenticationStatus + .map { status -> + (status as? SuccessFaceAuthenticationStatus)?.successResult?.isStrongBiometric + ?: false + } + .dumpWhileCollecting("unlockedWithStrongFaceUnlock") + + private val isUnlockedWithStrongFingerprintUnlock = + deviceEntryFingerprintAuthInteractor.authenticationStatus + .map { status -> + (status as? SuccessFingerprintAuthenticationStatus)?.isStrongBiometric ?: false + } + .dumpWhileCollecting("unlockedWithStrongFingerprintUnlock") + + private val faceWakeAndUnlockMode: Flow<BiometricUnlockMode> = + combine( + alternateBouncerInteractor.isVisible, + keyguardBypassInteractor.isBypassAvailable, + isUnlockedWithStrongFaceUnlock, + sceneContainerOcclusionInteractor.isOccludingActivityShown, + sceneInteractor.currentScene, + isShowingBouncerScene, + ) { + isAlternateBouncerVisible, + isBypassAvailable, + isFaceStrongBiometric, + isOccluded, + currentScene, + isShowingBouncerScene -> + val isUnlockingAllowed = + keyguardUpdateMonitor.isUnlockingWithBiometricAllowed(isFaceStrongBiometric) + val bypass = isBypassAvailable || authController.isUdfpsFingerDown() + + when { + !keyguardUpdateMonitor.isDeviceInteractive -> + when { + !isUnlockingAllowed -> if (bypass) MODE_SHOW_BOUNCER else MODE_NONE + bypass -> MODE_WAKE_AND_UNLOCK_PULSING + else -> MODE_ONLY_WAKE + } + + isUnlockingAllowed && currentScene == Scenes.Dream -> + if (bypass) MODE_WAKE_AND_UNLOCK_FROM_DREAM else MODE_ONLY_WAKE + + isUnlockingAllowed && isOccluded -> MODE_UNLOCK_COLLAPSING + + (isShowingBouncerScene || isAlternateBouncerVisible) && isUnlockingAllowed -> + MODE_DISMISS_BOUNCER + + isUnlockingAllowed && bypass -> MODE_UNLOCK_COLLAPSING + + bypass -> MODE_SHOW_BOUNCER + + else -> MODE_NONE + } + } + .map { biometricModeIntToObject(it) } + .dumpWhileCollecting("faceWakeAndUnlockMode") + + private val fingerprintWakeAndUnlockMode: Flow<BiometricUnlockMode> = + combine( + alternateBouncerInteractor.isVisible, + authenticationInteractor.authenticationMethod, + sceneInteractor.currentScene, + isUnlockedWithStrongFingerprintUnlock, + isShowingBouncerScene, + ) { + alternateBouncerVisible, + authenticationMethod, + currentScene, + isFingerprintStrongBiometric, + isShowingBouncerScene -> + val unlockingAllowed = + keyguardUpdateMonitor.isUnlockingWithBiometricAllowed( + isFingerprintStrongBiometric + ) + when { + !keyguardUpdateMonitor.isDeviceInteractive -> + when { + dozeScrimController.isPulsing && unlockingAllowed -> + MODE_WAKE_AND_UNLOCK_PULSING + + unlockingAllowed || !authenticationMethod.isSecure -> + MODE_WAKE_AND_UNLOCK + + else -> MODE_SHOW_BOUNCER + } + + unlockingAllowed && currentScene == Scenes.Dream -> + MODE_WAKE_AND_UNLOCK_FROM_DREAM + + isShowingBouncerScene && unlockingAllowed -> MODE_DISMISS_BOUNCER + + unlockingAllowed -> MODE_UNLOCK_COLLAPSING + + currentScene != Scenes.Bouncer && !alternateBouncerVisible -> MODE_SHOW_BOUNCER + + else -> MODE_NONE + } + } + .map { biometricModeIntToObject(it) } + .dumpWhileCollecting("fingerprintWakeAndUnlockMode") + + @VisibleForTesting + private val biometricUnlockStateOnKeyguardDismissed = + merge( + fingerprintWakeAndUnlockMode + .filter { BiometricUnlockMode.dismissesKeyguard(it) } + .map { mode -> + BiometricUnlockModel(mode, BiometricUnlockSource.FINGERPRINT_SENSOR) + }, + faceWakeAndUnlockMode + .filter { BiometricUnlockMode.dismissesKeyguard(it) } + .map { mode -> BiometricUnlockModel(mode, BiometricUnlockSource.FACE_SENSOR) }, + ) + .dumpWhileCollecting("biometricUnlockState") + + private val deviceEntryFingerprintAuthWakeAndUnlockEvents: + Flow<SuccessFingerprintAuthenticationStatus> = + deviceEntryFingerprintAuthInteractor.authenticationStatus + .filterIsInstance<SuccessFingerprintAuthenticationStatus>() + .dumpWhileCollecting("deviceEntryFingerprintAuthSuccessEvents") + + private val deviceEntryFaceAuthWakeAndUnlockEvents: Flow<SuccessFaceAuthenticationStatus> = + deviceEntryFaceAuthInteractor.authenticationStatus + .filterIsInstance<SuccessFaceAuthenticationStatus>() + .sampleFilter( + combine( + sceneContainerOcclusionInteractor.isOccludingActivityShown, + keyguardBypassInteractor.isBypassAvailable, + keyguardBypassInteractor.canBypass, + ::Triple, + ) + ) { (isOccludingActivityShown, isBypassAvailable, canBypass) -> + isOccludingActivityShown || !isBypassAvailable || canBypass + } + .dumpWhileCollecting("deviceEntryFaceAuthSuccessEvents") + + private val deviceEntryBiometricAuthSuccessEvents = + merge(deviceEntryFingerprintAuthWakeAndUnlockEvents, deviceEntryFaceAuthWakeAndUnlockEvents) + .dumpWhileCollecting("deviceEntryBiometricAuthSuccessEvents") + val deviceEntryFromBiometricSource: Flow<BiometricUnlockSource> = - keyguardInteractor.biometricUnlockState - .filter { BiometricUnlockMode.dismissesKeyguard(it.mode) } - .map { it.source } - .filterNotNull() + if (SceneContainerFlag.isEnabled) { + deviceEntryBiometricAuthSuccessEvents + .sample(biometricUnlockStateOnKeyguardDismissed) + .map { it.source } + .filterNotNull() + } else { + keyguardInteractor.biometricUnlockState + .filter { BiometricUnlockMode.dismissesKeyguard(it.mode) } + .map { it.source } + .filterNotNull() + } + .dumpWhileCollecting("deviceEntryFromBiometricSource") private val attemptEnterDeviceFromDeviceEntryIcon: MutableSharedFlow<Unit> = MutableSharedFlow() val deviceEntryFromDeviceEntryIcon: Flow<Unit> = @@ -60,4 +262,18 @@ constructor( suspend fun attemptEnterDeviceFromDeviceEntryIcon() { attemptEnterDeviceFromDeviceEntryIcon.emit(Unit) } + + private fun biometricModeIntToObject(@WakeAndUnlockMode value: Int): BiometricUnlockMode { + return when (value) { + MODE_NONE -> BiometricUnlockMode.NONE + MODE_WAKE_AND_UNLOCK -> BiometricUnlockMode.WAKE_AND_UNLOCK + MODE_WAKE_AND_UNLOCK_PULSING -> BiometricUnlockMode.WAKE_AND_UNLOCK_PULSING + MODE_SHOW_BOUNCER -> BiometricUnlockMode.SHOW_BOUNCER + MODE_ONLY_WAKE -> BiometricUnlockMode.ONLY_WAKE + MODE_UNLOCK_COLLAPSING -> BiometricUnlockMode.UNLOCK_COLLAPSING + MODE_WAKE_AND_UNLOCK_FROM_DREAM -> BiometricUnlockMode.WAKE_AND_UNLOCK_FROM_DREAM + MODE_DISMISS_BOUNCER -> BiometricUnlockMode.DISMISS_BOUNCER + else -> throw IllegalArgumentException("Invalid BiometricUnlockModel value: $value") + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractor.kt index a20556ca7e02..15631f8d7abd 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/OccludingAppDeviceEntryInteractor.kt @@ -18,6 +18,7 @@ package com.android.systemui.deviceentry.domain.interactor import android.content.Context import android.content.Intent +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor @@ -34,6 +35,7 @@ import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus import com.android.systemui.plugins.ActivityStarter import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.res.R import com.android.systemui.util.kotlin.combine import com.android.systemui.util.kotlin.sample import javax.inject.Inject @@ -48,7 +50,6 @@ import kotlinx.coroutines.flow.filterNot import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map -import com.android.app.tracing.coroutines.launchTraced as launch /** Business logic for handling authentication events when an app is occluding the lockscreen. */ @ExperimentalCoroutinesApi @@ -123,19 +124,28 @@ constructor( .ifKeyguardOccludedByApp(/* elseFlow */ flowOf(null)) init { - scope.launch { - // On fingerprint success when the screen is on and not dreaming, go to the home screen - fingerprintUnlockSuccessEvents - .sample( - combine(powerInteractor.isInteractive, keyguardInteractor.isDreaming, ::Pair) - ) - .collect { (interactive, dreaming) -> - if (interactive && !dreaming) { - goToHomeScreen() + // This seems undesirable in most cases, except when a video is playing and can PiP when + // unlocked. It was originally added for tablets, so allow it there + if (context.resources.getBoolean(R.bool.config_goToHomeFromOccludedApps)) { + scope.launch { + // On fingerprint success when the screen is on and not dreaming, go to the home + // screen + fingerprintUnlockSuccessEvents + .sample( + combine( + powerInteractor.isInteractive, + keyguardInteractor.isDreaming, + ::Pair, + ) + ) + .collect { (interactive, dreaming) -> + if (interactive && !dreaming) { + goToHomeScreen() + } + // don't go to the home screen if the authentication is from + // AOD/dozing/off/dreaming } - // don't go to the home screen if the authentication is from - // AOD/dozing/off/dreaming - } + } } scope.launch { diff --git a/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepository.kt b/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepository.kt index 7253621237f6..80eb9eea6c9b 100644 --- a/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepository.kt @@ -19,7 +19,9 @@ package com.android.systemui.display.data.repository import android.annotation.SuppressLint import android.content.Context import android.view.Display +import android.view.LayoutInflater import android.view.WindowManager +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background @@ -30,9 +32,7 @@ import com.google.common.collect.HashBasedTable import com.google.common.collect.Table import java.io.PrintWriter import javax.inject.Inject -import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineScope -import com.android.app.tracing.coroutines.launchTraced as launch /** Provides per display instances of [DisplayWindowProperties]. */ interface DisplayWindowPropertiesRepository { @@ -55,6 +55,7 @@ constructor( @Background private val backgroundApplicationScope: CoroutineScope, private val globalContext: Context, private val globalWindowManager: WindowManager, + private val globalLayoutInflater: LayoutInflater, private val displayRepository: DisplayRepository, ) : DisplayWindowPropertiesRepository, CoreStartable { @@ -93,12 +94,14 @@ constructor( windowType = windowType, context = globalContext, windowManager = globalWindowManager, + layoutInflater = globalLayoutInflater, ) } else { val context = createWindowContext(display, windowType) @SuppressLint("NonInjectedService") // Need to manually get the service val windowManager = context.getSystemService(WindowManager::class.java) as WindowManager - DisplayWindowProperties(displayId, windowType, context, windowManager) + val layoutInflater = LayoutInflater.from(context) + DisplayWindowProperties(displayId, windowType, context, windowManager, layoutInflater) } } diff --git a/packages/SystemUI/src/com/android/systemui/display/shared/model/DisplayWindowProperties.kt b/packages/SystemUI/src/com/android/systemui/display/shared/model/DisplayWindowProperties.kt index 6acc296367a9..3f1c0881df58 100644 --- a/packages/SystemUI/src/com/android/systemui/display/shared/model/DisplayWindowProperties.kt +++ b/packages/SystemUI/src/com/android/systemui/display/shared/model/DisplayWindowProperties.kt @@ -17,6 +17,7 @@ package com.android.systemui.display.shared.model import android.content.Context +import android.view.LayoutInflater import android.view.WindowManager /** Represents a display specific group of window related properties. */ @@ -40,4 +41,7 @@ data class DisplayWindowProperties( * associated with this instance. */ val windowManager: WindowManager, + + /** The [LayoutInflater] to be used with the associated [Context]. */ + val layoutInflater: LayoutInflater, ) diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java index a45ad157837b..3171bbc6d6b0 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java @@ -33,9 +33,10 @@ import com.android.systemui.dreams.DreamOverlayNotificationCountProvider; import com.android.systemui.dreams.DreamOverlayService; import com.android.systemui.dreams.SystemDialogsCloser; import com.android.systemui.dreams.complication.dagger.ComplicationComponent; -import com.android.systemui.dreams.homecontrols.DreamServiceDelegate; -import com.android.systemui.dreams.homecontrols.DreamServiceDelegateImpl; import com.android.systemui.dreams.homecontrols.HomeControlsDreamService; +import com.android.systemui.dreams.homecontrols.dagger.HomeControlsDataSourceModule; +import com.android.systemui.dreams.homecontrols.dagger.HomeControlsRemoteServiceComponent; +import com.android.systemui.dreams.homecontrols.system.HomeControlsRemoteService; import com.android.systemui.qs.QsEventLogger; import com.android.systemui.qs.pipeline.shared.TileSpec; import com.android.systemui.qs.shared.model.TileCategory; @@ -61,13 +62,15 @@ import javax.inject.Named; * Dagger Module providing Dream-related functionality. */ @Module(includes = { - RegisteredComplicationsModule.class, - LowLightDreamModule.class, - ScrimModule.class - }, + RegisteredComplicationsModule.class, + LowLightDreamModule.class, + ScrimModule.class, + HomeControlsDataSourceModule.class, +}, subcomponents = { - ComplicationComponent.class, - DreamOverlayComponent.class, + ComplicationComponent.class, + DreamOverlayComponent.class, + HomeControlsRemoteServiceComponent.class, }) public interface DreamModule { String DREAM_ONLY_ENABLED_FOR_DOCK_USER = "dream_only_enabled_for_dock_user"; @@ -113,6 +116,15 @@ public interface DreamModule { HomeControlsDreamService service); /** + * Provides Home Controls Remote Service + */ + @Binds + @IntoMap + @ClassKey(HomeControlsRemoteService.class) + Service bindHomeControlsRemoteService( + HomeControlsRemoteService service); + + /** * Provides a touch inset manager for dreams. */ @Provides @@ -202,10 +214,4 @@ public interface DreamModule { QSTilePolicy.NoRestrictions.INSTANCE ); } - - - /** Provides delegate to allow for testing of dream service */ - @Binds - DreamServiceDelegate bindDreamDelegate(DreamServiceDelegateImpl impl); - } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamServiceDelegate.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamServiceDelegate.kt deleted file mode 100644 index 2cfb02eadd19..000000000000 --- a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamServiceDelegate.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.systemui.dreams.homecontrols - -import android.app.Activity -import android.service.dreams.DreamService - -/** Provides abstraction for [DreamService] methods, so they can be mocked in tests. */ -interface DreamServiceDelegate { - /** Wrapper for [DreamService.getActivity] which can be mocked in tests. */ - fun getActivity(dreamService: DreamService): Activity? - - /** Wrapper for [DreamService.wakeUp] which can be mocked in tests. */ - fun wakeUp(dreamService: DreamService) - - /** Wrapper for [DreamService.finish] which can be mocked in tests. */ - fun finish(dreamService: DreamService) - - /** Wrapper for [DreamService.getRedirectWake] which can be mocked in tests. */ - fun redirectWake(dreamService: DreamService): Boolean -} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamService.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamService.kt index 6b14ebf31d84..9eec1b617792 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamService.kt +++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamService.kt @@ -16,75 +16,109 @@ package com.android.systemui.dreams.homecontrols +import android.annotation.SuppressLint import android.content.Intent import android.os.PowerManager import android.service.controls.ControlsProviderService import android.service.dreams.DreamService import android.window.TaskFragmentInfo -import com.android.systemui.controls.settings.ControlsSettingsRepository -import com.android.systemui.coroutines.newTracingContext -import com.android.systemui.dagger.qualifiers.Background +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ServiceLifecycleDispatcher +import androidx.lifecycle.lifecycleScope import com.android.systemui.dreams.DreamLogger -import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor -import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor.Companion.MAX_UPDATE_CORRELATION_DELAY +import com.android.systemui.dreams.homecontrols.service.TaskFragmentComponent +import com.android.systemui.dreams.homecontrols.shared.model.HomeControlsDataSource import com.android.systemui.log.LogBuffer import com.android.systemui.log.dagger.DreamLog +import com.android.systemui.util.time.SystemClock import com.android.systemui.util.wakelock.WakeLock -import com.android.systemui.util.wakelock.WakeLock.Builder.NO_TIMEOUT +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds -import kotlin.time.Duration.Companion.seconds -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob -import kotlinx.coroutines.cancel import kotlinx.coroutines.delay -import com.android.app.tracing.coroutines.launchTraced as launch +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +/** + * [DreamService] which embeds the user's chosen home controls app to allow it to display as a + * screensaver. This service will run in the foreground user context. + */ class HomeControlsDreamService @Inject +constructor(private val factory: HomeControlsDreamServiceImpl.Factory) : + DreamService(), LifecycleOwner { + + private val dispatcher = ServiceLifecycleDispatcher(this) + override val lifecycle: Lifecycle + get() = dispatcher.lifecycle + + private val impl: HomeControlsDreamServiceImpl by lazy { factory.create(this, this) } + + override fun onCreate() { + dispatcher.onServicePreSuperOnCreate() + super.onCreate() + } + + override fun onDreamingStarted() { + dispatcher.onServicePreSuperOnStart() + super.onDreamingStarted() + } + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + impl.onAttachedToWindow() + } + + override fun onDetachedFromWindow() { + dispatcher.onServicePreSuperOnDestroy() + super.onDetachedFromWindow() + impl.onDetachedFromWindow() + } +} + +/** + * Implementation of the home controls dream service, which allows for injecting a [DreamService] + * and [LifecycleOwner] for testing. + */ +class HomeControlsDreamServiceImpl +@AssistedInject constructor( - private val controlsSettingsRepository: ControlsSettingsRepository, private val taskFragmentFactory: TaskFragmentComponent.Factory, - private val homeControlsComponentInteractor: HomeControlsComponentInteractor, private val wakeLockBuilder: WakeLock.Builder, - private val dreamServiceDelegate: DreamServiceDelegate, - @Background private val bgDispatcher: CoroutineDispatcher, - @DreamLog logBuffer: LogBuffer -) : DreamService() { - - private val serviceJob = SupervisorJob() - private val serviceScope = - CoroutineScope(bgDispatcher + serviceJob + newTracingContext("HomeControlsDreamService")) + private val powerManager: PowerManager, + private val systemClock: SystemClock, + private val dataSource: HomeControlsDataSource, + @DreamLog logBuffer: LogBuffer, + @Assisted private val service: DreamService, + @Assisted lifecycleOwner: LifecycleOwner, +) : LifecycleOwner by lifecycleOwner { + private val logger = DreamLogger(logBuffer, TAG) private lateinit var taskFragmentComponent: TaskFragmentComponent private val wakeLock: WakeLock by lazy { wakeLockBuilder - .setMaxTimeout(NO_TIMEOUT) + .setMaxTimeout(WakeLock.Builder.NO_TIMEOUT) .setTag(TAG) .setLevelsAndFlags(PowerManager.SCREEN_BRIGHT_WAKE_LOCK) .build() } - override fun onAttachedToWindow() { - super.onAttachedToWindow() - val activity = dreamServiceDelegate.getActivity(this) + fun onAttachedToWindow() { + val activity = service.activity if (activity == null) { - finish() + service.finish() return } - - // Start monitoring package updates to possibly restart the dream if the home controls - // package is updated while we are dreaming. - serviceScope.launch { homeControlsComponentInteractor.monitorUpdatesAndRestart() } - taskFragmentComponent = taskFragmentFactory .create( activity = activity, onCreateCallback = { launchActivity() }, onInfoChangedCallback = this::onTaskFragmentInfoChanged, - hide = { endDream(false) } + hide = { endDream(false) }, ) .apply { createTaskFragment() } @@ -99,53 +133,61 @@ constructor( } private fun endDream(handleRedirect: Boolean) { - homeControlsComponentInteractor.onDreamEndUnexpectedly() - if (handleRedirect && dreamServiceDelegate.redirectWake(this)) { - dreamServiceDelegate.wakeUp(this) - serviceScope.launch { + pokeUserActivity() + if (handleRedirect && service.redirectWake) { + service.wakeUp() + lifecycleScope.launch { delay(ACTIVITY_RESTART_DELAY) launchActivity() } } else { - dreamServiceDelegate.finish(this) + service.finish() } } private fun launchActivity() { - val setting = controlsSettingsRepository.allowActionOnTrivialControlsInLockscreen.value - val componentName = homeControlsComponentInteractor.panelComponent.value - logger.d("Starting embedding $componentName") - val intent = - Intent().apply { - component = componentName - putExtra(ControlsProviderService.EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS, setting) - putExtra( - ControlsProviderService.EXTRA_CONTROLS_SURFACE, - ControlsProviderService.CONTROLS_SURFACE_DREAM - ) - } - taskFragmentComponent.startActivityInTaskFragment(intent) + lifecycleScope.launch { + val (componentName, setting) = dataSource.componentInfo.first() + logger.d("Starting embedding $componentName") + val intent = + Intent().apply { + component = componentName + putExtra( + ControlsProviderService.EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS, + setting, + ) + putExtra( + ControlsProviderService.EXTRA_CONTROLS_SURFACE, + ControlsProviderService.CONTROLS_SURFACE_DREAM, + ) + } + taskFragmentComponent.startActivityInTaskFragment(intent) + } } - override fun onDetachedFromWindow() { - super.onDetachedFromWindow() + fun onDetachedFromWindow() { wakeLock.release(TAG) taskFragmentComponent.destroy() - serviceScope.launch { - delay(CANCELLATION_DELAY_AFTER_DETACHED) - serviceJob.cancel("Dream detached from window") - } } - private companion object { - /** - * Defines how long after the dream ends that we should keep monitoring for package updates - * to attempt a restart of the dream. This should be larger than - * [MAX_UPDATE_CORRELATION_DELAY] as it also includes the time the package update takes to - * complete. - */ - val CANCELLATION_DELAY_AFTER_DETACHED = 5.seconds + @SuppressLint("MissingPermission") + private fun pokeUserActivity() { + powerManager.userActivity( + systemClock.uptimeMillis(), + PowerManager.USER_ACTIVITY_EVENT_OTHER, + PowerManager.USER_ACTIVITY_FLAG_NO_CHANGE_LIGHTS, + ) + } + + @AssistedFactory + interface Factory { + fun create( + service: DreamService, + lifecycleOwner: LifecycleOwner, + ): HomeControlsDreamServiceImpl + } + companion object { /** * Defines the delay after wakeup where we should attempt to restart the embedded activity. * When a wakeup is redirected, the dream service may keep running. In this case, we should @@ -153,6 +195,6 @@ constructor( * after the wakeup transition has played. */ val ACTIVITY_RESTART_DELAY = 334.milliseconds - const val TAG = "HomeControlsDreamService" + private const val TAG = "HomeControlsDreamServiceImpl" } } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/dagger/HomeControlsDataSourceModule.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/dagger/HomeControlsDataSourceModule.kt new file mode 100644 index 000000000000..3a2791fc503f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/dagger/HomeControlsDataSourceModule.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.dreams.homecontrols.dagger + +import com.android.systemui.Flags.homeControlsDreamHsum +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dreams.homecontrols.service.RemoteHomeControlsDataSourceDelegator +import com.android.systemui.dreams.homecontrols.shared.model.HomeControlsDataSource +import com.android.systemui.dreams.homecontrols.system.LocalHomeControlsDataSourceDelegator +import dagger.Lazy +import dagger.Module +import dagger.Provides + +@Module +interface HomeControlsDataSourceModule { + companion object { + @Provides + @SysUISingleton + fun providesHomeControlsDataSource( + localSource: Lazy<LocalHomeControlsDataSourceDelegator>, + remoteSource: Lazy<RemoteHomeControlsDataSourceDelegator>, + ): HomeControlsDataSource { + return if (homeControlsDreamHsum()) { + remoteSource.get() + } else { + localSource.get() + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/dagger/HomeControlsRemoteServiceComponent.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/dagger/HomeControlsRemoteServiceComponent.kt new file mode 100644 index 000000000000..500d15e56498 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/dagger/HomeControlsRemoteServiceComponent.kt @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.dreams.homecontrols.dagger + +import android.content.Context +import android.content.Intent +import android.os.IBinder +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dreams.homecontrols.service.HomeControlsRemoteProxy +import com.android.systemui.dreams.homecontrols.shared.IHomeControlsRemoteProxy +import com.android.systemui.dreams.homecontrols.system.HomeControlsRemoteService +import com.android.systemui.util.service.ObservableServiceConnection +import com.android.systemui.util.service.Observer +import com.android.systemui.util.service.PersistentConnectionManager +import com.android.systemui.util.service.dagger.ObservableServiceModule +import dagger.BindsInstance +import dagger.Module +import dagger.Provides +import dagger.Subcomponent +import javax.inject.Named + +/** + * This component is responsible for generating the connection to the home controls remote service + * which runs in the SYSTEM_USER context and provides the data needed to run the home controls dream + * in the foreground user context. + */ +@Subcomponent( + modules = + [ + ObservableServiceModule::class, + HomeControlsRemoteServiceComponent.HomeControlsRemoteServiceModule::class, + ] +) +interface HomeControlsRemoteServiceComponent { + /** Creates a [HomeControlsRemoteServiceComponent]. */ + @Subcomponent.Factory + interface Factory { + fun create( + @BindsInstance callback: ObservableServiceConnection.Callback<HomeControlsRemoteProxy> + ): HomeControlsRemoteServiceComponent + } + + /** A [PersistentConnectionManager] pointing to the home controls remote service. */ + val connectionManager: PersistentConnectionManager<HomeControlsRemoteProxy> + + /** Scoped module providing specific components for the [ObservableServiceConnection]. */ + @Module + interface HomeControlsRemoteServiceModule { + companion object { + @Provides + @Named(ObservableServiceModule.SERVICE_CONNECTION) + fun providesConnection( + connection: ObservableServiceConnection<HomeControlsRemoteProxy>, + callback: ObservableServiceConnection.Callback<HomeControlsRemoteProxy>, + ): ObservableServiceConnection<HomeControlsRemoteProxy> { + connection.addCallback(callback) + return connection + } + + /** Provides the wrapper around the home controls remote binder */ + @Provides + fun providesTransformer( + factory: HomeControlsRemoteProxy.Factory + ): ObservableServiceConnection.ServiceTransformer<HomeControlsRemoteProxy> { + return ObservableServiceConnection.ServiceTransformer { service: IBinder -> + factory.create(IHomeControlsRemoteProxy.Stub.asInterface(service)) + } + } + + /** Provides the intent to connect to [HomeControlsRemoteService] */ + @Provides + fun providesIntent(@Application context: Context): Intent { + return Intent(context, HomeControlsRemoteService::class.java) + } + + /** Provides no-op [Observer] since the remote service is in the same package */ + @Provides + @Named(ObservableServiceModule.OBSERVER) + fun providesObserver(): Observer { + return object : Observer { + override fun addCallback(callback: Observer.Callback?) { + // no-op, do nothing + } + + override fun removeCallback(callback: Observer.Callback?) { + // no-op, do nothing + } + } + } + + /** + * Provides a name that will be used by [PersistentConnectionManager] when logging + * state. + */ + @Provides + @Named(ObservableServiceModule.DUMPSYS_NAME) + fun providesDumpsysName(): String { + return "HomeControlsRemoteService" + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/service/HomeControlsRemoteProxy.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/service/HomeControlsRemoteProxy.kt new file mode 100644 index 000000000000..2bcfea8c1179 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/service/HomeControlsRemoteProxy.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.dreams.homecontrols.service + +import android.content.ComponentName +import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dreams.homecontrols.shared.IHomeControlsRemoteProxy +import com.android.systemui.dreams.homecontrols.shared.IOnControlsSettingsChangeListener +import com.android.systemui.dreams.homecontrols.shared.model.HomeControlsComponentInfo +import com.android.systemui.dump.DumpManager +import com.android.systemui.util.kotlin.FlowDumperImpl +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.stateIn + +/** Class to wrap [IHomeControlsRemoteProxy], which exposes the current user's home controls info */ +class HomeControlsRemoteProxy +@AssistedInject +constructor( + @Background bgScope: CoroutineScope, + dumpManager: DumpManager, + @Assisted private val proxy: IHomeControlsRemoteProxy, +) : FlowDumperImpl(dumpManager) { + + private companion object { + const val TAG = "HomeControlsRemoteProxy" + } + + val componentInfo: Flow<HomeControlsComponentInfo> = + conflatedCallbackFlow { + val listener = + object : IOnControlsSettingsChangeListener.Stub() { + override fun onControlsSettingsChanged( + panelComponent: ComponentName?, + allowTrivialControlsOnLockscreen: Boolean, + ) { + trySendWithFailureLogging( + HomeControlsComponentInfo( + panelComponent, + allowTrivialControlsOnLockscreen, + ), + TAG, + ) + } + } + proxy.registerListenerForCurrentUser(listener) + awaitClose { proxy.unregisterListenerForCurrentUser(listener) } + } + .distinctUntilChanged() + .stateIn(bgScope, SharingStarted.WhileSubscribed(), null) + .dumpValue("componentInfo") + .filterNotNull() + + @AssistedFactory + interface Factory { + fun create(proxy: IHomeControlsRemoteProxy): HomeControlsRemoteProxy + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/service/RemoteHomeControlsDataSourceDelegator.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/service/RemoteHomeControlsDataSourceDelegator.kt new file mode 100644 index 000000000000..b14903d7885f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/service/RemoteHomeControlsDataSourceDelegator.kt @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.dreams.homecontrols.service + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dreams.DreamLogger +import com.android.systemui.dreams.homecontrols.dagger.HomeControlsRemoteServiceComponent +import com.android.systemui.dreams.homecontrols.shared.model.HomeControlsComponentInfo +import com.android.systemui.dreams.homecontrols.shared.model.HomeControlsDataSource +import com.android.systemui.dump.DumpManager +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.dagger.DreamLog +import com.android.systemui.util.kotlin.FlowDumperImpl +import com.android.systemui.util.service.ObservableServiceConnection +import com.android.systemui.util.service.PersistentConnectionManager +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.dropWhile +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onEach + +/** + * Queries a remote service for [HomeControlsComponentInfo] necessary to show the home controls + * dream. + */ +@OptIn(ExperimentalCoroutinesApi::class) +@SysUISingleton +class RemoteHomeControlsDataSourceDelegator +@Inject +constructor( + @Background bgScope: CoroutineScope, + serviceFactory: HomeControlsRemoteServiceComponent.Factory, + @DreamLog logBuffer: LogBuffer, + dumpManager: DumpManager, +) : HomeControlsDataSource, FlowDumperImpl(dumpManager) { + private val logger = DreamLogger(logBuffer, TAG) + + private val connectionManager: PersistentConnectionManager<HomeControlsRemoteProxy> by lazy { + serviceFactory.create(callback).connectionManager + } + + private val proxyState = + MutableStateFlow<HomeControlsRemoteProxy?>(null) + .apply { + subscriptionCount + .map { it > 0 } + .dropWhile { !it } + .distinctUntilChanged() + .onEach { active -> + logger.d({ "Remote service connection active: $bool1" }) { bool1 = active } + if (active) { + connectionManager.start() + } else { + connectionManager.stop() + } + } + .launchIn(bgScope) + } + .dumpValue("proxyState") + + private val callback: ObservableServiceConnection.Callback<HomeControlsRemoteProxy> = + object : ObservableServiceConnection.Callback<HomeControlsRemoteProxy> { + override fun onConnected( + connection: ObservableServiceConnection<HomeControlsRemoteProxy>?, + proxy: HomeControlsRemoteProxy, + ) { + logger.d("Service connected") + proxyState.value = proxy + } + + override fun onDisconnected( + connection: ObservableServiceConnection<HomeControlsRemoteProxy>?, + reason: Int, + ) { + logger.d({ "Service disconnected with reason $int1" }) { int1 = reason } + proxyState.value = null + } + } + + override val componentInfo: Flow<HomeControlsComponentInfo> = + proxyState + .filterNotNull() + .flatMapLatest { proxy: HomeControlsRemoteProxy -> proxy.componentInfo } + .dumpWhileCollecting("componentInfo") + + private companion object { + const val TAG = "HomeControlsRemoteDataSourceDelegator" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/TaskFragmentComponent.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/service/TaskFragmentComponent.kt index d547de24beb5..67de30c8fa5c 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/TaskFragmentComponent.kt +++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/service/TaskFragmentComponent.kt @@ -14,29 +14,18 @@ * limitations under the License. */ -package com.android.systemui.dreams.homecontrols +package com.android.systemui.dreams.homecontrols.service import android.app.Activity -import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN +import android.app.WindowConfiguration import android.content.Intent import android.graphics.Rect import android.os.Binder import android.window.TaskFragmentCreationParams import android.window.TaskFragmentInfo import android.window.TaskFragmentOperation -import android.window.TaskFragmentOperation.OP_TYPE_DELETE_TASK_FRAGMENT -import android.window.TaskFragmentOperation.OP_TYPE_REORDER_TO_TOP_OF_TASK import android.window.TaskFragmentOrganizer -import android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_CHANGE -import android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_CLOSE -import android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_OPEN import android.window.TaskFragmentTransaction -import android.window.TaskFragmentTransaction.TYPE_ACTIVITY_REPARENTED_TO_TASK -import android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_APPEARED -import android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_ERROR -import android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_INFO_CHANGED -import android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED -import android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_VANISHED import android.window.WindowContainerTransaction import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.util.concurrency.DelayableExecutor @@ -65,7 +54,7 @@ constructor( activity: Activity, @Assisted("onCreateCallback") onCreateCallback: FragmentInfoCallback, @Assisted("onInfoChangedCallback") onInfoChangedCallback: FragmentInfoCallback, - hide: () -> Unit + hide: () -> Unit, ): TaskFragmentComponent } @@ -90,26 +79,28 @@ constructor( change.taskFragmentInfo?.let { taskFragmentInfo -> if (taskFragmentInfo.fragmentToken == fragmentToken) { when (change.type) { - TYPE_TASK_FRAGMENT_APPEARED -> { + TaskFragmentTransaction.TYPE_TASK_FRAGMENT_APPEARED -> { resultT.addTaskFragmentOperation( fragmentToken, - TaskFragmentOperation.Builder(OP_TYPE_REORDER_TO_TOP_OF_TASK) - .build() + TaskFragmentOperation.Builder( + TaskFragmentOperation.OP_TYPE_REORDER_TO_TOP_OF_TASK + ) + .build(), ) onCreateCallback(taskFragmentInfo) } - TYPE_TASK_FRAGMENT_INFO_CHANGED -> { + TaskFragmentTransaction.TYPE_TASK_FRAGMENT_INFO_CHANGED -> { onInfoChangedCallback(taskFragmentInfo) } - TYPE_TASK_FRAGMENT_VANISHED -> { + TaskFragmentTransaction.TYPE_TASK_FRAGMENT_VANISHED -> { hide() } - TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED -> {} - TYPE_TASK_FRAGMENT_ERROR -> { + TaskFragmentTransaction.TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED -> {} + TaskFragmentTransaction.TYPE_TASK_FRAGMENT_ERROR -> { hide() } - TYPE_ACTIVITY_REPARENTED_TO_TASK -> {} + TaskFragmentTransaction.TYPE_ACTIVITY_REPARENTED_TO_TASK -> {} else -> throw IllegalArgumentException( "Unknown TaskFragmentEvent=" + change.type @@ -121,8 +112,8 @@ constructor( organizer.onTransactionHandled( transaction.transactionToken, resultT, - TASK_FRAGMENT_TRANSIT_CHANGE, - false + TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_CHANGE, + false, ) } @@ -132,15 +123,15 @@ constructor( TaskFragmentCreationParams.Builder( organizer.organizerToken, fragmentToken, - activity.activityToken!! + activity.activityToken!!, ) .setInitialRelativeBounds(Rect()) - .setWindowingMode(WINDOWING_MODE_FULLSCREEN) + .setWindowingMode(WindowConfiguration.WINDOWING_MODE_FULLSCREEN) .build() organizer.applyTransaction( WindowContainerTransaction().createTaskFragment(fragmentOptions), - TASK_FRAGMENT_TRANSIT_CHANGE, - false + TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_CHANGE, + false, ) } @@ -151,8 +142,8 @@ constructor( fun startActivityInTaskFragment(intent: Intent) { organizer.applyTransaction( WindowContainerTransaction().startActivity(intent), - TASK_FRAGMENT_TRANSIT_OPEN, - false + TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_OPEN, + false, ) } @@ -162,10 +153,13 @@ constructor( WindowContainerTransaction() .addTaskFragmentOperation( fragmentToken, - TaskFragmentOperation.Builder(OP_TYPE_DELETE_TASK_FRAGMENT).build() + TaskFragmentOperation.Builder( + TaskFragmentOperation.OP_TYPE_DELETE_TASK_FRAGMENT + ) + .build(), ), - TASK_FRAGMENT_TRANSIT_CLOSE, - false + TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_CLOSE, + false, ) organizer.unregisterOrganizer() } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/shared/IHomeControlsRemoteProxy.aidl b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/shared/IHomeControlsRemoteProxy.aidl new file mode 100644 index 000000000000..115b62ca87a7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/shared/IHomeControlsRemoteProxy.aidl @@ -0,0 +1,9 @@ +package com.android.systemui.dreams.homecontrols.shared; + +import android.os.IRemoteCallback; +import com.android.systemui.dreams.homecontrols.shared.IOnControlsSettingsChangeListener; + +oneway interface IHomeControlsRemoteProxy { + void registerListenerForCurrentUser(in IOnControlsSettingsChangeListener callback); + void unregisterListenerForCurrentUser(in IOnControlsSettingsChangeListener callback); +} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/shared/IOnControlsSettingsChangeListener.aidl b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/shared/IOnControlsSettingsChangeListener.aidl new file mode 100644 index 000000000000..99e5fae97e45 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/shared/IOnControlsSettingsChangeListener.aidl @@ -0,0 +1,7 @@ +package com.android.systemui.dreams.homecontrols.shared; + +import android.content.ComponentName; + +oneway interface IOnControlsSettingsChangeListener { + void onControlsSettingsChanged(in ComponentName panelComponent, boolean allowTrivialControlsOnLockscreen); +} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/shared/model/HomeControlsComponentInfo.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/shared/model/HomeControlsComponentInfo.kt new file mode 100644 index 000000000000..b9e5080e92a9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/shared/model/HomeControlsComponentInfo.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.dreams.homecontrols.shared.model + +import android.content.ComponentName + +data class HomeControlsComponentInfo( + val componentName: ComponentName?, + val allowTrivialControlsOnLockscreen: Boolean, +) diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/shared/model/HomeControlsDataSource.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/shared/model/HomeControlsDataSource.kt new file mode 100644 index 000000000000..8187c5412fa4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/shared/model/HomeControlsDataSource.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.dreams.homecontrols.shared.model + +import kotlinx.coroutines.flow.Flow + +/** Source of data for home controls dream to get the necessary information it needs. */ +interface HomeControlsDataSource { + val componentInfo: Flow<HomeControlsComponentInfo> +} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartable.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/system/HomeControlsDreamStartable.kt index 03f58aca9fc6..644d5fd781c5 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/system/HomeControlsDreamStartable.kt @@ -14,24 +14,28 @@ * limitations under the License. */ -package com.android.systemui.dreams.homecontrols +package com.android.systemui.dreams.homecontrols.system import android.content.ComponentName import android.content.Context import android.content.pm.PackageManager import android.service.controls.flags.Flags.homePanelDream +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.CoreStartable +import com.android.systemui.Flags.homeControlsDreamHsum import com.android.systemui.dagger.qualifiers.Background -import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor +import com.android.systemui.dreams.homecontrols.HomeControlsDreamService +import com.android.systemui.dreams.homecontrols.system.domain.interactor.HomeControlsComponentInteractor +import com.android.systemui.settings.UserContextProvider import javax.inject.Inject import kotlinx.coroutines.CoroutineScope -import com.android.app.tracing.coroutines.launchTraced as launch class HomeControlsDreamStartable @Inject constructor( context: Context, - private val packageManager: PackageManager, + private val systemPackageManager: PackageManager, + private val userContextProvider: UserContextProvider, private val homeControlsComponentInteractor: HomeControlsComponentInteractor, @Background private val bgScope: CoroutineScope, ) : CoreStartable { @@ -57,10 +61,16 @@ constructor( } else { PackageManager.COMPONENT_ENABLED_STATE_DISABLED } + val packageManager = + if (homeControlsDreamHsum()) { + userContextProvider.userContext.packageManager + } else { + systemPackageManager + } packageManager.setComponentEnabledSetting( componentName, packageState, - PackageManager.DONT_KILL_APP + PackageManager.DONT_KILL_APP, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/system/HomeControlsRemoteService.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/system/HomeControlsRemoteService.kt new file mode 100644 index 000000000000..a65d216aa5a2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/system/HomeControlsRemoteService.kt @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.dreams.homecontrols.system + +import android.content.ComponentName +import android.content.Intent +import android.os.IBinder +import android.os.RemoteCallbackList +import android.os.RemoteException +import android.util.Log +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.LifecycleService +import androidx.lifecycle.lifecycleScope +import com.android.systemui.controls.settings.ControlsSettingsRepository +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dreams.DreamLogger +import com.android.systemui.dreams.homecontrols.shared.IHomeControlsRemoteProxy +import com.android.systemui.dreams.homecontrols.shared.IOnControlsSettingsChangeListener +import com.android.systemui.dreams.homecontrols.system.domain.interactor.HomeControlsComponentInteractor +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.dagger.DreamLog +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import java.util.concurrent.atomic.AtomicInteger +import javax.inject.Inject +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.launch + +/** + * Service which exports the current home controls component name, for use in SystemUI processes + * running in other users. This service should only run in the system user. + */ +class HomeControlsRemoteService +@Inject +constructor(binderFactory: HomeControlsRemoteServiceBinder.Factory) : LifecycleService() { + val binder by lazy { binderFactory.create(this) } + + override fun onBind(intent: Intent): IBinder? { + super.onBind(intent) + return binder + } +} + +class HomeControlsRemoteServiceBinder +@AssistedInject +constructor( + private val homeControlsComponentInteractor: HomeControlsComponentInteractor, + private val controlsSettingsRepository: ControlsSettingsRepository, + @Background private val bgContext: CoroutineContext, + @DreamLog logBuffer: LogBuffer, + @Assisted lifecycleOwner: LifecycleOwner, +) : IHomeControlsRemoteProxy.Stub(), LifecycleOwner by lifecycleOwner { + private val logger = DreamLogger(logBuffer, TAG) + private val callbacks = + object : RemoteCallbackList<IOnControlsSettingsChangeListener>() { + override fun onCallbackDied(listener: IOnControlsSettingsChangeListener?) { + if (callbackCount.decrementAndGet() == 0) { + logger.d("Cancelling collection due to callback death") + collectionJob?.cancel() + collectionJob = null + } + } + } + private val callbackCount = AtomicInteger(0) + private var collectionJob: Job? = null + + override fun registerListenerForCurrentUser(listener: IOnControlsSettingsChangeListener?) { + if (listener == null) return + logger.d("Register listener") + val registered = callbacks.register(listener) + if (registered && callbackCount.getAndIncrement() == 0) { + // If the first listener, start the collection job. This will also take + // care of notifying the listener of the initial state. + logger.d("Starting collection") + collectionJob = + lifecycleScope.launch(bgContext) { + combine( + homeControlsComponentInteractor.panelComponent, + controlsSettingsRepository.allowActionOnTrivialControlsInLockscreen, + ) { panelComponent, allowTrivialControls -> + callbacks.notifyAllCallbacks(panelComponent, allowTrivialControls) + } + .launchIn(this) + } + } else if (registered) { + // If not the first listener, notify the listener of the current value immediately. + listener.notify( + homeControlsComponentInteractor.panelComponent.value, + controlsSettingsRepository.allowActionOnTrivialControlsInLockscreen.value, + ) + } + } + + override fun unregisterListenerForCurrentUser(listener: IOnControlsSettingsChangeListener?) { + if (listener == null) return + logger.d("Unregister listener") + if (callbacks.unregister(listener) && callbackCount.decrementAndGet() == 0) { + logger.d("Cancelling collection due to unregister") + collectionJob?.cancel() + collectionJob = null + } + } + + private companion object { + const val TAG = "HomeControlsRemoteServiceBinder" + } + + private fun IOnControlsSettingsChangeListener.notify( + panelComponent: ComponentName?, + allowTrivialControlsOnLockscreen: Boolean, + ) { + try { + onControlsSettingsChanged(panelComponent, allowTrivialControlsOnLockscreen) + } catch (e: RemoteException) { + Log.e(TAG, "Error notifying callback", e) + } + } + + private fun RemoteCallbackList<IOnControlsSettingsChangeListener>.notifyAllCallbacks( + panelComponent: ComponentName?, + allowTrivialControlsOnLockscreen: Boolean, + ) { + val itemCount = beginBroadcast() + try { + for (i in 0 until itemCount) { + getBroadcastItem(i).notify(panelComponent, allowTrivialControlsOnLockscreen) + } + } finally { + finishBroadcast() + } + } + + @AssistedFactory + interface Factory { + fun create(lifecycleOwner: LifecycleOwner): HomeControlsRemoteServiceBinder + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/system/LocalHomeControlsDataSourceDelegator.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/system/LocalHomeControlsDataSourceDelegator.kt new file mode 100644 index 000000000000..ca255fda9e89 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/system/LocalHomeControlsDataSourceDelegator.kt @@ -0,0 +1,46 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.dreams.homecontrols.system + +import com.android.systemui.controls.settings.ControlsSettingsRepository +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dreams.homecontrols.shared.model.HomeControlsComponentInfo +import com.android.systemui.dreams.homecontrols.shared.model.HomeControlsDataSource +import com.android.systemui.dreams.homecontrols.system.domain.interactor.HomeControlsComponentInteractor +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine + +/** + * Queries local data sources for the [HomeControlsComponentInfo] necessary to show the home + * controls dream. + */ +@SysUISingleton +class LocalHomeControlsDataSourceDelegator +@Inject +constructor( + homeControlsComponentInteractor: HomeControlsComponentInteractor, + controlsSettingsRepository: ControlsSettingsRepository, +) : HomeControlsDataSource { + override val componentInfo: Flow<HomeControlsComponentInfo> = + combine( + homeControlsComponentInteractor.panelComponent, + controlsSettingsRepository.allowActionOnTrivialControlsInLockscreen, + ) { panelComponent, allowActionOnTrivialControlsInLockscreen -> + HomeControlsComponentInfo(panelComponent, allowActionOnTrivialControlsInLockscreen) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/domain/interactor/HomeControlsComponentInteractor.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/system/domain/interactor/HomeControlsComponentInteractor.kt index 20341389b75d..31bd70897f24 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/domain/interactor/HomeControlsComponentInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/system/domain/interactor/HomeControlsComponentInteractor.kt @@ -14,15 +14,9 @@ * limitations under the License. */ -package com.android.systemui.dreams.homecontrols.domain.interactor +package com.android.systemui.dreams.homecontrols.system.domain.interactor -import android.annotation.SuppressLint -import android.app.DreamManager import android.content.ComponentName -import android.os.PowerManager -import android.os.UserHandle -import com.android.systemui.common.domain.interactor.PackageChangeInteractor -import com.android.systemui.common.shared.model.PackageChangeModel import com.android.systemui.controls.ControlsServiceInfo import com.android.systemui.controls.dagger.ControlsComponent import com.android.systemui.controls.management.ControlsListingController @@ -32,25 +26,16 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.user.data.repository.UserRepository import com.android.systemui.util.kotlin.getOrNull -import com.android.systemui.util.kotlin.pairwiseBy -import com.android.systemui.util.kotlin.sample -import com.android.systemui.util.time.SystemClock import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import javax.inject.Inject -import kotlin.math.abs -import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.emptyFlow -import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart @@ -65,11 +50,7 @@ constructor( controlsComponent: ControlsComponent, authorizedPanelsRepository: AuthorizedPanelsRepository, userRepository: UserRepository, - private val packageChangeInteractor: PackageChangeInteractor, - private val systemClock: SystemClock, - private val powerManager: PowerManager, - private val dreamManager: DreamManager, - @Background private val bgScope: CoroutineScope + @Background private val bgScope: CoroutineScope, ) { private val controlsListingController: ControlsListingController? = controlsComponent.getControlsListingController().getOrNull() @@ -108,10 +89,7 @@ constructor( /** Gets all panels which are available and authorized by the user */ private val allAvailableAndAuthorizedPanels: Flow<List<PanelComponent>> = - combine( - allAvailableServices(), - allAuthorizedPanels, - ) { serviceInfos, authorizedPanels -> + combine(allAvailableServices(), allAuthorizedPanels) { serviceInfos, authorizedPanels -> serviceInfos.mapNotNull { val panelActivity = it.panelActivity if (it.componentName.packageName in authorizedPanels && panelActivity != null) { @@ -123,10 +101,7 @@ constructor( } val panelComponent: StateFlow<ComponentName?> = - combine( - allAvailableAndAuthorizedPanels, - selectedPanel, - ) { panels, selected -> + combine(allAvailableAndAuthorizedPanels, selectedPanel) { panels, selected -> val item = panels.firstOrNull { it.componentName == selected?.componentName } ?: panels.firstOrNull() @@ -134,67 +109,8 @@ constructor( } .stateIn(bgScope, SharingStarted.Eagerly, null) - private val taskFragmentFinished = - MutableSharedFlow<Long>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) - - fun onDreamEndUnexpectedly() { - powerManager.userActivity( - systemClock.uptimeMillis(), - PowerManager.USER_ACTIVITY_EVENT_OTHER, - PowerManager.USER_ACTIVITY_FLAG_NO_CHANGE_LIGHTS, - ) - taskFragmentFinished.tryEmit(systemClock.currentTimeMillis()) - } - - /** - * Monitors if the current home panel package is updated and causes the dream to finish, and - * attempts to restart the dream in this case. - */ - @SuppressLint("MissingPermission") - suspend fun monitorUpdatesAndRestart() { - taskFragmentFinished.resetReplayCache() - panelComponent - .flatMapLatest { component -> - if (component == null) return@flatMapLatest emptyFlow() - packageChangeInteractor.packageChanged(UserHandle.CURRENT, component.packageName) - } - .filter { it.isUpdate() } - // Wait for an UpdatedStarted - UpdateFinished pair to ensure the update has finished. - .pairwiseBy(::validateUpdatePair) - .filterNotNull() - .sample(taskFragmentFinished, ::Pair) - .filter { (updateStarted, lastFinishedTimestamp) -> - abs(updateStarted.timeMillis - lastFinishedTimestamp) <= - MAX_UPDATE_CORRELATION_DELAY.inWholeMilliseconds - } - .collect { dreamManager.startDream() } - } - private data class PanelComponent( val componentName: ComponentName, val panelActivity: ComponentName, ) - - companion object { - /** - * The maximum delay between a package update **starting** and the task fragment finishing - * which causes us to correlate the package update as the cause of the task fragment - * finishing. - */ - val MAX_UPDATE_CORRELATION_DELAY = 500.milliseconds - } } - -private fun PackageChangeModel.isUpdate() = - this is PackageChangeModel.UpdateStarted || this is PackageChangeModel.UpdateFinished - -private fun validateUpdatePair( - updateStarted: PackageChangeModel, - updateFinished: PackageChangeModel -): PackageChangeModel.UpdateStarted? = - when { - !updateStarted.isSamePackage(updateFinished) -> null - updateStarted !is PackageChangeModel.UpdateStarted -> null - updateFinished !is PackageChangeModel.UpdateFinished -> null - else -> updateStarted - } diff --git a/packages/SystemUI/src/com/android/systemui/haptics/msdl/qs/TileHapticsViewModel.kt b/packages/SystemUI/src/com/android/systemui/haptics/msdl/qs/TileHapticsViewModel.kt index 79059506b727..ed7d1823648a 100644 --- a/packages/SystemUI/src/com/android/systemui/haptics/msdl/qs/TileHapticsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/haptics/msdl/qs/TileHapticsViewModel.kt @@ -38,7 +38,7 @@ import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.transform -/** A view-model to trigger haptics feedback on Quick Settings tiles */ +/** A view-model to trigger haptic feedback on Quick Settings tiles */ @OptIn(ExperimentalCoroutinesApi::class) class TileHapticsViewModel @AssistedInject @@ -149,7 +149,7 @@ constructor( onActivityLaunchTransitionEnd = ::onActivityLaunchTransitionEnd, ) - /** Models the state of toggle haptics to play */ + /** Models the state of haptics to play */ enum class TileHapticsState { TOGGLE_ON, TOGGLE_OFF, diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/data/repository/TutorialSchedulerRepository.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/data/repository/TutorialSchedulerRepository.kt index d8d4bd686f07..a89ec7076e93 100644 --- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/data/repository/TutorialSchedulerRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/data/repository/TutorialSchedulerRepository.kt @@ -17,7 +17,6 @@ package com.android.systemui.inputdevice.tutorial.data.repository import android.content.Context -import androidx.annotation.VisibleForTesting import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.edit @@ -37,12 +36,12 @@ import kotlinx.coroutines.flow.map class TutorialSchedulerRepository( private val applicationContext: Context, backgroundScope: CoroutineScope, - dataStoreName: String + dataStoreName: String, ) { @Inject constructor( @Application applicationContext: Context, - @Background backgroundScope: CoroutineScope + @Background backgroundScope: CoroutineScope, ) : this(applicationContext, backgroundScope, dataStoreName = DATASTORE_NAME) private val Context.dataStore: DataStore<Preferences> by @@ -73,7 +72,7 @@ class TutorialSchedulerRepository( private fun getSchedulerInfo(pref: Preferences): Map<DeviceType, DeviceSchedulerInfo> { return mapOf( DeviceType.KEYBOARD to getDeviceSchedulerInfo(pref, DeviceType.KEYBOARD), - DeviceType.TOUCHPAD to getDeviceSchedulerInfo(pref, DeviceType.TOUCHPAD) + DeviceType.TOUCHPAD to getDeviceSchedulerInfo(pref, DeviceType.TOUCHPAD), ) } @@ -89,8 +88,7 @@ class TutorialSchedulerRepository( private fun getConnectKey(device: DeviceType) = longPreferencesKey(device.name + CONNECT_TIME_SUFFIX) - @VisibleForTesting - suspend fun clearDataStore() { + suspend fun clear() { applicationContext.dataStore.edit { it.clear() } } @@ -103,5 +101,5 @@ class TutorialSchedulerRepository( enum class DeviceType { KEYBOARD, - TOUCHPAD + TOUCHPAD, } diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialSchedulerInteractor.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialSchedulerInteractor.kt index 3b4d00db1a74..4a369e7e849e 100644 --- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialSchedulerInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialSchedulerInteractor.kt @@ -24,7 +24,10 @@ import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType.KEYB import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType.TOUCHPAD import com.android.systemui.inputdevice.tutorial.data.repository.TutorialSchedulerRepository import com.android.systemui.keyboard.data.repository.KeyboardRepository +import com.android.systemui.statusbar.commandline.Command +import com.android.systemui.statusbar.commandline.CommandRegistry import com.android.systemui.touchpad.data.repository.TouchpadRepository +import java.io.PrintWriter import java.time.Duration import java.time.Instant import javax.inject.Inject @@ -37,6 +40,7 @@ import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.runBlocking /** * When the first time a keyboard or touchpad is connected, wait for [LAUNCH_DELAY], and as soon as @@ -50,7 +54,12 @@ constructor( touchpadRepository: TouchpadRepository, private val repo: TutorialSchedulerRepository, private val logger: InputDeviceTutorialLogger, + commandRegistry: CommandRegistry, ) { + init { + commandRegistry.registerCommand(COMMAND) { TutorialCommand() } + } + private val isAnyDeviceConnected = mapOf( KEYBOARD to keyboardRepository.isAnyKeyboardConnected, @@ -118,8 +127,40 @@ constructor( return LAUNCH_DELAY.minus(elapsed).toKotlinDuration() } + inner class TutorialCommand : Command { + override fun execute(pw: PrintWriter, args: List<String>) { + if (args.isEmpty()) { + help(pw) + return + } + when (args[0]) { + "clear" -> + runBlocking { + repo.clear() + pw.println("Tutorial scheduler reset") + } + "info" -> + runBlocking { + pw.println("Keyboard connect time = ${repo.firstConnectionTime(KEYBOARD)}") + pw.println(" launch time = ${repo.launchTime(KEYBOARD)}") + pw.println("Touchpad connect time = ${repo.firstConnectionTime(TOUCHPAD)}") + pw.println(" launch time = ${repo.launchTime(TOUCHPAD)}") + } + else -> help(pw) + } + } + + override fun help(pw: PrintWriter) { + pw.println("Usage: adb shell cmd statusbar $COMMAND <command>") + pw.println("Available commands:") + pw.println(" clear") + pw.println(" info") + } + } + companion object { const val TAG = "TutorialSchedulerInteractor" + const val COMMAND = "peripheral_tutorial" private val DEFAULT_LAUNCH_DELAY_SEC = 72.hours.inWholeSeconds private val LAUNCH_DELAY: Duration get() = diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionTutorialContent.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionTutorialContent.kt index 6dd56de20ea6..1b044de5cf63 100644 --- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionTutorialContent.kt +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/ActionTutorialContent.kt @@ -77,7 +77,11 @@ fun ActionTutorialContent( } } val buttonAlpha by animateFloatAsState(if (actionState is Finished) 1f else 0f) - DoneButton(onDoneButtonClicked, Modifier.graphicsLayer { alpha = buttonAlpha }) + DoneButton( + onDoneButtonClicked = onDoneButtonClicked, + modifier = Modifier.graphicsLayer { alpha = buttonAlpha }, + enabled = actionState is Finished, + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialComponents.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialComponents.kt index 01ad585019d2..202dba357d45 100644 --- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialComponents.kt +++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/ui/composable/TutorialComponents.kt @@ -28,13 +28,17 @@ import androidx.compose.ui.res.stringResource import com.android.systemui.res.R @Composable -fun DoneButton(onDoneButtonClicked: () -> Unit, modifier: Modifier = Modifier) { +fun DoneButton( + onDoneButtonClicked: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, +) { Row( horizontalArrangement = Arrangement.End, verticalAlignment = Alignment.CenterVertically, - modifier = modifier.fillMaxWidth() + modifier = modifier.fillMaxWidth(), ) { - Button(onClick = onDoneButtonClicked) { + Button(onClick = onDoneButtonClicked, enabled = enabled) { Text(stringResource(R.string.touchpad_tutorial_done_button)) } } diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepository.kt index a08588750f85..12dd58176a71 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperCategoriesRepository.kt @@ -19,12 +19,14 @@ package com.android.systemui.keyboard.shortcut.data.repository import android.content.Context import android.graphics.drawable.Icon import android.hardware.input.InputManager +import android.hardware.input.KeyGlyphMap import android.util.Log import android.view.InputDevice import android.view.KeyCharacterMap import android.view.KeyEvent import android.view.KeyboardShortcutGroup import android.view.KeyboardShortcutInfo +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.source.KeyboardShortcutGroupsSource @@ -142,7 +144,10 @@ constructor( return if (type == null) { null } else { + val keyGlyphMap = + if (shortcutHelperKeyGlyph()) inputManager.getKeyGlyphMap(inputDevice.id) else null toShortcutCategory( + keyGlyphMap, inputDevice.keyCharacterMap, type, groups, @@ -163,6 +168,7 @@ constructor( } private fun toShortcutCategory( + keyGlyphMap: KeyGlyphMap?, keyCharacterMap: KeyCharacterMap, type: ShortcutCategoryType, shortcutGroups: List<KeyboardShortcutGroup>, @@ -175,6 +181,7 @@ constructor( ShortcutSubCategory( shortcutGroup.label.toString(), toShortcuts( + keyGlyphMap, keyCharacterMap, shortcutGroup.items, keepIcons, @@ -192,6 +199,7 @@ constructor( } private fun toShortcuts( + keyGlyphMap: KeyGlyphMap?, keyCharacterMap: KeyCharacterMap, infoList: List<KeyboardShortcutInfo>, keepIcons: Boolean, @@ -203,14 +211,16 @@ constructor( // keycode, or they could have a baseCharacter instead of a keycode. it.keycode == KeyEvent.KEYCODE_UNKNOWN || supportedKeyCodes.contains(it.keycode) } - .mapNotNull { toShortcut(keyCharacterMap, it, keepIcons) } + .mapNotNull { toShortcut(keyGlyphMap, keyCharacterMap, it, keepIcons) } private fun toShortcut( + keyGlyphMap: KeyGlyphMap?, keyCharacterMap: KeyCharacterMap, shortcutInfo: KeyboardShortcutInfo, keepIcon: Boolean, ): Shortcut? { - val shortcutCommand = toShortcutCommand(keyCharacterMap, shortcutInfo) ?: return null + val shortcutCommand = + toShortcutCommand(keyGlyphMap, keyCharacterMap, shortcutInfo) ?: return null return Shortcut( label = shortcutInfo.label!!.toString(), icon = toShortcutIcon(keepIcon, shortcutInfo), @@ -235,6 +245,7 @@ constructor( } private fun toShortcutCommand( + keyGlyphMap: KeyGlyphMap?, keyCharacterMap: KeyCharacterMap, info: KeyboardShortcutInfo, ): ShortcutCommand? { @@ -242,7 +253,7 @@ constructor( var remainingModifiers = info.modifiers SUPPORTED_MODIFIERS.forEach { supportedModifier -> if ((supportedModifier and remainingModifiers) != 0) { - keys += toShortcutModifierKey(supportedModifier) ?: return null + keys += toShortcutModifierKey(keyGlyphMap, supportedModifier) ?: return null // "Remove" the modifier from the remaining modifiers remainingModifiers = remainingModifiers and supportedModifier.inv() } @@ -253,7 +264,9 @@ constructor( return null } if (info.keycode != 0 || info.baseCharacter > Char.MIN_VALUE) { - keys += toShortcutKey(keyCharacterMap, info.keycode, info.baseCharacter) ?: return null + keys += + toShortcutKey(keyGlyphMap, keyCharacterMap, info.keycode, info.baseCharacter) + ?: return null } if (keys.isEmpty()) { Log.wtf(TAG, "No keys for $info") @@ -262,10 +275,15 @@ constructor( return ShortcutCommand(keys) } - private fun toShortcutModifierKey(modifierMask: Int): ShortcutKey? { + 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(iconResId) + return ShortcutKey.Icon.ResIdIcon(iconResId) } val modifierLabel = ShortcutHelperKeys.modifierLabels[modifierMask] @@ -277,13 +295,19 @@ constructor( } private fun toShortcutKey( + keyGlyphMap: KeyGlyphMap?, keyCharacterMap: KeyCharacterMap, keyCode: Int, baseCharacter: Char = Char.MIN_VALUE, ): ShortcutKey? { + val keycodeDrawable = keyGlyphMap?.getDrawableForKeycode(context, keyCode) + if (keycodeDrawable != null) { + return ShortcutKey.Icon.DrawableIcon(drawable = keycodeDrawable) + } + val iconResId = ShortcutHelperKeys.keyIcons[keyCode] if (iconResId != null) { - return ShortcutKey.Icon(iconResId) + return ShortcutKey.Icon.ResIdIcon(iconResId) } if (baseCharacter > Char.MIN_VALUE) { return ShortcutKey.Text(baseCharacter.uppercase()) diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/MultitaskingShortcutsSource.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/MultitaskingShortcutsSource.kt index eddac4d4ad73..05ff0cc30a44 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/MultitaskingShortcutsSource.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/source/MultitaskingShortcutsSource.kt @@ -70,7 +70,7 @@ class MultitaskingShortcutsSource @Inject constructor(@Main private val resource }, // Change split screen focus to LHS: // - Meta + Alt + Left arrow - shortcutInfo(resources.getString(R.string.system_multitasking_splitscreen_focus_rhs)) { + shortcutInfo(resources.getString(R.string.system_multitasking_splitscreen_focus_lhs)) { command(META_META_ON or META_ALT_ON, KEYCODE_DPAD_LEFT) }, ) diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCommand.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCommand.kt index e5b8096f2403..28451ae2bc14 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCommand.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCommand.kt @@ -28,7 +28,7 @@ class ShortcutCommandBuilder { } fun key(@DrawableRes drawableResId: Int) { - keys += ShortcutKey.Icon(drawableResId) + keys += ShortcutKey.Icon.ResIdIcon(drawableResId) } fun build() = ShortcutCommand(keys) diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutKey.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutKey.kt index 1abb78c54b99..1a609eab7da3 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutKey.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutKey.kt @@ -16,10 +16,15 @@ package com.android.systemui.keyboard.shortcut.shared.model +import android.graphics.drawable.Drawable import androidx.annotation.DrawableRes sealed interface ShortcutKey { data class Text(val value: String) : ShortcutKey - data class Icon(@DrawableRes val drawableResId: Int) : ShortcutKey + sealed interface Icon : ShortcutKey { + data class ResIdIcon(@DrawableRes val drawableResId: Int) : Icon + + data class DrawableIcon(val drawable: Drawable) : Icon + } } diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt index d53705605901..abddc7059ece 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt @@ -647,7 +647,11 @@ private fun BoxScope.ShortcutTextKey(key: ShortcutKey.Text) { @Composable private fun BoxScope.ShortcutIconKey(key: ShortcutKey.Icon) { Icon( - painter = painterResource(key.drawableResId), + painter = + when (key) { + is ShortcutKey.Icon.ResIdIcon -> painterResource(key.drawableResId) + is ShortcutKey.Icon.DrawableIcon -> rememberDrawablePainter(drawable = key.drawable) + }, contentDescription = null, modifier = Modifier.align(Alignment.Center).padding(6.dp), ) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java index d0a40ec3a361..7638079d7475 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java @@ -61,8 +61,6 @@ import com.android.systemui.keyguard.data.repository.KeyguardRepositoryModule; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionBootInteractor; import com.android.systemui.keyguard.domain.interactor.StartKeyguardTransitionModule; -import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancesMetricsLogger; -import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancesMetricsLoggerImpl; import com.android.systemui.keyguard.ui.transitions.DeviceEntryIconTransitionModule; import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordancesCombinedViewModelModule; import com.android.systemui.log.SessionTracker; @@ -239,12 +237,6 @@ public interface KeyguardModule { /** */ @Provides - static KeyguardQuickAffordancesMetricsLogger providesKeyguardQuickAffordancesMetricsLogger() { - return new KeyguardQuickAffordancesMetricsLoggerImpl(); - } - - /** */ - @Provides @SysUISingleton static ThreadAssert providesThreadAssert() { return new ThreadAssert(); diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt index e68d79937063..4d999df69588 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepository.kt @@ -106,7 +106,7 @@ constructor( trySendWithFailureLogging( getFpSensorType(), TAG, - "onAllAuthenticatorsRegistered, emitting fpSensorType" + "onAllAuthenticatorsRegistered, emitting fpSensorType", ) } } @@ -114,7 +114,7 @@ constructor( trySendWithFailureLogging( getFpSensorType(), TAG, - "initial value for fpSensorType" + "initial value for fpSensorType", ) awaitClose { authController.removeCallback(callback) } } @@ -134,7 +134,7 @@ constructor( trySendWithFailureLogging( keyguardUpdateMonitor.isFingerprintLockedOut, TAG, - "onLockedOutStateChanged" + "onLockedOutStateChanged", ) } val callback = @@ -154,7 +154,7 @@ constructor( .stateIn( scope, started = Eagerly, - initialValue = keyguardUpdateMonitor.isFingerprintLockedOut + initialValue = keyguardUpdateMonitor.isFingerprintLockedOut, ) } @@ -165,13 +165,13 @@ constructor( object : KeyguardUpdateMonitorCallback() { override fun onBiometricRunningStateChanged( running: Boolean, - biometricSourceType: BiometricSourceType? + biometricSourceType: BiometricSourceType?, ) { if (biometricSourceType == BiometricSourceType.FINGERPRINT) { trySendWithFailureLogging( running, TAG, - "Fingerprint running state changed" + "Fingerprint running state changed", ) } } @@ -180,7 +180,7 @@ constructor( trySendWithFailureLogging( keyguardUpdateMonitor.isFingerprintDetectionRunning, TAG, - "Initial fingerprint running state" + "Initial fingerprint running state", ) awaitClose { keyguardUpdateMonitor.removeCallback(callback) } } @@ -193,11 +193,7 @@ constructor( .map { it.isEngaged } .filterNotNull() .map { it } - .stateIn( - scope = scope, - started = WhileSubscribed(), - initialValue = false, - ) + .stateIn(scope = scope, started = WhileSubscribed(), initialValue = false) // TODO(b/322555228) Remove after consolidating device entry auth messages with BP auth messages // in BiometricStatusRepository @@ -232,10 +228,7 @@ constructor( ) { sendUpdateIfFingerprint( biometricSourceType, - ErrorFingerprintAuthenticationStatus( - msgId, - errString, - ), + ErrorFingerprintAuthenticationStatus(msgId, errString), ) } @@ -246,15 +239,12 @@ constructor( ) { sendUpdateIfFingerprint( biometricSourceType, - HelpFingerprintAuthenticationStatus( - msgId, - helpString, - ), + HelpFingerprintAuthenticationStatus(msgId, helpString), ) } override fun onBiometricAuthFailed( - biometricSourceType: BiometricSourceType, + biometricSourceType: BiometricSourceType ) { sendUpdateIfFingerprint( biometricSourceType, @@ -270,14 +260,14 @@ constructor( biometricSourceType, AcquiredFingerprintAuthenticationStatus( AuthenticationReason.DeviceEntryAuthentication, - acquireInfo + acquireInfo, ), ) } private fun sendUpdateIfFingerprint( biometricSourceType: BiometricSourceType, - authenticationStatus: FingerprintAuthenticationStatus + authenticationStatus: FingerprintAuthenticationStatus, ) { if (biometricSourceType != BiometricSourceType.FINGERPRINT) { return @@ -285,13 +275,14 @@ constructor( trySendWithFailureLogging( authenticationStatus, TAG, - "new fingerprint authentication status" + "new fingerprint authentication status", ) } } keyguardUpdateMonitor.registerCallback(callback) awaitClose { keyguardUpdateMonitor.removeCallback(callback) } } + .flowOn(mainDispatcher) .buffer(capacity = 4) override val shouldUpdateIndicatorVisibility: Flow<Boolean> = @@ -302,7 +293,7 @@ constructor( shouldUpdateIndicatorVisibility, TAG, "Error sending shouldUpdateIndicatorVisibility " + - "$shouldUpdateIndicatorVisibility" + "$shouldUpdateIndicatorVisibility", ) } @@ -310,7 +301,7 @@ constructor( object : KeyguardUpdateMonitorCallback() { override fun onBiometricRunningStateChanged( running: Boolean, - biometricSourceType: BiometricSourceType? + biometricSourceType: BiometricSourceType?, ) { sendShouldUpdateIndicatorVisibility(true) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBypassRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBypassRepository.kt new file mode 100644 index 000000000000..be4ab4b2c486 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBypassRepository.kt @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.data.repository + +import android.annotation.IntDef +import android.content.res.Resources +import android.provider.Settings +import com.android.systemui.common.coroutine.ChannelExt.trySendWithFailureLogging +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.dump.DumpManager +import com.android.systemui.keyguard.shared.model.DevicePosture +import com.android.systemui.keyguard.shared.model.DevicePosture.UNKNOWN +import com.android.systemui.res.R +import com.android.systemui.tuner.TunerService +import com.android.systemui.util.kotlin.FlowDumperImpl +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map + +@SysUISingleton +class KeyguardBypassRepository +@Inject +constructor( + @Main resources: Resources, + biometricSettingsRepository: BiometricSettingsRepository, + devicePostureRepository: DevicePostureRepository, + dumpManager: DumpManager, + private val tunerService: TunerService, + @Background backgroundDispatcher: CoroutineDispatcher, +) : FlowDumperImpl(dumpManager) { + + @get:BypassOverride + private val bypassOverride: Int by lazy { + resources.getInteger(R.integer.config_face_unlock_bypass_override) + } + + private val configFaceAuthSupportedPosture: DevicePosture by lazy { + DevicePosture.toPosture(resources.getInteger(R.integer.config_face_auth_supported_posture)) + } + + private val dismissByDefault: Int by lazy { + if (resources.getBoolean(com.android.internal.R.bool.config_faceAuthDismissesKeyguard)) { + 1 + } else { + 0 + } + } + + private var bypassEnabledSetting: Flow<Boolean> = + callbackFlow { + val updateBypassSetting = { state: Boolean -> + trySendWithFailureLogging(state, TAG, "Error sending bypassSetting $state") + } + + val tunable = + TunerService.Tunable { key, _ -> + updateBypassSetting(tunerService.getValue(key, dismissByDefault) != 0) + } + + updateBypassSetting(false) + tunerService.addTunable(tunable, Settings.Secure.FACE_UNLOCK_DISMISSES_KEYGUARD) + awaitClose { tunerService.removeTunable(tunable) } + } + .flowOn(backgroundDispatcher) + .dumpWhileCollecting("bypassEnabledSetting") + + val overrideFaceBypassSetting: Flow<Boolean> = + when (bypassOverride) { + FACE_UNLOCK_BYPASS_ALWAYS -> flowOf(true) + FACE_UNLOCK_BYPASS_NEVER -> flowOf(false) + else -> bypassEnabledSetting + } + + val isPostureAllowedForFaceAuth: Flow<Boolean> = + when (configFaceAuthSupportedPosture) { + UNKNOWN -> flowOf(true) + else -> + devicePostureRepository.currentDevicePosture + .map { posture -> posture == configFaceAuthSupportedPosture } + .distinctUntilChanged() + } + + /** + * Whether bypass is available. + * + * Bypass is the ability to skip the lockscreen when the device is unlocked using non-primary + * authentication types like face unlock, instead of requiring the user to explicitly dismiss + * the lockscreen by swiping after the device is already unlocked. + * + * "Available" refers to a combination of the user setting to skip the lockscreen being set, + * whether hard-wired OEM-overridable configs allow the feature, whether a foldable is in the + * right foldable posture, and other such things. It does _not_ model this based on more + * runtime-like states of the UI. + */ + val isBypassAvailable: Flow<Boolean> = + combine( + overrideFaceBypassSetting, + biometricSettingsRepository.isFaceAuthEnrolledAndEnabled, + isPostureAllowedForFaceAuth, + ) { + bypassOverride: Boolean, + isFaceEnrolledAndEnabled: Boolean, + isPostureAllowedForFaceAuth: Boolean -> + bypassOverride && isFaceEnrolledAndEnabled && isPostureAllowedForFaceAuth + } + .distinctUntilChanged() + .dumpWhileCollecting("isBypassAvailable") + + @IntDef(FACE_UNLOCK_BYPASS_NO_OVERRIDE, FACE_UNLOCK_BYPASS_ALWAYS, FACE_UNLOCK_BYPASS_NEVER) + @Retention(AnnotationRetention.SOURCE) + private annotation class BypassOverride + + companion object { + private const val FACE_UNLOCK_BYPASS_NO_OVERRIDE = 0 + private const val FACE_UNLOCK_BYPASS_ALWAYS = 1 + private const val FACE_UNLOCK_BYPASS_NEVER = 2 + + private const val TAG = "KeyguardBypassRepository" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt index d49550ef4c83..d0de21b45be0 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepository.kt @@ -36,12 +36,14 @@ import com.android.systemui.keyguard.shared.model.KeyguardQuickAffordancePickerR import com.android.systemui.keyguard.shared.model.KeyguardSlotPickerRepresentation import com.android.systemui.res.R import com.android.systemui.settings.UserTracker +import com.android.systemui.util.kotlin.FlowDumperImpl import java.io.PrintWriter import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.distinctUntilChanged @@ -64,7 +66,14 @@ constructor( configs: Set<@JvmSuppressWildcards KeyguardQuickAffordanceConfig>, dumpManager: DumpManager, userHandle: UserHandle, -) { +) : FlowDumperImpl(dumpManager) { + /** + * Whether a quick affordance is being launched. Quick Affordances are interactive lockscreen UI + * elements that allow the user to perform quick actions without unlocking their device. + */ + val launchingAffordance: MutableStateFlow<Boolean> = + MutableStateFlow(false).dumpValue("launchingAffordance") + // Configs for all keyguard quick affordances, mapped by the quick affordance ID as key private val configsByAffordanceId: Map<String, KeyguardQuickAffordanceConfig> = configs.associateBy { it.key } @@ -112,11 +121,7 @@ constructor( } } } - .stateIn( - scope = scope, - started = SharingStarted.Eagerly, - initialValue = emptyMap(), - ) + .stateIn(scope = scope, started = SharingStarted.Eagerly, initialValue = emptyMap()) init { legacySettingSyncer.startSyncing() @@ -144,14 +149,8 @@ constructor( * Updates the IDs of affordances to show at the slot with the given ID. The order of affordance * IDs should be descending priority order. */ - fun setSelections( - slotId: String, - affordanceIds: List<String>, - ) { - selectionManager.value.setSelections( - slotId = slotId, - affordanceIds = affordanceIds, - ) + fun setSelections(slotId: String, affordanceIds: List<String>) { + selectionManager.value.setSelections(slotId = slotId, affordanceIds = affordanceIds) } /** @@ -222,10 +221,7 @@ constructor( val (slotId, slotCapacity) = parseSlot(unparsedSlot) check(!seenSlotIds.contains(slotId)) { "Duplicate slot \"$slotId\"!" } seenSlotIds.add(slotId) - KeyguardSlotPickerRepresentation( - id = slotId, - maxSelectedAffordances = slotCapacity, - ) + KeyguardSlotPickerRepresentation(id = slotId, maxSelectedAffordances = slotCapacity) } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/PulseExpansionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/PulseExpansionRepository.kt new file mode 100644 index 000000000000..7699bab12785 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/PulseExpansionRepository.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.data.repository + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dump.DumpManager +import com.android.systemui.util.kotlin.FlowDumperImpl +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow + +@SysUISingleton +class PulseExpansionRepository @Inject constructor(dumpManager: DumpManager) : + FlowDumperImpl(dumpManager) { + /** + * Whether the notification panel is expanding from the user swiping downward on a notification + * from the pulsing state, or swiping anywhere on the screen when face bypass is enabled + */ + val isPulseExpanding: MutableStateFlow<Boolean> = + MutableStateFlow(false).dumpValue("pulseExpanding") +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt index b60e98a68162..8c6037107c5a 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt @@ -19,7 +19,6 @@ package com.android.systemui.keyguard.domain.interactor import android.animation.ValueAnimator import android.util.MathUtils import com.android.app.animation.Interpolators -import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.Flags.communalSceneKtfRefactor import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor import com.android.systemui.dagger.SysUISingleton @@ -39,19 +38,19 @@ import com.android.systemui.power.shared.model.WakeSleepReason.FOLD import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.data.repository.ShadeRepository -import com.android.systemui.util.kotlin.Utils.Companion.sample as sampleCombine import com.android.systemui.util.kotlin.sample -import java.util.UUID -import javax.inject.Inject -import kotlin.time.Duration.Companion.milliseconds -import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds +import java.util.UUID +import javax.inject.Inject import com.android.app.tracing.coroutines.launchTraced as launch @SysUISingleton @@ -176,98 +175,101 @@ constructor( if (SceneContainerFlag.isEnabled) return var transitionId: UUID? = null scope.launch("$TAG#listenForLockscreenToPrimaryBouncerDragging") { - shadeRepository.legacyShadeExpansion - .sampleCombine( - transitionInteractor.startedKeyguardTransitionStep, - keyguardInteractor.statusBarState, - keyguardInteractor.isKeyguardDismissible, - keyguardInteractor.isKeyguardOccluded, - ) - .collect { - ( - shadeExpansion, - startedStep, - statusBarState, - isKeyguardUnlocked, - isKeyguardOccluded) -> - val id = transitionId - val currentTransitionInfo = - internalTransitionInteractor.currentTransitionInfoInternal() - if (id != null) { - if (startedStep.to == KeyguardState.PRIMARY_BOUNCER) { - // An existing `id` means a transition is started, and calls to - // `updateTransition` will control it until FINISHED or CANCELED - var nextState = - if (shadeExpansion == 0f) { - TransitionState.FINISHED - } else if (shadeExpansion == 1f) { - TransitionState.CANCELED - } else { - TransitionState.RUNNING - } + shadeRepository.legacyShadeExpansion.collect { shadeExpansion -> + val statusBarState = keyguardInteractor.statusBarState.value + val isKeyguardUnlocked = keyguardInteractor.isKeyguardDismissible.value + val isKeyguardOccluded = keyguardInteractor.isKeyguardOccluded.value + val startedStep = transitionInteractor.startedKeyguardTransitionStep.value - // startTransition below will issue the CANCELED directly - if (nextState != TransitionState.CANCELED) { - transitionRepository.updateTransition( - id, - // This maps the shadeExpansion to a much faster curve, to match - // the existing logic - 1f - - MathUtils.constrainedMap(0f, 1f, 0.95f, 1f, shadeExpansion), - nextState, - ) - } - - if ( - nextState == TransitionState.CANCELED || - nextState == TransitionState.FINISHED - ) { - transitionId = null + val id = transitionId + val currentTransitionInfo = + internalTransitionInteractor.currentTransitionInfoInternal() + if (id != null) { + if (startedStep.to == KeyguardState.PRIMARY_BOUNCER) { + // An existing `id` means a transition is started, and calls to + // `updateTransition` will control it until FINISHED or CANCELED + var nextState = + if (shadeExpansion == 0f) { + TransitionState.FINISHED + } else if (shadeExpansion == 1f) { + TransitionState.CANCELED + } else { + TransitionState.RUNNING } - // If canceled, just put the state back - // TODO(b/278086361): This logic should happen in - // FromPrimaryBouncerInteractor. - if (nextState == TransitionState.CANCELED) { - transitionRepository.startTransition( - TransitionInfo( - ownerName = - "$name " + - "(on behalf of FromPrimaryBouncerInteractor)", - from = KeyguardState.PRIMARY_BOUNCER, - to = - if (isKeyguardOccluded) KeyguardState.OCCLUDED - else KeyguardState.LOCKSCREEN, - modeOnCanceled = TransitionModeOnCanceled.REVERSE, - animator = - getDefaultAnimatorForTransitionsToState( - KeyguardState.LOCKSCREEN - ) - .apply { duration = 100L }, - ) - ) - } + // startTransition below will issue the CANCELED directly + if (nextState != TransitionState.CANCELED) { + transitionRepository.updateTransition( + id, + // This maps the shadeExpansion to a much faster curve, to match + // the existing logic + 1f - MathUtils.constrainedMap(0f, 1f, 0.95f, 1f, shadeExpansion), + nextState, + ) } - } else { - // TODO (b/251849525): Remove statusbarstate check when that state is - // integrated into KeyguardTransitionRepository + if ( - // Use currentTransitionInfo to decide whether to start the transition. - currentTransitionInfo.to == KeyguardState.LOCKSCREEN && - shadeExpansion > 0f && - shadeExpansion < 1f && - shadeRepository.legacyShadeTracking.value && - !isKeyguardUnlocked && - statusBarState == KEYGUARD + nextState == TransitionState.CANCELED || + nextState == TransitionState.FINISHED ) { - transitionId = - startTransitionTo( - toState = KeyguardState.PRIMARY_BOUNCER, - animator = null, // transition will be manually controlled, - ownerReason = "#listenForLockscreenToPrimaryBouncerDragging", + transitionId = null + } + + // If canceled, just put the state back + // TODO(b/278086361): This logic should happen in + // FromPrimaryBouncerInteractor. + if (nextState == TransitionState.CANCELED) { + transitionRepository.startTransition( + TransitionInfo( + ownerName = + "$name " + "(on behalf of FromPrimaryBouncerInteractor)", + from = KeyguardState.PRIMARY_BOUNCER, + to = + if (isKeyguardOccluded) KeyguardState.OCCLUDED + else KeyguardState.LOCKSCREEN, + modeOnCanceled = TransitionModeOnCanceled.REVERSE, + animator = + getDefaultAnimatorForTransitionsToState( + KeyguardState.LOCKSCREEN + ) + .apply { duration = 100L }, ) + ) } } + } else { + // TODO (b/251849525): Remove statusbarstate check when that state is + // integrated into KeyguardTransitionRepository + if ( + // Use currentTransitionInfo to decide whether to start the transition. + currentTransitionInfo.to == KeyguardState.LOCKSCREEN && + shadeExpansion > 0f && + shadeExpansion < 1f && + shadeRepository.legacyShadeTracking.value && + !isKeyguardUnlocked && + statusBarState == KEYGUARD + ) { + transitionId = + startTransitionTo( + toState = KeyguardState.PRIMARY_BOUNCER, + animator = null, // transition will be manually controlled, + ownerReason = "#listenForLockscreenToPrimaryBouncerDragging", + ) + } + } + } + } + + // Ensure that transitionId is nulled out if external signals cause a PRIMARY_BOUNCER + // transition to be canceled. + scope.launch { + transitionInteractor.transitions + .filter { + it.transitionState == TransitionState.CANCELED && + it.to == KeyguardState.PRIMARY_BOUNCER + } + .collect { + transitionId = null } } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBypassInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBypassInteractor.kt new file mode 100644 index 000000000000..d793064f918e --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBypassInteractor.kt @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.domain.interactor + +import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dump.DumpManager +import com.android.systemui.keyguard.data.repository.KeyguardBypassRepository +import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.util.kotlin.FlowDumperImpl +import com.android.systemui.util.kotlin.combine +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map + +@SysUISingleton +class KeyguardBypassInteractor +@Inject +constructor( + keyguardBypassRepository: KeyguardBypassRepository, + alternateBouncerInteractor: AlternateBouncerInteractor, + keyguardQuickAffordanceInteractor: KeyguardQuickAffordanceInteractor, + pulseExpansionInteractor: PulseExpansionInteractor, + sceneInteractor: SceneInteractor, + shadeInteractor: ShadeInteractor, + dumpManager: DumpManager, +) : FlowDumperImpl(dumpManager) { + + /** + * Whether bypassing the keyguard is enabled by the user in user settings (skipping the + * lockscreen when authenticating using secondary authentication types like face unlock). + */ + val isBypassAvailable: Flow<Boolean> = + keyguardBypassRepository.isBypassAvailable.dumpWhileCollecting("isBypassAvailable") + + /** + * Models whether bypass is unavailable (no secondary authentication types enrolled), or if the + * keyguard can be bypassed as a combination of the settings toggle value set by the user and + * other factors related to device state. + */ + val canBypass: Flow<Boolean> = + isBypassAvailable + .flatMapLatest { isBypassAvailable -> + if (isBypassAvailable) { + combine( + sceneInteractor.currentScene.map { scene -> scene == Scenes.Bouncer }, + alternateBouncerInteractor.isVisible, + sceneInteractor.currentScene.map { scene -> scene == Scenes.Lockscreen }, + keyguardQuickAffordanceInteractor.launchingAffordance, + pulseExpansionInteractor.isPulseExpanding, + shadeInteractor.isQsExpanded, + ) { + isBouncerShowing, + isAlternateBouncerShowing, + isOnLockscreenScene, + isLaunchingAffordance, + isPulseExpanding, + isQsExpanded -> + when { + isBouncerShowing -> true + isAlternateBouncerShowing -> true + !isOnLockscreenScene -> false + isLaunchingAffordance -> false + isPulseExpanding -> false + isQsExpanded -> false + else -> true + } + } + } else { + flowOf(false) + } + } + .dumpWhileCollecting("canBypass") + + companion object { + private const val TAG: String = "KeyguardBypassInteractor" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt index b24ca1a8d345..2e0a160bfd16 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt @@ -284,7 +284,7 @@ constructor( } /** Observable for the [StatusBarState] */ - val statusBarState: Flow<StatusBarState> = repository.statusBarState + val statusBarState: StateFlow<StatusBarState> = repository.statusBarState /** Observable for [BiometricUnlockModel] when biometrics are used to unlock the device. */ val biometricUnlockState: StateFlow<BiometricUnlockModel> = repository.biometricUnlockState @@ -350,23 +350,21 @@ constructor( val dismissAlpha: Flow<Float> = shadeRepository.legacyShadeExpansion .sampleCombine( - statusBarState, keyguardTransitionInteractor.currentKeyguardState, keyguardTransitionInteractor.transitionState, isKeyguardDismissible, keyguardTransitionInteractor.isFinishedIn(Scenes.Communal, GLANCEABLE_HUB), ) - .filter { (_, _, _, step, _, _) -> !step.transitionState.isTransitioning() } + .filter { (_, _, step, _, _) -> !step.transitionState.isTransitioning() } .transform { ( legacyShadeExpansion, - statusBarState, currentKeyguardState, step, isKeyguardDismissible, onGlanceableHub) -> if ( - statusBarState == StatusBarState.KEYGUARD && + statusBarState.value == StatusBarState.KEYGUARD && isKeyguardDismissible && currentKeyguardState == LOCKSCREEN && legacyShadeExpansion != 1f diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt index 26bf26b34a8a..21afd3e4c444 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt @@ -61,6 +61,8 @@ import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest @@ -91,6 +93,11 @@ constructor( @Application private val appContext: Context, private val sceneInteractor: Lazy<SceneInteractor>, ) { + /** + * Whether a quick affordance is being launched. Quick Affordances are interactive lockscreen UI + * elements that allow the user to perform quick actions without unlocking their device. + */ + val launchingAffordance: StateFlow<Boolean> = repository.get().launchingAffordance.asStateFlow() /** * Whether the UI should use the long press gesture to activate quick affordances. @@ -167,11 +174,7 @@ constructor( * @param expandable An optional [Expandable] for the activity- or dialog-launch animation * @param slotId The id of the lockscreen slot that the affordance is in */ - fun onQuickAffordanceTriggered( - configKey: String, - expandable: Expandable?, - slotId: String, - ) { + fun onQuickAffordanceTriggered(configKey: String, expandable: Expandable?, slotId: String) { val (decodedSlotId, decodedConfigKey) = configKey.decode() val config = repository.get().selections.value[decodedSlotId]?.find { it.key == decodedConfigKey } @@ -191,10 +194,7 @@ constructor( ) is KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled -> Unit is KeyguardQuickAffordanceConfig.OnTriggeredResult.ShowDialog -> - showDialog( - result.dialog, - result.expandable, - ) + showDialog(result.dialog, result.expandable) } } @@ -225,12 +225,7 @@ constructor( selections.add(affordanceId) - repository - .get() - .setSelections( - slotId = slotId, - affordanceIds = selections, - ) + repository.get().setSelections(slotId = slotId, affordanceIds = selections) logger.logQuickAffordanceSelected(slotId, affordanceId) metricsLogger.logOnShortcutSelected(slotId, affordanceId) @@ -274,12 +269,7 @@ constructor( .getOrDefault(slotId, emptyList()) .toMutableList() return if (selections.remove(affordanceId)) { - repository - .get() - .setSelections( - slotId = slotId, - affordanceIds = selections, - ) + repository.get().setSelections(slotId = slotId, affordanceIds = selections) true } else { false @@ -399,11 +389,15 @@ constructor( intent, true /* dismissShade */, expandable?.activityTransitionController(), - true /* showOverLockscreenWhenLocked */, + true, /* showOverLockscreenWhenLocked */ ) } } + fun setLaunchingAffordance(isLaunchingAffordance: Boolean) { + repository.get().launchingAffordance.value = isLaunchingAffordance + } + private fun String.encode(slotId: String): String { return "$slotId$DELIMITER$this" } @@ -444,19 +438,19 @@ constructor( ), KeyguardPickerFlag( name = Contract.FlagsTable.FLAG_NAME_MONOCHROMATIC_THEME, - value = featureFlags.isEnabled(Flags.MONOCHROMATIC_THEME) + value = featureFlags.isEnabled(Flags.MONOCHROMATIC_THEME), ), KeyguardPickerFlag( name = Contract.FlagsTable.FLAG_NAME_WALLPAPER_PICKER_UI_FOR_AIWP, - value = featureFlags.isEnabled(Flags.WALLPAPER_PICKER_UI_FOR_AIWP) + value = featureFlags.isEnabled(Flags.WALLPAPER_PICKER_UI_FOR_AIWP), ), KeyguardPickerFlag( name = Contract.FlagsTable.FLAG_NAME_PAGE_TRANSITIONS, - value = featureFlags.isEnabled(Flags.WALLPAPER_PICKER_PAGE_TRANSITIONS) + value = featureFlags.isEnabled(Flags.WALLPAPER_PICKER_PAGE_TRANSITIONS), ), KeyguardPickerFlag( name = Contract.FlagsTable.FLAG_NAME_WALLPAPER_PICKER_PREVIEW_ANIMATION, - value = featureFlags.isEnabled(Flags.WALLPAPER_PICKER_PREVIEW_ANIMATION) + value = featureFlags.isEnabled(Flags.WALLPAPER_PICKER_PREVIEW_ANIMATION), ), ) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PulseExpansionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PulseExpansionInteractor.kt new file mode 100644 index 000000000000..377d7eaaddcc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/PulseExpansionInteractor.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dump.DumpManager +import com.android.systemui.keyguard.data.repository.PulseExpansionRepository +import com.android.systemui.util.kotlin.FlowDumperImpl +import javax.inject.Inject +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +@SysUISingleton +class PulseExpansionInteractor +@Inject +constructor(private val repository: PulseExpansionRepository, dumpManager: DumpManager) : + FlowDumperImpl(dumpManager) { + /** + * Whether the notification panel is expanding from the user swiping downward on a notification + * from the pulsing state, or swiping anywhere on the screen when face bypass is enabled + */ + val isPulseExpanding: StateFlow<Boolean> = + repository.isPulseExpanding.asStateFlow().dumpValue("isPulseExpanding") + + /** Updates whether a pulse expansion is occurring. */ + fun setPulseExpanding(pulseExpanding: Boolean) { + repository.isPulseExpanding.value = pulseExpanding + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SwipeToDismissInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SwipeToDismissInteractor.kt index e404f273a768..2e3a095740ab 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SwipeToDismissInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SwipeToDismissInteractor.kt @@ -20,13 +20,13 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.StatusBarState +import com.android.systemui.shade.data.repository.FlingInfo import com.android.systemui.shade.data.repository.ShadeRepository -import com.android.systemui.util.kotlin.Utils.Companion.sample import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.filter -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn /** @@ -39,8 +39,8 @@ class SwipeToDismissInteractor constructor( @Background backgroundScope: CoroutineScope, shadeRepository: ShadeRepository, - transitionInteractor: KeyguardTransitionInteractor, - keyguardInteractor: KeyguardInteractor, + private val transitionInteractor: KeyguardTransitionInteractor, + private val keyguardInteractor: KeyguardInteractor, ) { /** * Emits a [FlingInfo] whenever a swipe to dismiss gesture has started a fling animation on the @@ -50,20 +50,15 @@ constructor( * LOCKSCREEN -> GONE, and by [KeyguardSurfaceBehindInteractor] to match the surface remote * animation's velocity to the fling velocity, if applicable. */ - val dismissFling = + val dismissFling: StateFlow<FlingInfo?> = shadeRepository.currentFling - .sample( - transitionInteractor.startedKeyguardTransitionStep, - keyguardInteractor.isKeyguardDismissible, - keyguardInteractor.statusBarState, - ) - .filter { (flingInfo, startedStep, keyguardDismissable, statusBarState) -> + .filter { flingInfo -> flingInfo != null && !flingInfo.expand && - statusBarState != StatusBarState.SHADE_LOCKED && - startedStep.to == KeyguardState.LOCKSCREEN && - keyguardDismissable + keyguardInteractor.statusBarState.value != StatusBarState.SHADE_LOCKED && + transitionInteractor.startedKeyguardTransitionStep.value.to == + KeyguardState.LOCKSCREEN && + keyguardInteractor.isKeyguardDismissible.value } - .map { (flingInfo, _) -> flingInfo } .stateIn(backgroundScope, SharingStarted.Eagerly, null) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt index 40d4193202d2..0d816041d1be 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt @@ -27,6 +27,7 @@ import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.domain.interactor.PulseExpansionInteractor import com.android.systemui.keyguard.shared.model.Edge import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.KeyguardState.AOD @@ -81,6 +82,7 @@ constructor( private val communalInteractor: CommunalInteractor, private val keyguardTransitionInteractor: KeyguardTransitionInteractor, private val notificationsKeyguardInteractor: NotificationsKeyguardInteractor, + private val pulseExpansionInteractor: PulseExpansionInteractor, notificationShadeWindowModel: NotificationShadeWindowModel, private val aodNotificationIconViewModel: NotificationIconContainerAlwaysOnDisplayViewModel, private val alternateBouncerToAodTransitionViewModel: AlternateBouncerToAodTransitionViewModel, @@ -371,7 +373,7 @@ constructor( /** Is there an expanded pulse, are we animating in response? */ private fun isPulseExpandingAnimated(): Flow<AnimatedValue<Boolean>> { - return notificationsKeyguardInteractor.isPulseExpanding + return pulseExpansionInteractor.isPulseExpanding .pairwise(initialValue = null) // If pulsing changes, start animating, unless it's the first emission .map { (prev, expanding) -> AnimatableEvent(expanding, startAnimating = prev != null) } diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java index c70cd0a3a11b..47dacae6e0a0 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java @@ -131,7 +131,9 @@ public class MediaProjectionPermissionActivity extends Activity { // This activity is launched directly by an app, or system server. System server provides // the package name through the intent if so. - if (mPackageName == null) { + if (mPackageName == null || ( + com.android.systemui.Flags.mediaProjectionRequestAttributionFix() + && getCallingPackage() == null)) { if (launchingIntent.hasExtra(EXTRA_PACKAGE_REUSING_GRANTED_CONSENT)) { mPackageName = launchingIntent.getStringExtra( EXTRA_PACKAGE_REUSING_GRANTED_CONSENT); diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java b/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java index 0b37b5b7be3d..1ca3927ace33 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSImpl.java @@ -1146,4 +1146,4 @@ public class QSImpl implements QS, CommandQueue.Callbacks, StatusBarStateControl updateState(); } } -} +}
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt index 9c8e84f280a0..bacff99fe048 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt @@ -38,12 +38,14 @@ import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.togetherWith import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.layout.Arrangement.spacedBy import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.windowInsetsPadding @@ -73,13 +75,14 @@ import androidx.compose.ui.semantics.CustomAccessibilityAction import androidx.compose.ui.semantics.customActions import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.round import androidx.compose.ui.util.fastRoundToInt +import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle -import com.android.app.tracing.coroutines.launchTraced as launch import com.android.compose.animation.scene.ContentKey import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.ElementMatcher @@ -98,10 +101,7 @@ import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.dump.DumpManager import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.lifecycle.setSnapshotBinding -import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager import com.android.systemui.media.controls.ui.view.MediaHost -import com.android.systemui.media.dagger.MediaModule.QS_PANEL -import com.android.systemui.media.dagger.MediaModule.QUICK_QS_PANEL import com.android.systemui.plugins.qs.QS import com.android.systemui.plugins.qs.QSContainerController import com.android.systemui.qs.composefragment.SceneKeys.QuickQuickSettings @@ -127,7 +127,6 @@ import com.android.systemui.util.println import java.io.PrintWriter import java.util.function.Consumer import javax.inject.Inject -import javax.inject.Named import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.coroutineScope @@ -135,6 +134,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch @SuppressLint("ValidFragment") class QSFragmentCompose @@ -142,11 +142,11 @@ class QSFragmentCompose constructor( private val qsFragmentComposeViewModelFactory: QSFragmentComposeViewModel.Factory, private val dumpManager: DumpManager, - @Named(QUICK_QS_PANEL) private val qqsMediaHost: MediaHost, - @Named(QS_PANEL) private val qsMediaHost: MediaHost, ) : LifecycleFragment(), QS, Dumpable { private val scrollListener = MutableStateFlow<QS.ScrollListener?>(null) + private val collapsedMediaVisibilityChangedListener = + MutableStateFlow<(Consumer<Boolean>)?>(null) private val heightListener = MutableStateFlow<QS.HeightListener?>(null) private val qsContainerController = MutableStateFlow<QSContainerController?>(null) @@ -183,8 +183,6 @@ constructor( QSComposeFragment.isUnexpectedlyInLegacyMode() viewModel = qsFragmentComposeViewModelFactory.create(lifecycleScope) - qqsMediaHost.init(MediaHierarchyManager.LOCATION_QQS) - qsMediaHost.init(MediaHierarchyManager.LOCATION_QS) setListenerCollections() lifecycleScope.launch { viewModel.activate() } } @@ -491,7 +489,7 @@ constructor( } override fun setCollapsedMediaVisibilityChangedListener(listener: Consumer<Boolean>?) { - // TODO (b/353253280) + collapsedMediaVisibilityChangedListener.value = listener } override fun setScrollListener(scrollListener: QS.ScrollListener?) { @@ -534,6 +532,7 @@ constructor( lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { this@QSFragmentCompose.view?.setSnapshotBinding { scrollListener.value?.onQsPanelScrollChanged(scrollState.value) + collapsedMediaVisibilityChangedListener.value?.accept(viewModel.qqsMediaVisible) } launch { setListenerJob( @@ -569,7 +568,7 @@ constructor( .squishiness .collectAsStateWithLifecycle() - Column(modifier = Modifier.sysuiResTag("quick_qs_panel")) { + Column(modifier = Modifier.sysuiResTag(ResIdTags.quickQsPanel)) { Box( modifier = Modifier.fillMaxWidth() @@ -581,6 +580,9 @@ constructor( leftFromRoot + coordinates.size.width, topFromRoot + coordinates.size.height, ) + if (squishiness == 1f) { + viewModel.qqsHeight = coordinates.size.height + } } // Use an approach layout to determien the height without squishiness, as // that's the value that NPVC and QuickSettingsController care about @@ -595,8 +597,7 @@ constructor( .padding(top = { qqsPadding }, bottom = { bottomPadding }) ) { if (viewModel.isQsEnabled) { - QuickQuickSettings( - viewModel = viewModel.containerViewModel.quickQuickSettingsViewModel, + Column( modifier = Modifier.collapseExpandSemanticAction( stringResource( @@ -608,7 +609,16 @@ constructor( QuickSettingsShade.Dimensions.Padding.roundToPx() } ), - ) + verticalArrangement = + spacedBy(dimensionResource(R.dimen.qs_tile_margin_vertical)), + ) { + QuickQuickSettings( + viewModel = viewModel.containerViewModel.quickQuickSettingsViewModel + ) + if (viewModel.qqsMediaVisible) { + MediaObject(mediaHost = viewModel.qqsMediaHost) + } + } } } Spacer(modifier = Modifier.weight(1f)) @@ -645,14 +655,27 @@ constructor( } .onSizeChanged { viewModel.qsScrollHeight = it.height } .verticalScroll(scrollState) + .sysuiResTag(ResIdTags.qsScroll) ) { Spacer( modifier = Modifier.height { qqsPadding + qsExtraPadding.roundToPx() } ) QuickSettingsLayout( viewModel = viewModel.containerViewModel, - modifier = Modifier.sysuiResTag("quick_settings_panel"), + modifier = Modifier.sysuiResTag(ResIdTags.quickSettingsPanel), ) + Spacer(modifier = Modifier.height(8.dp)) + if (viewModel.qsMediaVisible) { + MediaObject( + mediaHost = viewModel.qsMediaHost, + modifier = + Modifier.padding( + horizontal = { + QuickSettingsShade.Dimensions.Padding.roundToPx() + } + ), + ) + } } } QuickSettingsTheme { @@ -660,7 +683,7 @@ constructor( viewModel = viewModel.footerActionsViewModel, qsVisibilityLifecycleOwner = this@QSFragmentCompose, modifier = - Modifier.sysuiResTag("qs_footer_actions") + Modifier.sysuiResTag(ResIdTags.qsFooterActions) .element(ElementKeys.FooterActions), ) } @@ -914,3 +937,29 @@ private fun Modifier.gesturesDisabled(disabled: Boolean) = } else { this } + +@Composable +private fun MediaObject(mediaHost: MediaHost, modifier: Modifier = Modifier) { + Box { + AndroidView( + modifier = modifier, + factory = { + mediaHost.hostView.apply { + layoutParams = + FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.WRAP_CONTENT, + ) + } + }, + onReset = {}, + ) + } +} + +private object ResIdTags { + const val quickSettingsPanel = "quick_settings_panel" + const val quickQsPanel = "quick_qs_panel" + const val qsScroll = "expanded_qs_scroll_view" + const val qsFooterActions = "qs_footer_actions" +} 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 new file mode 100644 index 000000000000..512732090036 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/dagger/QSFragmentComposeModule.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.composefragment.dagger + +import android.content.Context +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.util.Utils +import dagger.Module +import dagger.Provides +import javax.inject.Named + +@Module +interface QSFragmentComposeModule { + + companion object { + const val QS_USING_MEDIA_PLAYER = "compose_fragment_using_media_player" + + @Provides + @SysUISingleton + @Named(QS_USING_MEDIA_PLAYER) + fun providesUsingMedia(@Application context: Context): Boolean { + return Utils.useQsMediaPlayer(context) + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt index d30c6bec87dd..0ca621d7d2e2 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModel.kt @@ -38,11 +38,17 @@ import com.android.systemui.keyguard.shared.model.Edge import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.lifecycle.Hydrator +import com.android.systemui.media.controls.ui.controller.MediaHierarchyManager +import com.android.systemui.media.controls.ui.view.MediaHost +import com.android.systemui.media.controls.ui.view.MediaHostState +import com.android.systemui.media.dagger.MediaModule.QS_PANEL +import com.android.systemui.media.dagger.MediaModule.QUICK_QS_PANEL import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.qs.FooterActionsController +import com.android.systemui.qs.composefragment.dagger.QSFragmentComposeModule import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel import com.android.systemui.qs.panels.domain.interactor.TileSquishinessInteractor -import com.android.systemui.qs.panels.ui.viewmodel.PaginatedGridViewModel +import com.android.systemui.qs.panels.ui.viewmodel.InFirstPageViewModel import com.android.systemui.qs.ui.viewmodel.QuickSettingsContainerViewModel import com.android.systemui.res.R import com.android.systemui.scene.shared.model.Scenes @@ -60,10 +66,14 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import java.io.PrintWriter +import javax.inject.Named import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart @@ -83,7 +93,10 @@ constructor( configurationInteractor: ConfigurationInteractor, private val largeScreenHeaderHelper: LargeScreenHeaderHelper, private val squishinessInteractor: TileSquishinessInteractor, - private val paginatedGridViewModel: PaginatedGridViewModel, + private val inFirstPageViewModel: InFirstPageViewModel, + @Named(QUICK_QS_PANEL) val qqsMediaHost: MediaHost, + @Named(QS_PANEL) val qsMediaHost: MediaHost, + @Named(QSFragmentComposeModule.QS_USING_MEDIA_PLAYER) private val usingMedia: Boolean, @Assisted private val lifecycleScope: LifecycleCoroutineScope, ) : Dumpable, ExclusiveActivatable() { @@ -191,7 +204,7 @@ constructor( var collapseExpandAccessibilityAction: Runnable? = null val inFirstPage: Boolean - get() = paginatedGridViewModel.inFirstPage + get() = inFirstPageViewModel.inFirstPage var overScrollAmount by mutableStateOf(0) @@ -222,6 +235,30 @@ constructor( } } + val showingMirror: Boolean + get() = containerViewModel.brightnessSliderViewModel.showMirror + + // The initial values in these two are not meaningful. The flow will emit on start the correct + // values. This is because we need to lazily fetch them after initMediaHosts. + val qqsMediaVisible by + hydrator.hydratedStateOf( + traceName = "qqsMediaVisible", + initialValue = usingMedia, + source = + if (usingMedia) { + mediaHostVisible(qqsMediaHost) + } else { + flowOf(false) + }, + ) + + val qsMediaVisible by + hydrator.hydratedStateOf( + traceName = "qsMediaVisible", + initialValue = usingMedia, + source = if (usingMedia) mediaHostVisible(qsMediaHost) else flowOf(false), + ) + private var qsBounds by mutableStateOf(Rect()) private val constrainedSquishinessFraction: Float @@ -259,9 +296,6 @@ constructor( .onStart { emit(sysuiStatusBarStateController.state) }, ) - val showingMirror: Boolean - get() = containerViewModel.brightnessSliderViewModel.showMirror - private val isKeyguardState: Boolean get() = statusBarState == StatusBarState.KEYGUARD @@ -323,6 +357,7 @@ constructor( ) override suspend fun onActivated(): Nothing { + initMediaHosts() // init regardless of using media (same as current QS). coroutineScope { launch { hydrateSquishinessInteractor() } launch { hydrator.activate() } @@ -331,6 +366,19 @@ constructor( } } + private fun initMediaHosts() { + qqsMediaHost.apply { + expansion = MediaHostState.EXPANDED + showsOnlyActiveMedia = true + init(MediaHierarchyManager.LOCATION_QQS) + } + qsMediaHost.apply { + expansion = MediaHostState.EXPANDED + showsOnlyActiveMedia = false + init(MediaHierarchyManager.LOCATION_QS) + } + } + private suspend fun hydrateSquishinessInteractor() { snapshotFlow { constrainedSquishinessFraction } .collect { squishinessInteractor.setSquishinessValue(it) } @@ -373,6 +421,10 @@ constructor( println("qqsHeight", "${qqsHeight}px") println("qsScrollHeight", "${qsScrollHeight}px") } + printSection("Media") { + println("qqsMediaVisible", qqsMediaVisible) + println("qsMediaVisible", qsMediaVisible) + } } } @@ -390,3 +442,21 @@ private fun Float.constrainSquishiness(): Float { } private val SHORT_PARALLAX_AMOUNT = 0.1f + +/** + * Returns a flow to track the visibility of a [MediaHost]. The flow will emit on start the visible + * state of the view. + */ +private fun mediaHostVisible(mediaHost: MediaHost): Flow<Boolean> { + return callbackFlow { + val listener: (Boolean) -> Unit = { visible: Boolean -> trySend(visible) } + mediaHost.addVisibilityChangeListener(listener) + + awaitClose { mediaHost.removeVisibilityChangeListener(listener) } + } + // Need to use this to set initial state because on creation of the media host, the + // view visibility is not in sync with [MediaHost.visible], which is what we track with + // the listener. The correct state is set as part of init, so we need to get the state + // lazily. + .onStart { emit(mediaHost.visible) } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java index 29bcad4e0e0c..94b8a3ac5a3c 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java +++ b/packages/SystemUI/src/com/android/systemui/qs/dagger/QSModule.java @@ -19,6 +19,7 @@ package com.android.systemui.qs.dagger; import com.android.systemui.media.dagger.MediaModule; import com.android.systemui.qs.ReduceBrightColorsController; import com.android.systemui.qs.ReduceBrightColorsControllerImpl; +import com.android.systemui.qs.composefragment.dagger.QSFragmentComposeModule; import com.android.systemui.qs.external.QSExternalModule; import com.android.systemui.qs.panels.dagger.PanelsModule; import com.android.systemui.qs.pipeline.dagger.QSPipelineModule; @@ -40,6 +41,7 @@ import dagger.multibindings.Multibinds; includes = { MediaModule.class, PanelsModule.class, + QSFragmentComposeModule.class, QSExternalModule.class, QSFlagsModule.class, QSHostModule.class, 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 d55763aaeddb..6cc2cbc63d09 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 @@ -40,9 +40,9 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.compose.animation.scene.SceneScope import com.android.systemui.compose.modifiers.sysuiResTag +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 @@ -54,7 +54,7 @@ import javax.inject.Inject class PaginatedGridLayout @Inject constructor( - private val viewModel: PaginatedGridViewModel, + private val viewModelFactory: PaginatedGridViewModel.Factory, @PaginatedBaseLayoutType private val delegateGridLayout: PaginatableGridLayout, ) : GridLayout by delegateGridLayout { @Composable @@ -63,13 +63,18 @@ constructor( modifier: Modifier, editModeStart: () -> Unit, ) { + val viewModel = + rememberViewModel(traceName = "PaginatedGridLayout-TileGrid") { + viewModelFactory.create() + } + DisposableEffect(tiles) { val token = Any() tiles.forEach { it.startListening(token) } onDispose { tiles.forEach { it.stopListening(token) } } } - val columns by viewModel.columns.collectAsStateWithLifecycle() - val rows by viewModel.rows.collectAsStateWithLifecycle() + val columns by viewModel.columns + val rows = viewModel.rows val pages = remember(tiles, columns, rows) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt index 99a6cda8cbf5..ca28ab3e6ce3 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/QuickQuickSettings.kt @@ -41,7 +41,8 @@ fun SceneScope.QuickQuickSettings( viewModel: QuickQuickSettingsViewModel, modifier: Modifier = Modifier, ) { - val sizedTiles by viewModel.tileViewModels.collectAsStateWithLifecycle() + + val sizedTiles = viewModel.tileViewModels val tiles = sizedTiles.fastMap { it.tile } val bounceables = remember(sizedTiles) { List(sizedTiles.size) { BounceableTileViewModel() } } val squishiness by viewModel.squishinessViewModel.squishiness.collectAsStateWithLifecycle() @@ -52,7 +53,7 @@ fun SceneScope.QuickQuickSettings( tiles.forEach { it.startListening(token) } onDispose { tiles.forEach { it.stopListening(token) } } } - val columns by viewModel.columns.collectAsStateWithLifecycle() + val columns = viewModel.columns var cellIndex = 0 Box(modifier = modifier) { GridAnchor() diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt index 978a3534e95b..d10722287f5d 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/CommonTile.kt @@ -18,12 +18,12 @@ package com.android.systemui.qs.panels.ui.compose.infinitegrid import android.graphics.drawable.Animatable import android.text.TextUtils +import androidx.compose.animation.animateColorAsState import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi import androidx.compose.animation.graphics.res.animatedVectorResource import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter import androidx.compose.animation.graphics.vector.AnimatedImageVector import androidx.compose.foundation.Image -import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -32,8 +32,9 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicText +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.getValue @@ -44,6 +45,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.Shape @@ -57,7 +59,9 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.semantics.stateDescription import androidx.compose.ui.semantics.toggleableState import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import com.android.compose.modifiers.size import com.android.compose.modifiers.thenIf import com.android.systemui.Flags import com.android.systemui.common.shared.model.Icon @@ -88,12 +92,14 @@ fun LargeTileContent( ) { // Icon val longPressLabel = longPressLabel().takeIf { onLongClick != null } + val animatedBackgroundColor by + animateColorAsState(colors.iconBackground, label = "QSTileDualTargetBackgroundColor") Box( modifier = Modifier.size(CommonTileDefaults.ToggleTargetSize).thenIf(toggleClick != null) { Modifier.clip(iconShape) .verticalSquish(squishiness) - .background(colors.iconBackground) + .drawBehind { drawRect(animatedBackgroundColor) } .combinedClickable( onClick = toggleClick!!, onLongClick = onLongClick, @@ -117,6 +123,7 @@ fun LargeTileContent( SmallTileContent( icon = icon, color = colors.icon, + size = { CommonTileDefaults.LargeTileIconSize }, modifier = Modifier.align(Alignment.Center), ) } @@ -139,18 +146,22 @@ fun LargeTileLabels( modifier: Modifier = Modifier, accessibilityUiState: AccessibilityUiState? = null, ) { + val animatedLabelColor by animateColorAsState(colors.label, label = "QSTileLabelColor") + val animatedSecondaryLabelColor by + animateColorAsState(colors.secondaryLabel, label = "QSTileSecondaryLabelColor") Column(verticalArrangement = Arrangement.Center, modifier = modifier.fillMaxHeight()) { - Text( + BasicText( label, style = MaterialTheme.typography.labelLarge, - color = colors.label, + color = { animatedLabelColor }, maxLines = 1, overflow = TextOverflow.Ellipsis, ) if (!TextUtils.isEmpty(secondaryLabel)) { - Text( + BasicText( secondaryLabel ?: "", - color = colors.secondaryLabel, + color = { animatedSecondaryLabelColor }, + maxLines = 1, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.thenIf( @@ -170,9 +181,11 @@ fun SmallTileContent( modifier: Modifier = Modifier, icon: Icon, color: Color, + size: () -> Dp = { CommonTileDefaults.IconSize }, animateToEnd: Boolean = false, ) { - val iconModifier = modifier.size(CommonTileDefaults.IconSize) + val animatedColor by animateColorAsState(color, label = "QSTileIconColor") + val iconModifier = modifier.size({ size().roundToPx() }, { size().roundToPx() }) val context = LocalContext.current val loadedDrawable = remember(icon, context) { @@ -182,7 +195,7 @@ fun SmallTileContent( } } if (loadedDrawable !is Animatable) { - Icon(icon = icon, tint = color, modifier = iconModifier) + Icon(icon = icon, tint = animatedColor, modifier = iconModifier) } else if (icon is Icon.Resource) { val image = AnimatedImageVector.animatedVectorResource(id = icon.res) val painter = @@ -198,14 +211,15 @@ fun SmallTileContent( Image( painter = painter, contentDescription = icon.contentDescription?.load(), - colorFilter = ColorFilter.tint(color = color), + colorFilter = ColorFilter.tint(color = animatedColor), modifier = iconModifier, ) } } object CommonTileDefaults { - val IconSize = 24.dp + val IconSize = 32.dp + val LargeTileIconSize = 28.dp val ToggleTargetSize = 56.dp val TileHeight = 72.dp val TilePadding = 8.dp diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt index 418ed0be293f..b5cec120987f 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/EditTile.kt @@ -20,6 +20,7 @@ package com.android.systemui.qs.panels.ui.compose.infinitegrid import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.fadeIn @@ -54,7 +55,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.Clear import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -69,21 +69,22 @@ import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment -import androidx.compose.ui.BiasAlignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawBehind -import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.layout.MeasureScope +import androidx.compose.ui.layout.layout import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.positionInRoot @@ -103,6 +104,7 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.util.fastMap +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.compose.animation.bounceable import com.android.compose.modifiers.height import com.android.systemui.common.ui.compose.load @@ -134,9 +136,10 @@ import com.android.systemui.qs.panels.ui.viewmodel.EditTileViewModel import com.android.systemui.qs.pipeline.shared.TileSpec import com.android.systemui.qs.shared.model.groupAndSort import com.android.systemui.res.R +import kotlin.math.roundToInt import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.delay -import com.android.app.tracing.coroutines.launchTraced as launch +import kotlinx.coroutines.flow.collectLatest object TileType @@ -222,7 +225,7 @@ fun DefaultEditTileGrid( if (dragIsInProgress) { RemoveTileTarget() } else { - Text(text = "Hold and drag to rearrange tiles.") + Text(text = stringResource(id = R.string.drag_to_rearrange_tiles)) } } } @@ -240,7 +243,9 @@ fun DefaultEditTileGrid( spacedBy(dimensionResource(id = R.dimen.qs_label_container_margin)), modifier = modifier.fillMaxSize(), ) { - EditGridHeader { Text(text = "Hold and drag to add tiles.") } + EditGridHeader { + Text(text = stringResource(id = R.string.drag_to_add_tiles)) + } AvailableTileGrid(otherTiles, selectionState, columns, listState) } @@ -286,7 +291,7 @@ private fun RemoveTileTarget() { .padding(10.dp), ) { Icon(imageVector = Icons.Default.Clear, contentDescription = null) - Text(text = "Remove") + Text(text = stringResource(id = R.string.qs_customize_remove)) } } @@ -409,7 +414,7 @@ private fun GridCell.key(index: Int, dragAndDropState: DragAndDropState): Any { /** * Adds a list of [GridCell] to the lazy grid * - * @param cells the pairs of [GridCell] to [BounceableTileViewModel] + * @param cells the pairs of [GridCell] to [AnimatableTileViewModel] * @param dragAndDropState the [DragAndDropState] for this grid * @param selectionState the [MutableSelectionState] for this grid * @param onToggleSize the callback when a tile's size is toggled @@ -545,9 +550,27 @@ private fun TileGridCell( selectionState::unSelect, ) .tileBackground(colors.background) - .tilePadding() ) { - EditTile(tile = cell.tile, iconOnly = cell.isIcon) + val targetValue = if (cell.isIcon) 0f else 1f + val animatedProgress = remember { Animatable(targetValue) } + + if (selected) { + val resizingState = selectionState.resizingState + LaunchedEffect(targetValue, resizingState) { + if (resizingState == null) { + animatedProgress.animateTo(targetValue) + } else { + snapshotFlow { resizingState.progression } + .collectLatest { animatedProgress.snapTo(it) } + } + } + } + + EditTile( + tile = cell.tile, + tileWidths = { tileWidths }, + progress = { animatedProgress.value }, + ) } } } @@ -612,45 +635,72 @@ private fun SpacerGridCell(modifier: Modifier = Modifier) { } @Composable -fun BoxScope.EditTile( +fun EditTile( tile: EditTileViewModel, - iconOnly: Boolean, + tileWidths: () -> TileWidths?, + progress: () -> Float, colors: TileColors = EditModeTileDefaults.editTileColors(), ) { - // Animated horizontal alignment from center (0f) to start (-1f) - val alignmentValue by - animateFloatAsState( - targetValue = if (iconOnly) 0f else -1f, - label = "QSEditTileContentAlignment", - ) - val alignment by remember { - derivedStateOf { BiasAlignment(horizontalBias = alignmentValue, verticalBias = 0f) } - } - // Icon - Box(Modifier.size(ToggleTargetSize).align(alignment)) { - SmallTileContent( - icon = tile.icon, - color = colors.icon, - animateToEnd = true, - modifier = Modifier.align(Alignment.Center), - ) - } + val iconSizeDiff = CommonTileDefaults.IconSize - CommonTileDefaults.LargeTileIconSize + Row( + horizontalArrangement = spacedBy(6.dp), + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier.layout { measurable, constraints -> + // Always display the tile using the large size and trust the parent composable + // to clip the content as needed. This stop the labels from being truncated. + val width = tileWidths()?.max ?: constraints.maxWidth + val placeable = + measurable.measure(constraints.copy(minWidth = width, maxWidth = width)) + val currentProgress = progress() + val startPadding = + if (currentProgress == 0f) { + // Find the center of the max width when the tile is icon only + iconHorizontalCenter(constraints.maxWidth) + } else { + // Find the center of the minimum width to hold the same position as the + // tile is resized. + val basePadding = + tileWidths()?.min?.let { iconHorizontalCenter(it) } ?: 0f + // Large tiles, represented with a progress of 1f, have a 0.dp padding + basePadding * (1f - currentProgress) + } + + layout(constraints.maxWidth, constraints.maxHeight) { + placeable.place(startPadding.roundToInt(), 0) + } + } + .tilePadding(), + ) { + // Icon + Box(Modifier.size(ToggleTargetSize)) { + SmallTileContent( + icon = tile.icon, + color = colors.icon, + animateToEnd = true, + size = { CommonTileDefaults.IconSize - iconSizeDiff * progress() }, + modifier = Modifier.align(Alignment.Center), + ) + } - // Labels, positioned after the icon - AnimatedVisibility(visible = !iconOnly, enter = fadeIn(), exit = fadeOut()) { + // Labels, positioned after the icon LargeTileLabels( label = tile.label.text, secondaryLabel = tile.appName?.text, colors = colors, - modifier = Modifier.padding(start = ToggleTargetSize + TileArrangementPadding), + modifier = Modifier.weight(1f).graphicsLayer { alpha = progress() }, ) } } +private fun MeasureScope.iconHorizontalCenter(containerSize: Int): Float { + return (containerSize - ToggleTargetSize.roundToPx()) / 2f - + CommonTileDefaults.TilePadding.toPx() +} + private fun Modifier.tileBackground(color: Color): Modifier { - return drawBehind { - drawRoundRect(SolidColor(color), cornerRadius = CornerRadius(InactiveCornerRadius.toPx())) - } + // Clip tile contents from overflowing past the tile + return clip(RoundedCornerShape(InactiveCornerRadius)).drawBehind { drawRect(color) } } private object EditModeTileDefaults { diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt index 91f2da2b90f7..19ab29e6c796 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/infinitegrid/InfiniteGridLayout.kt @@ -72,7 +72,7 @@ constructor( rememberViewModel(traceName = "InfiniteGridLayout.TileGrid") { viewModel.dynamicIconTilesViewModelFactory.create() } - val columns by viewModel.gridSizeViewModel.columns.collectAsStateWithLifecycle() + val columns by viewModel.gridSizeViewModel.columns val sizedTiles = tiles.map { SizedTileImpl(it, it.spec.width()) } val bounceables = remember(sizedTiles) { List(sizedTiles.size) { BounceableTileViewModel() } } @@ -118,7 +118,7 @@ constructor( rememberViewModel(traceName = "InfiniteGridLayout.EditTileGrid") { viewModel.dynamicIconTilesViewModelFactory.create() } - val columns by viewModel.gridSizeViewModel.columns.collectAsStateWithLifecycle() + val columns by viewModel.gridSizeViewModel.columns val largeTiles by iconTilesViewModel.largeTiles.collectAsStateWithLifecycle() // Non-current tiles should always be displayed as icon tiles. 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 e1583e3fa3d3..5bebdbc7a13e 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 @@ -21,6 +21,7 @@ package com.android.systemui.qs.panels.ui.compose.infinitegrid import android.content.res.Resources import android.service.quicksettings.Tile.STATE_ACTIVE import android.service.quicksettings.Tile.STATE_INACTIVE +import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.combinedClickable @@ -61,6 +62,7 @@ import androidx.compose.ui.semantics.toggleableState import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.compose.animation.Expandable import com.android.compose.animation.bounceable import com.android.compose.modifiers.thenIf @@ -74,6 +76,7 @@ import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.plugins.qs.QSTile import com.android.systemui.qs.panels.ui.compose.BounceableInfo import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.InactiveCornerRadius +import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.TileHeight import com.android.systemui.qs.panels.ui.compose.infinitegrid.CommonTileDefaults.longPressLabel import com.android.systemui.qs.panels.ui.viewmodel.TileUiState import com.android.systemui.qs.panels.ui.viewmodel.TileViewModel @@ -82,7 +85,6 @@ import com.android.systemui.qs.tileimpl.QSTileImpl import com.android.systemui.res.R import java.util.function.Supplier import kotlinx.coroutines.CoroutineScope -import com.android.app.tracing.coroutines.launchTraced as launch private const val TEST_TAG_SMALL = "qs_tile_small" private const val TEST_TAG_LARGE = "qs_tile_large" @@ -128,14 +130,18 @@ fun Tile( // TODO(b/361789146): Draw the shapes instead of clipping val tileShape = TileDefaults.animateTileShape(uiState.state) - - TileExpandable( - color = + val animatedColor by + animateColorAsState( if (iconOnly || !uiState.handlesSecondaryClick) { colors.iconBackground } else { colors.background }, + label = "QSTileBackgroundColor", + ) + + TileExpandable( + color = { animatedColor }, shape = tileShape, squishiness = squishiness, hapticsViewModel = hapticsViewModel, @@ -212,7 +218,7 @@ fun Tile( @Composable private fun TileExpandable( - color: Color, + color: () -> Color, shape: Shape, squishiness: () -> Float, hapticsViewModel: TileHapticsViewModel?, @@ -220,7 +226,7 @@ private fun TileExpandable( content: @Composable (Expandable) -> Unit, ) { Expandable( - color = color, + color = color(), shape = shape, modifier = modifier.clip(shape).verticalSquish(squishiness), ) { @@ -238,7 +244,7 @@ fun TileContainer( ) { Box( modifier = - Modifier.height(CommonTileDefaults.TileHeight) + Modifier.height(TileHeight) .fillMaxWidth() .tileCombinedClickable( onClick = onClick, @@ -336,6 +342,16 @@ private object TileDefaults { ) @Composable + fun inactiveDualTargetTileColors(): TileColors = + TileColors( + background = MaterialTheme.colorScheme.surfaceVariant, + iconBackground = MaterialTheme.colorScheme.surfaceContainerHighest, + label = MaterialTheme.colorScheme.onSurfaceVariant, + secondaryLabel = MaterialTheme.colorScheme.onSurfaceVariant, + icon = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + @Composable fun inactiveTileColors(): TileColors = TileColors( background = MaterialTheme.colorScheme.surfaceVariant, @@ -365,7 +381,13 @@ private object TileDefaults { activeTileColors() } } - STATE_INACTIVE -> inactiveTileColors() + STATE_INACTIVE -> { + if (uiState.handlesSecondaryClick) { + inactiveDualTargetTileColors() + } else { + inactiveTileColors() + } + } else -> unavailableTileColors() } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/ResizingState.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/ResizingState.kt index a084bc2ef68a..9552aa935bbf 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/ResizingState.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/ResizingState.kt @@ -17,25 +17,30 @@ package com.android.systemui.qs.panels.ui.compose.selection import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.setValue import com.android.systemui.qs.panels.ui.compose.selection.ResizingDefaults.RESIZING_THRESHOLD class ResizingState(private val widths: TileWidths, private val onResize: () -> Unit) { - // Total drag offset of this resize operation - private var totalOffset = 0f + /** Total drag offset of this resize operation. */ + private var totalOffset by mutableFloatStateOf(0f) /** Width in pixels of the resizing tile. */ var width by mutableIntStateOf(widths.base) + /** Progression between icon (0) and large (1) sizes. */ + val progression + get() = calculateProgression() + // Whether the tile is currently over the threshold and should be a large tile - private var passedThreshold: Boolean = passedThreshold(calculateProgression(width)) + private var passedThreshold: Boolean = passedThreshold(progression) fun onDrag(offset: Float) { totalOffset += offset width = (widths.base + totalOffset).toInt().coerceIn(widths.min, widths.max) - passedThreshold(calculateProgression(width)).let { + passedThreshold(progression).let { // Resize if we went over the threshold if (passedThreshold != it) { passedThreshold = it @@ -49,7 +54,7 @@ class ResizingState(private val widths: TileWidths, private val onResize: () -> } /** The progression of the resizing tile between an icon tile (0f) and a large tile (1f) */ - private fun calculateProgression(width: Int): Float { + private fun calculateProgression(): Float { return ((width - widths.min) / (widths.max - widths.min).toFloat()).coerceIn(0f, 1f) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt index 9f13a3788f53..8a345ce97c84 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/compose/selection/Selection.kt @@ -16,7 +16,11 @@ package com.android.systemui.qs.panels.ui.compose.selection +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.animateIntAsState +import androidx.compose.animation.core.spring import androidx.compose.foundation.Canvas import androidx.compose.foundation.gestures.detectHorizontalDragGestures import androidx.compose.foundation.layout.Box @@ -78,7 +82,6 @@ fun ResizableTileContainer( ResizingHandle( enabled = selected, selectionState = selectionState, - transition = selectionAlpha, tileWidths = tileWidths, modifier = // Higher zIndex to make sure the handle is drawn above the content @@ -91,7 +94,6 @@ fun ResizableTileContainer( private fun ResizingHandle( enabled: Boolean, selectionState: MutableSelectionState, - transition: () -> Float, tileWidths: () -> TileWidths?, modifier: Modifier = Modifier, ) { @@ -126,19 +128,24 @@ private fun ResizingHandle( } } ) { - ResizingDot(transition = transition, modifier = Modifier.align(Alignment.Center)) + ResizingDot(enabled = enabled, modifier = Modifier.align(Alignment.Center)) } } @Composable private fun ResizingDot( - transition: () -> Float, + enabled: Boolean, modifier: Modifier = Modifier, color: Color = MaterialTheme.colorScheme.primary, ) { + val alpha by animateFloatAsState(if (enabled) 1f else 0f) + val radius by + animateDpAsState( + if (enabled) ResizingDotSize / 2 else 0.dp, + animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy), + ) Canvas(modifier = modifier.size(ResizingDotSize)) { - val v = transition() - drawCircle(color = color, radius = (ResizingDotSize / 2).toPx() * v, alpha = v) + drawCircle(color = color, radius = radius.toPx(), alpha = alpha) } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/dialog/QSResetDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/dialog/QSResetDialogDelegate.kt index 03fc425a4f9a..cbece2cbb382 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/dialog/QSResetDialogDelegate.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/dialog/QSResetDialogDelegate.kt @@ -23,7 +23,7 @@ import androidx.compose.ui.res.stringResource import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import com.android.compose.PlatformButton -import com.android.compose.PlatformOutlinedButton +import com.android.compose.PlatformTextButton import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dialog.ui.composable.AlertDialogContent import com.android.systemui.qs.panels.domain.interactor.EditTilesResetInteractor @@ -84,8 +84,8 @@ constructor( Text(stringResource(id = android.R.string.ok)) } }, - neutralButton = { - PlatformOutlinedButton(onClick = { dialog.dismiss() }) { + negativeButton = { + PlatformTextButton(onClick = { dialog.dismiss() }) { Text(stringResource(id = android.R.string.cancel)) } }, diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamServiceDelegateImpl.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/InFirstPageViewModel.kt index 7dc5434c595e..225f54235d47 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamServiceDelegateImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/InFirstPageViewModel.kt @@ -13,26 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.systemui.dreams.homecontrols -import android.app.Activity -import android.service.dreams.DreamService -import javax.inject.Inject - -class DreamServiceDelegateImpl @Inject constructor() : DreamServiceDelegate { - override fun getActivity(dreamService: DreamService): Activity { - return dreamService.activity - } - - override fun finish(dreamService: DreamService) { - dreamService.finish() - } +package com.android.systemui.qs.panels.ui.viewmodel - override fun wakeUp(dreamService: DreamService) { - dreamService.wakeUp() - } +import com.android.systemui.dagger.SysUISingleton +import javax.inject.Inject - override fun redirectWake(dreamService: DreamService): Boolean { - return dreamService.redirectWake - } +/* + * Tracks whether the current HorizontalPager (using this viewmodel) is in the first page. + * This requires it to be a `@SysUISingleton` to be shared between viewmodels. + */ +@SysUISingleton +class InFirstPageViewModel @Inject constructor() { + var inFirstPage = true } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/InfiniteGridViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/InfiniteGridViewModel.kt index 0d1206768db1..d68710048e13 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/InfiniteGridViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/InfiniteGridViewModel.kt @@ -16,6 +16,7 @@ package com.android.systemui.qs.panels.ui.viewmodel +import com.android.systemui.lifecycle.ExclusiveActivatable import com.android.systemui.qs.panels.ui.dialog.QSResetDialogDelegate import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject @@ -27,12 +28,16 @@ constructor( val gridSizeViewModel: QSColumnsViewModel, val squishinessViewModel: TileSquishinessViewModel, private val resetDialogDelegate: QSResetDialogDelegate, -) { +) : ExclusiveActivatable() { fun showResetDialog() { resetDialogDelegate.showDialog() } + override suspend fun onActivated(): Nothing { + gridSizeViewModel.activate() + } + @AssistedFactory interface Factory { fun create(): InfiniteGridViewModel 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 0f7dafcd941c..8bd9ed05c12c 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 @@ -16,33 +16,50 @@ package com.android.systemui.qs.panels.ui.viewmodel -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import com.android.systemui.lifecycle.ExclusiveActivatable +import com.android.systemui.lifecycle.Hydrator import com.android.systemui.qs.panels.domain.interactor.PaginatedGridInteractor -import javax.inject.Inject -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.stateIn +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch -@SysUISingleton class PaginatedGridViewModel -@Inject +@AssistedInject constructor( iconTilesViewModel: IconTilesViewModel, - gridSizeViewModel: QSColumnsViewModel, + private val gridSizeViewModel: QSColumnsViewModel, paginatedGridInteractor: PaginatedGridInteractor, - @Application applicationScope: CoroutineScope, -) : IconTilesViewModel by iconTilesViewModel, QSColumnsViewModel by gridSizeViewModel { - val rows = - paginatedGridInteractor.rows.stateIn( - applicationScope, - SharingStarted.WhileSubscribed(), - paginatedGridInteractor.defaultRows, + inFirstPageViewModel: InFirstPageViewModel, +) : IconTilesViewModel by iconTilesViewModel, ExclusiveActivatable() { + + private val hydrator = Hydrator("PaginatedGridViewModel") + + val rows by + hydrator.hydratedStateOf( + traceName = "rows", + initialValue = paginatedGridInteractor.defaultRows, + source = paginatedGridInteractor.rows, ) - /* - * Tracks whether the current HorizontalPager (using this viewmodel) is in the first page. - * This requires it to be a `@SysUISingleton` to be shared between viewmodels. - */ - var inFirstPage = true + var inFirstPage by inFirstPageViewModel::inFirstPage + + val columns: State<Int> + get() = gridSizeViewModel.columns + + override suspend fun onActivated(): Nothing { + coroutineScope { + launch { hydrator.activate() } + launch { gridSizeViewModel.activate() } + awaitCancellation() + } + } + + @AssistedFactory + interface Factory { + fun create(): PaginatedGridViewModel + } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QSColumnsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QSColumnsViewModel.kt index 0f1c77eb4711..8926d2ff107e 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QSColumnsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QSColumnsViewModel.kt @@ -16,17 +16,25 @@ package com.android.systemui.qs.panels.ui.viewmodel -import com.android.systemui.dagger.SysUISingleton +import androidx.compose.runtime.State +import com.android.systemui.lifecycle.Activatable +import com.android.systemui.lifecycle.ExclusiveActivatable +import com.android.systemui.lifecycle.Hydrator import com.android.systemui.qs.panels.domain.interactor.QSColumnsInteractor import javax.inject.Inject -import kotlinx.coroutines.flow.StateFlow -interface QSColumnsViewModel { - val columns: StateFlow<Int> +interface QSColumnsViewModel : Activatable { + val columns: State<Int> } -@SysUISingleton class QSColumnsSizeViewModelImpl @Inject constructor(interactor: QSColumnsInteractor) : - QSColumnsViewModel { - override val columns: StateFlow<Int> = interactor.columns + QSColumnsViewModel, ExclusiveActivatable() { + private val hydrator = Hydrator("QSColumnsSizeViewModelImpl") + + override val columns = + hydrator.hydratedStateOf(traceName = "columns", source = interactor.columns) + + override suspend fun onActivated(): Nothing { + hydrator.activate() + } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModel.kt index 887a70f39f6a..0859c86d74e1 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModel.kt @@ -16,67 +16,69 @@ package com.android.systemui.qs.panels.ui.viewmodel -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dagger.qualifiers.Application +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue import com.android.systemui.haptics.msdl.qs.TileHapticsViewModelFactoryProvider +import com.android.systemui.lifecycle.ExclusiveActivatable +import com.android.systemui.lifecycle.Hydrator import com.android.systemui.qs.panels.domain.interactor.QuickQuickSettingsRowInteractor -import com.android.systemui.qs.panels.shared.model.SizedTile import com.android.systemui.qs.panels.shared.model.SizedTileImpl import com.android.systemui.qs.panels.shared.model.splitInRowsSequence import com.android.systemui.qs.pipeline.domain.interactor.CurrentTilesInteractor import com.android.systemui.qs.pipeline.shared.TileSpec -import javax.inject.Inject -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.flatMapLatest -import kotlinx.coroutines.flow.mapLatest -import kotlinx.coroutines.flow.stateIn +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch -@OptIn(ExperimentalCoroutinesApi::class) -@SysUISingleton class QuickQuickSettingsViewModel -@Inject +@AssistedInject constructor( tilesInteractor: CurrentTilesInteractor, - qsColumnsViewModel: QSColumnsViewModel, + private val qsColumnsViewModel: QSColumnsViewModel, quickQuickSettingsRowInteractor: QuickQuickSettingsRowInteractor, val squishinessViewModel: TileSquishinessViewModel, - private val iconTilesViewModel: IconTilesViewModel, - @Application private val applicationScope: CoroutineScope, + iconTilesViewModel: IconTilesViewModel, val tileHapticsViewModelFactoryProvider: TileHapticsViewModelFactoryProvider, -) { +) : ExclusiveActivatable() { - val columns = qsColumnsViewModel.columns + private val hydrator = Hydrator("QuickQuickSettingsViewModel") - private val rows = - quickQuickSettingsRowInteractor.rows.stateIn( - applicationScope, - SharingStarted.WhileSubscribed(), - quickQuickSettingsRowInteractor.defaultRows, + val columns by qsColumnsViewModel.columns + + private val largeTiles by + hydrator.hydratedStateOf(traceName = "largeTiles", source = iconTilesViewModel.largeTiles) + + private val rows by + hydrator.hydratedStateOf( + traceName = "rows", + initialValue = quickQuickSettingsRowInteractor.defaultRows, + source = quickQuickSettingsRowInteractor.rows, ) - val tileViewModels: StateFlow<List<SizedTile<TileViewModel>>> = - columns - .flatMapLatest { columns -> - tilesInteractor.currentTiles.combine(rows, ::Pair).mapLatest { (tiles, rows) -> - tiles - .map { SizedTileImpl(TileViewModel(it.tile, it.spec), it.spec.width) } - .let { splitInRowsSequence(it, columns).take(rows).toList().flatten() } - } - } - .stateIn( - applicationScope, - SharingStarted.WhileSubscribed(), - tilesInteractor.currentTiles.value - .map { SizedTileImpl(TileViewModel(it.tile, it.spec), it.spec.width) } - .let { - splitInRowsSequence(it, columns.value).take(rows.value).toList().flatten() - }, - ) + private val currentTiles by + hydrator.hydratedStateOf(traceName = "currentTiles", source = tilesInteractor.currentTiles) + + val tileViewModels by derivedStateOf { + currentTiles + .map { SizedTileImpl(TileViewModel(it.tile, it.spec), it.spec.width) } + .let { splitInRowsSequence(it, columns).take(rows).toList().flatten() } + } private val TileSpec.width: Int - get() = if (iconTilesViewModel.isIconTile(this)) 1 else 2 + get() = if (largeTiles.contains(this)) 2 else 1 + + override suspend fun onActivated(): Nothing { + coroutineScope { + launch { hydrator.activate() } + launch { qsColumnsViewModel.activate() } + awaitCancellation() + } + } + + @AssistedFactory + interface Factory { + fun create(): QuickQuickSettingsViewModel + } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModel.kt index b1eb3bb38728..da175c9120ba 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModel.kt @@ -24,22 +24,31 @@ import com.android.systemui.qs.panels.ui.viewmodel.TileGridViewModel import dagger.assisted.Assisted import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch class QuickSettingsContainerViewModel @AssistedInject constructor( brightnessSliderViewModelFactory: BrightnessSliderViewModel.Factory, + quickQuickSettingsViewModelFactory: QuickQuickSettingsViewModel.Factory, @Assisted supportsBrightnessMirroring: Boolean, val tileGridViewModel: TileGridViewModel, val editModeViewModel: EditModeViewModel, - val quickQuickSettingsViewModel: QuickQuickSettingsViewModel, ) : ExclusiveActivatable() { val brightnessSliderViewModel = brightnessSliderViewModelFactory.create(supportsBrightnessMirroring) + val quickQuickSettingsViewModel = quickQuickSettingsViewModelFactory.create() + override suspend fun onActivated(): Nothing { - brightnessSliderViewModel.activate() + coroutineScope { + launch { brightnessSliderViewModel.activate() } + launch { quickQuickSettingsViewModel.activate() } + awaitCancellation() + } } @AssistedFactory 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 5229acc5f876..f3c6190d2f7b 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 @@ -19,6 +19,7 @@ package com.android.systemui.scene.domain.startable import android.app.StatusBarManager +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.compose.animation.scene.ObservableTransitionState import com.android.compose.animation.scene.SceneKey import com.android.internal.logging.UiEventLogger @@ -102,7 +103,6 @@ import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.stateIn -import com.android.app.tracing.coroutines.launchTraced as launch /** * Hooks up business logic that manipulates the state of the [SceneInteractor] for the system UI @@ -300,7 +300,7 @@ constructor( bouncerInteractor.onImeHiddenByUser.collectLatest { if (sceneInteractor.currentScene.value == Scenes.Bouncer) { sceneInteractor.changeScene( - toScene = Scenes.Lockscreen, // TODO(b/336581871): add sceneState? + toScene = Scenes.Lockscreen, loggingReason = "IME hidden", ) } @@ -318,7 +318,6 @@ constructor( when { isAnySimLocked -> { switchToScene( - // TODO(b/336581871): add sceneState? targetSceneKey = Scenes.Bouncer, loggingReason = "Need to authenticate locked SIM card.", ) @@ -326,7 +325,6 @@ constructor( unlockStatus.isUnlocked && deviceEntryInteractor.canSwipeToEnter.value == false -> { switchToScene( - // TODO(b/336581871): add sceneState? targetSceneKey = Scenes.Gone, loggingReason = "All SIM cards unlocked and device already unlocked and " + @@ -335,7 +333,6 @@ constructor( } else -> { switchToScene( - // TODO(b/336581871): add sceneState? targetSceneKey = Scenes.Lockscreen, loggingReason = "All SIM cards unlocked and device still locked" + diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt index cd38ca6cadef..7d7cab41cf96 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt @@ -77,6 +77,11 @@ class SceneWindowRootView(context: Context, attrs: AttributeSet?) : WindowRootVi } } + override fun onTouchEvent(event: MotionEvent?): Boolean { + event?.let { motionEventHandler?.onEmptySpaceMotionEvent(it) } + return super.onTouchEvent(event) + } + companion object { private const val TAG = "SceneWindowRootView" } diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt index 82f65cf55211..32d5cb460cd8 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt @@ -39,6 +39,7 @@ import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.ui.composable.Overlay import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.shared.model.ShadeMode +import com.android.systemui.statusbar.domain.interactor.RemoteInputInteractor import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -48,7 +49,6 @@ import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map -import com.android.app.tracing.coroutines.launchTraced as launch /** Models UI state for the scene container. */ class SceneContainerViewModel @@ -58,6 +58,7 @@ constructor( private val falsingInteractor: FalsingInteractor, private val powerInteractor: PowerInteractor, shadeInteractor: ShadeInteractor, + private val remoteInputInteractor: RemoteInputInteractor, private val splitEdgeDetector: SplitEdgeDetector, private val logger: SceneLogger, hapticsViewModelFactory: SceneContainerHapticsViewModel.Factory, @@ -101,6 +102,10 @@ constructor( this@SceneContainerViewModel.onMotionEvent(motionEvent) } + override fun onEmptySpaceMotionEvent(motionEvent: MotionEvent) { + this@SceneContainerViewModel.onEmptySpaceMotionEvent(motionEvent) + } + override fun onMotionEventComplete() { this@SceneContainerViewModel.onMotionEventComplete() } @@ -147,6 +152,23 @@ constructor( } /** + * Notifies that a [MotionEvent] has propagated through the entire [SharedNotificationContainer] + * and Composable scene container hierarchy without being handled. + * + * Call this after the [MotionEvent] has finished propagating through the UI hierarchy. + */ + fun onEmptySpaceMotionEvent(event: MotionEvent) { + // check if the touch is outside the window and if remote input is active. + // If true, close any active remote inputs. + if ( + event.action == MotionEvent.ACTION_OUTSIDE && + (remoteInputInteractor.isRemoteInputActive as StateFlow).value + ) { + remoteInputInteractor.closeRemoteInputs() + } + } + + /** * Notifies that a scene container user interaction has begun. * * This is a user interaction that has reached the Composable hierarchy of the scene container, @@ -263,6 +285,9 @@ constructor( /** Notifies that a [MotionEvent] has occurred. */ fun onMotionEvent(motionEvent: MotionEvent) + /** Notifies that a [MotionEvent] has occurred outside the root window. */ + fun onEmptySpaceMotionEvent(motionEvent: MotionEvent) + /** * Notifies that the previous [MotionEvent] reported by [onMotionEvent] has finished * processing. diff --git a/packages/SystemUI/src/com/android/systemui/shade/CameraLauncher.java b/packages/SystemUI/src/com/android/systemui/shade/CameraLauncher.java index fc61e90ab8f7..1d81e4083a35 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/CameraLauncher.java +++ b/packages/SystemUI/src/com/android/systemui/shade/CameraLauncher.java @@ -18,25 +18,33 @@ package com.android.systemui.shade; import com.android.systemui.camera.CameraGestureHelper; import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor; +import com.android.systemui.scene.shared.flag.SceneContainerFlag; import com.android.systemui.statusbar.phone.KeyguardBypassController; +import dagger.Lazy; + import javax.inject.Inject; + /** Handles launching camera from Shade. */ @SysUISingleton public class CameraLauncher { private final CameraGestureHelper mCameraGestureHelper; private final KeyguardBypassController mKeyguardBypassController; + private final Lazy<KeyguardQuickAffordanceInteractor> mKeyguardQuickAffordanceInteractorLazy; private boolean mLaunchingAffordance; @Inject public CameraLauncher( CameraGestureHelper cameraGestureHelper, - KeyguardBypassController keyguardBypassController + KeyguardBypassController keyguardBypassController, + Lazy<KeyguardQuickAffordanceInteractor> keyguardQuickAffordanceInteractorLazy ) { mCameraGestureHelper = cameraGestureHelper; mKeyguardBypassController = keyguardBypassController; + mKeyguardQuickAffordanceInteractorLazy = keyguardQuickAffordanceInteractorLazy; } /** Launches the camera. */ @@ -54,7 +62,12 @@ public class CameraLauncher { */ public void setLaunchingAffordance(boolean launchingAffordance) { mLaunchingAffordance = launchingAffordance; - mKeyguardBypassController.setLaunchingAffordance(launchingAffordance); + if (SceneContainerFlag.isEnabled()) { + mKeyguardQuickAffordanceInteractorLazy.get() + .setLaunchingAffordance(launchingAffordance); + } else { + mKeyguardBypassController.setLaunchingAffordance(launchingAffordance); + } } /** diff --git a/packages/SystemUI/src/com/android/systemui/shade/LongPressGestureDetector.kt b/packages/SystemUI/src/com/android/systemui/shade/LongPressGestureDetector.kt new file mode 100644 index 000000000000..6fb3ca5f86d2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/LongPressGestureDetector.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shade + +import android.content.Context +import android.view.GestureDetector +import android.view.GestureDetector.SimpleOnGestureListener +import android.view.MotionEvent +import com.android.systemui.dagger.SysUISingleton +import javax.inject.Inject + +/** Accepts touch events, detects long press, and calls ShadeViewController#onStatusBarLongPress. */ +@SysUISingleton +class LongPressGestureDetector +@Inject +constructor(context: Context, val shadeViewController: ShadeViewController) { + val gestureDetector = + GestureDetector( + context, + object : SimpleOnGestureListener() { + override fun onLongPress(event: MotionEvent) { + shadeViewController.onStatusBarLongPress(event) + } + }, + ) + + /** Accepts touch events to detect long presses. */ + fun handleTouch(ev: MotionEvent) { + gestureDetector.onTouchEvent(ev) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 1b281b14c2d1..0e82bf82fdf9 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -365,6 +365,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump private final TouchHandler mTouchHandler = new TouchHandler(); private long mDownTime; + private long mStatusBarLongPressDowntime; private boolean mTouchSlopExceededBeforeDown; private float mOverExpansion; private CentralSurfaces mCentralSurfaces; @@ -3098,6 +3099,25 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } } + /** @deprecated Temporary a11y solution until dual shade launch b/371224114 */ + @Override + @Deprecated + public void onStatusBarLongPress(MotionEvent event) { + mShadeLog.d("Status Bar was long pressed."); + ShadeExpandsOnStatusBarLongPress.assertInNewMode(); + mStatusBarLongPressDowntime = event.getDownTime(); + if (isTracking()) { + onTrackingStopped(true); + } + if (isExpanded() && !mQsController.getExpanded()) { + mShadeLog.d("Status Bar was long pressed. Expanding to QS."); + expandToQs(); + } else { + mShadeLog.d("Status Bar was long pressed. Expanding to Notifications."); + expandToNotifications(); + } + } + @Override public int getBarState() { return mBarState; @@ -3761,6 +3781,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump private void endMotionEvent(MotionEvent event, float x, float y, boolean forceCancel) { mShadeLog.logEndMotionEvent("endMotionEvent called", forceCancel, false); mTrackingPointer = -1; + mStatusBarLongPressDowntime = 0L; mAmbientState.setSwipingUp(false); if ((isTracking() && mTouchSlopExceeded) || Math.abs(x - mInitialExpandX) > mTouchSlop || Math.abs(y - mInitialExpandY) > mTouchSlop @@ -5077,6 +5098,13 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump } return true; } + // This touch session has already resulted in shade expansion. Ignore everything else. + if (ShadeExpandsOnStatusBarLongPress.isEnabled() + && event.getActionMasked() != MotionEvent.ACTION_DOWN + && event.getDownTime() == mStatusBarLongPressDowntime) { + mShadeLog.d("Touch has same down time as Status Bar long press. Ignoring."); + return false; + } if (event.getActionMasked() == MotionEvent.ACTION_DOWN && isFullyCollapsed()) { mMetricsLogger.count(COUNTER_PANEL_OPEN, 1); handled = true; @@ -5157,6 +5185,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump mUpdateFlingOnLayout = false; mMotionAborted = false; mDownTime = mSystemClock.uptimeMillis(); + mStatusBarLongPressDowntime = 0L; mTouchAboveFalsingThreshold = false; mCollapsedAndHeadsUpOnDown = isFullyCollapsed() && mHeadsUpManager.hasPinnedHeadsUp(); diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt index 49fa80c02d21..5b06ad2d1453 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt @@ -17,6 +17,7 @@ package com.android.systemui.shade import android.view.MotionEvent +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.assist.AssistManager import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background @@ -39,7 +40,6 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.delay import kotlinx.coroutines.flow.first -import com.android.app.tracing.coroutines.launchTraced as launch import kotlinx.coroutines.withContext /** @@ -136,7 +136,6 @@ constructor( } private fun animateCollapseShadeInternal() { - // TODO(b/336581871): add sceneState? shadeInteractor.collapseEitherShade( loggingReason = "ShadeController.animateCollapseShade", transitionKey = SlightlyFasterShadeCollapse, diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt index daea977c9d09..b085aec3de2f 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt @@ -98,6 +98,10 @@ interface ShadeViewController { /** Returns the ShadeHeadsUpTracker. */ val shadeHeadsUpTracker: ShadeHeadsUpTracker + @Deprecated("Temporary a11y solution until dual shade launch b/371224114") + /** Notifies the shade that a status bar detected a long press gesture. */ + fun onStatusBarLongPress(event: MotionEvent) + /** Returns the ShadeFoldAnimator. */ @Deprecated("This interface is deprecated in Scene Container") val shadeFoldAnimator: ShadeFoldAnimator @@ -179,9 +183,7 @@ interface ShadeViewStateProvider { /** Returns the expanded height of the panel view. */ @Deprecated("deprecated by SceneContainerFlag.isEnabled") val panelViewExpandedHeight: Float - /** - * Returns true if heads up should be visible. - */ + /** Returns true if heads up should be visible. */ @Deprecated("deprecated by SceneContainerFlag.isEnabled.") fun shouldHeadsUpBeVisible(): Boolean /** Return the fraction of the shade that's expanded, when in lockscreen. */ diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt index 9322d31fa2ce..53617d09fa1c 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt @@ -35,60 +35,95 @@ open class ShadeViewControllerEmptyImpl @Inject constructor() : ShadeLockscreenInteractor, PanelExpansionInteractor { @Deprecated("Use ShadeInteractor instead") override fun expandToNotifications() {} + @Deprecated("Use ShadeInteractor instead") override val isExpanded: Boolean = false override val isPanelExpanded: Boolean = false + override fun animateCollapseQs(fullyCollapse: Boolean) {} + override fun canBeCollapsed(): Boolean = false + @Deprecated("Use ShadeAnimationInteractor instead") override val isCollapsing: Boolean = false @Deprecated("Use !ShadeInteractor.isAnyExpanded instead") override val isFullyCollapsed: Boolean = false override val isTracking: Boolean = false override val isViewEnabled: Boolean = false + override fun shouldHideStatusBarIconsWhenExpanded() = false + @Deprecated("Not supported by scenes") override fun blockExpansionForCurrentTouch() {} + override fun startExpandLatencyTracking() {} + override fun startBouncerPreHideAnimation() {} + override fun dozeTimeTick() {} + override fun resetViews(animate: Boolean) {} + override val barState: Int = 0 + @Deprecated("Only supported by very old devices that will not adopt scenes.") override fun closeUserSwitcherIfOpen(): Boolean { return false } + override fun onBackPressed() {} + @Deprecated("According to b/318376223, shade predictive back is not be supported.") override fun onBackProgressed(progressFraction: Float) {} + override fun setAlpha(alpha: Int, animate: Boolean) {} + override fun setAlphaChangeAnimationEndAction(r: Runnable) {} + @Deprecated("Not supported by scenes") override fun setPulsing(pulsing: Boolean) {} + override fun setQsScrimEnabled(qsScrimEnabled: Boolean) {} + override fun setAmbientIndicationTop(ambientIndicationTop: Int, ambientTextVisible: Boolean) {} + override fun updateSystemUiStateFlags() {} + override fun updateTouchableRegion() {} + override fun transitionToExpandedShade(delay: Long) {} @Deprecated("Not supported by scenes") override fun resetViewGroupFade() {} + @Deprecated("Not supported by scenes") override fun setKeyguardTransitionProgress(keyguardAlpha: Float, keyguardTranslationY: Int) {} + @Deprecated("Not supported by scenes") override fun setOverStretchAmount(amount: Float) {} + @Deprecated("TODO(b/325072511) delete this") override fun setKeyguardStatusBarAlpha(alpha: Float) {} + override fun showAodUi() {} + @Deprecated( "depends on the state you check, use {@link #isShadeFullyExpanded()},\n" + "{@link #isOnAod()}, {@link #isOnKeyguard()} instead." ) override val isFullyExpanded = false + override fun handleExternalTouch(event: MotionEvent): Boolean { return false } + override fun handleExternalInterceptTouch(event: MotionEvent): Boolean { return false } override fun startInputFocusTransfer() {} + override fun cancelInputFocusTransfer() {} + override fun finishInputFocusTransfer(velocity: Float) {} + + @Deprecated("Temporary a11y solution until dual shade launch b/371224114") + override fun onStatusBarLongPress(event: MotionEvent) {} + override val shadeHeadsUpTracker = ShadeHeadsUpTrackerEmptyImpl() override val shadeFoldAnimator = ShadeFoldAnimatorEmptyImpl() @Deprecated("Use SceneInteractor.currentScene instead.") @@ -98,20 +133,26 @@ open class ShadeViewControllerEmptyImpl @Inject constructor() : class ShadeHeadsUpTrackerEmptyImpl : ShadeHeadsUpTracker { override fun addTrackingHeadsUpListener(listener: Consumer<ExpandableNotificationRow>) {} + override fun removeTrackingHeadsUpListener(listener: Consumer<ExpandableNotificationRow>) {} + override fun setHeadsUpAppearanceController( headsUpAppearanceController: HeadsUpAppearanceController? ) {} + override val trackedHeadsUpNotification: ExpandableNotificationRow? = null } class ShadeFoldAnimatorEmptyImpl : ShadeFoldAnimator { override fun prepareFoldToAodAnimation() {} + override fun startFoldToAodAnimation( startAction: Runnable, endAction: Runnable, cancelAction: Runnable, ) {} + override fun cancelFoldToAodAnimation() {} + override val view: ViewGroup? = null } diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImpl.kt index 3a483f460db7..f1513071de65 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeBackActionInteractorImpl.kt @@ -41,7 +41,6 @@ constructor( } else { Scenes.Shade } - // TODO(b/336581871): add sceneState? sceneInteractor.changeScene(key, "animateCollapseQs") } } diff --git a/packages/SystemUI/src/com/android/systemui/smartspace/config/BcSmartspaceConfigProvider.kt b/packages/SystemUI/src/com/android/systemui/smartspace/config/BcSmartspaceConfigProvider.kt index 5db1dcbb6c2c..f741b85cd1e0 100644 --- a/packages/SystemUI/src/com/android/systemui/smartspace/config/BcSmartspaceConfigProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/smartspace/config/BcSmartspaceConfigProvider.kt @@ -16,7 +16,7 @@ package com.android.systemui.smartspace.config -import com.android.systemui.Flags.smartspaceSwipeEventLogging +import com.android.systemui.Flags.smartspaceSwipeEventLoggingFix import com.android.systemui.Flags.smartspaceViewpager2 import com.android.systemui.flags.FeatureFlags import com.android.systemui.plugins.BcSmartspaceConfigPlugin @@ -30,5 +30,5 @@ class BcSmartspaceConfigProvider(private val featureFlags: FeatureFlags) : get() = smartspaceViewpager2() override val isSwipeEventLoggingEnabled: Boolean - get() = smartspaceSwipeEventLogging() + get() = smartspaceSwipeEventLoggingFix() } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java index 30f564f63fa1..3a24ec9408ad 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationGroupingUtil.java @@ -61,15 +61,6 @@ public class NotificationGroupingUtil { return row.getEntry().getSbn().getNotification(); } }; - private static final ResultApplicator GREY_APPLICATOR = new ResultApplicator() { - @Override - public void apply(View parent, View view, boolean apply, boolean reset) { - CachingIconView icon = view.findViewById(com.android.internal.R.id.icon); - if (icon != null) { - icon.setGrayedOut(apply); - } - } - }; private final ExpandableNotificationRow mRow; private final ArrayList<Processor> mProcessors = new ArrayList<>(); @@ -78,14 +69,18 @@ public class NotificationGroupingUtil { public NotificationGroupingUtil(ExpandableNotificationRow row) { mRow = row; - final IconComparator iconVisibilityComparator = new IconComparator(mRow) { + final IconComparator iconVisibilityComparator = new IconComparator() { public boolean compare(View parent, View child, Object parentData, Object childData) { + if (Flags.notificationsRedesignAppIcons() && mRow.isShowingAppIcon()) { + // Icon is always the same when we're showing the app icon. + return true; + } return hasSameIcon(parentData, childData) && hasSameColor(parentData, childData); } }; - final IconComparator greyComparator = new IconComparator(mRow) { + final IconComparator greyComparator = new IconComparator() { public boolean compare(View parent, View child, Object parentData, Object childData) { if (Flags.notificationsRedesignAppIcons() && mRow.isShowingAppIcon()) { @@ -95,6 +90,19 @@ public class NotificationGroupingUtil { || hasSameColor(parentData, childData); } }; + final ResultApplicator greyApplicator = new ResultApplicator() { + @Override + public void apply(View parent, View view, boolean apply, boolean reset) { + if (Flags.notificationsRedesignAppIcons() && mRow.isShowingAppIcon()) { + // Do nothing. + return; + } + CachingIconView icon = view.findViewById(com.android.internal.R.id.icon); + if (icon != null) { + icon.setGrayedOut(apply); + } + } + }; // To hide the icons if they are the same and the color is the same mProcessors.add(new Processor(mRow, @@ -107,7 +115,7 @@ public class NotificationGroupingUtil { com.android.internal.R.id.status_bar_latest_event_content, ICON_EXTRACTOR, greyComparator, - GREY_APPLICATOR)); + greyApplicator)); // To show the large icon on the left side instead if all the small icons are the same mProcessors.add(new Processor(mRow, com.android.internal.R.id.status_bar_latest_event_content, @@ -319,13 +327,14 @@ public class NotificationGroupingUtil { private interface ViewComparator { /** - * @param parent the view with the given id in the group header - * @param child the view with the given id in the child notification + * @param parent the view with the given id in the group header + * @param child the view with the given id in the child notification * @param parentData optional data for the parent - * @param childData optional data for the child + * @param childData optional data for the child * @return whether to views are the same */ boolean compare(View parent, View child, Object parentData, Object childData); + boolean isEmpty(View view); } @@ -368,21 +377,12 @@ public class NotificationGroupingUtil { } private abstract static class IconComparator implements ViewComparator { - private final ExpandableNotificationRow mRow; - - IconComparator(ExpandableNotificationRow row) { - mRow = row; - } - @Override public boolean compare(View parent, View child, Object parentData, Object childData) { return false; } protected boolean hasSameIcon(Object parentData, Object childData) { - if (Flags.notificationsRedesignAppIcons() && mRow.isShowingAppIcon()) { - return true; - } Icon parentIcon = getIcon((Notification) parentData); Icon childIcon = getIcon((Notification) childData); return parentIcon.sameAs(childIcon); @@ -420,9 +420,9 @@ public class NotificationGroupingUtil { private interface ResultApplicator { /** * @param parent the root view of the child notification - * @param view the view with the given id in the child notification - * @param apply whether the state should be applied or removed - * @param reset if [de]application is the result of a reset + * @param view the view with the given id in the child notification + * @param apply whether the state should be applied or removed + * @param reset if [de]application is the result of a reset */ void apply(View parent, View view, boolean apply, boolean reset); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java index 9b1e782fcba4..fdc1c0e4dd22 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationRemoteInputManager.java @@ -53,6 +53,7 @@ import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.power.domain.interactor.PowerInteractor; import com.android.systemui.res.R; +import com.android.systemui.scene.shared.flag.SceneContainerFlag; import com.android.systemui.shade.domain.interactor.ShadeInteractor; import com.android.systemui.statusbar.dagger.CentralSurfacesDependenciesModule; import com.android.systemui.statusbar.notification.NotifPipelineFlags; @@ -686,6 +687,7 @@ public class NotificationRemoteInputManager implements CoreStartable { } public void checkRemoteInputOutside(MotionEvent event) { + SceneContainerFlag.assertInLegacyMode(); if (event.getAction() == MotionEvent.ACTION_OUTSIDE // touch outside the source bar && event.getX() == 0 && event.getY() == 0 // a touch outside both bars && isRemoteInputActive()) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/core/CommandQueueInitializer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/core/CommandQueueInitializer.kt index 57c8bc6133e6..614f0f48d1fc 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/core/CommandQueueInitializer.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/core/CommandQueueInitializer.kt @@ -46,7 +46,7 @@ constructor( ) : CoreStartable { override fun start() { - StatusBarSimpleFragment.assertInNewMode() + StatusBarConnectedDisplays.assertInNewMode() val result: RegisterStatusBarResult = try { barService.registerStatusBar(commandQueue) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesModule.java b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesModule.java index 72cd63f3ae62..ad22caaf9156 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesModule.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/CentralSurfacesModule.java @@ -17,8 +17,6 @@ package com.android.systemui.statusbar.dagger; import com.android.systemui.dagger.SysUISingleton; -import com.android.systemui.statusbar.notification.dagger.NotificationsModule; -import com.android.systemui.statusbar.notification.row.NotificationRowModule; import com.android.systemui.statusbar.phone.CentralSurfaces; import com.android.systemui.statusbar.phone.CentralSurfacesImpl; import com.android.systemui.statusbar.phone.StatusBarNotificationPresenterModule; @@ -30,8 +28,7 @@ import dagger.Module; * Dagger Module providing {@link CentralSurfacesImpl}. */ @Module(includes = {CentralSurfacesDependenciesModule.class, - StatusBarNotificationPresenterModule.class, - NotificationsModule.class, NotificationRowModule.class}) + StatusBarNotificationPresenterModule.class}) public interface CentralSurfacesModule { /** * Provides our instance of CentralSurfaces which is considered optional. diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/data/StatusBarDataLayerModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/data/StatusBarDataLayerModule.kt index 8a850b0fb199..c416bf7b4f92 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/data/StatusBarDataLayerModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/data/StatusBarDataLayerModule.kt @@ -16,7 +16,6 @@ package com.android.systemui.statusbar.data import com.android.systemui.statusbar.data.repository.KeyguardStatusBarRepositoryModule -import com.android.systemui.statusbar.data.repository.PrivacyDotViewControllerStoreModule import com.android.systemui.statusbar.data.repository.RemoteInputRepositoryModule import com.android.systemui.statusbar.data.repository.StatusBarConfigurationControllerModule import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStoreModule @@ -28,7 +27,6 @@ import dagger.Module includes = [ KeyguardStatusBarRepositoryModule::class, - PrivacyDotViewControllerStoreModule::class, RemoteInputRepositoryModule::class, StatusBarConfigurationControllerModule::class, StatusBarContentInsetsProviderStoreModule::class, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/RemoteInputRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/RemoteInputRepository.kt index 9af4b8c18c86..cd1e2ceeca00 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/RemoteInputRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/data/repository/RemoteInputRepository.kt @@ -16,16 +16,21 @@ package com.android.systemui.statusbar.data.repository -import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.statusbar.NotificationRemoteInputManager import com.android.systemui.statusbar.RemoteInputController +import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow import dagger.Binds import dagger.Module import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.stateIn /** * Repository used for tracking the state of notification remote input (e.g. when the user presses @@ -42,30 +47,50 @@ interface RemoteInputRepository { val remoteInputRowBottomBound: Flow<Float?> fun setRemoteInputRowBottomBound(bottom: Float?) + + /** Close any active remote inputs */ + fun closeRemoteInputs() } @SysUISingleton class RemoteInputRepositoryImpl @Inject -constructor(private val notificationRemoteInputManager: NotificationRemoteInputManager) : - RemoteInputRepository { - override val isRemoteInputActive: Flow<Boolean> = conflatedCallbackFlow { - trySend(false) // initial value is false +constructor( + @Application applicationScope: CoroutineScope, + private val notificationRemoteInputManager: NotificationRemoteInputManager, +) : RemoteInputRepository { + private val _isRemoteInputActive = conflatedCallbackFlow { val callback = object : RemoteInputController.Callback { override fun onRemoteInputActive(active: Boolean) { trySend(active) } } + trySend(notificationRemoteInputManager.isRemoteInputActive) notificationRemoteInputManager.addControllerCallback(callback) awaitClose { notificationRemoteInputManager.removeControllerCallback(callback) } } + override val isRemoteInputActive = + if (SceneContainerFlag.isEnabled) { + _isRemoteInputActive.stateIn( + applicationScope, + SharingStarted.WhileSubscribed(), + notificationRemoteInputManager.isRemoteInputActive, + ) + } else { + _isRemoteInputActive + } + override val remoteInputRowBottomBound = MutableStateFlow<Float?>(null) override fun setRemoteInputRowBottomBound(bottom: Float?) { remoteInputRowBottomBound.value = bottom } + + override fun closeRemoteInputs() { + notificationRemoteInputManager.closeRemoteInputs() + } } @Module diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/domain/interactor/RemoteInputInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/domain/interactor/RemoteInputInteractor.kt index b83b0cc8d2c9..ea8c3e2cef91 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/domain/interactor/RemoteInputInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/domain/interactor/RemoteInputInteractor.kt @@ -40,4 +40,9 @@ constructor(private val remoteInputRepository: RemoteInputRepository) { fun setRemoteInputRowBottomBound(bottom: Float?) { remoteInputRepository.setRemoteInputRowBottomBound(bottom) } + + /** Close any active remote inputs */ + fun closeRemoteInputs() { + remoteInputRepository.closeRemoteInputs() + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotCorner.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotCorner.kt new file mode 100644 index 000000000000..8a6f355701cd --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotCorner.kt @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.events + +import android.view.Gravity +import android.view.Surface + +/** Represents a corner on the display for the privacy dot. */ +enum class PrivacyDotCorner( + val index: Int, + val gravity: Int, + val innerGravity: Int, + val title: String, +) { + TopLeft( + index = 0, + gravity = Gravity.TOP or Gravity.LEFT, + innerGravity = Gravity.CENTER_VERTICAL or Gravity.RIGHT, + title = "TopLeft", + ), + TopRight( + index = 1, + gravity = Gravity.TOP or Gravity.RIGHT, + innerGravity = Gravity.CENTER_VERTICAL or Gravity.LEFT, + title = "TopRight", + ), + BottomRight( + index = 2, + gravity = Gravity.BOTTOM or Gravity.RIGHT, + innerGravity = Gravity.CENTER_VERTICAL or Gravity.RIGHT, + title = "BottomRight", + ), + BottomLeft( + index = 3, + gravity = Gravity.BOTTOM or Gravity.LEFT, + innerGravity = Gravity.CENTER_VERTICAL or Gravity.LEFT, + title = "BottomLeft", + ), +} + +fun PrivacyDotCorner.rotatedCorner(@Surface.Rotation rotation: Int): PrivacyDotCorner { + var modded = index - rotation + if (modded < 0) { + modded += 4 + } + return PrivacyDotCorner.entries[modded] +} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt index 914cc50a4a3a..f7bc23c6eb17 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/events/PrivacyDotViewController.kt @@ -20,12 +20,13 @@ import android.annotation.UiThread import android.graphics.Point import android.graphics.Rect import android.util.Log -import android.view.Gravity import android.view.View import android.widget.FrameLayout import androidx.core.animation.Animator import com.android.app.animation.Interpolators +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.internal.annotations.GuardedBy +import com.android.systemui.ScreenDecorationsThread import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Main @@ -36,6 +37,10 @@ import com.android.systemui.statusbar.StatusBarState.SHADE import com.android.systemui.statusbar.StatusBarState.SHADE_LOCKED import com.android.systemui.statusbar.core.StatusBarConnectedDisplays import com.android.systemui.statusbar.data.repository.StatusBarContentInsetsProviderStore +import com.android.systemui.statusbar.events.PrivacyDotCorner.BottomLeft +import com.android.systemui.statusbar.events.PrivacyDotCorner.BottomRight +import com.android.systemui.statusbar.events.PrivacyDotCorner.TopLeft +import com.android.systemui.statusbar.events.PrivacyDotCorner.TopRight import com.android.systemui.statusbar.phone.StatusBarContentInsetsChangedListener import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider import com.android.systemui.statusbar.policy.ConfigurationController @@ -53,7 +58,6 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import java.util.concurrent.Executor import kotlinx.coroutines.CoroutineScope -import com.android.app.tracing.coroutines.launchTraced as launch /** * Understands how to keep the persistent privacy dot in the corner of the screen in @@ -81,10 +85,6 @@ interface PrivacyDotViewController { var showingListener: ShowingListener? - fun setUiExecutor(e: DelayableExecutor) - - fun getUiExecutor(): DelayableExecutor? - @UiThread fun setNewRotation(rot: Int) @UiThread fun hideDotView(dot: View, animate: Boolean) @@ -117,6 +117,7 @@ constructor( @Assisted private val contentInsetsProvider: StatusBarContentInsetsProvider, private val animationScheduler: SystemStatusAnimationScheduler, shadeInteractor: ShadeInteractor?, + @ScreenDecorationsThread val uiExecutor: DelayableExecutor, ) : PrivacyDotViewController { private lateinit var tl: View private lateinit var tr: View @@ -136,9 +137,6 @@ constructor( private val lock = Object() private var cancelRunnable: Runnable? = null - // Privacy dots are created in ScreenDecoration's UiThread, which is not the main thread - private var uiExecutor: DelayableExecutor? = null - private val views: Sequence<View> get() = if (!this::tl.isInitialized) sequenceOf() else sequenceOf(tl, tr, br, bl) @@ -155,7 +153,7 @@ constructor( private val configurationListener = object : ConfigurationController.ConfigurationListener { override fun onLayoutDirectionChanged(isRtl: Boolean) { - uiExecutor?.execute { + uiExecutor.execute { // If rtl changed, hide all dots until the next state resolves setCornerVisibilities(View.INVISIBLE) @@ -198,14 +196,6 @@ constructor( stateController.removeCallback(statusBarStateListener) } - override fun setUiExecutor(e: DelayableExecutor) { - uiExecutor = e - } - - override fun getUiExecutor(): DelayableExecutor? { - return uiExecutor - } - @UiThread override fun setNewRotation(rot: Int) { dlog("updateRotation: $rot") @@ -222,8 +212,8 @@ constructor( // If we rotated, hide all dotes until the next state resolves setCornerVisibilities(View.INVISIBLE) - val newCorner = selectDesignatedCorner(rot, isRtl) - val index = newCorner.cornerIndex() + val newCornerView = selectDesignatedCorner(rot, isRtl) + val corner = newCornerView.corner() val paddingTop = contentInsetsProvider.getStatusBarPaddingTop(rot) synchronized(lock) { @@ -231,8 +221,8 @@ constructor( nextViewState.copy( rotation = rot, paddingTop = paddingTop, - designatedCorner = newCorner, - cornerIndex = index, + designatedCorner = newCornerView, + corner = corner, ) } } @@ -284,24 +274,15 @@ constructor( views.forEach { corner -> corner.setPadding(0, paddingTop, 0, 0) - val rotatedCorner = rotatedCorner(cornerForView(corner), rotation) + val rotatedCorner = cornerForView(corner).rotatedCorner(rotation) (corner.layoutParams as FrameLayout.LayoutParams).apply { - gravity = rotatedCorner.toGravity() + gravity = rotatedCorner.gravity } // Set the dot's view gravity to hug the status bar (corner.requireViewById<View>(R.id.privacy_dot).layoutParams as FrameLayout.LayoutParams) - .gravity = rotatedCorner.innerGravity() - } - } - - @UiThread - private fun updateCornerSizes(l: Int, r: Int, rotation: Int) { - views.forEach { corner -> - val rotatedCorner = rotatedCorner(cornerForView(corner), rotation) - val w = widthForCorner(rotatedCorner, l, r) - (corner.layoutParams as FrameLayout.LayoutParams).width = w + .gravity = rotatedCorner.innerGravity } } @@ -419,25 +400,16 @@ constructor( } } - private fun cornerForView(v: View): Int { + private fun cornerForView(v: View): PrivacyDotCorner { return when (v) { - tl -> TOP_LEFT - tr -> TOP_RIGHT - bl -> BOTTOM_LEFT - br -> BOTTOM_RIGHT + tl -> TopLeft + tr -> TopRight + bl -> BottomLeft + br -> BottomRight else -> throw IllegalArgumentException("not a corner view") } } - private fun rotatedCorner(corner: Int, rotation: Int): Int { - var modded = corner - rotation - if (modded < 0) { - modded += 4 - } - - return modded - } - @Rotation private fun activeRotationForCorner(corner: View, rtl: Boolean): Int { // Each corner will only be visible in a single rotation, based on rtl @@ -449,16 +421,6 @@ constructor( } } - private fun widthForCorner(corner: Int, left: Int, right: Int): Int { - return when (corner) { - TOP_LEFT, - BOTTOM_LEFT -> left - TOP_RIGHT, - BOTTOM_RIGHT -> right - else -> throw IllegalArgumentException("Unknown corner") - } - } - override fun initialize(topLeft: View, topRight: View, bottomLeft: View, bottomRight: View) { if ( this::tl.isInitialized && @@ -478,9 +440,9 @@ constructor( val rtl = configurationController.isLayoutRtl val currentRotation = RotationUtils.getExactRotation(tl.context) - val dc = selectDesignatedCorner(currentRotation, rtl) + val designatedCornerView = selectDesignatedCorner(currentRotation, rtl) - val index = dc.cornerIndex() + val corner = designatedCornerView.corner() mainExecutor.execute { animationScheduler.addCallback(systemStatusAnimationCallback) } @@ -494,8 +456,8 @@ constructor( nextViewState = nextViewState.copy( viewInitialized = true, - designatedCorner = dc, - cornerIndex = index, + designatedCorner = designatedCornerView, + corner = corner, seascapeRect = left, portraitRect = top, landscapeRect = right, @@ -524,7 +486,7 @@ constructor( dlog("scheduleUpdate: ") cancelRunnable?.run() - cancelRunnable = uiExecutor?.executeDelayed({ processNextViewState() }, 100) + cancelRunnable = uiExecutor.executeDelayed({ processNextViewState() }, 100) } @UiThread @@ -613,11 +575,11 @@ constructor( } } - private fun View?.cornerIndex(): Int { + private fun View?.corner(): PrivacyDotCorner? { if (this != null) { return cornerForView(this) } - return -1 + return null } // Returns [left, top, right, bottom] aka [seascape, none, landscape, upside-down] @@ -666,35 +628,11 @@ private fun vlog(s: String) { } } -const val TOP_LEFT = 0 -const val TOP_RIGHT = 1 -const val BOTTOM_RIGHT = 2 -const val BOTTOM_LEFT = 3 private const val DURATION = 160L private const val TAG = "PrivacyDotViewController" private const val DEBUG = false private const val DEBUG_VERBOSE = false -private fun Int.toGravity(): Int { - return when (this) { - TOP_LEFT -> Gravity.TOP or Gravity.LEFT - TOP_RIGHT -> Gravity.TOP or Gravity.RIGHT - BOTTOM_LEFT -> Gravity.BOTTOM or Gravity.LEFT - BOTTOM_RIGHT -> Gravity.BOTTOM or Gravity.RIGHT - else -> throw IllegalArgumentException("Not a corner") - } -} - -private fun Int.innerGravity(): Int { - return when (this) { - TOP_LEFT -> Gravity.CENTER_VERTICAL or Gravity.RIGHT - TOP_RIGHT -> Gravity.CENTER_VERTICAL or Gravity.LEFT - BOTTOM_LEFT -> Gravity.CENTER_VERTICAL or Gravity.RIGHT - BOTTOM_RIGHT -> Gravity.CENTER_VERTICAL or Gravity.LEFT - else -> throw IllegalArgumentException("Not a corner") - } -} - data class ViewState( val viewInitialized: Boolean = false, val systemPrivacyEventIsActive: Boolean = false, @@ -707,7 +645,7 @@ data class ViewState( val layoutRtl: Boolean = false, val rotation: Int = 0, val paddingTop: Int = 0, - val cornerIndex: Int = -1, + val corner: PrivacyDotCorner? = null, val designatedCorner: View? = null, val contentDescription: String? = null, ) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt index ea515e0c2cf1..08ffbf2b29d4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/NotificationWakeUpCoordinator.kt @@ -22,11 +22,13 @@ import androidx.annotation.VisibleForTesting import androidx.core.animation.ObjectAnimator import com.android.app.animation.Interpolators import com.android.app.animation.InterpolatorsAndroidX +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.Dumpable import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dump.DumpManager +import com.android.systemui.keyguard.domain.interactor.PulseExpansionInteractor import com.android.systemui.plugins.statusbar.StatusBarStateController import com.android.systemui.shade.ShadeExpansionChangeEvent import com.android.systemui.shade.ShadeExpansionListener @@ -49,7 +51,6 @@ import javax.inject.Inject import kotlin.math.max import kotlin.math.min import kotlinx.coroutines.CoroutineScope -import com.android.app.tracing.coroutines.launchTraced as launch @SysUISingleton class NotificationWakeUpCoordinator @@ -65,6 +66,7 @@ constructor( private val logger: NotificationWakeUpCoordinatorLogger, private val notifsKeyguardInteractor: NotificationsKeyguardInteractor, private val communalInteractor: CommunalInteractor, + private val pulseExpansionInteractor: PulseExpansionInteractor, ) : OnHeadsUpChangedListener, StatusBarStateController.StateListener, @@ -115,7 +117,7 @@ constructor( // they were blocked by the proximity sensor updateNotificationVisibility( animate = shouldAnimateVisibility(), - increaseSpeed = false + increaseSpeed = false, ) } } @@ -139,7 +141,7 @@ constructor( // the waking up callback updateNotificationVisibility( animate = shouldAnimateVisibility(), - increaseSpeed = false + increaseSpeed = false, ) } } @@ -200,7 +202,7 @@ constructor( setNotificationsVisibleForExpansion( visible = false, animate = false, - increaseSpeed = false + increaseSpeed = false, ) } } @@ -226,7 +228,7 @@ constructor( for (listener in wakeUpListeners) { listener.onPulseExpandingChanged(pulseExpanding) } - notifsKeyguardInteractor.setPulseExpanding(pulseExpanding) + pulseExpansionInteractor.setPulseExpanding(pulseExpanding) } } } @@ -241,7 +243,7 @@ constructor( fun setNotificationsVisibleForExpansion( visible: Boolean, animate: Boolean, - increaseSpeed: Boolean + increaseSpeed: Boolean, ) { notificationsVisibleForExpansion = visible updateNotificationVisibility(animate, increaseSpeed) @@ -282,7 +284,7 @@ constructor( private fun setNotificationsVisible( visible: Boolean, animate: Boolean, - increaseSpeed: Boolean + increaseSpeed: Boolean, ) { if (notificationsVisible == visible) { return @@ -363,7 +365,7 @@ constructor( hardOverride = hardDozeAmountOverride, outputLinear = outputLinearDozeAmount, state = statusBarStateController.state, - changed = changed + changed = changed, ) stackScrollerController.setDozeAmount(outputEasedDozeAmount) updateHideAmount() @@ -372,7 +374,7 @@ constructor( setNotificationsVisibleForExpansion( visible = false, animate = false, - increaseSpeed = false + increaseSpeed = false, ) } } @@ -389,10 +391,7 @@ constructor( * call with `false` at some point in the near future. A call with `true` before that will * happen if the animation is not already running. */ - fun setWakingUp( - wakingUp: Boolean, - requestDelayedAnimation: Boolean, - ) { + fun setWakingUp(wakingUp: Boolean, requestDelayedAnimation: Boolean) { logger.logSetWakingUp(wakingUp, requestDelayedAnimation) this.wakingUp = wakingUp if (wakingUp && requestDelayedAnimation) { @@ -432,7 +431,7 @@ constructor( // See: UnlockedScreenOffAnimationController.onFinishedWakingUp() setHardDozeAmountOverride( dozing = false, - source = "Override: Shade->Shade (lock cancelled by unlock)" + source = "Override: Shade->Shade (lock cancelled by unlock)", ) this.state = newState return @@ -478,7 +477,7 @@ constructor( wasCollapsedEnoughToHide, isCollapsedEnoughToHide, couldShowPulsingHuns, - canShowPulsingHuns + canShowPulsingHuns, ) if (couldShowPulsingHuns && !canShowPulsingHuns) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/ReferenceNotificationsModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/ReferenceNotificationsModule.kt new file mode 100644 index 000000000000..925d4a588f09 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/dagger/ReferenceNotificationsModule.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.systemui.statusbar.notification.dagger + +import com.android.systemui.statusbar.notification.row.NotificationRowModule +import dagger.Module + +/** + * A module that includes the standard notifications classes that most SysUI variants need. Variants + * are free to not include this module and instead write a custom notifications module. + */ +@Module(includes = [NotificationsModule::class, NotificationRowModule::class]) +object ReferenceNotificationsModule diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepository.kt index bd6ea30c44e6..f9fd5caa0d25 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/data/repository/NotificationsKeyguardViewStateRepository.kt @@ -24,7 +24,4 @@ import kotlinx.coroutines.flow.MutableStateFlow class NotificationsKeyguardViewStateRepository @Inject constructor() { /** Are notifications fully hidden from view? */ val areNotificationsFullyHidden = MutableStateFlow(false) - - /** Is a pulse expansion occurring? */ - val isPulseExpanding = MutableStateFlow(false) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractor.kt index a6361cbc9f9c..1cb41448f257 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/NotificationsKeyguardInteractor.kt @@ -22,12 +22,7 @@ import kotlinx.coroutines.flow.Flow /** Domain logic pertaining to notifications on the keyguard. */ class NotificationsKeyguardInteractor @Inject -constructor( - private val repository: NotificationsKeyguardViewStateRepository, -) { - /** Is a pulse expansion occurring? */ - val isPulseExpanding: Flow<Boolean> = repository.isPulseExpanding - +constructor(private val repository: NotificationsKeyguardViewStateRepository) { /** Are notifications fully hidden from view? */ val areNotificationsFullyHidden: Flow<Boolean> = repository.areNotificationsFullyHidden @@ -35,9 +30,4 @@ constructor( fun setNotificationsFullyHidden(fullyHidden: Boolean) { repository.areNotificationsFullyHidden.value = fullyHidden } - - /** Updates whether a pulse expansion is occurring. */ - fun setPulseExpanding(pulseExpanding: Boolean) { - repository.isPulseExpanding.value = pulseExpanding - } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java index aa2a08f980f9..08d177f933c1 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRow.java @@ -1987,6 +1987,7 @@ public class ExpandableNotificationRow extends ActivatableNotificationView mColorUpdateLogger = colorUpdateLogger; mDismissibilityProvider = dismissibilityProvider; mFeatureFlags = featureFlags; + setHapticFeedbackEnabled(!com.android.systemui.Flags.msdlFeedback()); } private void initDimens() { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java index 23a2facf4c5a..baad616cbf02 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowController.java @@ -64,6 +64,9 @@ import com.android.systemui.statusbar.policy.dagger.RemoteInputViewSubcomponent; import com.android.systemui.util.time.SystemClock; import com.android.systemui.wmshell.BubblesManager; +import com.google.android.msdl.data.model.MSDLToken; +import com.google.android.msdl.domain.MSDLPlayer; + import java.util.List; import java.util.Optional; @@ -114,6 +117,7 @@ public class ExpandableNotificationRowController implements NotifViewController private final NotificationDismissibilityProvider mDismissibilityProvider; private final IStatusBarService mStatusBarService; private final UiEventLogger mUiEventLogger; + private final MSDLPlayer mMSDLPlayer; private final NotificationSettingsController mSettingsController; @@ -273,7 +277,8 @@ public class ExpandableNotificationRowController implements NotifViewController ExpandableNotificationRowDragController dragController, NotificationDismissibilityProvider dismissibilityProvider, IStatusBarService statusBarService, - UiEventLogger uiEventLogger) { + UiEventLogger uiEventLogger, + MSDLPlayer msdlPlayer) { mView = view; mListContainer = listContainer; mRemoteInputViewSubcomponentFactory = rivSubcomponentFactory; @@ -309,6 +314,7 @@ public class ExpandableNotificationRowController implements NotifViewController mDismissibilityProvider = dismissibilityProvider; mStatusBarService = statusBarService; mUiEventLogger = uiEventLogger; + mMSDLPlayer = msdlPlayer; } /** @@ -352,6 +358,9 @@ public class ExpandableNotificationRowController implements NotifViewController } mView.setLongPressListener((v, x, y, item) -> { + if (com.android.systemui.Flags.msdlFeedback()) { + mMSDLPlayer.playToken(MSDLToken.LONG_PRESS, null); + } if (mView.isSummaryWithChildren()) { mView.expandNotification(); return true; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index c1d72e4f7a2a..57af8ea19722 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -678,7 +678,9 @@ public class NotificationStackScrollLayout mGroupMembershipManager = Dependency.get(GroupMembershipManager.class); mGroupExpansionManager = Dependency.get(GroupExpansionManager.class); setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES); - setWindowInsetsAnimationCallback(mInsetsCallback); + if (!SceneContainerFlag.isEnabled()) { + setWindowInsetsAnimationCallback(mInsetsCallback); + } } /** @@ -795,7 +797,7 @@ public class NotificationStackScrollLayout && !onKeyguard() && mUpcomingStatusBarState != StatusBarState.KEYGUARD // quick settings don't affect notifications when not in full screen - && (mQsExpansionFraction != 1 || !mQsFullScreen) + && (getQsExpansionFraction() != 1 || !mQsFullScreen) && !mScreenOffAnimationController.shouldHideNotificationsFooter() && !mIsRemoteInputActive; } @@ -1526,7 +1528,7 @@ public class NotificationStackScrollLayout float fraction = mAmbientState.getExpansionFraction(); // If we are on quick settings, we need to quickly hide it to show the bouncer to avoid an // overlap. Otherwise, we maintain the normal fraction for smoothness. - if (mAmbientState.isBouncerInTransit() && mQsExpansionFraction > 0f) { + if (mAmbientState.isBouncerInTransit() && getQsExpansionFraction() > 0f) { fraction = BouncerPanelExpansionCalculator.aboutToShowBouncerProgress(fraction); } final float stackY = MathUtils.lerp(0, endTopPosition, fraction); @@ -1550,7 +1552,7 @@ public class NotificationStackScrollLayout } updateInterpolatedStackHeight(endHeight, fraction); } else { - if (mQsExpansionFraction <= 0 && !shouldSkipHeightUpdate()) { + if (getQsExpansionFraction() <= 0 && !shouldSkipHeightUpdate()) { final float endHeight = updateStackEndHeight( getHeight(), getEmptyBottomMarginInternal(), getTopPadding()); updateInterpolatedStackHeight(endHeight, fraction); @@ -1694,7 +1696,7 @@ public class NotificationStackScrollLayout } else if (mQsFullScreen) { int stackStartPosition = getContentHeight() - getTopPadding() + getIntrinsicPadding(); - int stackEndPosition = mMaxTopPadding + mShelf.getIntrinsicHeight(); + int stackEndPosition = getMaxTopPadding() + mShelf.getIntrinsicHeight(); if (stackStartPosition <= stackEndPosition) { stackHeight = stackEndPosition; } else { @@ -1703,7 +1705,7 @@ public class NotificationStackScrollLayout stackHeight = (int) height; } else { stackHeight = (int) NotificationUtils.interpolate(stackStartPosition, - stackEndPosition, mQsExpansionFraction); + stackEndPosition, getQsExpansionFraction()); } } } else { @@ -2086,6 +2088,7 @@ public class NotificationStackScrollLayout } private void updateImeInset(WindowInsets windowInsets) { + SceneContainerFlag.assertInLegacyMode(); mImeInset = windowInsets.getInsets(WindowInsets.Type.ime()).bottom; if (mFooterView != null && mFooterView.getViewState() != null) { @@ -2112,7 +2115,7 @@ public class NotificationStackScrollLayout if (cutout != null) { mWaterfallTopInset = cutout.getWaterfallInsets().top; } - if (!mIsInsetAnimationRunning) { + if (!SceneContainerFlag.isEnabled() && !mIsInsetAnimationRunning) { // update bottom inset e.g. after rotation updateImeInset(insets); } @@ -2513,6 +2516,7 @@ public class NotificationStackScrollLayout } private int getImeInset() { + SceneContainerFlag.assertInLegacyMode(); // The NotificationStackScrollLayout does not extend all the way to the bottom of the // display. Therefore, subtract that space from the mImeInset, in order to only include // the portion of the bottom inset that actually overlaps the NotificationStackScrollLayout. @@ -2851,11 +2855,6 @@ public class NotificationStackScrollLayout setExpandedHeight(mExpandedHeight); } - public void setMaxTopPadding(int maxTopPadding) { - SceneContainerFlag.assertInLegacyMode(); - mMaxTopPadding = maxTopPadding; - } - public int getLayoutMinHeight() { SceneContainerFlag.assertInLegacyMode(); return getLayoutMinHeightInternal(); @@ -3639,7 +3638,11 @@ public class NotificationStackScrollLayout * @return Whether a y coordinate is inside the content. */ public boolean isInContentBounds(float y) { - return y < getHeight() - getEmptyBottomMarginInternal(); + if (SceneContainerFlag.isEnabled()) { + return y < mAmbientState.getStackCutoff(); + } else { + return y < getHeight() - getEmptyBottomMarginInternal(); + } } private float getTouchSlop(MotionEvent event) { @@ -5268,17 +5271,22 @@ public class NotificationStackScrollLayout return mQsFullScreen; } + private float getQsExpansionFraction() { + SceneContainerFlag.assertInLegacyMode(); + return mQsExpansionFraction; + } + public void setQsExpansionFraction(float qsExpansionFraction) { SceneContainerFlag.assertInLegacyMode(); - boolean footerAffected = mQsExpansionFraction != qsExpansionFraction - && (mQsExpansionFraction == 1 || qsExpansionFraction == 1); + boolean footerAffected = getQsExpansionFraction() != qsExpansionFraction + && (getQsExpansionFraction() == 1 || qsExpansionFraction == 1); mQsExpansionFraction = qsExpansionFraction; updateUseRoundedRectClipping(); // If notifications are scrolled, // clear out scrollY by the time we push notifications offscreen if (getOwnScrollY() > 0) { - setOwnScrollY((int) MathUtils.lerp(getOwnScrollY(), 0, mQsExpansionFraction)); + setOwnScrollY((int) MathUtils.lerp(getOwnScrollY(), 0, getQsExpansionFraction())); } if (!FooterViewRefactor.isEnabled() && footerAffected) { updateFooter(); @@ -5510,9 +5518,7 @@ public class NotificationStackScrollLayout println(pw, "alpha", getAlpha()); println(pw, "suppressChildrenMeasureLayout", mSuppressChildrenMeasureAndLayout); println(pw, "scrollY", mAmbientState.getScrollY()); - println(pw, "maxTopPadding", mMaxTopPadding); println(pw, "showShelfOnly", mShouldShowShelfOnly); - println(pw, "qsExpandFraction", mQsExpansionFraction); println(pw, "isCurrentUserSetup", mIsCurrentUserSetup); println(pw, "hideAmount", mAmbientState.getHideAmount()); println(pw, "ambientStateSwipingUp", mAmbientState.isSwipingUp()); @@ -5547,6 +5553,8 @@ public class NotificationStackScrollLayout println(pw, "intrinsicContentHeight", getIntrinsicContentHeight()); println(pw, "contentHeight", getContentHeight()); println(pw, "topPadding", getTopPadding()); + println(pw, "maxTopPadding", getMaxTopPadding()); + println(pw, "qsExpandFraction", getQsExpansionFraction()); } }); pw.println(); @@ -5619,7 +5627,9 @@ public class NotificationStackScrollLayout pw.println("mIsCurrentUserSetup: " + mIsCurrentUserSetup); pw.println("onKeyguard: " + onKeyguard()); pw.println("mUpcomingStatusBarState: " + mUpcomingStatusBarState); - pw.println("mQsExpansionFraction: " + mQsExpansionFraction); + if (!SceneContainerFlag.isEnabled()) { + pw.println("QsExpansionFraction: " + getQsExpansionFraction()); + } pw.println("mQsFullScreen: " + mQsFullScreen); pw.println( "mScreenOffAnimationController" @@ -6243,7 +6253,8 @@ public class NotificationStackScrollLayout if (SceneContainerFlag.isEnabled()) return; // We don't want to clip notifications when QS is expanded, because incoming heads up on // the bottom would be clipped otherwise - boolean qsAllowsClipping = mQsExpansionFraction < 0.5f || mShouldUseSplitNotificationShade; + boolean qsAllowsClipping = + getQsExpansionFraction() < 0.5f || mShouldUseSplitNotificationShade; boolean clip = mIsExpanded && qsAllowsClipping; if (clip != mShouldUseRoundedRectClipping) { mShouldUseRoundedRectClipping = clip; @@ -6953,10 +6964,12 @@ public class NotificationStackScrollLayout /** Use {@link ScrollViewFields#intrinsicStackHeight}, when SceneContainerFlag is enabled. */ private int getContentHeight() { + SceneContainerFlag.assertInLegacyMode(); return mContentHeight; } private void setContentHeight(int contentHeight) { + SceneContainerFlag.assertInLegacyMode(); mContentHeight = contentHeight; } @@ -6965,10 +6978,23 @@ public class NotificationStackScrollLayout * @return the height of the content ignoring the footer. */ public float getIntrinsicContentHeight() { + SceneContainerFlag.assertInLegacyMode(); return mIntrinsicContentHeight; } private void setIntrinsicContentHeight(float intrinsicContentHeight) { + SceneContainerFlag.assertInLegacyMode(); mIntrinsicContentHeight = intrinsicContentHeight; } + + private int getMaxTopPadding() { + SceneContainerFlag.assertInLegacyMode(); + return mMaxTopPadding; + } + + /** Not used with SceneContainerFlag, because we rely on the placeholder for placement. */ + public void setMaxTopPadding(int maxTopPadding) { + SceneContainerFlag.assertInLegacyMode(); + mMaxTopPadding = maxTopPadding; + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/SharedNotificationContainer.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/SharedNotificationContainer.kt index ffab9ea00b35..42acd7bcdc8a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/SharedNotificationContainer.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/SharedNotificationContainer.kt @@ -29,19 +29,15 @@ import androidx.constraintlayout.widget.ConstraintSet.START import androidx.constraintlayout.widget.ConstraintSet.TOP import androidx.constraintlayout.widget.ConstraintSet.VERTICAL import com.android.systemui.res.R +import com.android.systemui.scene.shared.flag.SceneContainerFlag +import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel.HorizontalPosition /** * Container for the stack scroller, so that the bounds can be externally specified, such as from * the keyguard or shade scenes. */ -class SharedNotificationContainer( - context: Context, - private val attrs: AttributeSet?, -) : - ConstraintLayout( - context, - attrs, - ) { +class SharedNotificationContainer(context: Context, attrs: AttributeSet?) : + ConstraintLayout(context, attrs) { private val baseConstraintSet = ConstraintSet() @@ -59,24 +55,40 @@ class SharedNotificationContainer( } fun updateConstraints( - useSplitShade: Boolean, + horizontalPosition: HorizontalPosition, marginStart: Int, marginTop: Int, marginEnd: Int, - marginBottom: Int + marginBottom: Int, ) { val constraintSet = ConstraintSet() constraintSet.clone(baseConstraintSet) val startConstraintId = - if (useSplitShade) { + if (horizontalPosition is HorizontalPosition.MiddleToEdge) { R.id.nssl_guideline } else { PARENT_ID } + val nsslId = R.id.notification_stack_scroller constraintSet.apply { - connect(nsslId, START, startConstraintId, START, marginStart) + if (SceneContainerFlag.isEnabled) { + when (horizontalPosition) { + is HorizontalPosition.FloatAtEnd -> + constrainWidth(nsslId, horizontalPosition.width) + is HorizontalPosition.MiddleToEdge -> + setGuidelinePercent(R.id.nssl_guideline, horizontalPosition.ratio) + else -> Unit + } + } + + if ( + !SceneContainerFlag.isEnabled || + horizontalPosition !is HorizontalPosition.FloatAtEnd + ) { + connect(nsslId, START, startConstraintId, START, marginStart) + } connect(nsslId, END, PARENT_ID, END, marginEnd) connect(nsslId, BOTTOM, PARENT_ID, BOTTOM, marginBottom) connect(nsslId, TOP, PARENT_ID, TOP, marginTop) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt index ce89d786c350..4a55dfaaf7ea 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.notification.stack.ui.viewbinder import androidx.lifecycle.Lifecycle import androidx.lifecycle.repeatOnLifecycle +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.systemui.Flags import com.android.systemui.common.ui.view.onLayoutChanged import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor @@ -36,11 +37,8 @@ import com.android.systemui.util.kotlin.DisposableHandles import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.DisposableHandle -import kotlinx.coroutines.ExperimentalCoroutinesApi -import com.android.app.tracing.coroutines.launchTraced as launch /** Binds the shared notification container to its view-model. */ -@OptIn(ExperimentalCoroutinesApi::class) @SysUISingleton class SharedNotificationContainerBinder @Inject @@ -74,7 +72,7 @@ constructor( launch { viewModel.configurationBasedDimensions.collect { view.updateConstraints( - useSplitShade = it.useSplitShade, + horizontalPosition = it.horizontalPosition, marginStart = it.marginStart, marginTop = it.marginTop, marginEnd = it.marginEnd, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt index e6663d51aed5..827e2bfeba0f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt @@ -224,33 +224,56 @@ constructor( if (SceneContainerFlag.isEnabled) { combine( shadeInteractor.isShadeLayoutWide, + shadeInteractor.shadeMode, configurationInteractor.onAnyConfigurationChange, - ) { isShadeLayoutWide, _ -> + ) { isShadeLayoutWide, shadeMode, _ -> with(context.resources) { - // TODO(b/338033836): Define separate horizontal margins for dual shade. val marginHorizontal = - getDimensionPixelSize(R.dimen.notification_panel_margin_horizontal) + getDimensionPixelSize( + if (shadeMode is Dual) { + R.dimen.shade_panel_margin_horizontal + } else { + R.dimen.notification_panel_margin_horizontal + } + ) + + val horizontalPosition = + when (shadeMode) { + Single -> HorizontalPosition.EdgeToEdge + Split -> HorizontalPosition.MiddleToEdge(ratio = 0.5f) + Dual -> + if (isShadeLayoutWide) { + HorizontalPosition.FloatAtEnd( + width = getDimensionPixelSize(R.dimen.shade_panel_width) + ) + } else { + HorizontalPosition.EdgeToEdge + } + } + ConfigurationBasedDimensions( - marginStart = if (isShadeLayoutWide) 0 else marginHorizontal, + horizontalPosition = horizontalPosition, + marginStart = if (shadeMode is Split) 0 else marginHorizontal, marginEnd = marginHorizontal, marginBottom = getDimensionPixelSize(R.dimen.notification_panel_margin_bottom), // y position of the NSSL in the window needs to be 0 under scene // container marginTop = 0, - useSplitShade = isShadeLayoutWide, ) } } } else { interactor.configurationBasedDimensions.map { ConfigurationBasedDimensions( + horizontalPosition = + if (it.useSplitShade) HorizontalPosition.MiddleToEdge() + else HorizontalPosition.EdgeToEdge, marginStart = if (it.useSplitShade) 0 else it.marginHorizontal, marginEnd = it.marginHorizontal, marginBottom = it.marginBottom, marginTop = if (it.useLargeScreenHeader) it.marginTopLargeScreen else it.marginTop, - useSplitShade = it.useSplitShade, ) } } @@ -446,59 +469,63 @@ constructor( */ private val alphaForShadeAndQsExpansion: Flow<Float> = if (SceneContainerFlag.isEnabled) { - shadeInteractor.shadeMode.flatMapLatest { shadeMode -> - when (shadeMode) { - Single -> - combineTransform( - shadeInteractor.shadeExpansion, - shadeInteractor.qsExpansion, - ) { shadeExpansion, qsExpansion -> - if (qsExpansion == 1f) { - // Ensure HUNs will be visible in QS shade (at least while unlocked) - emit(1f) - } else if (shadeExpansion > 0f || qsExpansion > 0f) { - // Fade as QS shade expands - emit(1f - qsExpansion) + shadeInteractor.shadeMode.flatMapLatest { shadeMode -> + when (shadeMode) { + Single -> + combineTransform( + shadeInteractor.shadeExpansion, + shadeInteractor.qsExpansion, + ) { shadeExpansion, qsExpansion -> + if (qsExpansion == 1f) { + // Ensure HUNs will be visible in QS shade (at least while + // unlocked) + emit(1f) + } else if (shadeExpansion > 0f || qsExpansion > 0f) { + // Fade as QS shade expands + emit(1f - qsExpansion) + } } - } - Split -> isAnyExpanded.filter { it }.map { 1f } - Dual -> - combineTransform( - headsUpNotificationInteractor.get().isHeadsUpOrAnimatingAway, - shadeInteractor.shadeExpansion, - shadeInteractor.qsExpansion, - ) { isHeadsUpOrAnimatingAway, shadeExpansion, qsExpansion -> - if (isHeadsUpOrAnimatingAway) { - // Ensure HUNs will be visible in QS shade (at least while unlocked) + Split -> isAnyExpanded.filter { it }.map { 1f } + Dual -> + combineTransform( + headsUpNotificationInteractor.get().isHeadsUpOrAnimatingAway, + shadeInteractor.shadeExpansion, + shadeInteractor.qsExpansion, + ) { isHeadsUpOrAnimatingAway, shadeExpansion, qsExpansion -> + if (isHeadsUpOrAnimatingAway) { + // Ensure HUNs will be visible in QS shade (at least while + // unlocked) + emit(1f) + } else if (shadeExpansion > 0f || qsExpansion > 0f) { + // Fade out as QS shade expands + emit(1f - qsExpansion) + } + } + } + } + } else { + interactor.configurationBasedDimensions.flatMapLatest { configurationBasedDimensions + -> + combineTransform(shadeInteractor.shadeExpansion, shadeInteractor.qsExpansion) { + shadeExpansion, + qsExpansion -> + if (shadeExpansion > 0f || qsExpansion > 0f) { + if (configurationBasedDimensions.useSplitShade) { + emit(1f) + } else if (qsExpansion == 1f) { + // Ensure HUNs will be visible in QS shade (at least while + // unlocked) emit(1f) - } else if (shadeExpansion > 0f || qsExpansion > 0f) { - // Fade out as QS shade expands + } else { + // Fade as QS shade expands emit(1f - qsExpansion) } } - } - } - } else { - interactor.configurationBasedDimensions.flatMapLatest { configurationBasedDimensions -> - combineTransform(shadeInteractor.shadeExpansion, shadeInteractor.qsExpansion) { - shadeExpansion, - qsExpansion -> - if (shadeExpansion > 0f || qsExpansion > 0f) { - if (configurationBasedDimensions.useSplitShade) { - emit(1f) - } else if (qsExpansion == 1f) { - // Ensure HUNs will be visible in QS shade (at least while unlocked) - emit(1f) - } else { - // Fade as QS shade expands - emit(1f - qsExpansion) - } } } } - } - .onStart { emit(1f) } - .dumpWhileCollecting("alphaForShadeAndQsExpansion") + .onStart { emit(1f) } + .dumpWhileCollecting("alphaForShadeAndQsExpansion") val panelAlpha = keyguardInteractor.panelAlpha @@ -766,10 +793,25 @@ constructor( } data class ConfigurationBasedDimensions( + val horizontalPosition: HorizontalPosition, val marginStart: Int, val marginTop: Int, val marginEnd: Int, val marginBottom: Int, - val useSplitShade: Boolean, ) + + /** Specifies the horizontal layout constraints for the notification container. */ + sealed interface HorizontalPosition { + /** The container is using the full width of the screen (minus any margins). */ + data object EdgeToEdge : HorizontalPosition + + /** The container is laid out from the given [ratio] of the screen width to the end edge. */ + data class MiddleToEdge(val ratio: Float = 0.5f) : HorizontalPosition + + /** + * The container has a fixed [width] and is aligned to the end of the screen. In this + * layout, the start edge of the container is floating, i.e. unconstrained. + */ + data class FloatAtEnd(val width: Int) : HorizontalPosition + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java index 98869bef5bf0..4915b848e4b3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java @@ -45,6 +45,8 @@ import com.android.systemui.doze.AlwaysOnDisplayPolicy; import com.android.systemui.doze.DozeScreenState; import com.android.systemui.dump.DumpManager; import com.android.systemui.keyguard.domain.interactor.DozeInteractor; +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; +import com.android.systemui.keyguard.shared.model.KeyguardState; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.res.R; import com.android.systemui.settings.UserTracker; @@ -85,6 +87,7 @@ public class DozeParameters implements private final BatteryController mBatteryController; private final ScreenOffAnimationController mScreenOffAnimationController; private final DozeInteractor mDozeInteractor; + private final KeyguardTransitionInteractor mTransitionInteractor; private final FoldAodAnimationController mFoldAodAnimationController; private final UnlockedScreenOffAnimationController mUnlockedScreenOffAnimationController; private final UserTracker mUserTracker; @@ -134,6 +137,7 @@ public class DozeParameters implements StatusBarStateController statusBarStateController, UserTracker userTracker, DozeInteractor dozeInteractor, + KeyguardTransitionInteractor transitionInteractor, SecureSettings secureSettings) { mResources = resources; mAmbientDisplayConfiguration = ambientDisplayConfiguration; @@ -148,6 +152,7 @@ public class DozeParameters implements mUnlockedScreenOffAnimationController = unlockedScreenOffAnimationController; mUserTracker = userTracker; mDozeInteractor = dozeInteractor; + mTransitionInteractor = transitionInteractor; mSecureSettings = secureSettings; keyguardUpdateMonitor.registerCallback(mKeyguardVisibilityCallback); @@ -353,6 +358,9 @@ public class DozeParameters implements * delayed for a few seconds. This might be useful to play animations without reducing FPS. */ public boolean shouldDelayDisplayDozeTransition() { + if (mTransitionInteractor.getTransitionState().getValue().getTo() == KeyguardState.AOD) { + return true; + } return willAnimateFromLockScreenToAod() || mScreenOffAnimationController.shouldDelayDisplayDozeTransition(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java index e7d9717defa7..91c43ddf1ce4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarView.java @@ -39,6 +39,8 @@ import com.android.systemui.Dependency; import com.android.systemui.Flags; import com.android.systemui.Gefingerpoken; import com.android.systemui.res.R; +import com.android.systemui.shade.LongPressGestureDetector; +import com.android.systemui.shade.ShadeExpandsOnStatusBarLongPress; import com.android.systemui.statusbar.phone.userswitcher.StatusBarUserSwitcherContainer; import com.android.systemui.statusbar.window.StatusBarWindowControllerStore; import com.android.systemui.user.ui.binder.StatusBarUserChipViewBinder; @@ -67,6 +69,7 @@ public class PhoneStatusBarView extends FrameLayout { private InsetsFetcher mInsetsFetcher; private int mDensity; private float mFontScale; + private LongPressGestureDetector mLongPressGestureDetector; /** * Draw this many pixels into the left/right side of the cutout to optimally use the space @@ -78,6 +81,12 @@ public class PhoneStatusBarView extends FrameLayout { mStatusBarWindowControllerStore = Dependency.get(StatusBarWindowControllerStore.class); } + void setLongPressGestureDetector(LongPressGestureDetector longPressGestureDetector) { + if (ShadeExpandsOnStatusBarLongPress.isEnabled()) { + mLongPressGestureDetector = longPressGestureDetector; + } + } + void setTouchEventHandler(Gefingerpoken handler) { mTouchEventHandler = handler; } @@ -198,6 +207,9 @@ public class PhoneStatusBarView extends FrameLayout { @Override public boolean onTouchEvent(MotionEvent event) { + if (ShadeExpandsOnStatusBarLongPress.isEnabled() && mLongPressGestureDetector != null) { + mLongPressGestureDetector.handleTouch(event); + } if (mTouchEventHandler == null) { Log.w( TAG, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt index 746d6a75a567..c24f4327f471 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewController.kt @@ -33,7 +33,9 @@ import com.android.systemui.plugins.DarkIconDispatcher import com.android.systemui.res.R import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.scene.ui.view.WindowRootView +import com.android.systemui.shade.LongPressGestureDetector import com.android.systemui.shade.ShadeController +import com.android.systemui.shade.ShadeExpandsOnStatusBarLongPress import com.android.systemui.shade.ShadeLogger import com.android.systemui.shade.ShadeViewController import com.android.systemui.shade.domain.interactor.PanelExpansionInteractor @@ -66,6 +68,7 @@ private constructor( private val shadeController: ShadeController, private val shadeViewController: ShadeViewController, private val panelExpansionInteractor: PanelExpansionInteractor, + private val longPressGestureDetector: Provider<LongPressGestureDetector>, private val windowRootView: Provider<WindowRootView>, private val shadeLogger: ShadeLogger, private val moveFromCenterAnimationController: StatusBarMoveFromCenterAnimationController?, @@ -114,6 +117,10 @@ private constructor( addDarkReceivers() addCursorSupportToIconContainers() + if (ShadeExpandsOnStatusBarLongPress.isEnabled) { + mView.setLongPressGestureDetector(longPressGestureDetector.get()) + } + progressProvider?.setReadyToHandleTransition(true) configurationController.addCallback(configurationListener) @@ -328,6 +335,7 @@ private constructor( private val shadeController: ShadeController, private val shadeViewController: ShadeViewController, private val panelExpansionInteractor: PanelExpansionInteractor, + private val longPressGestureDetector: Provider<LongPressGestureDetector>, private val windowRootView: Provider<WindowRootView>, private val shadeLogger: ShadeLogger, private val viewUtil: ViewUtil, @@ -352,6 +360,7 @@ private constructor( shadeController, shadeViewController, panelExpansionInteractor, + longPressGestureDetector, windowRootView, shadeLogger, statusBarMoveFromCenterAnimationController, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java index 9cda199d5279..0c511aeae3e5 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java @@ -1348,6 +1348,7 @@ public class StatusBarKeyguardViewManager implements RemoteInputController.Callb if (!canHandleBackPressed()) { return; } + mStatusBarStateController.setLeaveOpenOnKeyguardHide(false); boolean hideBouncerOverDream = isBouncerShowing() && mDreamOverlayStateController.isOverlayActive(); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java index a6581159d33e..d2c2003112f6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManager.java @@ -37,6 +37,7 @@ import com.android.systemui.Dumpable; import com.android.systemui.ScreenDecorations; import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor; import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor; +import com.android.systemui.communal.domain.interactor.CommunalSceneInteractor; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.res.R; import com.android.systemui.scene.domain.interactor.SceneInteractor; @@ -78,6 +79,7 @@ public final class StatusBarTouchableRegionManager implements Dumpable { private View mNotificationShadeWindowView; private View mNotificationPanelView; private boolean mForceCollapsedUntilLayout = false; + private Boolean mCommunalVisible = false; private Region mTouchableRegion = new Region(); private int mDisplayCutoutTouchableRegionSize; @@ -98,7 +100,8 @@ public final class StatusBarTouchableRegionManager implements Dumpable { JavaAdapter javaAdapter, UnlockedScreenOffAnimationController unlockedScreenOffAnimationController, PrimaryBouncerInteractor primaryBouncerInteractor, - AlternateBouncerInteractor alternateBouncerInteractor + AlternateBouncerInteractor alternateBouncerInteractor, + CommunalSceneInteractor communalSceneInteractor ) { mContext = context; initResources(); @@ -145,6 +148,9 @@ public final class StatusBarTouchableRegionManager implements Dumpable { javaAdapter.alwaysCollectFlow( shadeInteractor.isAnyExpanded(), this::onShadeOrQsExpanded); + javaAdapter.alwaysCollectFlow( + communalSceneInteractor.isCommunalVisible(), + this::onCommunalVisible); } mPrimaryBouncerInteractor = primaryBouncerInteractor; @@ -196,6 +202,10 @@ public final class StatusBarTouchableRegionManager implements Dumpable { } } + private void onCommunalVisible(Boolean visible) { + mCommunalVisible = visible; + } + /** * Calculates the touch region needed for heads up notifications, taking into consideration * any existing display cutouts (notch) @@ -304,6 +314,9 @@ public final class StatusBarTouchableRegionManager implements Dumpable { && (!mIsSceneContainerUiEmpty || mIsRemoteUserInteractionOngoing)) || mPrimaryBouncerInteractor.isShowing().getValue() || mAlternateBouncerInteractor.isVisibleState() + // The glanceable hub is a full-screen UI within the notification shade window. When + // it's visible, the touchable region should be the full screen. + || mCommunalVisible || mUnlockedScreenOffAnimationController.isAnimationPlaying(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.kt index 99f25bd00839..23f3482d40bd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/dagger/StatusBarPhoneModule.kt @@ -31,6 +31,7 @@ import com.android.systemui.statusbar.core.StatusBarInitializerImpl import com.android.systemui.statusbar.core.StatusBarInitializerStore import com.android.systemui.statusbar.core.StatusBarOrchestrator import com.android.systemui.statusbar.core.StatusBarSimpleFragment +import com.android.systemui.statusbar.data.repository.PrivacyDotViewControllerStoreModule import com.android.systemui.statusbar.data.repository.StatusBarModeRepositoryStore import com.android.systemui.statusbar.events.PrivacyDotViewControllerModule import com.android.systemui.statusbar.phone.CentralSurfacesCommandQueueCallbacks @@ -46,7 +47,9 @@ import dagger.multibindings.IntoMap import kotlinx.coroutines.CoroutineScope /** Similar in purpose to [StatusBarModule], but scoped only to phones */ -@Module(includes = [PrivacyDotViewControllerModule::class]) +@Module( + includes = [PrivacyDotViewControllerModule::class, PrivacyDotViewControllerStoreModule::class] +) interface StatusBarPhoneModule { @Binds @@ -140,7 +143,7 @@ interface StatusBarPhoneModule { fun commandQueueInitializerCoreStartable( initializerLazy: Lazy<CommandQueueInitializer> ): CoreStartable { - return if (StatusBarSimpleFragment.isEnabled) { + return if (StatusBarConnectedDisplays.isEnabled) { initializerLazy.get() } else { CoreStartable.NOP diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt index 76389f39e484..b033b362f4c6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTile.kt @@ -46,7 +46,7 @@ import com.android.systemui.common.ui.compose.Icon import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.ModeTileViewModel @Composable -fun ModeTile(viewModel: ModeTileViewModel) { +fun ModeTile(viewModel: ModeTileViewModel, modifier: Modifier = Modifier) { val tileColor: Color by animateColorAsState( if (viewModel.enabled) MaterialTheme.colorScheme.primary @@ -59,7 +59,7 @@ fun ModeTile(viewModel: ModeTileViewModel) { ) CompositionLocalProvider(LocalContentColor provides contentColor) { - Surface(color = tileColor, shape = RoundedCornerShape(16.dp)) { + Surface(color = tileColor, shape = RoundedCornerShape(16.dp), modifier = modifier) { Row( modifier = Modifier.combinedClickable( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTileGrid.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTileGrid.kt index 5392e38823c6..16f24f1d5821 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTileGrid.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/composable/ModeTileGrid.kt @@ -17,29 +17,74 @@ package com.android.systemui.statusbar.policy.ui.dialog.composable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.android.systemui.Flags +import com.android.systemui.qs.panels.ui.compose.PagerDots import com.android.systemui.statusbar.policy.ui.dialog.viewmodel.ModesDialogViewModel @Composable fun ModeTileGrid(viewModel: ModesDialogViewModel) { val tiles by viewModel.tiles.collectAsStateWithLifecycle(initialValue = emptyList()) - LazyVerticalGrid( - columns = GridCells.Fixed(1), - modifier = Modifier.fillMaxWidth().heightIn(max = 320.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - items(tiles.size, key = { index -> tiles[index].id }) { index -> - ModeTile(viewModel = tiles[index]) + if (Flags.modesUiDialogPaging()) { + val tilesPerPage = 3 + val totalPages = { (tiles.size + tilesPerPage - 1) / tilesPerPage } + val pagerState = rememberPagerState(initialPage = 0, pageCount = totalPages) + + Column { + HorizontalPager( + state = pagerState, + modifier = Modifier.fillMaxWidth(), + pageSpacing = 16.dp, + verticalAlignment = Alignment.Top, + // Pre-emptively layout and compose the next page, to make sure the height stays + // the same even if we have fewer than [tilesPerPage] tiles on the last page. + beyondViewportPageCount = 1, + ) { page -> + Column( + modifier = Modifier.fillMaxWidth().fillMaxHeight(), + verticalArrangement = Arrangement.spacedBy(8.dp, alignment = Alignment.Top), + ) { + val startIndex = page * tilesPerPage + val endIndex = minOf((page + 1) * tilesPerPage, tiles.size) + for (index in startIndex until endIndex) { + ModeTile(viewModel = tiles[index], modifier = Modifier.fillMaxWidth()) + } + } + } + + PagerDots( + pagerState = pagerState, + activeColor = MaterialTheme.colorScheme.primary, + nonActiveColor = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.align(Alignment.CenterHorizontally).padding(top = 8.dp), + ) + } + } else { + LazyVerticalGrid( + columns = GridCells.Fixed(1), + modifier = Modifier.fillMaxWidth().heightIn(max = 280.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + items(tiles.size, key = { index -> tiles[index].id }) { index -> + ModeTile(viewModel = tiles[index]) + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/util/ConvenienceExtensions.kt b/packages/SystemUI/src/com/android/systemui/util/ConvenienceExtensions.kt index f2132248e4f7..70fd5ab767d0 100644 --- a/packages/SystemUI/src/com/android/systemui/util/ConvenienceExtensions.kt +++ b/packages/SystemUI/src/com/android/systemui/util/ConvenienceExtensions.kt @@ -25,9 +25,7 @@ import java.io.PrintWriter /** [Sequence] that yields all of the direct children of this [ViewGroup] */ val ViewGroup.children - get() = sequence { - for (i in 0 until childCount) yield(getChildAt(i)) - } + get() = sequence { for (i in 0 until childCount) yield(getChildAt(i)) } /** Inclusive version of [Iterable.takeWhile] */ fun <T> Sequence<T>.takeUntil(pred: (T) -> Boolean): Sequence<T> = sequence { @@ -62,3 +60,25 @@ val View.boundsOnScreen: Rect fun <T> Lazy<T>.toKotlinLazy(): kotlin.Lazy<T> { return lazy { this.get() } } + +/** + * Returns whether this [Collection] contains exactly all [elements]. + * + * Order of elements is not taken into account, but multiplicity is. For example, an element + * duplicated exactly 3 times in the parameter asserts that the element must likewise be duplicated + * exactly 3 times in this [Collection]. + */ +fun <T> Collection<T>.containsExactly(vararg elements: T): Boolean { + return eachCountMap() == elements.asList().eachCountMap() +} + +/** + * Returns a map where keys are the distinct elements of the collection and values are their + * corresponding counts. + * + * This is a convenient extension function for any [Collection] that allows you to easily count the + * occurrences of each element. + */ +fun <T> Collection<T>.eachCountMap(): Map<T, Int> { + return groupingBy { it }.eachCount() +} diff --git a/packages/SystemUI/src/com/android/systemui/util/wakelock/DelayedWakeLock.java b/packages/SystemUI/src/com/android/systemui/util/wakelock/DelayedWakeLock.java index d2ed71cc3af1..55cdfb2edded 100644 --- a/packages/SystemUI/src/com/android/systemui/util/wakelock/DelayedWakeLock.java +++ b/packages/SystemUI/src/com/android/systemui/util/wakelock/DelayedWakeLock.java @@ -19,11 +19,8 @@ package com.android.systemui.util.wakelock; import android.content.Context; import android.os.Handler; -import com.android.systemui.Flags; import com.android.systemui.dagger.qualifiers.Background; -import com.android.systemui.dagger.qualifiers.Main; -import dagger.Lazy; import dagger.assisted.Assisted; import dagger.assisted.AssistedFactory; import dagger.assisted.AssistedInject; @@ -40,13 +37,11 @@ public class DelayedWakeLock implements WakeLock { private final WakeLock mInner; @AssistedInject - public DelayedWakeLock(@Background Lazy<Handler> bgHandler, - @Main Lazy<Handler> mainHandler, + public DelayedWakeLock(@Background Handler bgHandler, Context context, WakeLockLogger logger, @Assisted String tag) { mInner = WakeLock.createPartial(context, logger, tag); - mHandler = Flags.delayedWakelockReleaseOnBackgroundThread() ? bgHandler.get() - : mainHandler.get(); + mHandler = bgHandler; } @Override diff --git a/packages/SystemUI/src/com/android/systemui/util/wakelock/WakeLock.java b/packages/SystemUI/src/com/android/systemui/util/wakelock/WakeLock.java index f763ee46666a..f00e3d12d238 100644 --- a/packages/SystemUI/src/com/android/systemui/util/wakelock/WakeLock.java +++ b/packages/SystemUI/src/com/android/systemui/util/wakelock/WakeLock.java @@ -18,14 +18,9 @@ package com.android.systemui.util.wakelock; import android.content.Context; import android.os.PowerManager; -import android.util.Log; import androidx.annotation.VisibleForTesting; -import com.android.systemui.Flags; - -import java.util.HashMap; - import javax.inject.Inject; /** WakeLock wrapper for testability */ @@ -114,59 +109,7 @@ public interface WakeLock { @VisibleForTesting static WakeLock wrap( final PowerManager.WakeLock inner, WakeLockLogger logger, long maxTimeout) { - if (Flags.delayedWakelockReleaseOnBackgroundThread()) { - return new ClientTrackingWakeLock(inner, logger, maxTimeout); - } - - // Non-thread safe implementation, remove when flag above is removed. - return new WakeLock() { - private final HashMap<String, Integer> mActiveClients = new HashMap<>(); - - /** @see PowerManager.WakeLock#acquire() */ - public void acquire(String why) { - mActiveClients.putIfAbsent(why, 0); - int count = mActiveClients.get(why) + 1; - mActiveClients.put(why, count); - if (logger != null) { - logger.logAcquire(inner, why, count); - } - if (maxTimeout == Builder.NO_TIMEOUT) { - inner.acquire(); - } else { - inner.acquire(maxTimeout); - } - } - - /** @see PowerManager.WakeLock#release() */ - public void release(String why) { - Integer count = mActiveClients.get(why); - if (count == null) { - Log.wtf(TAG, "Releasing WakeLock with invalid reason: " + why, - new Throwable()); - return; - } - count--; - if (count == 0) { - mActiveClients.remove(why); - } else { - mActiveClients.put(why, count); - } - if (logger != null) { - logger.logRelease(inner, why, count); - } - inner.release(); - } - - /** @see PowerManager.WakeLock#wrap(Runnable) */ - public Runnable wrap(Runnable runnable) { - return wrapImpl(this, runnable); - } - - @Override - public String toString() { - return "active clients= " + mActiveClients; - } - }; + return new ClientTrackingWakeLock(inner, logger, maxTimeout); } /** diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt index 097a60fdc440..4fc9a7c9ae01 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialog.kt @@ -19,10 +19,8 @@ package com.android.systemui.volume.dialog import android.app.Dialog import android.content.Context import android.os.Bundle -import android.view.ContextThemeWrapper import android.view.MotionEvent import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.res.R import com.android.systemui.volume.Events import com.android.systemui.volume.dialog.domain.interactor.VolumeDialogVisibilityInteractor import com.android.systemui.volume.dialog.ui.binder.VolumeDialogViewBinder @@ -34,7 +32,7 @@ constructor( @Application context: Context, private val viewBinder: VolumeDialogViewBinder, private val visibilityInteractor: VolumeDialogVisibilityInteractor, -) : Dialog(ContextThemeWrapper(context, R.style.volume_dialog_theme)) { +) : Dialog(context) { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt index c05a0b265ee9..6816d35f7699 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt @@ -16,15 +16,26 @@ package com.android.systemui.volume.dialog.ringer.ui.binder +import android.view.LayoutInflater import android.view.View +import android.view.ViewGroup +import android.widget.ImageButton +import androidx.annotation.LayoutRes +import androidx.compose.ui.util.fastForEachIndexed import com.android.systemui.lifecycle.WindowLifecycleState import com.android.systemui.lifecycle.repeatWhenAttached -import com.android.systemui.lifecycle.setSnapshotBinding import com.android.systemui.lifecycle.viewModel +import com.android.systemui.res.R import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope +import com.android.systemui.volume.dialog.ringer.ui.viewmodel.RingerButtonViewModel +import com.android.systemui.volume.dialog.ringer.ui.viewmodel.RingerDrawerState +import com.android.systemui.volume.dialog.ringer.ui.viewmodel.RingerViewModel +import com.android.systemui.volume.dialog.ringer.ui.viewmodel.RingerViewModelState import com.android.systemui.volume.dialog.ringer.ui.viewmodel.VolumeDialogRingerDrawerViewModel import javax.inject.Inject -import kotlinx.coroutines.awaitCancellation +import kotlin.math.abs +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach @VolumeDialogScope class VolumeDialogRingerViewBinder @@ -33,16 +44,124 @@ constructor(private val viewModelFactory: VolumeDialogRingerDrawerViewModel.Fact fun bind(view: View) { with(view) { + val drawerAndRingerContainer = + requireViewById<View>(R.id.volume_ringer_and_drawer_container) + val drawerContainer = requireViewById<View>(R.id.volume_drawer_container) + val selectedButtonView = + requireViewById<ImageButton>(R.id.volume_new_ringer_active_button) + val volumeDialogView = requireViewById<ViewGroup>(R.id.volume_dialog) repeatWhenAttached { viewModel( traceName = "VolumeDialogRingerViewBinder", minWindowLifecycleState = WindowLifecycleState.ATTACHED, factory = { viewModelFactory.create() }, ) { viewModel -> - setSnapshotBinding {} - awaitCancellation() + viewModel.ringerViewModel + .onEach { ringerState -> + when (ringerState) { + is RingerViewModelState.Available -> { + val uiModel = ringerState.uiModel + + bindSelectedButton(viewModel, uiModel, selectedButtonView) + bindDrawerButtons(viewModel, uiModel.availableButtons) + + // Set up views background and visibility + drawerAndRingerContainer.visibility = View.VISIBLE + when (uiModel.drawerState) { + is RingerDrawerState.Initial -> { + drawerContainer.visibility = View.GONE + selectedButtonView.visibility = View.VISIBLE + volumeDialogView.setBackgroundResource( + R.drawable.volume_dialog_background + ) + } + + is RingerDrawerState.Closed -> { + drawerContainer.visibility = View.GONE + selectedButtonView.visibility = View.VISIBLE + volumeDialogView.setBackgroundResource( + R.drawable.volume_dialog_background + ) + } + + is RingerDrawerState.Open -> { + drawerContainer.visibility = View.VISIBLE + selectedButtonView.visibility = View.GONE + if ( + uiModel.currentButtonIndex != + uiModel.availableButtons.size - 1 + ) { + volumeDialogView.setBackgroundResource( + R.drawable.volume_dialog_background_small_radius + ) + } + } + } + } + + is RingerViewModelState.Unavailable -> { + drawerAndRingerContainer.visibility = View.GONE + volumeDialogView.setBackgroundResource( + R.drawable.volume_dialog_background + ) + } + } + } + .launchIn(this) } } } } + + private fun View.bindDrawerButtons( + viewModel: VolumeDialogRingerDrawerViewModel, + availableButtons: List<RingerButtonViewModel?>, + ) { + val drawerOptions = requireViewById<ViewGroup>(R.id.volume_drawer_options) + val count = availableButtons.size + drawerOptions.ensureChildCount(R.layout.volume_ringer_button, count) + + availableButtons.fastForEachIndexed { index, ringerButton -> + ringerButton?.let { + drawerOptions.getChildAt(count - index - 1).bindDrawerButton(it, viewModel) + } + } + } + + private fun View.bindDrawerButton( + buttonViewModel: RingerButtonViewModel, + viewModel: VolumeDialogRingerDrawerViewModel, + ) { + with(requireViewById<ImageButton>(R.id.volume_drawer_button)) { + setImageResource(buttonViewModel.imageResId) + contentDescription = context.getString(buttonViewModel.contentDescriptionResId) + setOnClickListener { viewModel.onRingerButtonClicked(buttonViewModel.ringerMode) } + } + } + + private fun ViewGroup.ensureChildCount(@LayoutRes viewLayoutId: Int, count: Int) { + val childCountDelta = childCount - count + when { + childCountDelta > 0 -> { + removeViews(0, childCountDelta) + } + childCountDelta < 0 -> { + val inflater = LayoutInflater.from(context) + repeat(abs(childCountDelta)) { inflater.inflate(viewLayoutId, this, true) } + } + } + } + + private fun bindSelectedButton( + viewModel: VolumeDialogRingerDrawerViewModel, + uiModel: RingerViewModel, + selectedButtonView: ImageButton, + ) { + with(uiModel) { + selectedButtonView.setImageResource(selectedButton.imageResId) + selectedButtonView.setOnClickListener { + viewModel.onRingerButtonClicked(selectedButton.ringerMode) + } + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/RingerViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/RingerViewModel.kt index a09bfebfd3e3..96d4f62416bf 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/RingerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/RingerViewModel.kt @@ -22,6 +22,8 @@ data class RingerViewModel( val availableButtons: List<RingerButtonViewModel?>, /** The index of the currently selected button */ val currentButtonIndex: Int, + /** Currently selected button. */ + val selectedButton: RingerButtonViewModel, /** For open and close animations */ val drawerState: RingerDrawerState, ) diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModel.kt index 5b73107f32a3..d4da226152f3 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModel.kt @@ -41,8 +41,6 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.stateIn -private const val TAG = "VolumeDialogRingerDrawerViewModel" - class VolumeDialogRingerDrawerViewModel @AssistedInject constructor( @@ -111,31 +109,45 @@ constructor( return if (currentIndex == -1 || isSingleVolume) { RingerViewModelState.Unavailable } else { - RingerViewModelState.Available( - RingerViewModel( - availableButtons = availableModes.map { mode -> toButtonViewModel(mode) }, - currentButtonIndex = currentIndex, - drawerState = drawerState, + toButtonViewModel(currentRingerMode, isSelectedButton = true)?.let { + RingerViewModelState.Available( + RingerViewModel( + availableButtons = availableModes.map { mode -> toButtonViewModel(mode) }, + currentButtonIndex = currentIndex, + selectedButton = it, + drawerState = drawerState, + ) ) - ) + } ?: RingerViewModelState.Unavailable } } private fun VolumeDialogRingerModel.toButtonViewModel( - ringerMode: RingerMode + ringerMode: RingerMode, + isSelectedButton: Boolean = false, ): RingerButtonViewModel? { return when (ringerMode.value) { RINGER_MODE_SILENT -> RingerButtonViewModel( imageResId = R.drawable.ic_speaker_mute, - contentDescriptionResId = R.string.volume_ringer_status_silent, + contentDescriptionResId = + if (isSelectedButton) { + R.string.volume_ringer_status_silent + } else { + R.string.volume_ringer_hint_mute + }, hintLabelResId = R.string.volume_ringer_hint_unmute, ringerMode = ringerMode, ) RINGER_MODE_VIBRATE -> RingerButtonViewModel( imageResId = R.drawable.ic_volume_ringer_vibrate, - contentDescriptionResId = R.string.volume_ringer_status_vibrate, + contentDescriptionResId = + if (isSelectedButton) { + R.string.volume_ringer_status_vibrate + } else { + R.string.volume_ringer_hint_vibrate + }, hintLabelResId = R.string.volume_ringer_hint_vibrate, ringerMode = ringerMode, ) @@ -143,8 +155,18 @@ constructor( when { isMuted && isEnabled -> RingerButtonViewModel( - imageResId = R.drawable.ic_speaker_mute, - contentDescriptionResId = R.string.volume_ringer_status_normal, + imageResId = + if (isSelectedButton) { + R.drawable.ic_speaker_mute + } else { + R.drawable.ic_speaker_on + }, + contentDescriptionResId = + if (isSelectedButton) { + R.string.volume_ringer_status_normal + } else { + R.string.volume_ringer_hint_unmute + }, hintLabelResId = R.string.volume_ringer_hint_unmute, ringerMode = ringerMode, ) @@ -152,7 +174,12 @@ constructor( availableModes.contains(RingerMode(RINGER_MODE_VIBRATE)) -> RingerButtonViewModel( imageResId = R.drawable.ic_speaker_on, - contentDescriptionResId = R.string.volume_ringer_status_normal, + contentDescriptionResId = + if (isSelectedButton) { + R.string.volume_ringer_status_normal + } else { + R.string.volume_ringer_hint_unmute + }, hintLabelResId = R.string.volume_ringer_hint_vibrate, ringerMode = ringerMode, ) @@ -160,7 +187,12 @@ constructor( else -> RingerButtonViewModel( imageResId = R.drawable.ic_speaker_on, - contentDescriptionResId = R.string.volume_ringer_status_normal, + contentDescriptionResId = + if (isSelectedButton) { + R.string.volume_ringer_status_normal + } else { + R.string.volume_ringer_hint_unmute + }, hintLabelResId = R.string.volume_ringer_hint_mute, ringerMode = ringerMode, ) diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt index 68f789600dd0..cec3d1eb86f0 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt @@ -22,11 +22,13 @@ import android.media.AudioManager.STREAM_ALARM import android.media.AudioManager.STREAM_MUSIC import android.media.AudioManager.STREAM_NOTIFICATION import android.util.Log +import com.android.app.tracing.coroutines.launchTraced as launch import com.android.internal.logging.UiEventLogger import com.android.settingslib.volume.domain.interactor.AudioVolumeInteractor import com.android.settingslib.volume.shared.model.AudioStream import com.android.settingslib.volume.shared.model.AudioStreamModel import com.android.settingslib.volume.shared.model.RingerMode +import com.android.systemui.Flags import com.android.systemui.common.shared.model.Icon import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel import com.android.systemui.modes.shared.ModesUiIcons @@ -49,7 +51,6 @@ import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn -import com.android.app.tracing.coroutines.launchTraced as launch /** Models a particular slider state. */ class AudioStreamSliderViewModel @@ -62,7 +63,7 @@ constructor( private val zenModeInteractor: ZenModeInteractor, private val uiEventLogger: UiEventLogger, private val volumePanelLogger: VolumePanelLogger, - override val hapticsViewModelFactory: SliderHapticsViewModel.Factory, + private val hapticsViewModelFactory: SliderHapticsViewModel.Factory, ) : SliderViewModel { private val volumeChanges = MutableStateFlow<Int?>(null) @@ -171,6 +172,13 @@ constructor( } } + override fun getSliderHapticsViewModelFactory(): SliderHapticsViewModel.Factory? = + if (Flags.hapticsForComposeSliders() && slider.value != SliderState.Empty) { + hapticsViewModelFactory + } else { + null + } + private fun AudioStreamModel.toState( isEnabled: Boolean, ringerMode: RingerMode, diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt index 8efe915abda2..0d804525a68c 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt @@ -18,6 +18,8 @@ package com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel import android.content.Context import android.media.session.MediaController.PlaybackInfo +import com.android.app.tracing.coroutines.launchTraced as launch +import com.android.systemui.Flags import com.android.systemui.common.shared.model.Icon import com.android.systemui.haptics.slider.compose.ui.SliderHapticsViewModel import com.android.systemui.res.R @@ -32,7 +34,6 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.mapNotNull import kotlinx.coroutines.flow.stateIn -import com.android.app.tracing.coroutines.launchTraced as launch class CastVolumeSliderViewModel @AssistedInject @@ -41,7 +42,7 @@ constructor( @Assisted private val coroutineScope: CoroutineScope, private val context: Context, private val mediaDeviceSessionInteractor: MediaDeviceSessionInteractor, - override val hapticsViewModelFactory: SliderHapticsViewModel.Factory, + private val hapticsViewModelFactory: SliderHapticsViewModel.Factory, ) : SliderViewModel { override val slider: StateFlow<SliderState> = @@ -62,6 +63,13 @@ constructor( // do nothing because this action isn't supported for Cast sliders. } + override fun getSliderHapticsViewModelFactory(): SliderHapticsViewModel.Factory? = + if (Flags.hapticsForComposeSliders() && slider.value != SliderState.Empty) { + hapticsViewModelFactory + } else { + null + } + private fun PlaybackInfo.getCurrentState(): State { val volumeRange = 0..maxVolume return State( diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderViewModel.kt index 9c1783b99f78..4d9552ff3bb3 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderViewModel.kt @@ -24,11 +24,11 @@ interface SliderViewModel { val slider: StateFlow<SliderState> - val hapticsViewModelFactory: SliderHapticsViewModel.Factory - fun onValueChanged(state: SliderState, newValue: Float) fun onValueChangeFinished() fun toggleMuted(state: SliderState) + + fun getSliderHapticsViewModelFactory(): SliderHapticsViewModel.Factory? } diff --git a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java index 344d065979f9..0769ada805a2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/ScreenDecorationsTest.java @@ -104,6 +104,7 @@ import com.android.systemui.settings.FakeDisplayTracker; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.commandline.CommandRegistry; import com.android.systemui.statusbar.events.PrivacyDotViewController; +import com.android.systemui.util.concurrency.DelayableExecutor; import com.android.systemui.util.concurrency.FakeExecutor; import com.android.systemui.util.concurrency.FakeThreadFactory; import com.android.systemui.util.kotlin.JavaAdapter; @@ -186,16 +187,17 @@ public class ScreenDecorationsTest extends SysuiTestCase { private List<DecorProvider> mMockCutoutList; private final CameraProtectionLoader mCameraProtectionLoader = new CameraProtectionLoaderImpl(mContext); + private Handler mMainHandler; @Before public void setup() { MockitoAnnotations.initMocks(this); - Handler mainHandler = new Handler(TestableLooper.get(this).getLooper()); + mMainHandler = new Handler(TestableLooper.get(this).getLooper()); mSecureSettings = new FakeSettings(); mExecutor = new FakeExecutor(new FakeSystemClock()); mThreadFactory = new FakeThreadFactory(mExecutor); - mThreadFactory.setHandler(mainHandler); + mThreadFactory.setHandler(mMainHandler); mWindowManager = mock(WindowManager.class); WindowMetrics metrics = mContext.getSystemService(WindowManager.class) @@ -214,26 +216,26 @@ public class ScreenDecorationsTest extends SysuiTestCase { when(mMockTypedArray.length()).thenReturn(0); mPrivacyDotTopLeftDecorProvider = spy(new PrivacyDotCornerDecorProviderImpl( R.id.privacy_dot_top_left_container, - DisplayCutout.BOUNDS_POSITION_TOP, - DisplayCutout.BOUNDS_POSITION_LEFT, + BOUNDS_POSITION_TOP, + BOUNDS_POSITION_LEFT, R.layout.privacy_dot_top_left)); mPrivacyDotTopRightDecorProvider = spy(new PrivacyDotCornerDecorProviderImpl( R.id.privacy_dot_top_right_container, - DisplayCutout.BOUNDS_POSITION_TOP, - DisplayCutout.BOUNDS_POSITION_RIGHT, + BOUNDS_POSITION_TOP, + BOUNDS_POSITION_RIGHT, R.layout.privacy_dot_top_right)); mPrivacyDotBottomLeftDecorProvider = spy(new PrivacyDotCornerDecorProviderImpl( R.id.privacy_dot_bottom_left_container, - DisplayCutout.BOUNDS_POSITION_BOTTOM, - DisplayCutout.BOUNDS_POSITION_LEFT, + BOUNDS_POSITION_BOTTOM, + BOUNDS_POSITION_LEFT, R.layout.privacy_dot_bottom_left)); mPrivacyDotBottomRightDecorProvider = spy(new PrivacyDotCornerDecorProviderImpl( R.id.privacy_dot_bottom_right_container, - DisplayCutout.BOUNDS_POSITION_BOTTOM, - DisplayCutout.BOUNDS_POSITION_RIGHT, + BOUNDS_POSITION_BOTTOM, + BOUNDS_POSITION_RIGHT, R.layout.privacy_dot_bottom_right)); // Default no cutout @@ -256,11 +258,10 @@ public class ScreenDecorationsTest extends SysuiTestCase { mLazyViewCapture, false); mScreenDecorations = spy(new ScreenDecorations(mContext, mSecureSettings, mCommandRegistry, mUserTracker, mDisplayTracker, mDotViewController, - mThreadFactory, mPrivacyDotDecorProviderFactory, mFaceScanningProviderFactory, new ScreenDecorationsLogger(logcatLogBuffer("TestLogBuffer")), mFakeFacePropertyRepository, mJavaAdapter, mCameraProtectionLoader, - mViewCaptureAwareWindowManager) { + mViewCaptureAwareWindowManager, mMainHandler, mExecutor) { @Override public void start() { super.start(); @@ -1272,10 +1273,10 @@ public class ScreenDecorationsTest extends SysuiTestCase { ScreenDecorations screenDecorations = new ScreenDecorations(mContext, mSecureSettings, mCommandRegistry, mUserTracker, mDisplayTracker, mDotViewController, - mThreadFactory, mPrivacyDotDecorProviderFactory, mFaceScanningProviderFactory, + mPrivacyDotDecorProviderFactory, mFaceScanningProviderFactory, new ScreenDecorationsLogger(logcatLogBuffer("TestLogBuffer")), mFakeFacePropertyRepository, mJavaAdapter, mCameraProtectionLoader, - mViewCaptureAwareWindowManager); + mViewCaptureAwareWindowManager, mMainHandler, mExecutor); screenDecorations.start(); when(mContext.getDisplay()).thenReturn(mDisplay); when(mDisplay.getDisplayInfo(any())).thenAnswer(new Answer<Boolean>() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt index 61eeab3a8c07..e1b8a1d9971c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt @@ -22,7 +22,6 @@ import android.content.res.Configuration import android.hardware.biometrics.BiometricAuthenticator import android.hardware.biometrics.BiometricConstants import android.hardware.biometrics.BiometricManager -import android.hardware.biometrics.Flags.FLAG_CUSTOM_BIOMETRIC_PROMPT import android.hardware.biometrics.PromptContentViewWithMoreOptionsButton import android.hardware.biometrics.PromptInfo import android.hardware.biometrics.PromptVerticalListContentView @@ -138,6 +137,7 @@ open class AuthContainerViewTest : SysuiTestCase() { PromptSelectorInteractorImpl( fingerprintRepository, displayStateInteractor, + credentialInteractor, biometricPromptRepository, lockPatternUtils, ) @@ -412,7 +412,6 @@ open class AuthContainerViewTest : SysuiTestCase() { @Test fun testShowBiometricUI_ContentViewWithMoreOptionsButton() { - mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) var isButtonClicked = false val contentView = PromptContentViewWithMoreOptionsButton.Builder() @@ -449,7 +448,6 @@ open class AuthContainerViewTest : SysuiTestCase() { @Test fun testShowCredentialUI_withVerticalListContentView() { - mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) val container = initializeFingerprintContainer( authenticators = BiometricManager.Authenticators.DEVICE_CREDENTIAL, @@ -470,7 +468,6 @@ open class AuthContainerViewTest : SysuiTestCase() { @Test fun testShowCredentialUI_withContentViewWithMoreOptionsButton() { - mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT) val contentView = PromptContentViewWithMoreOptionsButton.Builder() .setMoreOptionsButtonListener(fakeExecutor) { _, _ -> } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java index fdfc25390a3f..6069b4409ec9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java @@ -91,6 +91,7 @@ import com.android.systemui.statusbar.notification.collection.render.GroupMember import com.android.systemui.statusbar.notification.emptyshade.shared.ModesEmptyShadeFix; import com.android.systemui.statusbar.notification.emptyshade.ui.view.EmptyShadeView; import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor; +import com.android.systemui.statusbar.notification.footer.shared.NotifRedesignFooter; import com.android.systemui.statusbar.notification.footer.ui.view.FooterView; import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; import com.android.systemui.statusbar.notification.row.ExpandableView; @@ -559,7 +560,7 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { } @Test - @DisableFlags(FooterViewRefactor.FLAG_NAME) + @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME}) public void manageNotifications_visible() { FooterView view = mock(FooterView.class); mStackScroller.setFooterView(view); @@ -572,7 +573,7 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { } @Test - @DisableFlags(FooterViewRefactor.FLAG_NAME) + @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME}) public void clearAll_visible() { FooterView view = mock(FooterView.class); mStackScroller.setFooterView(view); @@ -585,7 +586,7 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { } @Test - @DisableFlags(FooterViewRefactor.FLAG_NAME) + @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME}) public void testInflateFooterView() { mStackScroller.inflateFooterView(); ArgumentCaptor<FooterView> captor = ArgumentCaptor.forClass(FooterView.class); @@ -596,7 +597,7 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { } @Test - @DisableFlags(FooterViewRefactor.FLAG_NAME) + @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME}) public void testUpdateFooter_noNotifications() { setBarStateForTest(StatusBarState.SHADE); mStackScroller.setCurrentUserSetup(true); @@ -608,7 +609,8 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { } @Test - @DisableFlags(FooterViewRefactor.FLAG_NAME) + @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME}) + @DisableSceneContainer public void testUpdateFooter_remoteInput() { setBarStateForTest(StatusBarState.SHADE); mStackScroller.setCurrentUserSetup(true); @@ -625,7 +627,7 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { } @Test - @DisableFlags(FooterViewRefactor.FLAG_NAME) + @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME}) public void testUpdateFooter_withoutNotifications() { setBarStateForTest(StatusBarState.SHADE); mStackScroller.setCurrentUserSetup(true); @@ -641,7 +643,8 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { } @Test - @DisableFlags(FooterViewRefactor.FLAG_NAME) + @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME}) + @DisableSceneContainer public void testUpdateFooter_oneClearableNotification() { setBarStateForTest(StatusBarState.SHADE); mStackScroller.setCurrentUserSetup(true); @@ -657,7 +660,8 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { } @Test - @DisableFlags(FooterViewRefactor.FLAG_NAME) + @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME}) + @DisableSceneContainer public void testUpdateFooter_withoutHistory() { setBarStateForTest(StatusBarState.SHADE); mStackScroller.setCurrentUserSetup(true); @@ -674,7 +678,7 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { } @Test - @DisableFlags(FooterViewRefactor.FLAG_NAME) + @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME}) public void testUpdateFooter_oneClearableNotification_beforeUserSetup() { setBarStateForTest(StatusBarState.SHADE); mStackScroller.setCurrentUserSetup(false); @@ -690,7 +694,8 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { } @Test - @DisableFlags(FooterViewRefactor.FLAG_NAME) + @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME}) + @DisableSceneContainer public void testUpdateFooter_oneNonClearableNotification() { setBarStateForTest(StatusBarState.SHADE); mStackScroller.setCurrentUserSetup(true); @@ -1181,7 +1186,7 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { } @Test - @DisableFlags(FooterViewRefactor.FLAG_NAME) + @DisableFlags({FooterViewRefactor.FLAG_NAME, NotifRedesignFooter.FLAG_NAME}) public void hasFilteredOutSeenNotifs_updateFooter() { mStackScroller.setCurrentUserSetup(true); @@ -1206,6 +1211,7 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { } @Test + @DisableSceneContainer public void testWindowInsetAnimationProgress_updatesBottomInset() { int imeInset = 100; WindowInsets windowInsets = new WindowInsets.Builder() diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt index 008e8ce92736..638f195df00c 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarViewControllerTest.kt @@ -40,6 +40,7 @@ import com.android.systemui.flags.Flags import com.android.systemui.plugins.fakeDarkIconDispatcher import com.android.systemui.res.R import com.android.systemui.scene.ui.view.WindowRootView +import com.android.systemui.shade.LongPressGestureDetector import com.android.systemui.shade.ShadeControllerImpl import com.android.systemui.shade.ShadeLogger import com.android.systemui.shade.ShadeViewController @@ -97,6 +98,7 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() { @Mock private lateinit var windowRootView: Provider<WindowRootView> @Mock private lateinit var shadeLogger: ShadeLogger @Mock private lateinit var viewUtil: ViewUtil + @Mock private lateinit var longPressGestureDetector: LongPressGestureDetector private lateinit var statusBarWindowStateController: StatusBarWindowStateController private lateinit var view: PhoneStatusBarView @@ -393,6 +395,7 @@ class PhoneStatusBarViewControllerTest : SysuiTestCase() { shadeControllerImpl, shadeViewController, panelExpansionInteractor, + { longPressGestureDetector }, windowRootView, shadeLogger, viewUtil, diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/data/repository/KeyguardBypassRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/data/repository/KeyguardBypassRepositoryTest.kt new file mode 100644 index 000000000000..0c0b5baad821 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/data/repository/KeyguardBypassRepositoryTest.kt @@ -0,0 +1,238 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.statusbar.phone.data.repository + +import android.provider.Settings +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.flags.EnableSceneContainer +import com.android.systemui.keyguard.data.repository.KeyguardBypassRepository +import com.android.systemui.keyguard.data.repository.configureKeyguardBypass +import com.android.systemui.keyguard.data.repository.keyguardBypassRepository +import com.android.systemui.keyguard.data.repository.verifyCallback +import com.android.systemui.keyguard.data.repository.verifyNoCallback +import com.android.systemui.kosmos.testScope +import com.android.systemui.statusbar.policy.DevicePostureController +import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_CLOSED +import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_OPENED +import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_UNKNOWN +import com.android.systemui.statusbar.policy.devicePostureController +import com.android.systemui.testKosmos +import com.android.systemui.tuner.TunerService +import com.android.systemui.tuner.tunerService +import com.android.systemui.util.mockito.withArgCaptor +import com.google.common.truth.Truth.assertThat +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 +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule +import org.mockito.kotlin.whenever + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +@EnableSceneContainer +class KeyguardBypassRepositoryTest : SysuiTestCase() { + @JvmField @Rule val mockito: MockitoRule = MockitoJUnit.rule() + + private lateinit var tunableCallback: TunerService.Tunable + private lateinit var postureControllerCallback: DevicePostureController.Callback + + private val kosmos = testKosmos() + private lateinit var underTest: KeyguardBypassRepository + private val testScope = kosmos.testScope + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + } + + // overrideFaceBypassSetting overridden to true + // isFaceEnrolledAndEnabled true + // isPostureAllowedForFaceAuth true/false on posture changes + @Test + fun updatesBypassAvailableOnPostureChanges_bypassOverrideAlways() = + testScope.runTest { + // KeyguardBypassRepository#overrideFaceBypassSetting = true due to ALWAYS override + // Initialize face auth posture to DEVICE_POSTURE_OPENED config + initUnderTest( + faceUnlockBypassOverrideConfig = FACE_UNLOCK_BYPASS_ALWAYS, + faceAuthPostureConfig = DEVICE_POSTURE_CLOSED, + ) + val isBypassAvailable by collectLastValue(underTest.isBypassAvailable) + runCurrent() + + postureControllerCallback = kosmos.devicePostureController.verifyCallback() + + // Update face auth posture to match config + postureControllerCallback.onPostureChanged(DEVICE_POSTURE_CLOSED) + + // Assert bypass available + assertThat(isBypassAvailable).isTrue() + + // Set face auth posture to not match config + postureControllerCallback.onPostureChanged(DEVICE_POSTURE_OPENED) + + // Assert bypass not available + assertThat(isBypassAvailable).isFalse() + } + + // overrideFaceBypassSetting overridden to false + // isFaceEnrolledAndEnabled true + // isPostureAllowedForFaceAuth true/false on posture changes + @Test + fun updatesBypassEnabledOnPostureChanges_bypassOverrideNever() = + testScope.runTest { + // KeyguardBypassRepository#overrideFaceBypassSetting = false due to NEVER override + // Initialize face auth posture to DEVICE_POSTURE_OPENED config + initUnderTest( + faceUnlockBypassOverrideConfig = FACE_UNLOCK_BYPASS_NEVER, + faceAuthPostureConfig = DEVICE_POSTURE_CLOSED, + ) + val bypassEnabled by collectLastValue(underTest.isBypassAvailable) + runCurrent() + postureControllerCallback = kosmos.devicePostureController.verifyCallback() + + // Update face auth posture to match config + postureControllerCallback.onPostureChanged(DEVICE_POSTURE_CLOSED) + + // Assert bypass not enabled + assertThat(bypassEnabled).isFalse() + + // Set face auth posture to not match config + postureControllerCallback.onPostureChanged(DEVICE_POSTURE_OPENED) + + // Assert bypass not enabled + assertThat(bypassEnabled).isFalse() + } + + // overrideFaceBypassSetting set true/false depending on Setting + // isFaceEnrolledAndEnabled true + // isPostureAllowedForFaceAuth true + @Test + fun updatesBypassEnabledOnSettingsChanges_bypassNoOverride_devicePostureMatchesConfig() = + testScope.runTest { + // No bypass override + // Initialize face auth posture to DEVICE_POSTURE_OPENED config + initUnderTest( + faceUnlockBypassOverrideConfig = FACE_UNLOCK_BYPASS_NO_OVERRIDE, + faceAuthPostureConfig = DEVICE_POSTURE_CLOSED, + ) + + val bypassEnabled by collectLastValue(underTest.isBypassAvailable) + runCurrent() + postureControllerCallback = kosmos.devicePostureController.verifyCallback() + tunableCallback = kosmos.tunerService.captureCallback() + + // Update face auth posture to match config + postureControllerCallback.onPostureChanged(DEVICE_POSTURE_CLOSED) + + // FACE_UNLOCK_DISMISSES_KEYGUARD setting true + whenever(kosmos.tunerService.getValue(eq(faceUnlockDismissesKeyguard), anyInt())) + .thenReturn(1) + tunableCallback.onTuningChanged(faceUnlockDismissesKeyguard, "") + + runCurrent() + // Assert bypass enabled + assertThat(bypassEnabled).isTrue() + + // FACE_UNLOCK_DISMISSES_KEYGUARD setting false + whenever(kosmos.tunerService.getValue(eq(faceUnlockDismissesKeyguard), anyInt())) + .thenReturn(0) + tunableCallback.onTuningChanged(faceUnlockDismissesKeyguard, "") + + runCurrent() + // Assert bypass not enabled + assertThat(bypassEnabled).isFalse() + } + + // overrideFaceBypassSetting overridden to true + // isFaceEnrolledAndEnabled true + // isPostureAllowedForFaceAuth always true given DEVICE_POSTURE_UNKNOWN config + @Test + fun bypassEnabledTrue_bypassAlways_unknownDevicePostureConfig() = + testScope.runTest { + // KeyguardBypassRepository#overrideFaceBypassSetting = true due to ALWAYS override + // Set face auth posture config to unknown + initUnderTest( + faceUnlockBypassOverrideConfig = FACE_UNLOCK_BYPASS_ALWAYS, + faceAuthPostureConfig = DEVICE_POSTURE_UNKNOWN, + ) + val bypassEnabled by collectLastValue(underTest.isBypassAvailable) + kosmos.devicePostureController.verifyNoCallback() + + // Assert bypass enabled + assertThat(bypassEnabled).isTrue() + } + + // overrideFaceBypassSetting overridden to false + // isFaceEnrolledAndEnabled true + // isPostureAllowedForFaceAuth always true given DEVICE_POSTURE_UNKNOWN config + @Test + fun bypassEnabledFalse_bypassNever_unknownDevicePostureConfig() = + testScope.runTest { + // KeyguardBypassRepository#overrideFaceBypassSetting = false due to NEVER override + // Set face auth posture config to unknown + initUnderTest( + faceUnlockBypassOverrideConfig = FACE_UNLOCK_BYPASS_NEVER, + faceAuthPostureConfig = DEVICE_POSTURE_UNKNOWN, + ) + val bypassEnabled by collectLastValue(underTest.isBypassAvailable) + kosmos.devicePostureController.verifyNoCallback() + + // Assert bypass enabled + assertThat(bypassEnabled).isFalse() + } + + private fun TestScope.initUnderTest( + faceUnlockBypassOverrideConfig: Int, + faceAuthPostureConfig: Int, + ) { + kosmos.configureKeyguardBypass( + faceAuthEnrolledAndEnabled = true, + faceUnlockBypassOverrideConfig = faceUnlockBypassOverrideConfig, + faceAuthPostureConfig = faceAuthPostureConfig, + ) + underTest = kosmos.keyguardBypassRepository + runCurrent() + } + + companion object { + private const val FACE_UNLOCK_BYPASS_NO_OVERRIDE = 0 + private const val FACE_UNLOCK_BYPASS_ALWAYS = 1 + private const val FACE_UNLOCK_BYPASS_NEVER = 2 + } +} + +private const val faceUnlockDismissesKeyguard = Settings.Secure.FACE_UNLOCK_DISMISSES_KEYGUARD + +private fun TunerService.captureCallback() = + withArgCaptor<TunerService.Tunable> { + verify(this@captureCallback).addTunable(capture(), eq(faceUnlockDismissesKeyguard)) + } 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 c4c2aa7934f2..48106de5225b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java @@ -2533,6 +2533,42 @@ public class BubblesTest extends SysuiTestCase { verify(mBubbleLogger).log(BubbleLogger.Event.BUBBLE_BAR_DISMISSED_DRAG_BAR); } + @EnableFlags(FLAG_ENABLE_BUBBLE_BAR) + @Test + public void testEventLogging_bubbleBar_expandAndCollapse() { + mBubbleProperties.mIsBubbleBarEnabled = true; + mPositioner.setIsLargeScreen(true); + FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener(); + mBubbleController.registerBubbleStateListener(bubbleStateListener); + + mEntryListener.onEntryAdded(mRow); + mBubbleController.expandStackAndSelectBubbleFromLauncher(mRow.getKey(), 0); + + verify(mBubbleLogger).log(eqBubbleWithKey(mRow.getKey()), + eq(BubbleLogger.Event.BUBBLE_BAR_EXPANDED)); + + mBubbleController.collapseStack(); + + verify(mBubbleLogger).log(eqBubbleWithKey(mRow.getKey()), + eq(BubbleLogger.Event.BUBBLE_BAR_COLLAPSED)); + } + + @EnableFlags(FLAG_ENABLE_BUBBLE_BAR) + @Test + public void testEventLogging_bubbleBar_autoExpandingBubble() { + mBubbleProperties.mIsBubbleBarEnabled = true; + mPositioner.setIsLargeScreen(true); + FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener(); + mBubbleController.registerBubbleStateListener(bubbleStateListener); + + setMetadataFlags(mRow, + Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE, true /* enableFlag */); + mEntryListener.onEntryAdded(mRow); + + verify(mBubbleLogger).log(eqBubbleWithKey(mRow.getKey()), + eq(BubbleLogger.Event.BUBBLE_BAR_EXPANDED)); + } + /** 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/com/android/systemui/SysUITestModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/SysUITestModule.kt index 020f7fa6256b..c9458125e762 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/SysUITestModule.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/SysUITestModule.kt @@ -30,6 +30,8 @@ import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.deviceentry.data.repository.FaceWakeUpTriggersConfigModule import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor import com.android.systemui.deviceentry.domain.interactor.SystemUIDeviceEntryFaceAuthInteractor +import com.android.systemui.dump.DumpManager +import com.android.systemui.keyguard.data.repository.PulseExpansionRepository import com.android.systemui.keyguard.ui.composable.blueprint.DefaultBlueprintModule import com.android.systemui.scene.SceneContainerFrameworkModule import com.android.systemui.scene.shared.flag.SceneContainerFlag @@ -70,11 +72,17 @@ import kotlinx.coroutines.test.runTest interface SysUITestModule { @Binds fun bindTestableContext(sysuiTestableContext: SysuiTestableContext): TestableContext + @Binds fun bindContext(testableContext: TestableContext): Context + @Binds @Application fun bindAppContext(context: Context): Context + @Binds @Application fun bindAppResources(resources: Resources): Resources + @Binds @Main fun bindMainResources(resources: Resources): Resources + @Binds fun bindBroadcastDispatcher(fake: FakeBroadcastDispatcher): BroadcastDispatcher + @Binds @SysUISingleton fun bindsShadeInteractor(sii: ShadeInteractorImpl): ShadeInteractor @Binds @@ -108,7 +116,7 @@ interface SysUITestModule { @Provides fun provideBaseShadeInteractor( sceneContainerOn: Provider<ShadeInteractorSceneContainerImpl>, - sceneContainerOff: Provider<ShadeInteractorLegacyImpl> + sceneContainerOff: Provider<ShadeInteractorLegacyImpl>, ): BaseShadeInteractor { return if (SceneContainerFlag.isEnabled) { sceneContainerOn.get() @@ -125,6 +133,12 @@ interface SysUITestModule { ): SceneDataSourceDelegator { return SceneDataSourceDelegator(applicationScope, config) } + + @Provides + @SysUISingleton + fun providesPulseExpansionRepository(dumpManager: DumpManager): PulseExpansionRepository { + return PulseExpansionRepository(dumpManager) + } } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorKosmos.kt index 56297f0d7f43..787a4715067f 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorKosmos.kt @@ -27,6 +27,7 @@ val Kosmos.promptSelectorInteractor by Fixture { fingerprintPropertyRepository = fingerprintPropertyRepository, displayStateInteractor = displayStateInteractor, promptRepository = promptRepository, - lockPatternUtils = lockPatternUtils + credentialInteractor = FakeCredentialInteractor(), + lockPatternUtils = lockPatternUtils, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt index 62d221dd9790..b27dadc9035b 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt @@ -40,8 +40,18 @@ class FakeCommunalWidgetRepository(private val coroutineScope: CoroutineScope) : ) { coroutineScope.launch { val id = nextWidgetId++ - val providerInfo = AppWidgetProviderInfo().apply { this.provider = provider } - val configured = configurator?.configureWidget(id) ?: true + val providerInfo = createAppWidgetProviderInfo(provider, user.identifier) + + fakeDatabase[id] = + CommunalWidgetContentModel.Available( + appWidgetId = id, + rank = rank ?: 0, + providerInfo = providerInfo, + spanY = 3, + ) + updateListFromDatabase() + + val configured = configurator?.configureWidget(id) != false if (configured) { onConfigured(id, providerInfo, rank ?: -1) } @@ -61,20 +71,15 @@ class FakeCommunalWidgetRepository(private val coroutineScope: CoroutineScope) : appWidgetId = appWidgetId, rank = rank, providerInfo = - AppWidgetProviderInfo().apply { - provider = ComponentName.unflattenFromString(componentName)!! - widgetCategory = category - providerInfo = - ActivityInfo().apply { - applicationInfo = - ApplicationInfo().apply { - uid = userId * UserHandle.PER_USER_RANGE - } - } - }, + createAppWidgetProviderInfo( + ComponentName.unflattenFromString(componentName)!!, + userId, + category, + ), spanY = spanY, ) updateListFromDatabase() + nextWidgetId = appWidgetId + 1 } fun addPendingWidget( @@ -151,4 +156,20 @@ class FakeCommunalWidgetRepository(private val coroutineScope: CoroutineScope) : ) updateListFromDatabase() } + + private fun createAppWidgetProviderInfo( + componentName: ComponentName, + userId: Int, + category: Int = AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD, + ): AppWidgetProviderInfo { + return AppWidgetProviderInfo().apply { + provider = componentName + widgetCategory = category + providerInfo = + ActivityInfo().apply { + applicationInfo = + ApplicationInfo().apply { uid = userId * UserHandle.PER_USER_RANGE } + } + } + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt index 629fda6b610c..1f68195a9acc 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorKosmos.kt @@ -54,7 +54,6 @@ val Kosmos.communalInteractor by Fixture { keyguardInteractor = keyguardInteractor, keyguardTransitionInteractor = keyguardTransitionInteractor, communalSettingsInteractor = communalSettingsInteractor, - appWidgetHost = mock(), editWidgetsActivityStarter = editWidgetsActivityStarter, userTracker = userTracker, activityStarter = activityStarter, @@ -62,7 +61,7 @@ val Kosmos.communalInteractor by Fixture { sceneInteractor = sceneInteractor, logBuffer = logcatLogBuffer("CommunalInteractor"), tableLogBuffer = mock(), - managedProfileController = fakeManagedProfileController + managedProfileController = fakeManagedProfileController, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/shared/model/FakeGlanceableHubMultiUserHelper.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/shared/model/FakeGlanceableHubMultiUserHelper.kt new file mode 100644 index 000000000000..de44399b4a5b --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/shared/model/FakeGlanceableHubMultiUserHelper.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.shared.model + +class FakeGlanceableHubMultiUserHelper( + override val glanceableHubHsumFlagEnabled: Boolean = true, + private var isHeadlessSystemUserMode: Boolean = false, + private var isInHeadlessSystemUser: Boolean = false, +) : GlanceableHubMultiUserHelper { + + override fun isHeadlessSystemUserMode(): Boolean { + return isHeadlessSystemUserMode + } + + fun setIsHeadlessSystemUserMode(isHeadlessSystemUserMode: Boolean) { + this.isHeadlessSystemUserMode = isHeadlessSystemUserMode + } + + override fun isInHeadlessSystemUser(): Boolean { + return isInHeadlessSystemUser + } + + fun setIsInHeadlessSystemUser(isInHeadlessSystemUser: Boolean) { + this.isInHeadlessSystemUser = isInHeadlessSystemUser + } + + override fun assertInHeadlessSystemUser() { + check(isInHeadlessSystemUser()) + } + + override fun assertNotInHeadlessSystemUser() { + check(!isInHeadlessSystemUser()) + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/shared/model/GlanceableHubMultiUserHelperKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/shared/model/GlanceableHubMultiUserHelperKosmos.kt new file mode 100644 index 000000000000..adee44074e51 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/shared/model/GlanceableHubMultiUserHelperKosmos.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.shared.model + +import com.android.systemui.kosmos.Kosmos + +val Kosmos.fakeGlanceableHubMultiUserHelper by Kosmos.Fixture { FakeGlanceableHubMultiUserHelper() } + +val Kosmos.glanceableHubMultiUserHelper by + Kosmos.Fixture<GlanceableHubMultiUserHelper> { fakeGlanceableHubMultiUserHelper } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/widgets/GlanceableHubWidgetManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/widgets/GlanceableHubWidgetManagerKosmos.kt new file mode 100644 index 000000000000..583ae443c95f --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/widgets/GlanceableHubWidgetManagerKosmos.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 com.android.systemui.communal.widgets + +import com.android.systemui.kosmos.Kosmos +import org.mockito.kotlin.mock + +val Kosmos.mockGlanceableHubWidgetManager by Kosmos.Fixture<GlanceableHubWidgetManager> { mock() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt index 878e38594fe1..2a7e3e903737 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractorKosmos.kt @@ -20,6 +20,7 @@ package com.android.systemui.deviceentry.domain.interactor import com.android.keyguard.logging.biometricUnlockLogger import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository +import com.android.systemui.dump.dumpManager import com.android.systemui.keyevent.domain.interactor.keyEventInteractor import com.android.systemui.keyguard.data.repository.biometricSettingsRepository import com.android.systemui.kosmos.Kosmos @@ -27,17 +28,19 @@ import com.android.systemui.power.domain.interactor.powerInteractor import com.android.systemui.util.time.systemClock import kotlinx.coroutines.ExperimentalCoroutinesApi +@OptIn(ExperimentalCoroutinesApi::class) val Kosmos.deviceEntryHapticsInteractor by Kosmos.Fixture { DeviceEntryHapticsInteractor( - deviceEntrySourceInteractor = deviceEntrySourceInteractor, - deviceEntryFingerprintAuthInteractor = deviceEntryFingerprintAuthInteractor, + biometricSettingsRepository = biometricSettingsRepository, deviceEntryBiometricAuthInteractor = deviceEntryBiometricAuthInteractor, + deviceEntryFingerprintAuthInteractor = deviceEntryFingerprintAuthInteractor, + deviceEntrySourceInteractor = deviceEntrySourceInteractor, fingerprintPropertyRepository = fingerprintPropertyRepository, - biometricSettingsRepository = biometricSettingsRepository, keyEventInteractor = keyEventInteractor, + logger = biometricUnlockLogger, powerInteractor = powerInteractor, systemClock = systemClock, - logger = biometricUnlockLogger, + dumpManager = dumpManager, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntrySourceInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntrySourceInteractorKosmos.kt index 0b9ec92af2b5..f91a044ad802 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntrySourceInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntrySourceInteractorKosmos.kt @@ -16,14 +16,34 @@ package com.android.systemui.deviceentry.domain.interactor +import com.android.keyguard.keyguardUpdateMonitor +import com.android.systemui.authentication.domain.interactor.authenticationInteractor +import com.android.systemui.biometrics.authController +import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor +import com.android.systemui.dump.dumpManager +import com.android.systemui.keyguard.domain.interactor.keyguardBypassInteractor import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.kosmos.Kosmos +import com.android.systemui.scene.domain.interactor.sceneContainerOcclusionInteractor +import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.statusbar.phone.dozeScrimController import kotlinx.coroutines.ExperimentalCoroutinesApi @ExperimentalCoroutinesApi val Kosmos.deviceEntrySourceInteractor by Kosmos.Fixture { DeviceEntrySourceInteractor( + authenticationInteractor = authenticationInteractor, + authController = authController, + alternateBouncerInteractor = alternateBouncerInteractor, + deviceEntryFaceAuthInteractor = deviceEntryFaceAuthInteractor, + deviceEntryFingerprintAuthInteractor = deviceEntryFingerprintAuthInteractor, + dozeScrimController = dozeScrimController, + keyguardBypassInteractor = keyguardBypassInteractor, + keyguardUpdateMonitor = keyguardUpdateMonitor, keyguardInteractor = keyguardInteractor, + sceneContainerOcclusionInteractor = sceneContainerOcclusionInteractor, + sceneInteractor = sceneInteractor, + dumpManager = dumpManager, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayWindowPropertiesRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayWindowPropertiesRepository.kt index 9282f275b20b..92dc89747db8 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayWindowPropertiesRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayWindowPropertiesRepository.kt @@ -31,6 +31,7 @@ class FakeDisplayWindowPropertiesRepository : DisplayWindowPropertiesRepository windowType = windowType, context = mock(), windowManager = mock(), + layoutInflater = mock(), ) .also { properties.put(displayId, windowType, it) } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/service/FakeIHomeControlsRemoteProxyBinder.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/service/FakeIHomeControlsRemoteProxyBinder.kt new file mode 100644 index 000000000000..f469d745ef73 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/service/FakeIHomeControlsRemoteProxyBinder.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.dreams.homecontrols.service + +import android.content.ComponentName +import com.android.systemui.dreams.homecontrols.shared.IHomeControlsRemoteProxy +import com.android.systemui.dreams.homecontrols.shared.IOnControlsSettingsChangeListener + +/** Fake binder implementation of [IHomeControlsRemoteProxy] */ +class FakeIHomeControlsRemoteProxyBinder : IHomeControlsRemoteProxy.Stub() { + val callbacks = mutableSetOf<IOnControlsSettingsChangeListener>() + + override fun registerListenerForCurrentUser(callback: IOnControlsSettingsChangeListener) { + callbacks.add(callback) + } + + override fun unregisterListenerForCurrentUser(callback: IOnControlsSettingsChangeListener) { + callbacks.remove(callback) + } + + fun notifyCallbacks(component: ComponentName, allowTrivialControlsOnLockscreen: Boolean) { + for (callback in callbacks.toSet()) { + callback.onControlsSettingsChanged(component, allowTrivialControlsOnLockscreen) + } + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/service/HomeControlsRemoteProxyKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/service/HomeControlsRemoteProxyKosmos.kt new file mode 100644 index 000000000000..58ebbf4c2d9e --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/service/HomeControlsRemoteProxyKosmos.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.dreams.homecontrols.service + +import com.android.systemui.dump.dumpManager +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope + +val Kosmos.homeControlsRemoteProxy by + Kosmos.Fixture { + HomeControlsRemoteProxy( + bgScope = applicationCoroutineScope, + proxy = fakeHomeControlsRemoteBinder, + dumpManager = dumpManager, + ) + } + +val Kosmos.fakeHomeControlsRemoteBinder by + Kosmos.Fixture<FakeIHomeControlsRemoteProxyBinder> { FakeIHomeControlsRemoteProxyBinder() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/service/RemoteHomeControlsDataSourceDelegatorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/service/RemoteHomeControlsDataSourceDelegatorKosmos.kt new file mode 100644 index 000000000000..c85c8884e6f3 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/service/RemoteHomeControlsDataSourceDelegatorKosmos.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.dreams.homecontrols.service + +import com.android.systemui.dreams.homecontrols.dagger.HomeControlsRemoteServiceComponent +import com.android.systemui.dump.dumpManager +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.log.logcatLogBuffer +import org.mockito.kotlin.mock + +val Kosmos.remoteHomeControlsDataSourceDelegator by + Kosmos.Fixture { + RemoteHomeControlsDataSourceDelegator( + bgScope = applicationCoroutineScope, + serviceFactory = homeControlsRemoteServiceFactory, + logBuffer = logcatLogBuffer("HomeControlsDreamInteractor"), + dumpManager = dumpManager, + ) + } + +var Kosmos.homeControlsRemoteServiceFactory by + Kosmos.Fixture<HomeControlsRemoteServiceComponent.Factory> { mock() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/shared/model/FakeHomeControlsDataSource.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/shared/model/FakeHomeControlsDataSource.kt new file mode 100644 index 000000000000..f8ea022be0d8 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/shared/model/FakeHomeControlsDataSource.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.dreams.homecontrols.shared.model + +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.filterNotNull + +class FakeHomeControlsDataSource : HomeControlsDataSource { + + private val _componentInfo = MutableStateFlow<HomeControlsComponentInfo?>(null) + + override val componentInfo: Flow<HomeControlsComponentInfo> + get() = _componentInfo.filterNotNull() + + fun setComponentInfo(info: HomeControlsComponentInfo) { + _componentInfo.value = info + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/shared/model/HomeControlsDataSourceKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/shared/model/HomeControlsDataSourceKosmos.kt new file mode 100644 index 000000000000..942216b6a6cc --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/shared/model/HomeControlsDataSourceKosmos.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.dreams.homecontrols.shared.model + +import com.android.systemui.kosmos.Kosmos + +val Kosmos.homeControlsDataSource by + Kosmos.Fixture<HomeControlsDataSource> { fakeHomeControlsDataSource } + +val Kosmos.fakeHomeControlsDataSource by + Kosmos.Fixture<FakeHomeControlsDataSource> { FakeHomeControlsDataSource() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/system/domain/interactor/HomeControlsComponentInteractorKosmos.kt index 7d7841f0da5b..a1182a178ead 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/dreams/homecontrols/system/domain/interactor/HomeControlsComponentInteractorKosmos.kt @@ -13,21 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.systemui.dreams.homecontrols +package com.android.systemui.dreams.homecontrols.system.domain.interactor -import android.os.powerManager -import android.service.dream.dreamManager -import com.android.systemui.common.domain.interactor.packageChangeInteractor import com.android.systemui.controls.dagger.ControlsComponent import com.android.systemui.controls.management.ControlsListingController import com.android.systemui.controls.panels.authorizedPanelsRepository import com.android.systemui.controls.panels.selectedComponentRepository -import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.user.data.repository.fakeUserRepository import com.android.systemui.util.mockito.mock -import com.android.systemui.util.time.fakeSystemClock val Kosmos.homeControlsComponentInteractor by Kosmos.Fixture { @@ -37,10 +32,6 @@ val Kosmos.homeControlsComponentInteractor by authorizedPanelsRepository = authorizedPanelsRepository, userRepository = fakeUserRepository, bgScope = applicationCoroutineScope, - systemClock = fakeSystemClock, - powerManager = powerManager, - dreamManager = dreamManager, - packageChangeInteractor = packageChangeInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceProviderClientFactoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceProviderClientFactoryKosmos.kt new file mode 100644 index 000000000000..fd882a8eed4a --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceProviderClientFactoryKosmos.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.data.quickaffordance + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.settings.userTracker + +val Kosmos.keyguardQuickAffordanceProviderClientFactory by + Kosmos.Fixture { FakeKeyguardQuickAffordanceProviderClientFactory(userTracker) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/quickaffordance/LocalUserSelectionManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/quickaffordance/LocalUserSelectionManagerKosmos.kt new file mode 100644 index 000000000000..21d1a76088fa --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/quickaffordance/LocalUserSelectionManagerKosmos.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.data.quickaffordance + +import android.content.applicationContext +import com.android.systemui.broadcast.broadcastDispatcher +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.settings.userFileManager +import com.android.systemui.settings.userTracker + +val Kosmos.localUserSelectionManager by + Kosmos.Fixture { + KeyguardQuickAffordanceLocalUserSelectionManager( + context = applicationContext, + userFileManager = userFileManager, + userTracker = userTracker, + broadcastDispatcher = broadcastDispatcher, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/quickaffordance/RemoteUserSelectionManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/quickaffordance/RemoteUserSelectionManagerKosmos.kt new file mode 100644 index 000000000000..ec630719ee09 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/quickaffordance/RemoteUserSelectionManagerKosmos.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.data.quickaffordance + +import android.os.UserHandle +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testScope +import com.android.systemui.settings.userTracker + +val Kosmos.remoteUserSelectionManager by + Kosmos.Fixture { + KeyguardQuickAffordanceRemoteUserSelectionManager( + scope = testScope.backgroundScope, + userTracker = userTracker, + clientFactory = keyguardQuickAffordanceProviderClientFactory, + userHandle = UserHandle.SYSTEM, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/DevicePostureRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/DevicePostureRepositoryKosmos.kt index 9bbb34c970d0..84eea6240e97 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/DevicePostureRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/DevicePostureRepositoryKosmos.kt @@ -17,6 +17,8 @@ package com.android.systemui.keyguard.data.repository import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.statusbar.policy.devicePostureController val Kosmos.devicePostureRepository: DevicePostureRepository by - Kosmos.Fixture { FakeDevicePostureRepository() } + Kosmos.Fixture { DevicePostureRepositoryImpl(devicePostureController, testDispatcher) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt index 4976cc26068a..19e077c57de0 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt @@ -53,6 +53,15 @@ class FakeKeyguardTransitionRepository( private val initInLockscreen: Boolean = true, /** + * Initial value for [FakeKeyguardTransitionRepository.sendTransitionStepsOnStartTransition]. + * This needs to be configurable in the constructor since some transitions are triggered on + * init, before a test has the chance to set sendTransitionStepsOnStartTransition to false. + */ + private val initiallySendTransitionStepsOnStartTransition: Boolean = true, + private val testScope: TestScope, +) : KeyguardTransitionRepository { + + /** * If true, calls to [startTransition] will automatically emit STARTED, RUNNING, and FINISHED * transition steps from/to the given states. * @@ -64,11 +73,9 @@ class FakeKeyguardTransitionRepository( * * If your test needs to make assertions at specific points between STARTED/FINISHED, or if it's * difficult to set up all of the conditions to make the transition interactors actually call - * startTransition, then construct a FakeKeyguardTransitionRepository with this value false. + * startTransition, set this value to false. */ - private val sendTransitionStepsOnStartTransition: Boolean = true, - private val testScope: TestScope, -) : KeyguardTransitionRepository { + var sendTransitionStepsOnStartTransition = initiallySendTransitionStepsOnStartTransition private val _transitions = MutableSharedFlow<TransitionStep>(replay = 3, onBufferOverflow = BufferOverflow.DROP_OLDEST) @@ -77,7 +84,11 @@ class FakeKeyguardTransitionRepository( @Inject constructor( testScope: TestScope - ) : this(initInLockscreen = true, sendTransitionStepsOnStartTransition = true, testScope) + ) : this( + initInLockscreen = true, + initiallySendTransitionStepsOnStartTransition = true, + testScope + ) private val _currentTransitionInfo: MutableStateFlow<TransitionInfo> = MutableStateFlow( @@ -176,6 +187,20 @@ class FakeKeyguardTransitionRepository( testScheduler: TestCoroutineScheduler, throughTransitionState: TransitionState = TransitionState.FINISHED, ) { + val lastStep = _transitions.replayCache.lastOrNull() + if (lastStep != null && lastStep.transitionState != TransitionState.FINISHED) { + sendTransitionStep( + step = + TransitionStep( + transitionState = TransitionState.CANCELED, + from = lastStep.from, + to = lastStep.to, + value = 0f, + ) + ) + testScheduler.runCurrent() + } + sendTransitionStep( step = TransitionStep( diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardBypassRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardBypassRepositoryKosmos.kt new file mode 100644 index 000000000000..c91823cc2999 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardBypassRepositoryKosmos.kt @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.data.repository + +import android.content.testableContext +import com.android.systemui.dump.dumpManager +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.res.R +import com.android.systemui.statusbar.policy.DevicePostureController +import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_CLOSED +import com.android.systemui.statusbar.policy.DevicePostureController.DEVICE_POSTURE_UNKNOWN +import com.android.systemui.tuner.tunerService +import com.android.systemui.util.mockito.withArgCaptor +import org.mockito.ArgumentMatchers.any +import org.mockito.Mockito.never +import org.mockito.Mockito.verify + +val Kosmos.keyguardBypassRepository: KeyguardBypassRepository by Fixture { + KeyguardBypassRepository( + testableContext.resources, + biometricSettingsRepository, + devicePostureRepository, + dumpManager, + tunerService, + testDispatcher, + ) +} + +fun Kosmos.configureKeyguardBypass( + isBypassAvailable: Boolean? = null, + faceAuthEnrolledAndEnabled: Boolean = true, + faceUnlockBypassOverrideConfig: Int = 0, /* FACE_UNLOCK_BYPASS_NO_OVERRIDE */ + faceAuthPostureConfig: Int = DEVICE_POSTURE_UNKNOWN, +) { + when (isBypassAvailable) { + null -> { + biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(faceAuthEnrolledAndEnabled) + testableContext.orCreateTestableResources.addOverride( + R.integer.config_face_unlock_bypass_override, + faceUnlockBypassOverrideConfig, + ) + testableContext.orCreateTestableResources.addOverride( + R.integer.config_face_auth_supported_posture, + faceAuthPostureConfig, + ) + } + true -> { + biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true) + testableContext.orCreateTestableResources.addOverride( + R.integer.config_face_unlock_bypass_override, + 1, /* FACE_UNLOCK_BYPASS_ALWAYS */ + ) + testableContext.orCreateTestableResources.addOverride( + R.integer.config_face_auth_supported_posture, + DEVICE_POSTURE_UNKNOWN, + ) + } + false -> { + biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false) + testableContext.orCreateTestableResources.addOverride( + R.integer.config_face_unlock_bypass_override, + 2, /* FACE_UNLOCK_BYPASS_NEVER */ + ) + testableContext.orCreateTestableResources.addOverride( + R.integer.config_face_auth_supported_posture, + DEVICE_POSTURE_CLOSED, + ) + } + } +} + +fun DevicePostureController.verifyCallback() = + withArgCaptor<DevicePostureController.Callback> { + verify(this@verifyCallback).addCallback(capture()) + } + +fun DevicePostureController.verifyNoCallback() = + verify(this@verifyNoCallback, never()).addCallback(any()) diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryKosmos.kt new file mode 100644 index 000000000000..a6d4ec5dfb2c --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/KeyguardQuickAffordanceRepositoryKosmos.kt @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.data.repository + +import android.content.applicationContext +import android.os.UserHandle +import com.android.systemui.dump.dumpManager +import com.android.systemui.keyguard.data.quickaffordance.localUserSelectionManager +import com.android.systemui.keyguard.data.quickaffordance.remoteUserSelectionManager +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.kosmos.testScope +import com.android.systemui.settings.userTracker +import org.mockito.kotlin.mock + +val Kosmos.keyguardQuickAffordanceRepository by Fixture { + KeyguardQuickAffordanceRepository( + appContext = applicationContext, + scope = testScope.backgroundScope, + localUserSelectionManager = localUserSelectionManager, + remoteUserSelectionManager = remoteUserSelectionManager, + userTracker = userTracker, + legacySettingSyncer = mock(), + configs = setOf(), + dumpManager = dumpManager, + userHandle = UserHandle.SYSTEM, + ) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/PulseExpansionRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/PulseExpansionRepositoryKosmos.kt new file mode 100644 index 000000000000..1851774418a3 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/PulseExpansionRepositoryKosmos.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.data.repository + +import com.android.systemui.dump.dumpManager +import com.android.systemui.kosmos.Kosmos + +val Kosmos.pulseExpansionRepository: PulseExpansionRepository by + Kosmos.Fixture { PulseExpansionRepository(dumpManager) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardBypassInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardBypassInteractorKosmos.kt new file mode 100644 index 000000000000..59ef9df70058 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardBypassInteractorKosmos.kt @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.domain.interactor + +import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor +import com.android.systemui.dump.dumpManager +import com.android.systemui.keyguard.data.repository.keyguardBypassRepository +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.shade.domain.interactor.shadeInteractor + +val Kosmos.keyguardBypassInteractor by Fixture { + KeyguardBypassInteractor( + keyguardBypassRepository, + alternateBouncerInteractor, + keyguardQuickAffordanceInteractor, + pulseExpansionInteractor, + sceneInteractor, + shadeInteractor, + dumpManager, + ) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorKosmos.kt new file mode 100644 index 000000000000..009d17e1a329 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorKosmos.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.domain.interactor + +import android.app.admin.devicePolicyManager +import android.content.applicationContext +import com.android.internal.widget.lockPatternUtils +import com.android.keyguard.logging.KeyguardQuickAffordancesLogger +import com.android.systemui.animation.dialogTransitionAnimator +import com.android.systemui.dock.dockManager +import com.android.systemui.flags.featureFlagsClassic +import com.android.systemui.keyguard.data.repository.biometricSettingsRepository +import com.android.systemui.keyguard.data.repository.keyguardQuickAffordanceRepository +import com.android.systemui.keyguard.shared.quickaffordance.KeyguardQuickAffordancesMetricsLogger +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.plugins.activityStarter +import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.settings.userTracker +import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.statusbar.policy.keyguardStateController +import org.mockito.kotlin.mock + +var Kosmos.keyguardQuickAffordanceInteractor by Fixture { + KeyguardQuickAffordanceInteractor( + keyguardInteractor = keyguardInteractor, + shadeInteractor = shadeInteractor, + lockPatternUtils = lockPatternUtils, + keyguardStateController = keyguardStateController, + userTracker = userTracker, + activityStarter = activityStarter, + featureFlags = featureFlagsClassic, + repository = { keyguardQuickAffordanceRepository }, + launchAnimator = dialogTransitionAnimator, + logger = mock<KeyguardQuickAffordancesLogger>(), + metricsLogger = mock<KeyguardQuickAffordancesMetricsLogger>(), + devicePolicyManager = devicePolicyManager, + dockManager = dockManager, + biometricSettingsRepository = biometricSettingsRepository, + backgroundDispatcher = testDispatcher, + appContext = applicationContext, + sceneInteractor = { sceneInteractor }, + ) +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/PulseExpansionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/PulseExpansionInteractorKosmos.kt new file mode 100644 index 000000000000..f02e0a44ff5d --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/PulseExpansionInteractorKosmos.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.domain.interactor + +import com.android.systemui.dump.dumpManager +import com.android.systemui.keyguard.data.repository.pulseExpansionRepository +import com.android.systemui.kosmos.Kosmos + +val Kosmos.pulseExpansionInteractor: PulseExpansionInteractor by + Kosmos.Fixture { PulseExpansionInteractor(pulseExpansionRepository, dumpManager) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt index 3c87106bf5aa..3ab686da1a6c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt @@ -21,6 +21,7 @@ import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor +import com.android.systemui.keyguard.domain.interactor.pulseExpansionInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.kosmos.applicationCoroutineScope @@ -41,6 +42,7 @@ val Kosmos.keyguardRootViewModel by Fixture { communalInteractor = communalInteractor, keyguardTransitionInteractor = keyguardTransitionInteractor, notificationsKeyguardInteractor = notificationsKeyguardInteractor, + pulseExpansionInteractor = pulseExpansionInteractor, aodNotificationIconViewModel = notificationIconContainerAlwaysOnDisplayViewModel, notificationShadeWindowModel = notificationShadeWindowModel, alternateBouncerToAodTransitionViewModel = alternateBouncerToAodTransitionViewModel, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt index a9a80b52f591..ddae58168201 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/GeneralKosmos.kt @@ -6,6 +6,8 @@ import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest var Kosmos.testDispatcher by Fixture { StandardTestDispatcher() } @@ -34,3 +36,12 @@ var Kosmos.backgroundCoroutineContext: CoroutineContext by Fixture { testScope.backgroundScope.coroutineContext } var Kosmos.mainCoroutineContext: CoroutineContext by Fixture { testScope.coroutineContext } + +/** + * Run this test body with a [Kosmos] as receiver, and using the [testScope] currently installed in + * that kosmos instance + */ +fun Kosmos.runTest(testBody: suspend Kosmos.() -> Unit) = + testScope.runTest { this@runTest.testBody() } + +fun Kosmos.runCurrent() = testScope.runCurrent() diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt index 522c387a0b08..5eaa198fb2a6 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt @@ -50,6 +50,7 @@ import com.android.systemui.keyguard.domain.interactor.fromPrimaryBouncerTransit import com.android.systemui.keyguard.domain.interactor.keyguardClockInteractor import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor +import com.android.systemui.keyguard.domain.interactor.pulseExpansionInteractor import com.android.systemui.model.sceneContainerPlugin import com.android.systemui.plugins.statusbar.statusBarStateController import com.android.systemui.power.data.repository.fakePowerRepository @@ -124,6 +125,7 @@ class KosmosJavaAdapter() { val sceneBackInteractor by lazy { kosmos.sceneBackInteractor } val falsingCollector by lazy { kosmos.falsingCollector } val powerInteractor by lazy { kosmos.powerInteractor } + val pulseExpansionInteractor by lazy { kosmos.pulseExpansionInteractor } val deviceEntryInteractor by lazy { kosmos.deviceEntryInteractor } val deviceEntryUdfpsInteractor by lazy { kosmos.deviceEntryUdfpsInteractor } val deviceUnlockedInteractor by lazy { kosmos.deviceUnlockedInteractor } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplKosmos.kt new file mode 100644 index 000000000000..c337298fa8c8 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplKosmos.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.controls.domain.pipeline + +import com.android.systemui.kosmos.Kosmos +import org.mockito.kotlin.mock + +/** Empty mock */ +val Kosmos.legacyMediaDataManagerImpl by Kosmos.Fixture { mock<LegacyMediaDataManagerImpl>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManagerKosmos.kt new file mode 100644 index 000000000000..f733da12a95e --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/domain/pipeline/MediaDataManagerKosmos.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.media.controls.domain.pipeline + +import com.android.systemui.kosmos.Kosmos + +var Kosmos.mediaDataManager: MediaDataManager by Kosmos.Fixture() diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerKosmos.kt new file mode 100644 index 000000000000..3c35ce9bf003 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerKosmos.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.controls.ui.controller + +import com.android.systemui.kosmos.Kosmos +import org.mockito.kotlin.mock + +/** Empty mock */ +val Kosmos.mediaCarouselController by Kosmos.Fixture { mock<MediaCarouselController>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerLoggerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerLoggerKosmos.kt new file mode 100644 index 000000000000..9f5a8328d403 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/controller/MediaCarouselControllerLoggerKosmos.kt @@ -0,0 +1,23 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.controls.ui.controller + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.log.core.FakeLogBuffer + +val Kosmos.mediaCarouselControllerLogger by + Kosmos.Fixture { MediaCarouselControllerLogger(FakeLogBuffer.Factory.create()) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManagerKosmos.kt index 7c24b4cc3d60..624e174321ec 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManagerKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/controller/MediaHierarchyManagerKosmos.kt @@ -16,8 +16,15 @@ package com.android.systemui.media.controls.ui.controller +import android.content.testableContext import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture -import com.android.systemui.util.mockito.mock +import com.android.systemui.util.animation.UniqueObjectHostView +import org.mockito.kotlin.any +import org.mockito.kotlin.mock -var Kosmos.mediaHierarchyManager by Fixture { mock<MediaHierarchyManager>() } +var Kosmos.mediaHierarchyManager by Fixture { + mock<MediaHierarchyManager> { + on { register(any()) }.thenAnswer { UniqueObjectHostView(this@Fixture.testableContext) } + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/controller/MediaHostStatesManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/controller/MediaHostStatesManagerKosmos.kt new file mode 100644 index 000000000000..8b02c579d91d --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/controller/MediaHostStatesManagerKosmos.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.media.controls.ui.controller + +import com.android.systemui.kosmos.Kosmos + +val Kosmos.mediaHostStatesManager by Kosmos.Fixture { MediaHostStatesManager() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/view/MediaHostKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/view/MediaHostKosmos.kt new file mode 100644 index 000000000000..9638e117362c --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/controls/ui/view/MediaHostKosmos.kt @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.controls.ui.view + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.media.controls.domain.pipeline.mediaDataManager +import com.android.systemui.media.controls.ui.controller.mediaCarouselController +import com.android.systemui.media.controls.ui.controller.mediaCarouselControllerLogger +import com.android.systemui.media.controls.ui.controller.mediaHierarchyManager +import com.android.systemui.media.controls.ui.controller.mediaHostStatesManager +import java.util.function.Supplier + +private val Kosmos.mediaHostProvider by + Kosmos.Fixture { + Supplier<MediaHost> { + MediaHost( + MediaHost.MediaHostStateHolder(), + mediaHierarchyManager, + mediaDataManager, + mediaHostStatesManager, + mediaCarouselController, + mediaCarouselControllerLogger, + ) + } + } + +val Kosmos.qqsMediaHost by Kosmos.Fixture { mediaHostProvider.get() } +val Kosmos.qsMediaHost by Kosmos.Fixture { mediaHostProvider.get() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/dagger/QSFragmentComposeModuleKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/dagger/QSFragmentComposeModuleKosmos.kt new file mode 100644 index 000000000000..fb8ffb0c5eb2 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/dagger/QSFragmentComposeModuleKosmos.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.qs.composefragment.dagger + +import com.android.systemui.kosmos.Kosmos + +var Kosmos.usingMediaInComposeFragment by Kosmos.Fixture<Boolean>() diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelKosmos.kt index 462b4083aa44..bda3192085ed 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/composefragment/viewmodel/QSFragmentComposeViewModelKosmos.kt @@ -22,10 +22,13 @@ import com.android.systemui.common.ui.domain.interactor.configurationInteractor import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.kosmos.Kosmos +import com.android.systemui.media.controls.ui.view.qqsMediaHost +import com.android.systemui.media.controls.ui.view.qsMediaHost +import com.android.systemui.qs.composefragment.dagger.usingMediaInComposeFragment import com.android.systemui.qs.footerActionsController import com.android.systemui.qs.footerActionsViewModelFactory import com.android.systemui.qs.panels.domain.interactor.tileSquishinessInteractor -import com.android.systemui.qs.panels.ui.viewmodel.paginatedGridViewModel +import com.android.systemui.qs.panels.ui.viewmodel.inFirstPageViewModel import com.android.systemui.qs.ui.viewmodel.quickSettingsContainerViewModelFactory import com.android.systemui.shade.largeScreenHeaderHelper import com.android.systemui.shade.transition.largeScreenShadeInterpolator @@ -53,7 +56,10 @@ val Kosmos.qsFragmentComposeViewModelFactory by configurationInteractor, largeScreenHeaderHelper, tileSquishinessInteractor, - paginatedGridViewModel, + inFirstPageViewModel, + qqsMediaHost, + qsMediaHost, + usingMediaInComposeFragment, lifecycleScope, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/InFirstPageViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/InFirstPageViewModelKosmos.kt new file mode 100644 index 000000000000..1fa74cabcdeb --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/InFirstPageViewModelKosmos.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.qs.panels.ui.viewmodel + +import com.android.systemui.kosmos.Kosmos + +val Kosmos.inFirstPageViewModel by Kosmos.Fixture { InFirstPageViewModel() } 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 48ef57e9a62c..5c8ca83ff2ae 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 @@ -17,7 +17,6 @@ package com.android.systemui.qs.panels.ui.viewmodel import com.android.systemui.kosmos.Kosmos -import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.qs.panels.domain.interactor.paginatedGridInteractor val Kosmos.paginatedGridViewModel by @@ -26,6 +25,6 @@ val Kosmos.paginatedGridViewModel by iconTilesViewModel, qsColumnsViewModel, paginatedGridInteractor, - applicationCoroutineScope, + inFirstPageViewModel, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModelKosmos.kt index 41ee260fd5dd..20be5c675851 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/panels/ui/viewmodel/QuickQuickSettingsViewModelKosmos.kt @@ -18,19 +18,21 @@ package com.android.systemui.qs.panels.ui.viewmodel import com.android.systemui.haptics.msdl.tileHapticsViewModelFactoryProvider import com.android.systemui.kosmos.Kosmos -import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.qs.panels.domain.interactor.quickQuickSettingsRowInteractor import com.android.systemui.qs.pipeline.domain.interactor.currentTilesInteractor -val Kosmos.quickQuickSettingsViewModel by +val Kosmos.quickQuickSettingsViewModelFactory by Kosmos.Fixture { - QuickQuickSettingsViewModel( - currentTilesInteractor, - qsColumnsViewModel, - quickQuickSettingsRowInteractor, - tileSquishinessViewModel, - iconTilesViewModel, - applicationCoroutineScope, - tileHapticsViewModelFactoryProvider, - ) + object : QuickQuickSettingsViewModel.Factory { + override fun create(): QuickQuickSettingsViewModel { + return QuickQuickSettingsViewModel( + currentTilesInteractor, + qsColumnsViewModel, + quickQuickSettingsRowInteractor, + tileSquishinessViewModel, + iconTilesViewModel, + tileHapticsViewModelFactoryProvider, + ) + } + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModelKosmos.kt index 6ded7517f1d2..ce103ec57a86 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsContainerViewModelKosmos.kt @@ -19,7 +19,7 @@ package com.android.systemui.qs.ui.viewmodel import com.android.systemui.brightness.ui.viewmodel.brightnessSliderViewModelFactory import com.android.systemui.kosmos.Kosmos import com.android.systemui.qs.panels.ui.viewmodel.editModeViewModel -import com.android.systemui.qs.panels.ui.viewmodel.quickQuickSettingsViewModel +import com.android.systemui.qs.panels.ui.viewmodel.quickQuickSettingsViewModelFactory import com.android.systemui.qs.panels.ui.viewmodel.tileGridViewModel val Kosmos.quickSettingsContainerViewModelFactory by @@ -30,10 +30,10 @@ val Kosmos.quickSettingsContainerViewModelFactory by ): QuickSettingsContainerViewModel { return QuickSettingsContainerViewModel( brightnessSliderViewModelFactory, + quickQuickSettingsViewModelFactory, supportsBrightnessMirroring, tileGridViewModel, editModeViewModel, - quickQuickSettingsViewModel, ) } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt index 4f414d995aab..3300c96b87fd 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/SceneKosmos.kt @@ -17,6 +17,7 @@ import com.android.systemui.scene.ui.viewmodel.SceneContainerHapticsViewModel import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel import com.android.systemui.scene.ui.viewmodel.splitEdgeDetector import com.android.systemui.shade.domain.interactor.shadeInteractor +import com.android.systemui.statusbar.domain.interactor.remoteInputInteractor import kotlinx.coroutines.flow.MutableStateFlow import org.mockito.kotlin.mock @@ -87,6 +88,7 @@ val Kosmos.sceneContainerViewModelFactory by Fixture { falsingInteractor = falsingInteractor, powerInteractor = powerInteractor, shadeInteractor = shadeInteractor, + remoteInputInteractor = remoteInputInteractor, splitEdgeDetector = splitEdgeDetector, logger = sceneLogger, hapticsViewModelFactory = sceneContainerHapticsViewModelFactory, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/CameraLauncherKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/CameraLauncherKosmos.kt new file mode 100644 index 000000000000..d6dd867a4f35 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/CameraLauncherKosmos.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.shade + +import com.android.systemui.camera.cameraGestureHelper +import com.android.systemui.keyguard.domain.interactor.keyguardQuickAffordanceInteractor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.statusbar.phone.keyguardBypassController + +val Kosmos.cameraLauncher by + Kosmos.Fixture { + CameraLauncher(cameraGestureHelper, keyguardBypassController) { + keyguardQuickAffordanceInteractor + } + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/FakeRemoteInputRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/FakeRemoteInputRepository.kt index 91602c23ac17..375bede594b3 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/FakeRemoteInputRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/data/repository/FakeRemoteInputRepository.kt @@ -23,6 +23,11 @@ import kotlinx.coroutines.flow.flowOf class FakeRemoteInputRepository : RemoteInputRepository { override val isRemoteInputActive = MutableStateFlow(false) override val remoteInputRowBottomBound: Flow<Float?> = flowOf(null) + var areRemoteInputsClosed: Boolean = false override fun setRemoteInputRowBottomBound(bottom: Float?) {} + + override fun closeRemoteInputs() { + areRemoteInputsClosed = true + } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationsKeyguardInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationsKeyguardInteractorKosmos.kt index 61a38b864c40..9c2a2be52e92 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationsKeyguardInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationsKeyguardInteractorKosmos.kt @@ -22,7 +22,5 @@ import com.android.systemui.statusbar.notification.data.repository.notifications import com.android.systemui.statusbar.notification.domain.interactor.NotificationsKeyguardInteractor val Kosmos.notificationsKeyguardInteractor by Fixture { - NotificationsKeyguardInteractor( - repository = notificationsKeyguardViewStateRepository, - ) + NotificationsKeyguardInteractor(repository = notificationsKeyguardViewStateRepository) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManagerKosmos.kt index 8785256de452..87ea1473bba1 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManagerKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/StatusBarTouchableRegionManagerKosmos.kt @@ -19,6 +19,7 @@ package com.android.systemui.statusbar.phone import android.content.applicationContext import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor +import com.android.systemui.communal.domain.interactor.communalSceneInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope import com.android.systemui.scene.domain.interactor.sceneInteractor @@ -43,5 +44,6 @@ var Kosmos.statusBarTouchableRegionManager by mock<UnlockedScreenOffAnimationController>(), primaryBouncerInteractor, alternateBouncerInteractor, + communalSceneInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/tuner/TunerServiceKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/tuner/TunerServiceKosmos.kt new file mode 100644 index 000000000000..9bc3ae9c1c24 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/tuner/TunerServiceKosmos.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 com.android.systemui.tuner + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import org.mockito.kotlin.mock + +val Kosmos.tunerService by Fixture { mock<TunerService>() } diff --git a/packages/WallpaperBackup/src/com/android/wallpaperbackup/WallpaperBackupAgent.java b/packages/WallpaperBackup/src/com/android/wallpaperbackup/WallpaperBackupAgent.java index 866359399128..44ea9a2848ac 100644 --- a/packages/WallpaperBackup/src/com/android/wallpaperbackup/WallpaperBackupAgent.java +++ b/packages/WallpaperBackup/src/com/android/wallpaperbackup/WallpaperBackupAgent.java @@ -945,10 +945,11 @@ public class WallpaperBackupAgent extends BackupAgent { String tag = parser.getName(); if (!sectionTag.equals(tag)) continue; for (Pair<Integer, String> pair : List.of( - new Pair<>(WallpaperManager.PORTRAIT, "Portrait"), - new Pair<>(WallpaperManager.LANDSCAPE, "Landscape"), - new Pair<>(WallpaperManager.SQUARE_PORTRAIT, "SquarePortrait"), - new Pair<>(WallpaperManager.SQUARE_LANDSCAPE, "SquareLandscape"))) { + new Pair<>(WallpaperManager.ORIENTATION_PORTRAIT, "Portrait"), + new Pair<>(WallpaperManager.ORIENTATION_LANDSCAPE, "Landscape"), + new Pair<>(WallpaperManager.ORIENTATION_SQUARE_PORTRAIT, "SquarePortrait"), + new Pair<>(WallpaperManager.ORIENTATION_SQUARE_LANDSCAPE, + "SquareLandscape"))) { Rect cropHint = new Rect( getAttributeInt(parser, "cropLeft" + pair.second, 0), getAttributeInt(parser, "cropTop" + pair.second, 0), diff --git a/packages/WallpaperBackup/test/src/com/android/wallpaperbackup/WallpaperBackupAgentTest.java b/packages/WallpaperBackup/test/src/com/android/wallpaperbackup/WallpaperBackupAgentTest.java index 3ecdf3f101a5..f5fb644502ab 100644 --- a/packages/WallpaperBackup/test/src/com/android/wallpaperbackup/WallpaperBackupAgentTest.java +++ b/packages/WallpaperBackup/test/src/com/android/wallpaperbackup/WallpaperBackupAgentTest.java @@ -827,16 +827,17 @@ public class WallpaperBackupAgentTest { @Test public void testOnRestore_singleCropHint() throws Exception { - Map<Integer, Rect> testMap = Map.of(WallpaperManager.PORTRAIT, new Rect(1, 2, 3, 4)); + Map<Integer, Rect> testMap = Map.of( + WallpaperManager.ORIENTATION_PORTRAIT, new Rect(1, 2, 3, 4)); testParseCropHints(testMap); } @Test public void testOnRestore_multipleCropHints() throws Exception { Map<Integer, Rect> testMap = Map.of( - WallpaperManager.PORTRAIT, new Rect(1, 2, 3, 4), - WallpaperManager.SQUARE_PORTRAIT, new Rect(5, 6, 7, 8), - WallpaperManager.SQUARE_LANDSCAPE, new Rect(9, 10, 11, 12)); + WallpaperManager.ORIENTATION_PORTRAIT, new Rect(1, 2, 3, 4), + WallpaperManager.ORIENTATION_SQUARE_PORTRAIT, new Rect(5, 6, 7, 8), + WallpaperManager.ORIENTATION_SQUARE_LANDSCAPE, new Rect(9, 10, 11, 12)); testParseCropHints(testMap); } @@ -936,10 +937,10 @@ public class WallpaperBackupAgentTest { out.startTag(null, "wp"); for (Map.Entry<Integer, Rect> entry: crops.entrySet()) { String orientation = switch (entry.getKey()) { - case WallpaperManager.PORTRAIT -> "Portrait"; - case WallpaperManager.LANDSCAPE -> "Landscape"; - case WallpaperManager.SQUARE_PORTRAIT -> "SquarePortrait"; - case WallpaperManager.SQUARE_LANDSCAPE -> "SquareLandscape"; + case WallpaperManager.ORIENTATION_PORTRAIT -> "Portrait"; + case WallpaperManager.ORIENTATION_LANDSCAPE -> "Landscape"; + case WallpaperManager.ORIENTATION_SQUARE_PORTRAIT -> "SquarePortrait"; + case WallpaperManager.ORIENTATION_SQUARE_LANDSCAPE -> "SquareLandscape"; default -> throw new IllegalArgumentException("Invalid orientation"); }; Rect rect = entry.getValue(); diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp index 65ea9fe3a496..b3f78ab30021 100644 --- a/ravenwood/Android.bp +++ b/ravenwood/Android.bp @@ -360,6 +360,239 @@ java_library { ], } +filegroup { + name: "ravenwood-data", + device_common_srcs: [ + ":system-build.prop", + ":framework-res", + ":ravenwood-empty-res", + ":framework-platform-compat-config", + ":services-platform-compat-config", + ], + device_first_srcs: [ + ":apex_icu.dat", + ], + visibility: ["//visibility:private"], +} + +// Keep in sync with build/make/target/product/generic/Android.bp +filegroup { + name: "ravenwood-fonts", + device_common_srcs: [ + ":AndroidClock.ttf", + ":CarroisGothicSC-Regular.ttf", + ":ComingSoon.ttf", + ":CutiveMono.ttf", + ":DancingScript-Regular.ttf", + ":DroidSansMono.ttf", + ":NotoColorEmoji.ttf", + ":NotoColorEmojiFlags.ttf", + ":NotoNaskhArabic-Bold.ttf", + ":NotoNaskhArabic-Regular.ttf", + ":NotoNaskhArabicUI-Bold.ttf", + ":NotoNaskhArabicUI-Regular.ttf", + ":NotoSansAdlam-VF.ttf", + ":NotoSansAhom-Regular.otf", + ":NotoSansAnatolianHieroglyphs-Regular.otf", + ":NotoSansArmenian-VF.ttf", + ":NotoSansAvestan-Regular.ttf", + ":NotoSansBalinese-Regular.ttf", + ":NotoSansBamum-Regular.ttf", + ":NotoSansBassaVah-Regular.otf", + ":NotoSansBatak-Regular.ttf", + ":NotoSansBengali-VF.ttf", + ":NotoSansBengaliUI-VF.ttf", + ":NotoSansBhaiksuki-Regular.otf", + ":NotoSansBrahmi-Regular.ttf", + ":NotoSansBuginese-Regular.ttf", + ":NotoSansBuhid-Regular.ttf", + ":NotoSansCJK-Regular.ttc", + ":NotoSansCanadianAboriginal-Regular.ttf", + ":NotoSansCarian-Regular.ttf", + ":NotoSansChakma-Regular.otf", + ":NotoSansCham-Bold.ttf", + ":NotoSansCham-Regular.ttf", + ":NotoSansCherokee-Regular.ttf", + ":NotoSansCoptic-Regular.ttf", + ":NotoSansCuneiform-Regular.ttf", + ":NotoSansCypriot-Regular.ttf", + ":NotoSansDeseret-Regular.ttf", + ":NotoSansDevanagari-VF.ttf", + ":NotoSansDevanagariUI-VF.ttf", + ":NotoSansEgyptianHieroglyphs-Regular.ttf", + ":NotoSansElbasan-Regular.otf", + ":NotoSansEthiopic-VF.ttf", + ":NotoSansGeorgian-VF.ttf", + ":NotoSansGlagolitic-Regular.ttf", + ":NotoSansGothic-Regular.ttf", + ":NotoSansGrantha-Regular.ttf", + ":NotoSansGujarati-Bold.ttf", + ":NotoSansGujarati-Regular.ttf", + ":NotoSansGujaratiUI-Bold.ttf", + ":NotoSansGujaratiUI-Regular.ttf", + ":NotoSansGunjalaGondi-Regular.otf", + ":NotoSansGurmukhi-VF.ttf", + ":NotoSansGurmukhiUI-VF.ttf", + ":NotoSansHanifiRohingya-Regular.otf", + ":NotoSansHanunoo-Regular.ttf", + ":NotoSansHatran-Regular.otf", + ":NotoSansHebrew-Bold.ttf", + ":NotoSansHebrew-Regular.ttf", + ":NotoSansImperialAramaic-Regular.ttf", + ":NotoSansInscriptionalPahlavi-Regular.ttf", + ":NotoSansInscriptionalParthian-Regular.ttf", + ":NotoSansJavanese-Regular.otf", + ":NotoSansKaithi-Regular.ttf", + ":NotoSansKannada-VF.ttf", + ":NotoSansKannadaUI-VF.ttf", + ":NotoSansKayahLi-Regular.ttf", + ":NotoSansKharoshthi-Regular.ttf", + ":NotoSansKhmer-VF.ttf", + ":NotoSansKhmerUI-Bold.ttf", + ":NotoSansKhmerUI-Regular.ttf", + ":NotoSansKhojki-Regular.otf", + ":NotoSansLao-Bold.ttf", + ":NotoSansLao-Regular.ttf", + ":NotoSansLaoUI-Bold.ttf", + ":NotoSansLaoUI-Regular.ttf", + ":NotoSansLepcha-Regular.ttf", + ":NotoSansLimbu-Regular.ttf", + ":NotoSansLinearA-Regular.otf", + ":NotoSansLinearB-Regular.ttf", + ":NotoSansLisu-Regular.ttf", + ":NotoSansLycian-Regular.ttf", + ":NotoSansLydian-Regular.ttf", + ":NotoSansMalayalam-VF.ttf", + ":NotoSansMalayalamUI-VF.ttf", + ":NotoSansMandaic-Regular.ttf", + ":NotoSansManichaean-Regular.otf", + ":NotoSansMarchen-Regular.otf", + ":NotoSansMasaramGondi-Regular.otf", + ":NotoSansMedefaidrin-VF.ttf", + ":NotoSansMeeteiMayek-Regular.ttf", + ":NotoSansMeroitic-Regular.otf", + ":NotoSansMiao-Regular.otf", + ":NotoSansModi-Regular.ttf", + ":NotoSansMongolian-Regular.ttf", + ":NotoSansMro-Regular.otf", + ":NotoSansMultani-Regular.otf", + ":NotoSansMyanmar-Bold.otf", + ":NotoSansMyanmar-Medium.otf", + ":NotoSansMyanmar-Regular.otf", + ":NotoSansMyanmarUI-Bold.otf", + ":NotoSansMyanmarUI-Medium.otf", + ":NotoSansMyanmarUI-Regular.otf", + ":NotoSansNKo-Regular.ttf", + ":NotoSansNabataean-Regular.otf", + ":NotoSansNewTaiLue-Regular.ttf", + ":NotoSansNewa-Regular.otf", + ":NotoSansOgham-Regular.ttf", + ":NotoSansOlChiki-Regular.ttf", + ":NotoSansOldItalic-Regular.ttf", + ":NotoSansOldNorthArabian-Regular.otf", + ":NotoSansOldPermic-Regular.otf", + ":NotoSansOldPersian-Regular.ttf", + ":NotoSansOldSouthArabian-Regular.ttf", + ":NotoSansOldTurkic-Regular.ttf", + ":NotoSansOriya-Bold.ttf", + ":NotoSansOriya-Regular.ttf", + ":NotoSansOriyaUI-Bold.ttf", + ":NotoSansOriyaUI-Regular.ttf", + ":NotoSansOsage-Regular.ttf", + ":NotoSansOsmanya-Regular.ttf", + ":NotoSansPahawhHmong-Regular.otf", + ":NotoSansPalmyrene-Regular.otf", + ":NotoSansPauCinHau-Regular.otf", + ":NotoSansPhagsPa-Regular.ttf", + ":NotoSansPhoenician-Regular.ttf", + ":NotoSansRejang-Regular.ttf", + ":NotoSansRunic-Regular.ttf", + ":NotoSansSamaritan-Regular.ttf", + ":NotoSansSaurashtra-Regular.ttf", + ":NotoSansSharada-Regular.otf", + ":NotoSansShavian-Regular.ttf", + ":NotoSansSinhala-VF.ttf", + ":NotoSansSinhalaUI-VF.ttf", + ":NotoSansSoraSompeng-Regular.otf", + ":NotoSansSoyombo-VF.ttf", + ":NotoSansSundanese-Regular.ttf", + ":NotoSansSylotiNagri-Regular.ttf", + ":NotoSansSymbols-Regular-Subsetted.ttf", + ":NotoSansSymbols-Regular-Subsetted2.ttf", + ":NotoSansSyriacEastern-Regular.ttf", + ":NotoSansSyriacEstrangela-Regular.ttf", + ":NotoSansSyriacWestern-Regular.ttf", + ":NotoSansTagalog-Regular.ttf", + ":NotoSansTagbanwa-Regular.ttf", + ":NotoSansTaiLe-Regular.ttf", + ":NotoSansTaiTham-Regular.ttf", + ":NotoSansTaiViet-Regular.ttf", + ":NotoSansTakri-VF.ttf", + ":NotoSansTamil-VF.ttf", + ":NotoSansTamilUI-VF.ttf", + ":NotoSansTelugu-VF.ttf", + ":NotoSansTeluguUI-VF.ttf", + ":NotoSansThaana-Bold.ttf", + ":NotoSansThaana-Regular.ttf", + ":NotoSansThai-Bold.ttf", + ":NotoSansThai-Regular.ttf", + ":NotoSansThaiUI-Bold.ttf", + ":NotoSansThaiUI-Regular.ttf", + ":NotoSansTifinagh-Regular.otf", + ":NotoSansUgaritic-Regular.ttf", + ":NotoSansVai-Regular.ttf", + ":NotoSansWancho-Regular.otf", + ":NotoSansWarangCiti-Regular.otf", + ":NotoSansYi-Regular.ttf", + ":NotoSerif-Bold.ttf", + ":NotoSerif-BoldItalic.ttf", + ":NotoSerif-Italic.ttf", + ":NotoSerif-Regular.ttf", + ":NotoSerifArmenian-VF.ttf", + ":NotoSerifBengali-VF.ttf", + ":NotoSerifCJK-Regular.ttc", + ":NotoSerifDevanagari-VF.ttf", + ":NotoSerifDogra-Regular.ttf", + ":NotoSerifEthiopic-VF.ttf", + ":NotoSerifGeorgian-VF.ttf", + ":NotoSerifGujarati-VF.ttf", + ":NotoSerifGurmukhi-VF.ttf", + ":NotoSerifHebrew-Bold.ttf", + ":NotoSerifHebrew-Regular.ttf", + ":NotoSerifHentaigana.ttf", + ":NotoSerifKannada-VF.ttf", + ":NotoSerifKhmer-Bold.otf", + ":NotoSerifKhmer-Regular.otf", + ":NotoSerifLao-Bold.ttf", + ":NotoSerifLao-Regular.ttf", + ":NotoSerifMalayalam-VF.ttf", + ":NotoSerifMyanmar-Bold.otf", + ":NotoSerifMyanmar-Regular.otf", + ":NotoSerifNyiakengPuachueHmong-VF.ttf", + ":NotoSerifSinhala-VF.ttf", + ":NotoSerifTamil-VF.ttf", + ":NotoSerifTelugu-VF.ttf", + ":NotoSerifThai-Bold.ttf", + ":NotoSerifThai-Regular.ttf", + ":NotoSerifTibetan-VF.ttf", + ":NotoSerifYezidi-VF.ttf", + ":Roboto-Regular.ttf", + ":RobotoFlex-Regular.ttf", + ":RobotoStatic-Regular.ttf", + ":SourceSansPro-Bold.ttf", + ":SourceSansPro-BoldItalic.ttf", + ":SourceSansPro-Italic.ttf", + ":SourceSansPro-Regular.ttf", + ":SourceSansPro-SemiBold.ttf", + ":SourceSansPro-SemiBoldItalic.ttf", + ], + device_first_srcs: [ + ":font_fallback.xml", + ":fonts.xml", + ], + visibility: ["//visibility:private"], +} + // JARs in "ravenwood-runtime" are set to the classpath, sorted alphabetically. // Rename some of the dependencies to make sure they're included in the intended order. @@ -386,13 +619,8 @@ java_library { android_ravenwood_libgroup { name: "ravenwood-runtime", - data: [ - ":system-build.prop", - ":framework-res", - ":ravenwood-empty-res", - ":framework-platform-compat-config", - ":services-platform-compat-config", - ], + data: [":ravenwood-data"], + fonts: [":ravenwood-fonts"], libs: [ "100-framework-minus-apex.ravenwood", "200-kxml2-android", diff --git a/ravenwood/TEST_MAPPING b/ravenwood/TEST_MAPPING index 7fa0ef114c82..a1243e37003e 100644 --- a/ravenwood/TEST_MAPPING +++ b/ravenwood/TEST_MAPPING @@ -35,18 +35,18 @@ }, { "name": "CarLibHostUnitTest", - "host": true, - "keywords": ["automotive_code_coverage"] + "keywords": ["automotive_code_coverage"], + "host": true }, { "name": "CarServiceHostUnitTest", - "host": true, - "keywords": ["automotive_code_coverage"] + "keywords": ["automotive_code_coverage"], + "host": true }, { "name": "CarSystemUIRavenTests", - "host": true, - "keywords": ["automotive_code_coverage"] + "keywords": ["automotive_code_coverage"], + "host": true }, { "name": "CtsAccountManagerTestCasesRavenwood", @@ -65,6 +65,10 @@ "host": true }, { + "name": "CtsDeviceConfigTestCasesRavenwood", + "host": true + }, + { "name": "CtsGraphicsTestCasesRavenwood", "host": true }, 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 9002e40bba32..0f163524d2fe 100644 --- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java +++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java @@ -165,6 +165,17 @@ public class RavenwoodRuntimeEnvironmentController { RavenwoodSystemProperties.initialize(RAVENWOOD_BUILD_PROP); setSystemProperties(null); + // Do this after loading RAVENWOOD_NATIVE_RUNTIME_NAME (which backs Os.setenv()), + // before loadFrameworkNativeCode() (which uses $ANDROID_LOG_TAGS). + if (RAVENWOOD_VERBOSE_LOGGING) { + RavenwoodCommonUtils.log(TAG, "Force enabling verbose logging"); + try { + Os.setenv("ANDROID_LOG_TAGS", "*:v", true); + } catch (ErrnoException e) { + throw new RuntimeException(e); + } + } + // Make sure libandroid_runtime is loaded. RavenwoodNativeLoader.loadFrameworkNativeCode(); @@ -175,15 +186,6 @@ public class RavenwoodRuntimeEnvironmentController { Objects.requireNonNull(Build.TYPE); Objects.requireNonNull(Build.VERSION.SDK); - if (RAVENWOOD_VERBOSE_LOGGING) { - RavenwoodCommonUtils.log(TAG, "Force enabling verbose logging"); - try { - Os.setenv("ANDROID_LOG_TAGS", "*:v", true); - } catch (ErrnoException e) { - // Shouldn't happen. - } - } - System.setProperty(RAVENWOOD_VERSION_JAVA_SYSPROP, "1"); // This will let AndroidJUnit4 use the original runner. System.setProperty("android.junit.runner", diff --git a/ravenwood/runtime-jni/ravenwood_runtime.cpp b/ravenwood/runtime-jni/ravenwood_runtime.cpp index 3ff08483c956..2a3c26ed3ea3 100644 --- a/ravenwood/runtime-jni/ravenwood_runtime.cpp +++ b/ravenwood/runtime-jni/ravenwood_runtime.cpp @@ -173,6 +173,24 @@ static void Linux_setenv(JNIEnv* env, jobject, jstring javaName, jstring javaVal throwIfMinusOne(env, "setenv", setenv(name.c_str(), value.c_str(), overwrite ? 1 : 0)); } +static void maybeRedirectLog() { + auto ravenwoodLogOut = getenv("RAVENWOOD_LOG_OUT"); + if (ravenwoodLogOut == NULL) { + return; + } + ALOGI("RAVENWOOD_LOG_OUT set. Redirecting output to %s", ravenwoodLogOut); + + // Redirect stdin / stdout to /dev/tty. + int ttyFd = open(ravenwoodLogOut, O_WRONLY); + if (ttyFd == -1) { + ALOGW("$RAVENWOOD_LOG_OUT is set to %s, but failed to open: %s ", ravenwoodLogOut, + strerror(errno)); + return; + } + dup2(ttyFd, 1); + dup2(ttyFd, 2); +} + // ---- Registration ---- extern void register_android_system_OsConstants(JNIEnv* env); @@ -192,6 +210,8 @@ static const JNINativeMethod sMethods[] = }; extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */) { + maybeRedirectLog(); + ALOGI("%s: JNI_OnLoad", __FILE__); JNIEnv* env = GetJNIEnvOrDie(vm); diff --git a/ravenwood/scripts/update-test-mapping.sh b/ravenwood/scripts/update-test-mapping.sh index e478b50cc2b9..ab37baf01ea5 100755 --- a/ravenwood/scripts/update-test-mapping.sh +++ b/ravenwood/scripts/update-test-mapping.sh @@ -23,6 +23,14 @@ set -e # Tests that shouldn't be in presubmit. EXEMPT='^(SystemUiRavenTests)$' +is_car() { + local module="$1" + + # If the module name starts with "Car", then it's a test for "Car". + [[ "$module" =~ ^Car ]] + return $? +} + main() { local script_name="${0##*/}" local script_dir="${0%/*}" @@ -62,6 +70,10 @@ main() { fi echo " {" echo " \"name\": \"${tests[$i]}\"," + if is_car "${tests[$i]}"; then + echo ' "keywords": ["automotive_code_coverage"],' + fi + echo " \"host\": true" echo " }$comma" diff --git a/ravenwood/tests/bivalenttest/Android.bp b/ravenwood/tests/bivalenttest/Android.bp index ce0033d02910..4895a1a6d1a2 100644 --- a/ravenwood/tests/bivalenttest/Android.bp +++ b/ravenwood/tests/bivalenttest/Android.bp @@ -43,9 +43,12 @@ java_defaults { // To make sure it won't cause VerifyError (b/324063814) "platformprotosnano", + + "com.android.internal.os.flags-aconfig-java", ], srcs: [ "test/**/*.java", + "test/**/*.kt", ], jni_libs: [ "libravenwoodbivalenttest_jni", @@ -58,10 +61,12 @@ java_defaults { // TODO(b/371215487): migrate bivalenttest.ravenizer tests to another architecture exclude_srcs: [ "test/**/ravenizer/*.java", + "test/**/ravenizer/*.kt", ], static_libs: [ "junit", "truth", + "flag-junit", "ravenwood-junit", ], test_suites: [ diff --git a/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/aconfig/RavenwoodAconfigFlagTest.kt b/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/aconfig/RavenwoodAconfigFlagTest.kt new file mode 100644 index 000000000000..fd6d6fb66465 --- /dev/null +++ b/ravenwood/tests/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/aconfig/RavenwoodAconfigFlagTest.kt @@ -0,0 +1,102 @@ +/* + * 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.ravenwoodtest.bivalenttest.aconfig + +import android.platform.test.annotations.EnableFlags +import android.platform.test.annotations.RequiresFlagsDisabled +import android.platform.test.annotations.RequiresFlagsEnabled +import android.platform.test.flag.junit.DeviceFlagsValueProvider +import android.platform.test.flag.junit.SetFlagsRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.internal.os.Flags +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Assert.fail +import org.junit.Ignore +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + + +@RunWith(AndroidJUnit4::class) +class RavenwoodAconfigSimpleReadTests { + @Test + fun testFalseFlags() { + assertFalse(Flags.ravenwoodFlagRo1()) + assertFalse(Flags.ravenwoodFlagRw1()) + } + + @Test + @Ignore // TODO: Enable this test after rolling out the "2" flags. + fun testTrueFlags() { + assertTrue(Flags.ravenwoodFlagRo2()) + assertTrue(Flags.ravenwoodFlagRw2()) + } +} + +@RunWith(AndroidJUnit4::class) +class RavenwoodAconfigCheckFlagsRuleTests { + @Rule + @JvmField + val checkFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() + + @Test + @RequiresFlagsEnabled(Flags.FLAG_RAVENWOOD_FLAG_RO_1) + fun testRequireFlagsEnabledRo() { + fail("This test shouldn't be executed") + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_RAVENWOOD_FLAG_RW_1) + fun testRequireFlagsEnabledRw() { + fail("This test shouldn't be executed") + } + + @Test + @RequiresFlagsDisabled(Flags.FLAG_RAVENWOOD_FLAG_RO_2) + @Ignore // TODO: Enable this test after rolling out the "2" flags. + fun testRequireFlagsDisabledRo() { + fail("This test shouldn't be executed") + } + + @Test + @RequiresFlagsDisabled(Flags.FLAG_RAVENWOOD_FLAG_RW_2) + @Ignore // TODO: Enable this test after rolling out the "2" flags. + fun testRequireFlagsDisabledRw() { + fail("This test shouldn't be executed") + } +} + +@RunWith(AndroidJUnit4::class) +class RavenwoodAconfigSetFlagsRuleWithDefaultTests { + @Rule + @JvmField + val setFlagsRule = SetFlagsRule() + + @Test + @EnableFlags(Flags.FLAG_RAVENWOOD_FLAG_RO_1) + fun testSetRoFlag() { + assertTrue(Flags.ravenwoodFlagRo1()) + assertFalse(Flags.ravenwoodFlagRw1()) + } + + @Test + @EnableFlags(Flags.FLAG_RAVENWOOD_FLAG_RW_1) + fun testSetRwFlag() { + assertFalse(Flags.ravenwoodFlagRo1()) + assertTrue(Flags.ravenwoodFlagRw1()) + } +} diff --git a/ravenwood/texts/ravenwood-annotation-allowed-classes.txt b/ravenwood/texts/ravenwood-annotation-allowed-classes.txt index a26fe66da2ab..70c1d781587f 100644 --- a/ravenwood/texts/ravenwood-annotation-allowed-classes.txt +++ b/ravenwood/texts/ravenwood-annotation-allowed-classes.txt @@ -2,6 +2,9 @@ com.android.internal.ravenwood.* +com.android.server.FgThread +com.android.server.ServiceThread + com.android.internal.display.BrightnessSynchronizer com.android.internal.util.ArrayUtils com.android.internal.logging.MetricsLogger diff --git a/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/asm/AsmUtils.kt b/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/asm/AsmUtils.kt index a02082d12934..f47aaba8ef22 100644 --- a/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/asm/AsmUtils.kt +++ b/ravenwood/tools/hoststubgen/src/com/android/hoststubgen/asm/AsmUtils.kt @@ -327,6 +327,10 @@ fun ClassNode.isSynthetic(): Boolean { return (this.access and Opcodes.ACC_SYNTHETIC) != 0 } +fun ClassNode.isAbstract(): Boolean { + return (this.access and Opcodes.ACC_ABSTRACT) != 0 +} + fun MethodNode.isSynthetic(): Boolean { return (this.access and Opcodes.ACC_SYNTHETIC) != 0 } diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerOptions.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerOptions.kt index 32dcbe546f3c..a0e5599c0a7c 100644 --- a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerOptions.kt +++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerOptions.kt @@ -46,7 +46,7 @@ class RavenizerOptions( var enableValidation: SetOnce<Boolean> = SetOnce(true), /** Whether the validation failure is fatal or not. */ - var fatalValidation: SetOnce<Boolean> = SetOnce(false), + var fatalValidation: SetOnce<Boolean> = SetOnce(true), /** Whether to remove mockito and dexmaker classes. */ var stripMockito: SetOnce<Boolean> = SetOnce(false), diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Validator.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Validator.kt index 27092d28ae5e..8ec0932d89dd 100644 --- a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Validator.kt +++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Validator.kt @@ -16,10 +16,12 @@ package com.android.platform.test.ravenwood.ravenizer import com.android.hoststubgen.asm.ClassNodes +import com.android.hoststubgen.asm.isAbstract import com.android.hoststubgen.asm.startsWithAny import com.android.hoststubgen.asm.toHumanReadableClassName import com.android.hoststubgen.log import org.objectweb.asm.tree.ClassNode +import java.util.regex.Pattern fun validateClasses(classes: ClassNodes): Boolean { var allOk = true @@ -41,25 +43,35 @@ fun checkClass(cn: ClassNode, classes: ClassNodes): Boolean { } var allOk = true + log.i("Checking ${cn.name.toHumanReadableClassName()}") + // See if there's any class that extends a legacy base class. // But ignore the base classes in android.test. - if (!cn.name.startsWithAny("android/test/")) { - allOk = checkSuperClass(cn, cn, classes) && allOk + if (!cn.isAbstract() && !cn.name.startsWith("android/test/") + && !isAllowListedLegacyTest(cn) + ) { + allOk = checkSuperClassForJunit3(cn, cn, classes) && allOk } return allOk } -fun checkSuperClass(targetClass: ClassNode, currentClass: ClassNode, classes: ClassNodes): Boolean { +fun checkSuperClassForJunit3( + targetClass: ClassNode, + currentClass: ClassNode, + classes: ClassNodes, +): Boolean { if (currentClass.superName == null || currentClass.superName == "java/lang/Object") { return true // No parent class } + // Make sure the class doesn't extend a junit3 TestCase class. if (currentClass.superName.isLegacyTestBaseClass()) { log.e("Error: Class ${targetClass.name.toHumanReadableClassName()} extends" - + " a legacy test class ${currentClass.superName.toHumanReadableClassName()}.") + + " a legacy test class ${currentClass.superName.toHumanReadableClassName()}" + + ", which is not supported on Ravenwood. Please migrate to Junit4 syntax.") return false } classes.findClass(currentClass.superName)?.let { - return checkSuperClass(targetClass, it, classes) + return checkSuperClassForJunit3(targetClass, it, classes) } // Super class not found. // log.w("Class ${currentClass.superName} not found.") @@ -73,9 +85,64 @@ fun String.isLegacyTestBaseClass(): Boolean { return this.startsWithAny( "junit/framework/TestCase", - // In case the test doesn't statically include JUnit, we need + // In case the test doesn't statically include JUnit, we need the following. "android/test/AndroidTestCase", "android/test/InstrumentationTestCase", "android/test/InstrumentationTestSuite", ) } + +private val allowListedLegacyTests = setOf( +// List of existing test classes that use the JUnit3 syntax. We exempt them for now, but +// will reject any more of them. +// +// Note, we want internal class names, but for convenience, we use '.'s and '%'s here +// and replace them later. (a '$' would be parsed as a string template.) + *""" +android.util.proto.cts.DebuggingTest +android.util.proto.cts.EncodedBufferTest +android.util.proto.cts.ProtoOutputStreamBoolTest +android.util.proto.cts.ProtoOutputStreamBytesTest +android.util.proto.cts.ProtoOutputStreamDoubleTest +android.util.proto.cts.ProtoOutputStreamEnumTest +android.util.proto.cts.ProtoOutputStreamFixed32Test +android.util.proto.cts.ProtoOutputStreamFixed64Test +android.util.proto.cts.ProtoOutputStreamFloatTest +android.util.proto.cts.ProtoOutputStreamInt32Test +android.util.proto.cts.ProtoOutputStreamInt64Test +android.util.proto.cts.ProtoOutputStreamObjectTest +android.util.proto.cts.ProtoOutputStreamSFixed32Test +android.util.proto.cts.ProtoOutputStreamSFixed64Test +android.util.proto.cts.ProtoOutputStreamSInt32Test +android.util.proto.cts.ProtoOutputStreamSInt64Test +android.util.proto.cts.ProtoOutputStreamStringTest +android.util.proto.cts.ProtoOutputStreamSwitchedWriteTest +android.util.proto.cts.ProtoOutputStreamTagTest +android.util.proto.cts.ProtoOutputStreamUInt32Test +android.util.proto.cts.ProtoOutputStreamUInt64Test + +android.os.cts.BadParcelableExceptionTest +android.os.cts.DeadObjectExceptionTest +android.os.cts.ParcelFormatExceptionTest +android.os.cts.PatternMatcherTest +android.os.cts.RemoteExceptionTest + +android.os.storage.StorageManagerBaseTest +android.os.storage.StorageManagerIntegrationTest +android.util.LogTest%PerformanceTest + +com.android.server.power.stats.BatteryStatsCounterTest +com.android.server.power.stats.BatteryStatsDualTimerTest +com.android.server.power.stats.BatteryStatsDurationTimerTest +com.android.server.power.stats.BatteryStatsSamplingTimerTest +com.android.server.power.stats.BatteryStatsStopwatchTimerTest +com.android.server.power.stats.BatteryStatsTimeBaseTest +com.android.server.power.stats.BatteryStatsTimerTest + + """.trim().replace('%', '$').replace('.', '/') + .split(Pattern.compile("""\s+""")).toTypedArray() +) + +private fun isAllowListedLegacyTest(targetClass: ClassNode): Boolean { + return allowListedLegacyTests.contains(targetClass.name) +}
\ No newline at end of file diff --git a/services/Android.bp b/services/Android.bp index f04c692c12d0..899e224c6fd7 100644 --- a/services/Android.bp +++ b/services/Android.bp @@ -188,6 +188,28 @@ art_profile_java_defaults { }, } +// Conditionally add crashrecovery stubs library +soong_config_module_type { + name: "crashrecovery_java_defaults", + module_type: "java_defaults", + config_namespace: "ANDROID", + bool_variables: [ + "release_crashrecovery_module", + ], + properties: [ + "libs", + ], +} + +crashrecovery_java_defaults { + name: "services_crashrecovery_stubs_conditionally", + soong_config_variables: { + release_crashrecovery_module: { + libs: ["service-crashrecovery.stubs.system_server"], + }, + }, +} + // merge all required services into one jar // ============================================================ soong_config_module_type { @@ -213,6 +235,7 @@ system_java_library { defaults: [ "services_java_defaults", "art_profile_java_defaults", + "services_crashrecovery_stubs_conditionally", ], installable: true, diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index c6fe4971b9e5..974cba2450f4 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -52,6 +52,7 @@ import static com.android.internal.accessibility.AccessibilityShortcutController import static com.android.internal.accessibility.common.ShortcutConstants.CHOOSER_PACKAGE_NAME; import static com.android.internal.accessibility.common.ShortcutConstants.USER_SHORTCUT_TYPES; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType; +import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.ALL; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.GESTURE; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.HARDWARE; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.QUICK_SETTINGS; @@ -3897,6 +3898,7 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub userState.getShortcutTargetsLocked(HARDWARE); final Set<String> qsShortcutTargets = userState.getShortcutTargetsLocked(QUICK_SETTINGS); + final Set<String> shortcutTargets = userState.getShortcutTargetsLocked(ALL); userState.mEnabledServices.forEach(componentName -> { if (packageName != null && componentName != null && !packageName.equals(componentName.getPackageName())) { @@ -3917,7 +3919,11 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub if (TextUtils.isEmpty(serviceName)) { return; } - if (doesShortcutTargetsStringContain(buttonTargets, serviceName) + if (android.provider.Flags.a11yStandaloneGestureEnabled()) { + if (doesShortcutTargetsStringContain(shortcutTargets, serviceName)) { + return; + } + } else if (doesShortcutTargetsStringContain(buttonTargets, serviceName) || doesShortcutTargetsStringContain(shortcutKeyTargets, serviceName) || doesShortcutTargetsStringContain(qsShortcutTargets, serviceName)) { return; diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java index 0bf7ec001d4d..67b40632dde8 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java @@ -106,21 +106,17 @@ class AccessibilityUserState { final Set<ComponentName> mTouchExplorationGrantedServices = new HashSet<>(); - private final ArraySet<String> mAccessibilityShortcutKeyTargets = new ArraySet<>(); - - private final ArraySet<String> mAccessibilityButtonTargets = new ArraySet<>(); - private final ArraySet<String> mAccessibilityGestureTargets = new ArraySet<>(); - private final ArraySet<String> mAccessibilityQsTargets = new ArraySet<>(); + private final HashMap<Integer, ArraySet<String>> mShortcutTargets = new HashMap<>(); /** - * The QuickSettings tiles in the QS Panel. This can be different from - * {@link #mAccessibilityQsTargets} in that {@link #mA11yTilesInQsPanel} stores the + * The QuickSettings tiles in the QS Panel. This can be different from the QS targets in + * {@link #mShortcutTargets} in that {@link #mA11yTilesInQsPanel} stores the * TileService's or the a11y framework tile component names (e.g. * {@link AccessibilityShortcutController#COLOR_INVERSION_TILE_COMPONENT_NAME}) instead of the * A11y Feature's component names. * <p/> * In addition, {@link #mA11yTilesInQsPanel} stores what's on the QS Panel, whereas - * {@link #mAccessibilityQsTargets} stores the targets that configured qs as their shortcut and + * {@link #mShortcutTargets} stores the targets that configured qs as their shortcut and * also grant full device control permission. */ private final ArraySet<ComponentName> mA11yTilesInQsPanel = new ArraySet<>(); @@ -208,6 +204,11 @@ class AccessibilityUserState { mSupportWindowMagnification = mContext.getResources().getBoolean( R.bool.config_magnification_area) && mContext.getPackageManager().hasSystemFeature( PackageManager.FEATURE_WINDOW_MAGNIFICATION); + + mShortcutTargets.put(HARDWARE, new ArraySet<>()); + mShortcutTargets.put(SOFTWARE, new ArraySet<>()); + mShortcutTargets.put(GESTURE, new ArraySet<>()); + mShortcutTargets.put(QUICK_SETTINGS, new ArraySet<>()); } boolean isHandlingAccessibilityEventsLocked() { @@ -233,10 +234,7 @@ class AccessibilityUserState { // Clear state persisted in settings. mEnabledServices.clear(); mTouchExplorationGrantedServices.clear(); - mAccessibilityShortcutKeyTargets.clear(); - mAccessibilityButtonTargets.clear(); - mAccessibilityGestureTargets.clear(); - mAccessibilityQsTargets.clear(); + mShortcutTargets.forEach((type, targets) -> targets.clear()); mA11yTilesInQsPanel.clear(); mTargetAssignedToAccessibilityButton = null; mIsTouchExplorationEnabled = false; @@ -541,7 +539,7 @@ class AccessibilityUserState { private void dumpShortcutTargets( PrintWriter pw, @UserShortcutType int shortcutType, String name) { pw.append(" ").append(name).append(":{"); - ArraySet<String> targets = getShortcutTargetsInternalLocked(shortcutType); + ArraySet<String> targets = getShortcutTargetsLocked(shortcutType); int size = targets.size(); for (int i = 0; i < size; i++) { if (i > 0) { @@ -712,7 +710,7 @@ class AccessibilityUserState { */ public boolean isShortcutMagnificationEnabledLocked() { for (int shortcutType : ShortcutConstants.USER_SHORTCUT_TYPES) { - if (getShortcutTargetsInternalLocked(shortcutType) + if (getShortcutTargetsLocked(shortcutType) .contains(MAGNIFICATION_CONTROLLER_NAME)) { return true; } @@ -788,43 +786,29 @@ class AccessibilityUserState { } /** - * Disable both shortcuts' magnification function. - */ - public void disableShortcutMagnificationLocked() { - mAccessibilityShortcutKeyTargets.remove(MAGNIFICATION_CONTROLLER_NAME); - mAccessibilityButtonTargets.remove(MAGNIFICATION_CONTROLLER_NAME); - } - - /** * Returns a set which contains the flattened component names and the system class names - * assigned to the given shortcut. The set is a defensive copy. To apply any changes to the set, - * use {@link #updateShortcutTargetsLocked(Set, int)} + * assigned to the given shortcut. <strong>The set is a defensive copy.</strong> + * To apply any changes to the set, use {@link #updateShortcutTargetsLocked(Set, int)} * - * @param shortcutType The shortcut type. + * @param shortcutTypes The shortcut type or types (in bitmask format). * @return The array set of the strings */ - public ArraySet<String> getShortcutTargetsLocked(@UserShortcutType int shortcutType) { - return new ArraySet<>(getShortcutTargetsInternalLocked(shortcutType)); - } - - private ArraySet<String> getShortcutTargetsInternalLocked(@UserShortcutType int shortcutType) { - if (shortcutType == HARDWARE) { - return mAccessibilityShortcutKeyTargets; - } else if (shortcutType == SOFTWARE) { - return mAccessibilityButtonTargets; - } else if (shortcutType == GESTURE) { - return mAccessibilityGestureTargets; - } else if (shortcutType == QUICK_SETTINGS) { - return mAccessibilityQsTargets; - } else if ((shortcutType == TRIPLETAP - && isMagnificationSingleFingerTripleTapEnabledLocked()) || ( - shortcutType == TWOFINGER_DOUBLETAP - && isMagnificationTwoFingerTripleTapEnabledLocked())) { - ArraySet<String> targets = new ArraySet<>(); - targets.add(MAGNIFICATION_CONTROLLER_NAME); - return targets; + public ArraySet<String> getShortcutTargetsLocked(int shortcutTypes) { + ArraySet<String> targets = new ArraySet<>(); + for (int shortcutType : ShortcutConstants.USER_SHORTCUT_TYPES) { + if ((shortcutTypes & shortcutType) != shortcutType) { + continue; + } + if ((shortcutType == TRIPLETAP + && isMagnificationSingleFingerTripleTapEnabledLocked()) || ( + shortcutType == TWOFINGER_DOUBLETAP + && isMagnificationTwoFingerTripleTapEnabledLocked())) { + targets.add(MAGNIFICATION_CONTROLLER_NAME); + } else if (mShortcutTargets.containsKey(shortcutType)) { + targets.addAll(mShortcutTargets.get(shortcutType)); + } } - return new ArraySet<>(); + return targets; } /** @@ -843,8 +827,10 @@ class AccessibilityUserState { if ((shortcutType & mask) != 0) { throw new IllegalArgumentException("Tap shortcuts cannot be updated with target sets."); } - - final Set<String> currentTargets = getShortcutTargetsInternalLocked(shortcutType); + if (!mShortcutTargets.containsKey(shortcutType)) { + mShortcutTargets.put(shortcutType, new ArraySet<>()); + } + ArraySet<String> currentTargets = mShortcutTargets.get(shortcutType); if (newTargets.equals(currentTargets)) { return false; } @@ -904,7 +890,7 @@ class AccessibilityUserState { } // getting internal set lets us directly modify targets, as it's not a copy. - Set<String> targets = getShortcutTargetsInternalLocked(shortcutType); + Set<String> targets = mShortcutTargets.get(shortcutType); return targets.removeIf(name -> { ComponentName componentName; if (name == null @@ -1169,13 +1155,6 @@ class AccessibilityUserState { ); } - /** - * Returns a copy of the targets which has qs shortcut turned on - */ - public ArraySet<String> getA11yQsTargets() { - return new ArraySet<>(mAccessibilityQsTargets); - } - public void updateA11yTilesInQsPanelLocked(Set<ComponentName> componentNames) { mA11yTilesInQsPanel.clear(); mA11yTilesInQsPanel.addAll(componentNames); diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java index 89f14b09d397..268e56487c4b 100644 --- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java +++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java @@ -86,7 +86,6 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { private final Context mContext; private final Map<String, Object> mLocks = new WeakHashMap<>(); - public AppFunctionManagerServiceImpl(@NonNull Context context) { this( context, @@ -201,7 +200,7 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { if (mCallerValidator.isUserOrganizationManaged(targetUser)) { safeExecuteAppFunctionCallback.onResult( ExecuteAppFunctionResponse.newFailure( - ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR, + ExecuteAppFunctionResponse.RESULT_SYSTEM_ERROR, "Cannot run on a device with a device owner or from the managed" + " profile.", /* extras= */ null)); @@ -256,7 +255,7 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { if (serviceIntent == null) { safeExecuteAppFunctionCallback.onResult( ExecuteAppFunctionResponse.newFailure( - ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR, + ExecuteAppFunctionResponse.RESULT_SYSTEM_ERROR, "Cannot find the target service.", /* extras= */ null)); return; @@ -449,7 +448,7 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { Slog.e(TAG, "Failed to bind to the AppFunctionService"); safeExecuteAppFunctionCallback.onResult( ExecuteAppFunctionResponse.newFailure( - ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR, + ExecuteAppFunctionResponse.RESULT_SYSTEM_ERROR, "Failed to bind the AppFunctionService.", /* extras= */ null)); } @@ -464,7 +463,7 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { if (e instanceof CompletionException) { e = e.getCause(); } - int resultCode = ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR; + int resultCode = ExecuteAppFunctionResponse.RESULT_SYSTEM_ERROR; if (e instanceof AppSearchException appSearchException) { resultCode = mapAppSearchResultFailureCodeToExecuteAppFunctionResponse( @@ -486,13 +485,13 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { switch (resultCode) { case AppSearchResult.RESULT_NOT_FOUND: - return ExecuteAppFunctionResponse.RESULT_INVALID_ARGUMENT; + return ExecuteAppFunctionResponse.RESULT_FUNCTION_NOT_FOUND; case AppSearchResult.RESULT_INVALID_ARGUMENT: case AppSearchResult.RESULT_INTERNAL_ERROR: case AppSearchResult.RESULT_SECURITY_ERROR: // fall-through } - return ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR; + return ExecuteAppFunctionResponse.RESULT_SYSTEM_ERROR; } private void registerAppSearchObserver(@NonNull TargetUser user) { @@ -543,12 +542,13 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { }); } } + /** * Retrieves the lock object associated with the given package name. * - * This method returns the lock object from the {@code mLocks} map if it exists. - * If no lock is found for the given package name, a new lock object is created, - * stored in the map, and returned. + * <p>This method returns the lock object from the {@code mLocks} map if it exists. If no lock + * is found for the given package name, a new lock object is created, stored in the map, and + * returned. */ @VisibleForTesting @NonNull diff --git a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java index f9abd8558ca8..68ff9725ce5c 100644 --- a/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java +++ b/services/appwidget/java/com/android/server/appwidget/AppWidgetServiceImpl.java @@ -1469,9 +1469,7 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku mSecurityPolicy.enforceCallFromPackage(callingPackage); // Check that if a cross-profile binding is attempted, it is allowed. - // Cross-profile binding is also allowed if the caller has interact across users permission. - if (!mSecurityPolicy.isEnabledGroupProfile(providerProfileId) - && !mSecurityPolicy.hasCallerInteractAcrossUsersPermission()) { + if (!mSecurityPolicy.isEnabledGroupProfile(providerProfileId)) { return false; } @@ -2440,10 +2438,8 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku Slog.i(TAG, "getInstalledProvidersForProfiles() " + userId); } - // Ensure the profile is in the group and enabled, or that the caller has permission to - // interact across users. - if (!mSecurityPolicy.isEnabledGroupProfile(profileId) - && !mSecurityPolicy.hasCallerInteractAcrossUsersPermission()) { + // Ensure the profile is in the group and enabled. + if (!mSecurityPolicy.isEnabledGroupProfile(profileId)) { return null; } @@ -5235,11 +5231,14 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku return true; } final int userId = UserHandle.getUserId(uid); - if ((widget.host.getUserId() == userId || (widget.provider != null - && widget.provider.getUserId() == userId)) + if ((widget.host.getUserId() == userId + || (widget.provider != null && widget.provider.getUserId() == userId) + || hasCallerInteractAcrossUsersPermission()) && callerHasPermission(android.Manifest.permission.BIND_APPWIDGET)) { - // Apps that run in the same user as either the host or the provider and - // have the bind widget permission have access to the widget. + // Access to the widget requires the app to: + // - Run in the same user as the host or provider, or have permission to interact + // across users + // - Have bind widget permission return true; } if (DEBUG) { @@ -5260,16 +5259,12 @@ class AppWidgetServiceImpl extends IAppWidgetService.Stub implements WidgetBacku * The provider is accessible by the caller if any of the following is true: * - The provider belongs to the caller * - The provider belongs to a profile of the caller and is allowlisted - * - The caller has permission to interact across users */ public boolean canAccessProvider(String packageName, int profileId) { final int callerId = UserHandle.getCallingUserId(); if (profileId == callerId) { return true; } - if (hasCallerInteractAcrossUsersPermission()) { - return true; - } final int parentId = getProfileParent(profileId); if (parentId != callerId) { return false; diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java index c9f892907b59..b52c65054e51 100644 --- a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java +++ b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java @@ -92,6 +92,7 @@ import com.android.server.autofill.ui.AutoFillUI; import com.android.server.contentcapture.ContentCaptureManagerInternal; import com.android.server.infra.AbstractPerUserSystemService; import com.android.server.inputmethod.InputMethodManagerInternal; +import com.android.server.pm.UserManagerInternal; import com.android.server.wm.ActivityTaskManagerInternal; import java.io.PrintWriter; @@ -192,6 +193,8 @@ final class AutofillManagerServiceImpl private final ContentCaptureManagerInternal mContentCaptureManagerInternal; + private final UserManagerInternal mUserManagerInternal; + private final DisabledInfoCache mDisabledInfoCache; AutofillManagerServiceImpl(AutofillManagerService master, Object lock, @@ -208,6 +211,7 @@ final class AutofillManagerServiceImpl mInputMethodManagerInternal = LocalServices.getService(InputMethodManagerInternal.class); mContentCaptureManagerInternal = LocalServices.getService( ContentCaptureManagerInternal.class); + mUserManagerInternal = LocalServices.getService(UserManagerInternal.class); mDisabledInfoCache = disableCache; updateLocked(disabled); } @@ -379,6 +383,13 @@ final class AutofillManagerServiceImpl return 0; } + // TODO(b/376482880): remove this check once autofill service supports visible + // background users. + if (mUserManagerInternal.isVisibleBackgroundFullUser(mUserId)) { + Slog.d(TAG, "Currently, autofill service does not support visible background users."); + return 0; + } + if (!forAugmentedAutofillOnly && isAutofillDisabledLocked(clientActivity)) { // Standard autofill is enabled, but service disabled autofill for this activity; that // means no session, unless the activity is allowlisted for augmented autofill diff --git a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java index a960015ecba6..51034d24df14 100644 --- a/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java +++ b/services/companion/java/com/android/server/companion/CompanionDeviceManagerService.java @@ -31,13 +31,16 @@ import static android.os.UserHandle.getCallingUserId; import static com.android.internal.util.CollectionUtils.any; import static com.android.internal.util.Preconditions.checkState; +import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; import static com.android.server.companion.utils.PackageUtils.enforceUsesCompanionDeviceFeature; +import static com.android.server.companion.utils.PackageUtils.getPackageInfo; import static com.android.server.companion.utils.PackageUtils.isRestrictedSettingsAllowed; import static com.android.server.companion.utils.PermissionsUtils.enforceCallerCanManageAssociationsForPackage; import static com.android.server.companion.utils.PermissionsUtils.enforceCallerIsSystemOr; import static com.android.server.companion.utils.PermissionsUtils.enforceCallerIsSystemOrCanInteractWithUserId; import static java.util.Objects.requireNonNull; +import static java.util.concurrent.TimeUnit.MINUTES; import android.annotation.EnforcePermission; import android.annotation.NonNull; @@ -66,22 +69,31 @@ import android.companion.datatransfer.PermissionSyncRequest; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; import android.net.MacAddress; +import android.net.NetworkPolicyManager; import android.os.Binder; +import android.os.Environment; import android.os.Parcel; import android.os.ParcelFileDescriptor; import android.os.PowerExemptionManager; import android.os.PowerManagerInternal; import android.os.RemoteException; +import android.os.ServiceManager; import android.os.UserHandle; import android.os.UserManager; import android.permission.flags.Flags; +import android.util.ArraySet; import android.util.ExceptionUtils; import android.util.Slog; +import com.android.internal.app.IAppOpsService; import com.android.internal.content.PackageMonitor; import com.android.internal.notification.NotificationAccessConfirmationActivityContract; +import com.android.internal.os.BackgroundThread; import com.android.internal.util.ArrayUtils; import com.android.internal.util.DumpUtils; import com.android.server.FgThread; @@ -102,27 +114,35 @@ import com.android.server.companion.devicepresence.DevicePresenceProcessor; import com.android.server.companion.devicepresence.ObservableUuid; import com.android.server.companion.devicepresence.ObservableUuidStore; import com.android.server.companion.transport.CompanionTransportManager; +import com.android.server.pm.UserManagerInternal; import com.android.server.wm.ActivityTaskManagerInternal; +import java.io.File; import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.Collection; import java.util.List; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; +import java.util.Set; @SuppressLint("LongLogTag") public class CompanionDeviceManagerService extends SystemService { private static final String TAG = "CDM_CompanionDeviceManagerService"; private static final long PAIR_WITHOUT_PROMPT_WINDOW_MS = 10 * 60 * 1000; // 10 min + + private static final String PREF_FILE_NAME = "companion_device_preferences.xml"; + private static final String PREF_KEY_AUTO_REVOKE_GRANTS_DONE = "auto_revoke_grants_done"; private static final int MAX_CN_LENGTH = 500; + private final ActivityTaskManagerInternal mAtmInternal; + private final ActivityManagerInternal mAmInternal; + private final IAppOpsService mAppOpsManager; + private final PowerExemptionManager mPowerExemptionManager; + private final PackageManagerInternal mPackageManagerInternal; + private final AssociationStore mAssociationStore; private final SystemDataTransferRequestStore mSystemDataTransferRequestStore; private final ObservableUuidStore mObservableUuidStore; - - private final CompanionExemptionProcessor mCompanionExemptionProcessor; private final AssociationRequestsProcessor mAssociationRequestsProcessor; private final SystemDataTransferProcessor mSystemDataTransferProcessor; private final BackupRestoreProcessor mBackupRestoreProcessor; @@ -136,15 +156,12 @@ public class CompanionDeviceManagerService extends SystemService { super(context); final ActivityManager activityManager = context.getSystemService(ActivityManager.class); - final PowerExemptionManager powerExemptionManager = context.getSystemService( - PowerExemptionManager.class); - final AppOpsManager appOpsManager = context.getSystemService(AppOpsManager.class); - final ActivityTaskManagerInternal atmInternal = LocalServices.getService( - ActivityTaskManagerInternal.class); - final ActivityManagerInternal amInternal = LocalServices.getService( - ActivityManagerInternal.class); - final PackageManagerInternal packageManagerInternal = LocalServices.getService( - PackageManagerInternal.class); + mPowerExemptionManager = context.getSystemService(PowerExemptionManager.class); + mAppOpsManager = IAppOpsService.Stub.asInterface( + ServiceManager.getService(Context.APP_OPS_SERVICE)); + mAtmInternal = LocalServices.getService(ActivityTaskManagerInternal.class); + mAmInternal = LocalServices.getService(ActivityManagerInternal.class); + mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class); final UserManager userManager = context.getSystemService(UserManager.class); final PowerManagerInternal powerManagerInternal = LocalServices.getService( PowerManagerInternal.class); @@ -156,29 +173,25 @@ public class CompanionDeviceManagerService extends SystemService { // Init processors mAssociationRequestsProcessor = new AssociationRequestsProcessor(context, - packageManagerInternal, mAssociationStore); - mBackupRestoreProcessor = new BackupRestoreProcessor(context, packageManagerInternal, + mPackageManagerInternal, mAssociationStore); + mBackupRestoreProcessor = new BackupRestoreProcessor(context, mPackageManagerInternal, mAssociationStore, associationDiskStore, mSystemDataTransferRequestStore, mAssociationRequestsProcessor); mCompanionAppBinder = new CompanionAppBinder(context); - mCompanionExemptionProcessor = new CompanionExemptionProcessor(context, - powerExemptionManager, appOpsManager, packageManagerInternal, atmInternal, - amInternal, mAssociationStore); - mDevicePresenceProcessor = new DevicePresenceProcessor(context, mCompanionAppBinder, userManager, mAssociationStore, mObservableUuidStore, - powerManagerInternal, mCompanionExemptionProcessor); + powerManagerInternal); mTransportManager = new CompanionTransportManager(context, mAssociationStore); mDisassociationProcessor = new DisassociationProcessor(context, activityManager, - mAssociationStore, packageManagerInternal, mDevicePresenceProcessor, + mAssociationStore, mPackageManagerInternal, mDevicePresenceProcessor, mCompanionAppBinder, mSystemDataTransferRequestStore, mTransportManager); mSystemDataTransferProcessor = new SystemDataTransferProcessor(this, - packageManagerInternal, mAssociationStore, + mPackageManagerInternal, mAssociationStore, mSystemDataTransferRequestStore, mTransportManager); // TODO(b/279663946): move context sync to a dedicated system service @@ -189,6 +202,7 @@ public class CompanionDeviceManagerService extends SystemService { public void onStart() { // Init association stores mAssociationStore.refreshCache(); + mAssociationStore.registerLocalListener(mAssociationStoreChangeListener); // Init UUID store mObservableUuidStore.getObservableUuidsForUser(getContext().getUserId()); @@ -226,11 +240,11 @@ public class CompanionDeviceManagerService extends SystemService { if (associations.isEmpty()) return; - mCompanionExemptionProcessor.updateAtm(userId, associations); + updateAtm(userId, associations); - try (ExecutorService executor = Executors.newSingleThreadExecutor()) { - executor.execute(mCompanionExemptionProcessor::updateAutoRevokeExemptions); - } + BackgroundThread.getHandler().sendMessageDelayed( + obtainMessage(CompanionDeviceManagerService::maybeGrantAutoRevokeExemptions, this), + MINUTES.toMillis(10)); } @Override @@ -248,12 +262,9 @@ public class CompanionDeviceManagerService extends SystemService { if (!associationsForPackage.isEmpty()) { Slog.i(TAG, "Package removed or data cleared for user=[" + userId + "], package=[" + packageName + "]. Cleaning up CDM data..."); - - for (AssociationInfo association : associationsForPackage) { - mDisassociationProcessor.disassociate(association.getId()); - } - - mCompanionAppBinder.onPackagesChanged(userId, packageName); + } + for (AssociationInfo association : associationsForPackage) { + mDisassociationProcessor.disassociate(association.getId()); } // Clear observable UUIDs for the package. @@ -262,16 +273,19 @@ public class CompanionDeviceManagerService extends SystemService { for (ObservableUuid uuid : uuidsTobeObserved) { mObservableUuidStore.removeObservableUuid(userId, uuid.getUuid(), packageName); } + + mCompanionAppBinder.onPackagesChanged(userId); } private void onPackageModifiedInternal(@UserIdInt int userId, @NonNull String packageName) { - final List<AssociationInfo> associations = + final List<AssociationInfo> associationsForPackage = mAssociationStore.getAssociationsByPackage(userId, packageName); - if (!associations.isEmpty()) { - mCompanionExemptionProcessor.exemptPackage(userId, packageName, false); - - mCompanionAppBinder.onPackagesChanged(userId, packageName); + for (AssociationInfo association : associationsForPackage) { + updateSpecialAccessPermissionForAssociatedPackage(association.getUserId(), + association.getPackageName()); } + + mCompanionAppBinder.onPackagesChanged(userId); } private void onPackageAddedInternal(@UserIdInt int userId, @NonNull String packageName) { @@ -751,6 +765,130 @@ public class CompanionDeviceManagerService extends SystemService { } } + /** + * Update special access for the association's package + */ + public void updateSpecialAccessPermissionForAssociatedPackage(int userId, String packageName) { + final PackageInfo packageInfo = + getPackageInfo(getContext(), userId, packageName); + + Binder.withCleanCallingIdentity(() -> updateSpecialAccessPermissionAsSystem(packageInfo)); + } + + private void updateSpecialAccessPermissionAsSystem(PackageInfo packageInfo) { + if (packageInfo == null) { + return; + } + + if (containsEither(packageInfo.requestedPermissions, + android.Manifest.permission.RUN_IN_BACKGROUND, + android.Manifest.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND)) { + mPowerExemptionManager.addToPermanentAllowList(packageInfo.packageName); + } else { + try { + mPowerExemptionManager.removeFromPermanentAllowList(packageInfo.packageName); + } catch (UnsupportedOperationException e) { + Slog.w(TAG, packageInfo.packageName + " can't be removed from power save" + + " whitelist. It might due to the package is whitelisted by the system."); + } + } + + NetworkPolicyManager networkPolicyManager = NetworkPolicyManager.from(getContext()); + try { + if (containsEither(packageInfo.requestedPermissions, + android.Manifest.permission.USE_DATA_IN_BACKGROUND, + android.Manifest.permission.REQUEST_COMPANION_USE_DATA_IN_BACKGROUND)) { + networkPolicyManager.addUidPolicy( + packageInfo.applicationInfo.uid, + NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND); + } else { + networkPolicyManager.removeUidPolicy( + packageInfo.applicationInfo.uid, + NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND); + } + } catch (IllegalArgumentException e) { + Slog.e(TAG, e.getMessage()); + } + + exemptFromAutoRevoke(packageInfo.packageName, packageInfo.applicationInfo.uid); + } + + private void exemptFromAutoRevoke(String packageName, int uid) { + try { + mAppOpsManager.setMode( + AppOpsManager.OP_AUTO_REVOKE_PERMISSIONS_IF_UNUSED, + uid, + packageName, + AppOpsManager.MODE_IGNORED); + } catch (RemoteException e) { + Slog.w(TAG, "Error while granting auto revoke exemption for " + packageName, e); + } + } + + private void updateAtm(int userId, List<AssociationInfo> associations) { + final Set<Integer> companionAppUids = new ArraySet<>(); + for (AssociationInfo association : associations) { + final int uid = mPackageManagerInternal.getPackageUid(association.getPackageName(), + 0, userId); + if (uid >= 0) { + companionAppUids.add(uid); + } + } + if (mAtmInternal != null) { + mAtmInternal.setCompanionAppUids(userId, companionAppUids); + } + if (mAmInternal != null) { + // Make a copy of the set and send it to ActivityManager. + mAmInternal.setCompanionAppUids(userId, new ArraySet<>(companionAppUids)); + } + } + + private void maybeGrantAutoRevokeExemptions() { + Slog.d(TAG, "maybeGrantAutoRevokeExemptions()"); + + PackageManager pm = getContext().getPackageManager(); + for (int userId : LocalServices.getService(UserManagerInternal.class).getUserIds()) { + SharedPreferences pref = getContext().getSharedPreferences( + new File(Environment.getUserSystemDirectory(userId), PREF_FILE_NAME), + Context.MODE_PRIVATE); + if (pref.getBoolean(PREF_KEY_AUTO_REVOKE_GRANTS_DONE, false)) { + continue; + } + + try { + final List<AssociationInfo> associations = + mAssociationStore.getActiveAssociationsByUser(userId); + for (AssociationInfo a : associations) { + try { + int uid = pm.getPackageUidAsUser(a.getPackageName(), userId); + exemptFromAutoRevoke(a.getPackageName(), uid); + } catch (PackageManager.NameNotFoundException e) { + Slog.w(TAG, "Unknown companion package: " + a.getPackageName(), e); + } + } + } finally { + pref.edit().putBoolean(PREF_KEY_AUTO_REVOKE_GRANTS_DONE, true).apply(); + } + } + } + + private final AssociationStore.OnChangeListener mAssociationStoreChangeListener = + new AssociationStore.OnChangeListener() { + @Override + public void onAssociationChanged(int changeType, AssociationInfo association) { + Slog.d(TAG, "onAssociationChanged changeType=[" + changeType + + "], association=[" + association); + + final int userId = association.getUserId(); + final List<AssociationInfo> updatedAssociations = + mAssociationStore.getActiveAssociationsByUser(userId); + + updateAtm(userId, updatedAssociations); + updateSpecialAccessPermissionForAssociatedPackage(association.getUserId(), + association.getPackageName()); + } + }; + private final PackageMonitor mPackageMonitor = new PackageMonitor() { @Override public void onPackageRemoved(String packageName, int uid) { @@ -773,6 +911,10 @@ public class CompanionDeviceManagerService extends SystemService { } }; + private static <T> boolean containsEither(T[] array, T a, T b) { + return ArrayUtils.contains(array, a) || ArrayUtils.contains(array, b); + } + private class LocalService implements CompanionDeviceManagerServiceInternal { @Override diff --git a/services/companion/java/com/android/server/companion/CompanionExemptionProcessor.java b/services/companion/java/com/android/server/companion/CompanionExemptionProcessor.java deleted file mode 100644 index 4969ffbfa08a..000000000000 --- a/services/companion/java/com/android/server/companion/CompanionExemptionProcessor.java +++ /dev/null @@ -1,219 +0,0 @@ -/* - * Copyright (C) 2024 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.server.companion; - -import static android.app.AppOpsManager.MODE_ALLOWED; -import static android.app.AppOpsManager.MODE_IGNORED; - -import static com.android.server.companion.utils.PackageUtils.getPackageInfo; - -import android.annotation.SuppressLint; -import android.app.ActivityManagerInternal; -import android.app.AppOpsManager; -import android.companion.AssociationInfo; -import android.content.Context; -import android.content.SharedPreferences; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; -import android.content.pm.PackageManagerInternal; -import android.net.NetworkPolicyManager; -import android.os.Binder; -import android.os.Environment; -import android.os.PowerExemptionManager; -import android.util.ArraySet; -import android.util.Slog; - -import com.android.internal.util.ArrayUtils; -import com.android.server.LocalServices; -import com.android.server.companion.association.AssociationStore; -import com.android.server.pm.UserManagerInternal; -import com.android.server.wm.ActivityTaskManagerInternal; - -import java.io.File; -import java.util.List; -import java.util.Set; - -@SuppressLint("LongLogTag") -public class CompanionExemptionProcessor { - - private static final String TAG = "CDM_CompanionExemptionProcessor"; - - private static final String PREF_FILE_NAME = "companion_device_preferences.xml"; - private static final String PREF_KEY_AUTO_REVOKE_GRANTS_DONE = "auto_revoke_grants_done"; - - private final Context mContext; - private final PowerExemptionManager mPowerExemptionManager; - private final AppOpsManager mAppOpsManager; - private final PackageManagerInternal mPackageManager; - private final ActivityTaskManagerInternal mAtmInternal; - private final ActivityManagerInternal mAmInternal; - private final AssociationStore mAssociationStore; - - public CompanionExemptionProcessor(Context context, PowerExemptionManager powerExemptionManager, - AppOpsManager appOpsManager, PackageManagerInternal packageManager, - ActivityTaskManagerInternal atmInternal, ActivityManagerInternal amInternal, - AssociationStore associationStore) { - mContext = context; - mPowerExemptionManager = powerExemptionManager; - mAppOpsManager = appOpsManager; - mPackageManager = packageManager; - mAtmInternal = atmInternal; - mAmInternal = amInternal; - mAssociationStore = associationStore; - - mAssociationStore.registerLocalListener(new AssociationStore.OnChangeListener() { - @Override - public void onAssociationChanged(int changeType, AssociationInfo association) { - final int userId = association.getUserId(); - final List<AssociationInfo> updatedAssociations = - mAssociationStore.getActiveAssociationsByUser(userId); - - updateAtm(userId, updatedAssociations); - } - }); - } - - /** - * Update ActivityManager and ActivityTaskManager exemptions - */ - public void updateAtm(int userId, List<AssociationInfo> associations) { - final Set<Integer> companionAppUids = new ArraySet<>(); - for (AssociationInfo association : associations) { - int uid = mPackageManager.getPackageUid(association.getPackageName(), 0, userId); - if (uid >= 0) { - companionAppUids.add(uid); - } - } - if (mAtmInternal != null) { - mAtmInternal.setCompanionAppUids(userId, companionAppUids); - } - if (mAmInternal != null) { - // Make a copy of the set and send it to ActivityManager. - mAmInternal.setCompanionAppUids(userId, new ArraySet<>(companionAppUids)); - } - } - - /** - * Update special access for the association's package - */ - public void exemptPackage(int userId, String packageName, boolean hasPresentDevices) { - Slog.i(TAG, "Exempting package [" + packageName + "]..."); - - final PackageInfo packageInfo = getPackageInfo(mContext, userId, packageName); - - Binder.withCleanCallingIdentity( - () -> exemptPackageAsSystem(userId, packageInfo, hasPresentDevices)); - } - - @SuppressLint("MissingPermission") - private void exemptPackageAsSystem(int userId, PackageInfo packageInfo, - boolean hasPresentDevices) { - if (packageInfo == null) { - return; - } - - // If the app has run-in-bg permission and present devices, add it to power saver allowlist. - if (containsEither(packageInfo.requestedPermissions, - android.Manifest.permission.RUN_IN_BACKGROUND, - android.Manifest.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND) - && hasPresentDevices) { - mPowerExemptionManager.addToPermanentAllowList(packageInfo.packageName); - } else { - try { - mPowerExemptionManager.removeFromPermanentAllowList(packageInfo.packageName); - } catch (UnsupportedOperationException e) { - Slog.w(TAG, packageInfo.packageName + " can't be removed from power save" - + " allowlist. It might be due to the package being allowlisted by the" - + " system."); - } - } - - // If the app has run-in-bg permission and present device, allow metered network use. - NetworkPolicyManager networkPolicyManager = NetworkPolicyManager.from(mContext); - try { - if (containsEither(packageInfo.requestedPermissions, - android.Manifest.permission.USE_DATA_IN_BACKGROUND, - android.Manifest.permission.REQUEST_COMPANION_USE_DATA_IN_BACKGROUND) - && hasPresentDevices) { - networkPolicyManager.addUidPolicy( - packageInfo.applicationInfo.uid, - NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND); - } else { - networkPolicyManager.removeUidPolicy( - packageInfo.applicationInfo.uid, - NetworkPolicyManager.POLICY_ALLOW_METERED_BACKGROUND); - } - } catch (IllegalArgumentException e) { - Slog.e(TAG, e.getMessage()); - } - - updateAutoRevokeExemption(packageInfo.packageName, packageInfo.applicationInfo.uid, - !mAssociationStore.getActiveAssociationsByPackage(userId, - packageInfo.packageName).isEmpty()); - } - - /** - * Update auto revoke exemptions. - * If the app has any association, exempt it from permission auto revoke. - */ - public void updateAutoRevokeExemptions() { - Slog.d(TAG, "maybeGrantAutoRevokeExemptions()"); - - PackageManager pm = mContext.getPackageManager(); - for (int userId : LocalServices.getService(UserManagerInternal.class).getUserIds()) { - SharedPreferences pref = mContext.getSharedPreferences( - new File(Environment.getUserSystemDirectory(userId), PREF_FILE_NAME), - Context.MODE_PRIVATE); - if (pref.getBoolean(PREF_KEY_AUTO_REVOKE_GRANTS_DONE, false)) { - continue; - } - - try { - final List<AssociationInfo> associations = - mAssociationStore.getActiveAssociationsByUser(userId); - for (AssociationInfo a : associations) { - try { - int uid = pm.getPackageUidAsUser(a.getPackageName(), userId); - updateAutoRevokeExemption(a.getPackageName(), uid, true); - } catch (PackageManager.NameNotFoundException e) { - Slog.w(TAG, "Unknown companion package: " + a.getPackageName(), e); - } - } - } finally { - pref.edit().putBoolean(PREF_KEY_AUTO_REVOKE_GRANTS_DONE, true).apply(); - } - } - } - - @SuppressLint("MissingPermission") - private void updateAutoRevokeExemption(String packageName, int uid, boolean hasAssociations) { - try { - mAppOpsManager.setMode( - AppOpsManager.OP_AUTO_REVOKE_PERMISSIONS_IF_UNUSED, - uid, - packageName, - hasAssociations ? MODE_IGNORED : MODE_ALLOWED); - } catch (Exception e) { - Slog.e(TAG, "Error while granting auto revoke exemption for " + packageName, e); - } - } - - private <T> boolean containsEither(T[] array, T a, T b) { - return ArrayUtils.contains(array, a) || ArrayUtils.contains(array, b); - } - -} diff --git a/services/companion/java/com/android/server/companion/devicepresence/CompanionAppBinder.java b/services/companion/java/com/android/server/companion/devicepresence/CompanionAppBinder.java index 1eb6d13feeac..60f46887fa5c 100644 --- a/services/companion/java/com/android/server/companion/devicepresence/CompanionAppBinder.java +++ b/services/companion/java/com/android/server/companion/devicepresence/CompanionAppBinder.java @@ -95,10 +95,7 @@ public class CompanionAppBinder { /** * On package changed. */ - public void onPackagesChanged(@UserIdInt int userId, String packageName) { - // TODO: We shouldn't need to clean up the whole user registry. We only need to remove the - // package. I will do it in a separate change since it's not appropriate to use - // PerUser anymore. + public void onPackagesChanged(@UserIdInt int userId) { mCompanionServicesRegister.invalidate(userId); } diff --git a/services/companion/java/com/android/server/companion/devicepresence/DevicePresenceProcessor.java b/services/companion/java/com/android/server/companion/devicepresence/DevicePresenceProcessor.java index 7b4dd7df8be3..a374d279af0b 100644 --- a/services/companion/java/com/android/server/companion/devicepresence/DevicePresenceProcessor.java +++ b/services/companion/java/com/android/server/companion/devicepresence/DevicePresenceProcessor.java @@ -57,7 +57,6 @@ import android.util.SparseBooleanArray; import com.android.internal.annotations.GuardedBy; import com.android.internal.util.CollectionUtils; -import com.android.server.companion.CompanionExemptionProcessor; import com.android.server.companion.association.AssociationStore; import java.io.PrintWriter; @@ -102,8 +101,6 @@ public class DevicePresenceProcessor implements AssociationStore.OnChangeListene private final PowerManagerInternal mPowerManagerInternal; @NonNull private final UserManager mUserManager; - @NonNull - private final CompanionExemptionProcessor mCompanionExemptionProcessor; // NOTE: Same association may appear in more than one of the following sets at the same time. // (E.g. self-managed devices that have MAC addresses, could be reported as present by their @@ -114,7 +111,7 @@ public class DevicePresenceProcessor implements AssociationStore.OnChangeListene @NonNull private final Set<Integer> mNearbyBleDevices = new HashSet<>(); @NonNull - private final Set<Integer> mConnectedSelfManagedDevices = new HashSet<>(); + private final Set<Integer> mReportedSelfManagedDevices = new HashSet<>(); @NonNull private final Set<ParcelUuid> mConnectedUuidDevices = new HashSet<>(); @NonNull @@ -149,8 +146,7 @@ public class DevicePresenceProcessor implements AssociationStore.OnChangeListene @NonNull UserManager userManager, @NonNull AssociationStore associationStore, @NonNull ObservableUuidStore observableUuidStore, - @NonNull PowerManagerInternal powerManagerInternal, - @NonNull CompanionExemptionProcessor companionExemptionProcessor) { + @NonNull PowerManagerInternal powerManagerInternal) { mContext = context; mCompanionAppBinder = companionAppBinder; mAssociationStore = associationStore; @@ -160,7 +156,6 @@ public class DevicePresenceProcessor implements AssociationStore.OnChangeListene mObservableUuidStore, this); mBleDeviceProcessor = new BleDeviceProcessor(associationStore, this); mPowerManagerInternal = powerManagerInternal; - mCompanionExemptionProcessor = companionExemptionProcessor; } /** Initialize {@link DevicePresenceProcessor} */ @@ -409,7 +404,7 @@ public class DevicePresenceProcessor implements AssociationStore.OnChangeListene * nearby (for "self-managed" associations). */ public boolean isDevicePresent(int associationId) { - return mConnectedSelfManagedDevices.contains(associationId) + return mReportedSelfManagedDevices.contains(associationId) || mConnectedBtDevices.contains(associationId) || mNearbyBleDevices.contains(associationId) || mSimulated.contains(associationId); @@ -456,7 +451,7 @@ public class DevicePresenceProcessor implements AssociationStore.OnChangeListene * notifyDeviceAppeared()} */ public void onSelfManagedDeviceConnected(int associationId) { - onDevicePresenceEvent(mConnectedSelfManagedDevices, + onDevicePresenceEvent(mReportedSelfManagedDevices, associationId, EVENT_SELF_MANAGED_APPEARED); } @@ -472,7 +467,7 @@ public class DevicePresenceProcessor implements AssociationStore.OnChangeListene * notifyDeviceDisappeared()} */ public void onSelfManagedDeviceDisconnected(int associationId) { - onDevicePresenceEvent(mConnectedSelfManagedDevices, + onDevicePresenceEvent(mReportedSelfManagedDevices, associationId, EVENT_SELF_MANAGED_DISAPPEARED); } @@ -480,7 +475,7 @@ public class DevicePresenceProcessor implements AssociationStore.OnChangeListene * Marks a "self-managed" device as disconnected when binderDied. */ public void onSelfManagedDeviceReporterBinderDied(int associationId) { - onDevicePresenceEvent(mConnectedSelfManagedDevices, + onDevicePresenceEvent(mReportedSelfManagedDevices, associationId, EVENT_SELF_MANAGED_DISAPPEARED); } @@ -688,7 +683,6 @@ public class DevicePresenceProcessor implements AssociationStore.OnChangeListene if (association.shouldBindWhenPresent()) { bindApplicationIfNeeded(userId, packageName, association.isSelfManaged()); - mCompanionExemptionProcessor.exemptPackage(userId, packageName, true); } else { return; } @@ -721,7 +715,6 @@ public class DevicePresenceProcessor implements AssociationStore.OnChangeListene // Check if there are other devices associated to the app that are present. if (!shouldBindPackage(userId, packageName)) { mCompanionAppBinder.unbindCompanionApp(userId, packageName); - mCompanionExemptionProcessor.exemptPackage(userId, packageName, false); } break; default: @@ -947,7 +940,7 @@ public class DevicePresenceProcessor implements AssociationStore.OnChangeListene mConnectedBtDevices.remove(id); mNearbyBleDevices.remove(id); - mConnectedSelfManagedDevices.remove(id); + mReportedSelfManagedDevices.remove(id); mSimulated.remove(id); synchronized (mBtDisconnectedDevices) { mBtDisconnectedDevices.remove(id); @@ -1107,7 +1100,7 @@ public class DevicePresenceProcessor implements AssociationStore.OnChangeListene out.append("Companion Device Present: "); if (mConnectedBtDevices.isEmpty() && mNearbyBleDevices.isEmpty() - && mConnectedSelfManagedDevices.isEmpty()) { + && mReportedSelfManagedDevices.isEmpty()) { out.append("<empty>\n"); return; } else { @@ -1137,11 +1130,11 @@ public class DevicePresenceProcessor implements AssociationStore.OnChangeListene } out.append(" Self-Reported Devices: "); - if (mConnectedSelfManagedDevices.isEmpty()) { + if (mReportedSelfManagedDevices.isEmpty()) { out.append("<empty>\n"); } else { out.append("\n"); - for (int associationId : mConnectedSelfManagedDevices) { + for (int associationId : mReportedSelfManagedDevices) { AssociationInfo a = mAssociationStore.getAssociationById(associationId); out.append(" ").append(a.toShortString()).append('\n'); } diff --git a/services/core/Android.bp b/services/core/Android.bp index 348d83fea81d..6cfd44bb2d1a 100644 --- a/services/core/Android.bp +++ b/services/core/Android.bp @@ -126,6 +126,7 @@ java_library_static { "platform_service_defaults", "android.hardware.power-java_shared", "latest_android_hardware_broadcastradio_java_static", + "services_crashrecovery_stubs_conditionally", ], srcs: [ ":android.hardware.tv.hdmi.connection-V1-java-source", diff --git a/services/core/java/com/android/server/TelephonyRegistry.java b/services/core/java/com/android/server/TelephonyRegistry.java index 363807d2aa8c..72a9a2d6de26 100644 --- a/services/core/java/com/android/server/TelephonyRegistry.java +++ b/services/core/java/com/android/server/TelephonyRegistry.java @@ -102,6 +102,7 @@ import com.android.internal.telephony.ICarrierConfigChangeListener; import com.android.internal.telephony.ICarrierPrivilegesCallback; import com.android.internal.telephony.IOnSubscriptionsChangedListener; import com.android.internal.telephony.IPhoneStateListener; +import com.android.internal.telephony.ISatelliteStateChangeListener; import com.android.internal.telephony.ITelephonyRegistry; import com.android.internal.telephony.TelephonyPermissions; import com.android.internal.telephony.flags.Flags; @@ -126,6 +127,7 @@ import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; /** @@ -164,6 +166,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { IOnSubscriptionsChangedListener onOpportunisticSubscriptionsChangedListenerCallback; ICarrierPrivilegesCallback carrierPrivilegesCallback; ICarrierConfigChangeListener carrierConfigChangeListener; + ISatelliteStateChangeListener satelliteStateChangeListener; int callerUid; int callerPid; @@ -196,6 +199,10 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { return carrierConfigChangeListener != null; } + boolean matchSatelliteStateChangeListener() { + return satelliteStateChangeListener != null; + } + boolean canReadCallLog() { try { return TelephonyPermissions.checkReadCallLog( @@ -215,6 +222,7 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { + onOpportunisticSubscriptionsChangedListenerCallback + " carrierPrivilegesCallback=" + carrierPrivilegesCallback + " carrierConfigChangeListener=" + carrierConfigChangeListener + + " satelliteStateChangeListener=" + satelliteStateChangeListener + " subId=" + subId + " phoneId=" + phoneId + " events=" + eventList + "}"; } } @@ -433,6 +441,10 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { private List<IntArray> mCarrierRoamingNtnAvailableServices; + // Local cache to check if Satellite Modem is enabled + private AtomicBoolean mIsSatelliteEnabled; + private AtomicBoolean mWasSatelliteEnabledNotified; + /** * Per-phone map of precise data connection state. The key of the map is the pair of transport * type and APN setting. This is the cache to prevent redundant callbacks to the listeners. @@ -871,6 +883,9 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { mCarrierRoamingNtnMode = new boolean[numPhones]; mCarrierRoamingNtnEligible = new boolean[numPhones]; mCarrierRoamingNtnAvailableServices = new ArrayList<>(); + mIsSatelliteEnabled = new AtomicBoolean(); + mWasSatelliteEnabledNotified = new AtomicBoolean(); + for (int i = 0; i < numPhones; i++) { mCallState[i] = TelephonyManager.CALL_STATE_IDLE; @@ -3425,6 +3440,94 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { } @Override + public void addSatelliteStateChangeListener(@NonNull ISatelliteStateChangeListener listener, + @NonNull String pkg, @Nullable String featureId) { + final int callerUserId = UserHandle.getCallingUserId(); + mAppOps.checkPackage(Binder.getCallingUid(), pkg); + enforceCallingOrSelfAtLeastReadBasicPhoneStatePermission(pkg, featureId, + "addSatelliteStateChangeListener"); + if (VDBG) { + log("addSatelliteStateChangeListener pkg=" + pii(pkg) + + " uid=" + Binder.getCallingUid() + + " myUserId=" + UserHandle.myUserId() + " callerUerId" + callerUserId + + " listener=" + listener + " listener.asBinder=" + listener.asBinder()); + } + + synchronized (mRecords) { + final IBinder b = listener.asBinder(); + boolean doesLimitApply = doesLimitApplyForListeners(Binder.getCallingUid(), + Process.myUid()); + Record r = add(b, Binder.getCallingUid(), Binder.getCallingPid(), doesLimitApply); + + if (r == null) { + loge("addSatelliteStateChangeListener: can not create Record instance!"); + return; + } + + r.context = mContext; + r.satelliteStateChangeListener = listener; + r.callingPackage = pkg; + r.callingFeatureId = featureId; + r.callerUid = Binder.getCallingUid(); + r.callerPid = Binder.getCallingPid(); + r.eventList = new ArraySet<>(); + if (DBG) { + log("addSatelliteStateChangeListener: Register r=" + r); + } + + // Always notify registrants on registration if it has been notified before + if (mWasSatelliteEnabledNotified.get() && r.matchSatelliteStateChangeListener()) { + try { + r.satelliteStateChangeListener.onSatelliteEnabledStateChanged( + mIsSatelliteEnabled.get()); + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + } + } + + @Override + public void removeSatelliteStateChangeListener(@NonNull ISatelliteStateChangeListener listener, + @NonNull String pkg) { + if (DBG) log("removeSatelliteStateChangeListener listener=" + listener + ", pkg=" + pkg); + mAppOps.checkPackage(Binder.getCallingUid(), pkg); + enforceCallingOrSelfAtLeastReadBasicPhoneStatePermission(pkg, null, + "removeSatelliteStateChangeListener"); + remove(listener.asBinder()); + } + + @Override + public void notifySatelliteStateChanged(boolean isEnabled) { + if (!checkNotifyPermission("notifySatelliteStateChanged")) { + loge("notifySatelliteStateChanged: Caller has no notify permission!"); + return; + } + if (VDBG) { + log("notifySatelliteStateChanged: isEnabled=" + isEnabled); + } + + mWasSatelliteEnabledNotified.set(true); + mIsSatelliteEnabled.set(isEnabled); + + synchronized (mRecords) { + mRemoveList.clear(); + for (Record r : mRecords) { + // Listeners are "global", neither per-slot nor per-sub, so no idMatch check here + if (!r.matchSatelliteStateChangeListener()) { + continue; + } + try { + r.satelliteStateChangeListener.onSatelliteEnabledStateChanged(isEnabled); + } catch (RemoteException re) { + mRemoveList.add(r.binder); + } + } + handleRemoveListLocked(); + } + } + + @Override public void notifyMediaQualityStatusChanged(int phoneId, int subId, MediaQualityStatus status) { if (!checkNotifyPermission("notifyMediaQualityStatusChanged()")) { return; @@ -4622,4 +4725,32 @@ public class TelephonyRegistry extends ITelephonyRegistry.Stub { if (packageNames.isEmpty() || Build.IS_DEBUGGABLE) return packageNames.toString(); return "[***, size=" + packageNames.size() + "]"; } + + /** + * The method enforces the calling package at least has READ_BASIC_PHONE_STATE permission. + * That is, calling package either has READ_PRIVILEGED_PHONE_STATE, READ_PHONE_STATE or Carrier + * Privileges on ANY active subscription, or has READ_BASIC_PHONE_STATE permission. + */ + private void enforceCallingOrSelfAtLeastReadBasicPhoneStatePermission(String pkgName, + String featureId, String message) { + // Check if calling app has READ_PHONE_STATE on ANY active subscription + boolean hasReadPhoneState = false; + SubscriptionManager sm = mContext.getSystemService(SubscriptionManager.class); + if (sm != null) { + for (int subId : sm.getActiveSubscriptionIdList()) { + if (TelephonyPermissions.checkCallingOrSelfReadPhoneStateNoThrow(mContext, subId, + pkgName, featureId, message)) { + hasReadPhoneState = true; + break; + } + } + } + + // If yes, pass. If not, then enforce READ_BASIC_PHONE_STATE permission + if (!hasReadPhoneState) { + mContext.enforceCallingOrSelfPermission( + Manifest.permission.READ_BASIC_PHONE_STATE, + message); + } + } } diff --git a/services/core/java/com/android/server/VcnManagementService.java b/services/core/java/com/android/server/VcnManagementService.java index 51c768b80eff..06e6c8b1ec53 100644 --- a/services/core/java/com/android/server/VcnManagementService.java +++ b/services/core/java/com/android/server/VcnManagementService.java @@ -48,6 +48,7 @@ import android.net.LinkProperties; import android.net.Network; import android.net.NetworkCapabilities; import android.net.NetworkRequest; +import android.net.vcn.Flags; import android.net.vcn.IVcnManagementService; import android.net.vcn.IVcnStatusCallback; import android.net.vcn.IVcnUnderlyingNetworkPolicyListener; @@ -524,6 +525,9 @@ public class VcnManagementService extends IVcnManagementService.Stub { @Override public void onReceive(Context context, Intent intent) { final String action = intent.getAction(); + if (action == null) { + return; + } switch (action) { case Intent.ACTION_PACKAGE_ADDED: // Fallthrough @@ -878,6 +882,7 @@ public class VcnManagementService extends IVcnManagementService.Stub { private void garbageCollectAndWriteVcnConfigsLocked() { final SubscriptionManager subMgr = mContext.getSystemService(SubscriptionManager.class); + final Set<ParcelUuid> subGroups = mLastSnapshot.getAllSubscriptionGroups(); boolean shouldWrite = false; @@ -885,11 +890,20 @@ public class VcnManagementService extends IVcnManagementService.Stub { while (configsIterator.hasNext()) { final ParcelUuid subGrp = configsIterator.next(); - final List<SubscriptionInfo> subscriptions = subMgr.getSubscriptionsInGroup(subGrp); - if (subscriptions == null || subscriptions.isEmpty()) { - // Trim subGrps with no more subscriptions; must have moved to another subGrp - configsIterator.remove(); - shouldWrite = true; + if (Flags.fixConfigGarbageCollection()) { + if (!subGroups.contains(subGrp)) { + // Trim subGrps with no more subscriptions; must have moved to another subGrp + logDbg("Garbage collect VcnConfig for group=" + subGrp); + configsIterator.remove(); + shouldWrite = true; + } + } else { + final List<SubscriptionInfo> subscriptions = subMgr.getSubscriptionsInGroup(subGrp); + if (subscriptions == null || subscriptions.isEmpty()) { + // Trim subGrps with no more subscriptions; must have moved to another subGrp + configsIterator.remove(); + shouldWrite = true; + } } } @@ -1094,13 +1108,7 @@ public class VcnManagementService extends IVcnManagementService.Stub { synchronized (mLock) { final Vcn vcn = mVcns.get(subGrp); final VcnConfig vcnConfig = mConfigs.get(subGrp); - if (vcn != null) { - if (vcnConfig == null) { - // TODO: b/284381334 Investigate for the root cause of this issue - // and handle it properly - logWtf("Vcn instance exists but VcnConfig does not for " + subGrp); - } - + if (vcn != null && vcnConfig != null) { if (vcn.getStatus() == VCN_STATUS_CODE_ACTIVE) { isVcnManagedNetwork = true; } @@ -1120,6 +1128,8 @@ public class VcnManagementService extends IVcnManagementService.Stub { } } } + } else if (vcn != null && vcnConfig == null) { + logWtf("Vcn instance exists but VcnConfig does not for " + subGrp); } } diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 2a9cb435e01d..1c3569dd52d0 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -60,7 +60,6 @@ import static android.app.ProcessMemoryState.HOSTING_COMPONENT_TYPE_BACKUP; import static android.app.ProcessMemoryState.HOSTING_COMPONENT_TYPE_INSTRUMENTATION; import static android.app.ProcessMemoryState.HOSTING_COMPONENT_TYPE_PERSISTENT; import static android.app.ProcessMemoryState.HOSTING_COMPONENT_TYPE_SYSTEM; -import static android.content.Intent.isPreventIntentRedirectEnabled; import static android.content.pm.ApplicationInfo.HIDDEN_API_ENFORCEMENT_DEFAULT; import static android.content.pm.PackageManager.GET_SHARED_LIBRARY_FILES; import static android.content.pm.PackageManager.MATCH_ALL; @@ -131,6 +130,7 @@ import static android.os.Process.setThreadScheduler; import static android.provider.Settings.Global.ALWAYS_FINISH_ACTIVITIES; import static android.provider.Settings.Global.DEBUG_APP; import static android.provider.Settings.Global.WAIT_FOR_DEBUGGER; +import static android.security.Flags.preventIntentRedirect; import static android.util.FeatureFlagUtils.SETTINGS_ENABLE_MONITOR_PHANTOM_PROCS; import static android.view.Display.INVALID_DISPLAY; @@ -14301,6 +14301,10 @@ public class ActivityManagerService extends IActivityManager.Stub mBroadcastController.unregisterReceiver(receiver); } + public List<IntentFilter> getRegisteredIntentFilters(IIntentReceiver receiver) { + return mBroadcastController.getRegisteredIntentFilters(receiver); + } + @GuardedBy("this") final int broadcastIntentLocked(ProcessRecord callerApp, String callerPackage, String callerFeatureId, Intent intent, String resolvedType, @@ -19281,7 +19285,7 @@ public class ActivityManagerService extends IActivityManager.Stub * @hide */ public void addCreatorToken(@Nullable Intent intent, String creatorPackage) { - if (!isPreventIntentRedirectEnabled()) return; + if (!preventIntentRedirect()) return; if (intent == null || intent.getExtraIntentKeys() == null) return; for (String key : intent.getExtraIntentKeys()) { diff --git a/services/core/java/com/android/server/am/BroadcastController.java b/services/core/java/com/android/server/am/BroadcastController.java index 71ddf05eb692..b0f880710eb6 100644 --- a/services/core/java/com/android/server/am/BroadcastController.java +++ b/services/core/java/com/android/server/am/BroadcastController.java @@ -679,6 +679,21 @@ class BroadcastController { } } + List<IntentFilter> getRegisteredIntentFilters(IIntentReceiver receiver) { + synchronized (mService) { + final ReceiverList rl = mRegisteredReceivers.get(receiver.asBinder()); + if (rl == null) { + return null; + } + final ArrayList<IntentFilter> filters = new ArrayList<>(); + final int count = rl.size(); + for (int i = 0; i < count; ++i) { + filters.add(rl.get(i)); + } + return filters; + } + } + int broadcastIntentWithFeature(IApplicationThread caller, String callingFeatureId, Intent intent, String resolvedType, IIntentReceiver resultTo, int resultCode, String resultData, Bundle resultExtras, diff --git a/services/core/java/com/android/server/am/BroadcastFilter.java b/services/core/java/com/android/server/am/BroadcastFilter.java index f016590b6539..3c7fb52b11b4 100644 --- a/services/core/java/com/android/server/am/BroadcastFilter.java +++ b/services/core/java/com/android/server/am/BroadcastFilter.java @@ -19,7 +19,6 @@ package com.android.server.am; import android.annotation.Nullable; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledSince; -import android.compat.annotation.Overridable; import android.content.IntentFilter; import android.os.UserHandle; import android.util.PrintWriterPrinter; @@ -40,7 +39,6 @@ public final class BroadcastFilter extends IntentFilter { */ @ChangeId @EnabledSince(targetSdkVersion = android.os.Build.VERSION_CODES.BASE) - @Overridable @VisibleForTesting static final long CHANGE_RESTRICT_PRIORITY_VALUES = 371309185L; diff --git a/services/core/java/com/android/server/am/OWNERS b/services/core/java/com/android/server/am/OWNERS index 61079fc78f8a..c1d55971bdce 100644 --- a/services/core/java/com/android/server/am/OWNERS +++ b/services/core/java/com/android/server/am/OWNERS @@ -49,6 +49,7 @@ per-file User* = file:/MULTIUSER_OWNERS # Broadcasts per-file Broadcast* = file:/BROADCASTS_OWNERS +per-file broadcasts_flags.aconfig = file:/BROADCASTS_OWNERS # Permissions & Packages per-file *Permission* = patb@google.com diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java index 7cfe8292d52f..8dc7c7345f79 100644 --- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java +++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java @@ -41,6 +41,7 @@ import android.aconfigd.Aconfigd.StorageReturnMessage; import android.aconfigd.Aconfigd.StorageReturnMessages; import static com.android.aconfig_new_storage.Flags.enableAconfigStorageDaemon; import static com.android.aconfig_new_storage.Flags.supportImmediateLocalOverrides; +import static com.android.aconfig_new_storage.Flags.supportClearLocalOverridesImmediately; import java.io.DataInputStream; import java.io.DataOutputStream; @@ -209,6 +210,7 @@ public class SettingsToPropertiesMapper { "pixel_perf", "pixel_sensors", "pixel_system_sw_video", + "pixel_video_sw", "pixel_watch", "platform_compat", "platform_security", @@ -525,14 +527,21 @@ public class SettingsToPropertiesMapper { * @param proto * @param packageName the package of the flag * @param flagName the name of the flag + * @param immediate if true, clear immediately; otherwise, clear on next reboot + * + * @hide */ - static void writeFlagOverrideRemovalRequest( - ProtoOutputStream proto, String packageName, String flagName) { + public static void writeFlagOverrideRemovalRequest( + ProtoOutputStream proto, String packageName, String flagName, boolean immediate) { long msgsToken = proto.start(StorageRequestMessages.MSGS); long msgToken = proto.start(StorageRequestMessage.REMOVE_LOCAL_OVERRIDE_MESSAGE); proto.write(StorageRequestMessage.RemoveLocalOverrideMessage.PACKAGE_NAME, packageName); proto.write(StorageRequestMessage.RemoveLocalOverrideMessage.FLAG_NAME, flagName); proto.write(StorageRequestMessage.RemoveLocalOverrideMessage.REMOVE_ALL, false); + proto.write(StorageRequestMessage.RemoveLocalOverrideMessage.REMOVE_OVERRIDE_TYPE, + immediate + ? StorageRequestMessage.REMOVE_LOCAL_IMMEDIATE + : StorageRequestMessage.REMOVE_LOCAL_ON_REBOOT); proto.end(msgToken); proto.end(msgsToken); } @@ -609,7 +618,11 @@ public class SettingsToPropertiesMapper { String realFlagName = fullFlagName.substring(idx+1); if (Flags.syncLocalOverridesRemovalNewStorage() && flagValue == null) { - writeFlagOverrideRemovalRequest(requests, packageName, realFlagName); + if (supportClearLocalOverridesImmediately()) { + writeFlagOverrideRemovalRequest(requests, packageName, realFlagName, true); + } else { + writeFlagOverrideRemovalRequest(requests, packageName, realFlagName, false); + } } else { writeFlagOverrideRequest(requests, packageName, realFlagName, flagValue, true); } diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java index 31ae966d7682..c31b9ef60bd2 100644 --- a/services/core/java/com/android/server/am/UserController.java +++ b/services/core/java/com/android/server/am/UserController.java @@ -2383,7 +2383,7 @@ class UserController implements Handler.Callback { // If running in background is disabled or mStopUserOnSwitch mode, stop the user. if (hasRestriction || isStopUserOnSwitchEnabled()) { Slogf.i(TAG, "Stopping user %d and its profiles on user switch", oldUserId); - stopUsersLU(oldUserId, /* allowDelayedLocking= */ false, null, null); + stopUsersLU(oldUserId, /* allowDelayedLocking= */ !hasRestriction, null, null); return; } } diff --git a/services/core/java/com/android/server/am/broadcasts_flags.aconfig b/services/core/java/com/android/server/am/broadcasts_flags.aconfig new file mode 100644 index 000000000000..b1185d552941 --- /dev/null +++ b/services/core/java/com/android/server/am/broadcasts_flags.aconfig @@ -0,0 +1,10 @@ +package: "com.android.server.am" +container: "system" + +flag { + name: "restrict_priority_values" + namespace: "backstage_power" + description: "Restrict priority values defined by non-system apps" + is_fixed_read_only: true + bug: "369487976" +}
\ No newline at end of file diff --git a/services/core/java/com/android/server/am/flags.aconfig b/services/core/java/com/android/server/am/flags.aconfig index 883482053490..56cfdfb7edde 100644 --- a/services/core/java/com/android/server/am/flags.aconfig +++ b/services/core/java/com/android/server/am/flags.aconfig @@ -235,14 +235,6 @@ flag { } flag { - name: "restrict_priority_values" - namespace: "backstage_power" - description: "Restrict priority values defined by non-system apps" - is_fixed_read_only: true - bug: "369487976" -} - -flag { name: "unfreeze_bind_policy_fix" namespace: "backstage_power" description: "Make sure shouldNotFreeze state change correctly triggers updates." diff --git a/services/core/java/com/android/server/appop/AppOpsService.java b/services/core/java/com/android/server/appop/AppOpsService.java index b4cce7da4416..702ad9541af8 100644 --- a/services/core/java/com/android/server/appop/AppOpsService.java +++ b/services/core/java/com/android/server/appop/AppOpsService.java @@ -70,8 +70,10 @@ import static android.content.Intent.ACTION_PACKAGE_REMOVED; import static android.content.Intent.EXTRA_REPLACING; import static android.content.pm.PermissionInfo.PROTECTION_DANGEROUS; import static android.content.pm.PermissionInfo.PROTECTION_FLAG_APPOP; -import static android.permission.flags.Flags.deviceAwareAppOpNewSchemaEnabled; +import static android.os.Flags.binderFrozenStateChangeCallback; import static android.permission.flags.Flags.checkOpValidatePackage; +import static android.permission.flags.Flags.deviceAwareAppOpNewSchemaEnabled; +import static android.permission.flags.Flags.useFrozenAwareRemoteCallbackList; import static com.android.internal.util.FrameworkStatsLog.APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED; import static com.android.internal.util.FrameworkStatsLog.APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED__BINDER_API__CHECK_OPERATION; @@ -182,6 +184,7 @@ import com.android.server.pm.UserManagerInternal; import com.android.server.pm.pkg.AndroidPackage; import com.android.server.pm.pkg.PackageState; import com.android.server.policy.AppOpsPolicy; +import com.android.server.selinux.RateLimiter; import dalvik.annotation.optimization.NeverCompile; @@ -201,6 +204,7 @@ import java.io.FileWriter; import java.io.IOException; import java.io.PrintWriter; import java.text.SimpleDateFormat; +import java.time.Duration; import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.ArrayList; @@ -351,6 +355,10 @@ public class AppOpsService extends IAppOpsService.Stub { @GuardedBy("this") private boolean mUidStatesInitialized; + // A rate limiter to prevent excessive Atom pushing. Used by noteOperation. + private static final Duration RATE_LIMITER_WINDOW = Duration.ofMillis(10); + private final RateLimiter mRateLimiter = new RateLimiter(RATE_LIMITER_WINDOW); + volatile @NonNull HistoricalRegistry mHistoricalRegistry = new HistoricalRegistry(this); /* @@ -3135,10 +3143,12 @@ public class AppOpsService extends IAppOpsService.Stub { boolean shouldCollectMessage) { if (Binder.getCallingPid() != Process.myPid() && Flags.appopAccessTrackingLoggingEnabled()) { - FrameworkStatsLog.write( - APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED, uid, code, - APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED__BINDER_API__NOTE_OPERATION, - attributionTag != null); + if (mRateLimiter.tryAcquire()) { + FrameworkStatsLog.write( + APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED, uid, code, + APP_OP_NOTE_OP_OR_CHECK_OP_BINDER_API_CALLED__BINDER_API__NOTE_OPERATION, + attributionTag != null); + } } return mCheckOpsDelegateDispatcher.noteOperation(code, uid, packageName, attributionTag, Context.DEVICE_ID_DEFAULT, shouldCollectAsyncNotedOp, message, @@ -3543,20 +3553,23 @@ public class AppOpsService extends IAppOpsService.Stub { synchronized (this) { RemoteCallbackList<IAppOpsAsyncNotedCallback> callbacks = mAsyncOpWatchers.get(key); + if (callbacks == null && binderFrozenStateChangeCallback() + && useFrozenAwareRemoteCallbackList()) { + callbacks = new RemoteCallbackList.Builder<IAppOpsAsyncNotedCallback>( + RemoteCallbackList.FROZEN_CALLEE_POLICY_DROP) + .setInterfaceDiedCallback((rcl, cb, cookie) -> + stopWatchingAsyncNoted(packageName, callback) + ).build(); + } if (callbacks == null) { callbacks = new RemoteCallbackList<IAppOpsAsyncNotedCallback>() { - @Override - public void onCallbackDied(IAppOpsAsyncNotedCallback callback) { - synchronized (AppOpsService.this) { - if (getRegisteredCallbackCount() == 0) { - mAsyncOpWatchers.remove(key); - } + @Override + public void onCallbackDied(IAppOpsAsyncNotedCallback cb) { + stopWatchingAsyncNoted(packageName, callback); } - } - }; - mAsyncOpWatchers.put(key, callbacks); + }; } - + mAsyncOpWatchers.put(key, callbacks); callbacks.register(callback); } } diff --git a/services/core/java/com/android/server/audio/AudioDeviceBroker.java b/services/core/java/com/android/server/audio/AudioDeviceBroker.java index 906e584af1ad..a3b20b93ef02 100644 --- a/services/core/java/com/android/server/audio/AudioDeviceBroker.java +++ b/services/core/java/com/android/server/audio/AudioDeviceBroker.java @@ -693,6 +693,8 @@ public class AudioDeviceBroker { elapsed = System.currentTimeMillis() - start; if (elapsed >= SET_COMMUNICATION_DEVICE_TIMEOUT_MS) { Log.e(TAG, "Timeout waiting for communication device update."); + // reset counter to avoid sticky out of sync condition + mCommunicationDeviceUpdateCount = 0; break; } } @@ -1342,8 +1344,8 @@ public class AudioDeviceBroker { } /*package*/ void postSetModeOwner(int mode, int pid, int uid, boolean signal) { - sendILMsgNoDelay(MSG_IL_SET_MODE_OWNER, SENDMSG_REPLACE, - signal ? 1 : 0, new AudioModeInfo(mode, pid, uid)); + sendLMsgNoDelay(signal ? MSG_L_SET_MODE_OWNER_SIGNAL : MSG_L_SET_MODE_OWNER, + SENDMSG_REPLACE, new AudioModeInfo(mode, pid, uid)); } /*package*/ void postBluetoothDeviceConfigChange(@NonNull BtDeviceInfo info) { @@ -2026,7 +2028,8 @@ public class AudioDeviceBroker { mBtHelper.setAvrcpAbsoluteVolumeIndex(msg.arg1); } break; - case MSG_IL_SET_MODE_OWNER: + case MSG_L_SET_MODE_OWNER: + case MSG_L_SET_MODE_OWNER_SIGNAL: synchronized (mSetModeLock) { synchronized (mDeviceStateLock) { mAudioModeOwner = (AudioModeInfo) msg.obj; @@ -2037,7 +2040,7 @@ public class AudioDeviceBroker { } } } - if (msg.arg1 == 1 /*signal*/) { + if (msg.what == MSG_L_SET_MODE_OWNER_SIGNAL) { mAudioService.decrementAudioModeResetCount(); } break; @@ -2199,7 +2202,8 @@ public class AudioDeviceBroker { private static final int MSG_REPORT_NEW_ROUTES = 13; private static final int MSG_II_SET_HEARING_AID_VOLUME = 14; private static final int MSG_I_SET_AVRCP_ABSOLUTE_VOLUME = 15; - private static final int MSG_IL_SET_MODE_OWNER = 16; + private static final int MSG_L_SET_MODE_OWNER = 16; + private static final int MSG_L_SET_MODE_OWNER_SIGNAL = 17; private static final int MSG_I_BT_SERVICE_DISCONNECTED_PROFILE = 22; private static final int MSG_IL_BT_SERVICE_CONNECTED_PROFILE = 23; diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java index 3dbe48a2e540..985155d0d891 100644 --- a/services/core/java/com/android/server/audio/AudioService.java +++ b/services/core/java/com/android/server/audio/AudioService.java @@ -457,7 +457,7 @@ public class AudioService extends IAudioService.Stub private static final int MSG_UPDATE_AUDIO_MODE = 36; private static final int MSG_RECORDING_CONFIG_CHANGE = 37; private static final int MSG_BT_DEV_CHANGED = 38; - + private static final int MSG_UPDATE_AUDIO_MODE_SIGNAL = 39; private static final int MSG_DISPATCH_AUDIO_MODE = 40; private static final int MSG_ROUTING_UPDATED = 41; private static final int MSG_INIT_HEADTRACKING_SENSORS = 42; @@ -4804,16 +4804,14 @@ public class AudioService extends IAudioService.Stub } static class UpdateAudioModeInfo { - UpdateAudioModeInfo(int mode, int pid, String packageName, boolean signal) { + UpdateAudioModeInfo(int mode, int pid, String packageName) { mMode = mode; mPid = pid; mPackageName = packageName; - mSignal = signal; } private final int mMode; private final int mPid; private final String mPackageName; - private final boolean mSignal; int getMode() { return mMode; @@ -4824,9 +4822,6 @@ public class AudioService extends IAudioService.Stub String getPackageName() { return mPackageName; } - boolean getSignal() { - return mSignal; - } } void postUpdateAudioMode(int msgPolicy, int mode, int pid, String packageName, @@ -4835,8 +4830,8 @@ public class AudioService extends IAudioService.Stub if (signal) { mAudioModeResetCount++; } - sendMsg(mAudioHandler, MSG_UPDATE_AUDIO_MODE, msgPolicy, 0, 0, - new UpdateAudioModeInfo(mode, pid, packageName, signal), delay); + sendMsg(mAudioHandler, signal ? MSG_UPDATE_AUDIO_MODE_SIGNAL : MSG_UPDATE_AUDIO_MODE, + msgPolicy, 0, 0, new UpdateAudioModeInfo(mode, pid, packageName), delay); } } @@ -6654,6 +6649,9 @@ public class AudioService extends IAudioService.Stub // connections not started by the application changing the mode when pid changes mDeviceBroker.postSetModeOwner(mode, pid, uid, signal); } else { + // reset here to avoid sticky out of sync condition (would have been reset + // by AudioDeviceBroker processing MSG_L_SET_MODE_OWNER_SIGNAL message) + resetAudioModeResetCount(); Log.w(TAG, "onUpdateAudioMode: failed to set audio mode to: " + mode); } } @@ -10420,10 +10418,11 @@ public class AudioService extends IAudioService.Stub break; case MSG_UPDATE_AUDIO_MODE: + case MSG_UPDATE_AUDIO_MODE_SIGNAL: synchronized (mDeviceBroker.mSetModeLock) { UpdateAudioModeInfo info = (UpdateAudioModeInfo) msg.obj; onUpdateAudioMode(info.getMode(), info.getPid(), info.getPackageName(), - false /*force*/, info.getSignal()); + false /*force*/, msg.what == MSG_UPDATE_AUDIO_MODE_SIGNAL); } break; @@ -11164,6 +11163,8 @@ public class AudioService extends IAudioService.Stub elapsed = java.lang.System.currentTimeMillis() - start; if (elapsed >= AUDIO_MODE_RESET_TIMEOUT_MS) { Log.e(TAG, "Timeout waiting for audio mode reset"); + // reset count to avoid sticky out of sync state. + resetAudioModeResetCount(); break; } } @@ -11175,7 +11176,7 @@ public class AudioService extends IAudioService.Stub return mMediaFocusControl.abandonAudioFocus(fd, clientId, aa, callingPackageName); } - /** synchronization between setMode(NORMAL) and abandonAudioFocus() frmo Telecom */ + /** synchronization between setMode(NORMAL) and abandonAudioFocus() from Telecom */ private static final long AUDIO_MODE_RESET_TIMEOUT_MS = 3000; private final Object mAudioModeResetLock = new Object(); @@ -11194,6 +11195,13 @@ public class AudioService extends IAudioService.Stub } } + private void resetAudioModeResetCount() { + synchronized (mAudioModeResetLock) { + mAudioModeResetCount = 0; + mAudioModeResetLock.notify(); + } + } + /** see {@link AudioManager#abandonAudioFocusForTest(AudioFocusRequest, String)} */ public int abandonAudioFocusForTest(IAudioFocusDispatcher fd, String clientId, AudioAttributes aa, String callingPackageName) { diff --git a/services/core/java/com/android/server/audio/FocusRequester.java b/services/core/java/com/android/server/audio/FocusRequester.java index f462539d5bbf..5ebe6a1553b9 100644 --- a/services/core/java/com/android/server/audio/FocusRequester.java +++ b/services/core/java/com/android/server/audio/FocusRequester.java @@ -16,6 +16,9 @@ package com.android.server.audio; +import static com.android.server.utils.EventLogger.Event.ALOGE; +import static com.android.server.utils.EventLogger.Event.ALOGW; + import android.annotation.NonNull; import android.annotation.Nullable; import android.content.pm.UserProperties; @@ -24,6 +27,7 @@ import android.media.AudioFocusInfo; import android.media.AudioManager; import android.media.IAudioFocusDispatcher; import android.os.IBinder; +import android.os.RemoteException; import android.os.UserHandle; import android.util.Log; @@ -31,6 +35,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.server.LocalServices; import com.android.server.audio.MediaFocusControl.AudioFocusDeathHandler; import com.android.server.pm.UserManagerInternal; +import com.android.server.utils.EventLogger; import java.io.PrintWriter; import java.util.List; @@ -84,6 +89,8 @@ public class FocusRequester { */ private final @NonNull AudioAttributes mAttributes; + private final EventLogger mEventLogger; + /** * Class constructor * @param aa @@ -100,7 +107,7 @@ public class FocusRequester { FocusRequester(@NonNull AudioAttributes aa, int focusRequest, int grantFlags, IAudioFocusDispatcher afl, IBinder source, @NonNull String id, AudioFocusDeathHandler hdlr, @NonNull String pn, int uid, - @NonNull MediaFocusControl ctlr, int sdk) { + @NonNull MediaFocusControl ctlr, int sdk, EventLogger eventLogger) { mAttributes = aa; mFocusDispatcher = afl; mSourceRef = source; @@ -115,10 +122,12 @@ public class FocusRequester { mFocusLossFadeLimbo = false; mFocusController = ctlr; mSdkTarget = sdk; + mEventLogger = eventLogger; } FocusRequester(AudioFocusInfo afi, IAudioFocusDispatcher afl, - IBinder source, AudioFocusDeathHandler hdlr, @NonNull MediaFocusControl ctlr) { + IBinder source, AudioFocusDeathHandler hdlr, @NonNull MediaFocusControl ctlr, + EventLogger eventLogger) { mAttributes = afi.getAttributes(); mClientId = afi.getClientId(); mPackageName = afi.getPackageName(); @@ -134,6 +143,7 @@ public class FocusRequester { mSourceRef = source; mDeathHandler = hdlr; mFocusController = ctlr; + mEventLogger = eventLogger; } boolean hasSameClient(String otherClient) { @@ -357,18 +367,22 @@ public class FocusRequester { mFocusController.notifyExtPolicyFocusGrant_syncAf(toAudioFocusInfo(), AudioManager.AUDIOFOCUS_REQUEST_GRANTED); final IAudioFocusDispatcher fd = mFocusDispatcher; - if (fd != null) { + if (fd != null && mFocusLossWasNotified) { if (DEBUG) { Log.v(TAG, "dispatching " + focusChangeToString(focusGain) + " to " + mClientId); } - if (mFocusLossWasNotified) { - fd.dispatchAudioFocusChange(focusGain, mClientId); - } + fd.dispatchAudioFocusChange(focusGain, mClientId); + mEventLogger.enqueue(new FocusRequestEvent( + this, focusGain, "handleGain")); + } else if (mFocusLossWasNotified) { + mEventLogger.enqueue(new FocusRequestEvent( + this, focusGain, "handleGain no listener").printSlog(ALOGW, TAG)); } mFocusController.restoreVShapedPlayers(this); - } catch (android.os.RemoteException e) { - Log.e(TAG, "Failure to signal gain of audio focus due to: ", e); + } catch (RemoteException e) { + mEventLogger.enqueue(new FocusRequestEvent( + this, focusGain, "handleGain exc: " + e).printSlog(ALOGE, TAG)); } } @@ -385,62 +399,67 @@ public class FocusRequester { if (DEBUG) { Log.i(TAG, "handleFocusLoss for " + mClientId + " loss:" + focusLoss); } - try { - if (focusLoss != mFocusLossReceived) { - mFocusLossReceived = focusLoss; - mFocusLossWasNotified = false; - // before dispatching a focus loss, check if the following conditions are met: - // 1/ the framework is not supposed to notify the focus loser on a DUCK loss - // (i.e. it has a focus controller that implements a ducking policy) - // 2/ it is a DUCK loss - // 3/ the focus loser isn't flagged as pausing in a DUCK loss - // if they are, do not notify the focus loser - if (!mFocusController.mustNotifyFocusOwnerOnDuck() - && mFocusLossReceived == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK - && (mGrantFlags - & AudioManager.AUDIOFOCUS_FLAG_PAUSES_ON_DUCKABLE_LOSS) == 0) { - if (DEBUG) { - Log.v(TAG, "NOT dispatching " + focusChangeToString(mFocusLossReceived) - + " to " + mClientId + ", to be handled externally"); - } - mFocusController.notifyExtPolicyFocusLoss_syncAf( - toAudioFocusInfo(), false /* wasDispatched */); - return; + if (focusLoss != mFocusLossReceived) { + mFocusLossReceived = focusLoss; + mFocusLossWasNotified = false; + // before dispatching a focus loss, check if the following conditions are met: + // 1/ the framework is not supposed to notify the focus loser on a DUCK loss + // (i.e. it has a focus controller that implements a ducking policy) + // 2/ it is a DUCK loss + // 3/ the focus loser isn't flagged as pausing in a DUCK loss + // if they are, do not notify the focus loser + if (!mFocusController.mustNotifyFocusOwnerOnDuck() + && mFocusLossReceived == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK + && (mGrantFlags + & AudioManager.AUDIOFOCUS_FLAG_PAUSES_ON_DUCKABLE_LOSS) == 0) { + if (DEBUG) { + Log.v(TAG, "NOT dispatching " + focusChangeToString(mFocusLossReceived) + + " to " + mClientId + ", to be handled externally"); } + mFocusController.notifyExtPolicyFocusLoss_syncAf( + toAudioFocusInfo(), false /* wasDispatched */); + return; + } - // check enforcement by the framework - boolean handled = false; - if (frWinner != null) { - handled = frameworkHandleFocusLoss(focusLoss, frWinner, forceDuck); - } + // check enforcement by the framework + boolean handled = false; + if (frWinner != null) { + handled = frameworkHandleFocusLoss(focusLoss, frWinner, forceDuck); + } - if (handled) { - if (DEBUG) { - Log.v(TAG, "NOT dispatching " + focusChangeToString(mFocusLossReceived) - + " to " + mClientId + ", response handled by framework"); - } - mFocusController.notifyExtPolicyFocusLoss_syncAf( - toAudioFocusInfo(), false /* wasDispatched */); - return; // with mFocusLossWasNotified = false + if (handled) { + if (DEBUG) { + Log.v(TAG, "NOT dispatching " + focusChangeToString(mFocusLossReceived) + + " to " + mClientId + ", response handled by framework"); } + mFocusController.notifyExtPolicyFocusLoss_syncAf( + toAudioFocusInfo(), false /* wasDispatched */); + return; // with mFocusLossWasNotified = false + } - final IAudioFocusDispatcher fd = mFocusDispatcher; - if (fd != null) { - if (DEBUG) { - Log.v(TAG, "dispatching " + focusChangeToString(mFocusLossReceived) + " to " - + mClientId); - } - mFocusController.notifyExtPolicyFocusLoss_syncAf( - toAudioFocusInfo(), true /* wasDispatched */); - mFocusLossWasNotified = true; + final IAudioFocusDispatcher fd = mFocusDispatcher; + if (fd != null) { + if (DEBUG) { + Log.v(TAG, "dispatching " + focusChangeToString(mFocusLossReceived) + " to " + + mClientId); + } + mFocusController.notifyExtPolicyFocusLoss_syncAf( + toAudioFocusInfo(), true /* wasDispatched */); + mFocusLossWasNotified = true; + try { fd.dispatchAudioFocusChange(mFocusLossReceived, mClientId); - } else if (DEBUG) { - Log.i(TAG, "NOT dispatching " + focusChangeToString(mFocusLossReceived) - + " to " + mClientId + " no IAudioFocusDispatcher"); + mEventLogger.enqueue(new FocusRequestEvent( + this, mFocusLossReceived, "handleLoss")); + } catch (RemoteException e) { + mEventLogger.enqueue(new FocusRequestEvent( + this, mFocusLossReceived, "handleLoss failed exc: " + e) + .printSlog(ALOGE,TAG)); } + } else { + mEventLogger.enqueue(new FocusRequestEvent( + this, mFocusLossReceived, "handleLoss failed no listener") + .printSlog(ALOGE, TAG)); } - } catch (android.os.RemoteException e) { - Log.e(TAG, "Failure to signal loss of audio focus due to:", e); } } @@ -505,7 +524,7 @@ public class FocusRequester { return false; } - int dispatchFocusChange(int focusChange) { + int dispatchFocusChange(int focusChange, String reason) { final IAudioFocusDispatcher fd = mFocusDispatcher; if (fd == null) { if (MediaFocusControl.DEBUG) { Log.e(TAG, "dispatchFocusChange: no focus dispatcher"); } @@ -528,8 +547,11 @@ public class FocusRequester { } try { fd.dispatchAudioFocusChange(focusChange, mClientId); - } catch (android.os.RemoteException e) { - Log.e(TAG, "dispatchFocusChange: error talking to focus listener " + mClientId, e); + mEventLogger.enqueue(new FocusRequestEvent(this, + focusChange, "dispatch: " + reason)); + } catch (RemoteException e) { + mEventLogger.enqueue(new FocusRequestEvent( + this, focusChange, "dispatch failed: " + e).printSlog(ALOGE, TAG)); return AudioManager.AUDIOFOCUS_REQUEST_FAILED; } return AudioManager.AUDIOFOCUS_REQUEST_GRANTED; @@ -559,7 +581,7 @@ public class FocusRequester { } } } - return dispatchFocusChange(focusChange); + return dispatchFocusChange(focusChange, "focus with fade"); } void dispatchFocusResultFromExtPolicy(int requestResult) { @@ -575,7 +597,7 @@ public class FocusRequester { } try { fd.dispatchFocusResultFromExtPolicy(requestResult, mClientId); - } catch (android.os.RemoteException e) { + } catch (RemoteException e) { Log.e(TAG, "dispatchFocusResultFromExtPolicy: error talking to focus listener" + mClientId, e); } @@ -585,4 +607,32 @@ public class FocusRequester { return new AudioFocusInfo(mAttributes, mCallingUid, mClientId, mPackageName, mFocusGainRequest, mFocusLossReceived, mGrantFlags, mSdkTarget); } + + static class FocusRequestEvent extends EventLogger.Event { + private final String mClientId; + private final int mUid; + private final String mPackageName; + private final int mCode; + private final String mDescription; + + public FocusRequestEvent(FocusRequester fr, String description) { + this(fr, -1, description); + } + + public FocusRequestEvent(FocusRequester fr, int code, String description) { + mClientId = fr.getClientId(); + mUid = fr.getClientUid(); + mPackageName = fr.getPackageName(); + mCode = code; + mDescription = description != null ? description : ""; + } + @Override + public String eventToString() { + return "focus owner: " + mClientId + " in uid: " + mUid + + " pack: " + mPackageName + + ((mCode != -1) ? " code: " + mCode : "") + + " event: " + mDescription; + } + } + } diff --git a/services/core/java/com/android/server/audio/MediaFocusControl.java b/services/core/java/com/android/server/audio/MediaFocusControl.java index b4af46efcb38..1604e94e5a6d 100644 --- a/services/core/java/com/android/server/audio/MediaFocusControl.java +++ b/services/core/java/com/android/server/audio/MediaFocusControl.java @@ -231,13 +231,8 @@ public class MediaFocusControl implements PlayerFocusEnforcer { final FocusRequester focusOwner = stackIterator.next(); if (focusOwner.hasSameUid(uid) && focusOwner.hasSamePackage(packageName)) { clientsToRemove.add(focusOwner.getClientId()); - mEventLogger.enqueue((new EventLogger.StringEvent( - "focus owner:" + focusOwner.getClientId() - + " in uid:" + uid + " pack: " + packageName - + " getting AUDIOFOCUS_LOSS due to app suspension")) - .printLog(TAG)); // make the suspended app lose focus through its focus listener (if any) - focusOwner.dispatchFocusChange(AudioManager.AUDIOFOCUS_LOSS); + focusOwner.dispatchFocusChange(AudioManager.AUDIOFOCUS_LOSS, "app suspension"); } } for (String clientToRemove : clientsToRemove) { @@ -548,11 +543,7 @@ public class MediaFocusControl implements PlayerFocusEnforcer { FocusRequester fr = stackIterator.next(); if(fr.hasSameBinder(cb)) { Log.i(TAG, "AudioFocus removeFocusStackEntryOnDeath(): removing entry for " + cb); - mEventLogger.enqueue(new EventLogger.StringEvent( - "focus requester:" + fr.getClientId() - + " in uid:" + fr.getClientUid() - + " pack:" + fr.getPackageName() - + " died")); + mEventLogger.enqueue(new FocusRequester.FocusRequestEvent(fr, " died")); notifyExtPolicyFocusLoss_syncAf(fr.toAudioFocusInfo(), false); stackIterator.remove(); @@ -585,11 +576,7 @@ public class MediaFocusControl implements PlayerFocusEnforcer { final FocusRequester fr = owner.getValue(); if (fr.hasSameBinder(cb)) { ownerIterator.remove(); - mEventLogger.enqueue(new EventLogger.StringEvent( - "focus requester:" + fr.getClientId() - + " in uid:" + fr.getClientUid() - + " pack:" + fr.getPackageName() - + " died")); + mEventLogger.enqueue(new FocusRequester.FocusRequestEvent(fr, "died")); fr.release(); notifyExtFocusPolicyFocusAbandon_syncAf(fr.toAudioFocusInfo()); break; @@ -900,7 +887,7 @@ public class MediaFocusControl implements PlayerFocusEnforcer { } // new focus (future) focus owner to keep track of mFocusOwnersForFocusPolicy.put(afi.getClientId(), - new FocusRequester(afi, fd, cb, hdlr, this)); + new FocusRequester(afi, fd, cb, hdlr, this, mEventLogger)); } try { @@ -972,7 +959,7 @@ public class MediaFocusControl implements PlayerFocusEnforcer { } return AudioManager.AUDIOFOCUS_REQUEST_FAILED; } - return fr.dispatchFocusChange(focusChange); + return fr.dispatchFocusChange(focusChange, "audiomanager"); } } @@ -1006,6 +993,7 @@ public class MediaFocusControl implements PlayerFocusEnforcer { otherActiveFrs.add(otherFr); } + // TODO log int status = fr.dispatchFocusChangeWithFadeLocked(focusChange, otherActiveFrs); if (status != AudioManager.AUDIOFOCUS_REQUEST_DELAYED && focusChange == AudioManager.AUDIOFOCUS_LOSS) { @@ -1080,6 +1068,7 @@ public class MediaFocusControl implements PlayerFocusEnforcer { switch (attr.getUsage()) { case AudioAttributes.USAGE_MEDIA: case AudioAttributes.USAGE_GAME: + case AudioAttributes.USAGE_SPEAKER_CLEANUP: return 1000; case AudioAttributes.USAGE_ALARM: case AudioAttributes.USAGE_NOTIFICATION_RINGTONE: @@ -1260,7 +1249,7 @@ public class MediaFocusControl implements PlayerFocusEnforcer { removeFocusStackEntry(clientId, false /* signal */, false /*notifyFocusFollowers*/); final FocusRequester nfr = new FocusRequester(aa, focusChangeHint, flags, fd, cb, - clientId, afdh, callingPackageName, uid, this, sdk); + clientId, afdh, callingPackageName, uid, this, sdk, mEventLogger); if (mMultiAudioFocusEnabled && (focusChangeHint == AudioManager.AUDIOFOCUS_GAIN)) { @@ -1594,7 +1583,7 @@ public class MediaFocusControl implements PlayerFocusEnforcer { synchronized (mAudioFocusLock) { final FocusRequester loser = (FocusRequester) msg.obj; if (loser.isInFocusLossLimbo()) { - loser.dispatchFocusChange(AudioManager.AUDIOFOCUS_LOSS); + loser.dispatchFocusChange(AudioManager.AUDIOFOCUS_LOSS, "loss after fade"); loser.release(); postForgetUidLater(loser); } diff --git a/services/core/java/com/android/server/biometrics/AuthSession.java b/services/core/java/com/android/server/biometrics/AuthSession.java index 3afecf1d8bbf..290aab28a82d 100644 --- a/services/core/java/com/android/server/biometrics/AuthSession.java +++ b/services/core/java/com/android/server/biometrics/AuthSession.java @@ -305,7 +305,7 @@ public final class AuthSession implements IBinder.DeathRecipient { mSensors /* sensorIds */, true /* credentialAllowed */, false /* requireConfirmation */, - mUserId, + mPreAuthInfo.callingUserId, mOperationId, mOpPackageName, mRequestId); @@ -357,7 +357,7 @@ public final class AuthSession implements IBinder.DeathRecipient { mSensors, mPreAuthInfo.shouldShowCredential(), requireConfirmation, - mUserId, + mPreAuthInfo.callingUserId, mOperationId, mOpPackageName, mRequestId); @@ -491,7 +491,7 @@ public final class AuthSession implements IBinder.DeathRecipient { mSensors /* sensorIds */, true /* credentialAllowed */, false /* requireConfirmation */, - mUserId, + mPreAuthInfo.callingUserId, mOperationId, mOpPackageName, mRequestId); diff --git a/services/core/java/com/android/server/biometrics/BiometricService.java b/services/core/java/com/android/server/biometrics/BiometricService.java index 4c917894100a..00280c8f9c04 100644 --- a/services/core/java/com/android/server/biometrics/BiometricService.java +++ b/services/core/java/com/android/server/biometrics/BiometricService.java @@ -1133,7 +1133,7 @@ public class BiometricService extends SystemService { return PreAuthInfo.create(mTrustManager, mDevicePolicyManager, mSettingObserver, mSensors, userId, promptInfo, opPackageName, false /* checkDevicePolicyManager */, - getContext(), mBiometricCameraManager); + getContext(), mBiometricCameraManager, mUserManager); } /** @@ -1520,9 +1520,9 @@ public class BiometricService extends SystemService { mHandler.post(() -> { try { final PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager, - mDevicePolicyManager, mSettingObserver, mSensors, userId, promptInfo, - opPackageName, promptInfo.isDisallowBiometricsIfPolicyExists(), - getContext(), mBiometricCameraManager); + mDevicePolicyManager, mSettingObserver, mSensors, userId, + promptInfo, opPackageName, promptInfo.isDisallowBiometricsIfPolicyExists(), + getContext(), mBiometricCameraManager, mUserManager); // Set the default title if necessary. if (promptInfo.isUseDefaultTitle()) { @@ -1572,8 +1572,8 @@ public class BiometricService extends SystemService { promptInfo.setAuthenticators(Authenticators.DEVICE_CREDENTIAL); } - authenticateInternal(token, requestId, operationId, userId, receiver, - opPackageName, promptInfo, preAuthInfo); + authenticateInternal(token, requestId, operationId, preAuthInfo.userId, + receiver, opPackageName, promptInfo, preAuthInfo); } else { receiver.onError(preAuthStatus.first /* modality */, preAuthStatus.second /* errorCode */, diff --git a/services/core/java/com/android/server/biometrics/PreAuthInfo.java b/services/core/java/com/android/server/biometrics/PreAuthInfo.java index 96c178ae9934..e8fa41749473 100644 --- a/services/core/java/com/android/server/biometrics/PreAuthInfo.java +++ b/services/core/java/com/android/server/biometrics/PreAuthInfo.java @@ -32,6 +32,7 @@ import android.hardware.biometrics.BiometricManager; import android.hardware.biometrics.Flags; import android.hardware.biometrics.PromptInfo; import android.os.RemoteException; +import android.os.UserManager; import android.util.Pair; import android.util.Slog; @@ -72,6 +73,7 @@ class PreAuthInfo { final boolean confirmationRequested; final boolean ignoreEnrollmentState; final int userId; + final int callingUserId; final Context context; private final boolean mBiometricRequested; private final int mBiometricStrengthRequested; @@ -82,7 +84,7 @@ class PreAuthInfo { private PreAuthInfo(boolean biometricRequested, int biometricStrengthRequested, boolean credentialRequested, List<BiometricSensor> eligibleSensors, List<Pair<BiometricSensor, Integer>> ineligibleSensors, boolean credentialAvailable, - PromptInfo promptInfo, int userId, Context context, + PromptInfo promptInfo, int userId, int callingUserId, Context context, BiometricCameraManager biometricCameraManager, boolean isOnlyMandatoryBiometricsRequested, boolean isMandatoryBiometricsAuthentication) { @@ -97,6 +99,7 @@ class PreAuthInfo { this.confirmationRequested = promptInfo.isConfirmationRequested(); this.ignoreEnrollmentState = promptInfo.isIgnoreEnrollmentState(); this.userId = userId; + this.callingUserId = callingUserId; this.context = context; this.mOnlyMandatoryBiometricsRequested = isOnlyMandatoryBiometricsRequested; this.mIsMandatoryBiometricsAuthentication = isMandatoryBiometricsAuthentication; @@ -108,7 +111,8 @@ class PreAuthInfo { List<BiometricSensor> sensors, int userId, PromptInfo promptInfo, String opPackageName, boolean checkDevicePolicyManager, Context context, - BiometricCameraManager biometricCameraManager) + BiometricCameraManager biometricCameraManager, + UserManager userManager) throws RemoteException { final boolean isOnlyMandatoryBiometricsRequested = promptInfo.getAuthenticators() @@ -141,14 +145,20 @@ class PreAuthInfo { final List<BiometricSensor> eligibleSensors = new ArrayList<>(); final List<Pair<BiometricSensor, Integer>> ineligibleSensors = new ArrayList<>(); + final int effectiveUserId; + if (Flags.effectiveUserBp()) { + effectiveUserId = userManager.getCredentialOwnerProfile(userId); + } else { + effectiveUserId = userId; + } + if (biometricRequested) { for (BiometricSensor sensor : sensors) { @AuthenticatorStatus int status = getStatusForBiometricAuthenticator( - devicePolicyManager, settingObserver, sensor, userId, opPackageName, - checkDevicePolicyManager, requestedStrength, - promptInfo.getAllowedSensorIds(), - promptInfo.isIgnoreEnrollmentState(), + devicePolicyManager, settingObserver, sensor, effectiveUserId, + opPackageName, checkDevicePolicyManager, requestedStrength, + promptInfo.getAllowedSensorIds(), promptInfo.isIgnoreEnrollmentState(), biometricCameraManager); Slog.d(TAG, "Package: " + opPackageName @@ -172,9 +182,9 @@ class PreAuthInfo { } return new PreAuthInfo(biometricRequested, requestedStrength, credentialRequested, - eligibleSensors, ineligibleSensors, credentialAvailable, promptInfo, userId, - context, biometricCameraManager, isOnlyMandatoryBiometricsRequested, - isMandatoryBiometricsAuthentication); + eligibleSensors, ineligibleSensors, credentialAvailable, promptInfo, + effectiveUserId, userId, context, biometricCameraManager, + isOnlyMandatoryBiometricsRequested, isMandatoryBiometricsAuthentication); } private static boolean dropCredentialFallback(int authenticators, diff --git a/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java b/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java index a1184156f86e..21c51403ecac 100644 --- a/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java +++ b/services/core/java/com/android/server/biometrics/sensors/BiometricSchedulerOperation.java @@ -162,7 +162,7 @@ public class BiometricSchedulerOperation { STATE_WAITING_IN_QUEUE, STATE_WAITING_FOR_COOKIE, STATE_WAITING_IN_QUEUE_CANCELING)) { - return false; + return hasOperationAlreadyStarted(); } if (mClientMonitor.getCookie() != 0) { @@ -191,7 +191,7 @@ public class BiometricSchedulerOperation { STATE_WAITING_IN_QUEUE, STATE_WAITING_FOR_COOKIE, STATE_WAITING_IN_QUEUE_CANCELING)) { - return false; + return hasOperationAlreadyStarted(); } return doStart(callback); @@ -230,6 +230,10 @@ public class BiometricSchedulerOperation { return true; } + private boolean hasOperationAlreadyStarted() { + return mState == STATE_STARTED; + } + /** * Abort a pending operation. * diff --git a/services/core/java/com/android/server/broadcastradio/aidl/ConversionUtils.java b/services/core/java/com/android/server/broadcastradio/aidl/ConversionUtils.java index a3c68f9dc827..afffa6603b3d 100644 --- a/services/core/java/com/android/server/broadcastradio/aidl/ConversionUtils.java +++ b/services/core/java/com/android/server/broadcastradio/aidl/ConversionUtils.java @@ -21,6 +21,7 @@ import android.annotation.SuppressLint; import android.app.compat.CompatChanges; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledSince; +import android.hardware.broadcastradio.Alert; import android.hardware.broadcastradio.AmFmRegionConfig; import android.hardware.broadcastradio.Announcement; import android.hardware.broadcastradio.ConfigFlag; @@ -36,6 +37,7 @@ import android.hardware.broadcastradio.VendorKeyValue; import android.hardware.radio.Flags; import android.hardware.radio.ProgramList; import android.hardware.radio.ProgramSelector; +import android.hardware.radio.RadioAlert; import android.hardware.radio.RadioManager; import android.hardware.radio.RadioMetadata; import android.hardware.radio.RadioTuner; @@ -573,6 +575,86 @@ final class ConversionUtils { return builder.build(); } + @Nullable private static RadioAlert.Polygon polygonFromHalPolygon( + android.hardware.broadcastradio.Polygon halPolygon) { + if (halPolygon.coordinates.length < 4) { + Slogf.e(TAG, "Number of coordinates in alert polygon cannot be less than 4"); + return null; + } else if (halPolygon.coordinates[0].latitude + != halPolygon.coordinates[halPolygon.coordinates.length - 1].latitude + || halPolygon.coordinates[0].longitude + != halPolygon.coordinates[halPolygon.coordinates.length - 1].longitude) { + Slogf.e(TAG, "The first and the last coordinate in alert polygon cannot be different"); + return null; + } + List<RadioAlert.Coordinate> coordinates = new ArrayList<>(halPolygon.coordinates.length); + for (int idx = 0; idx < halPolygon.coordinates.length; idx++) { + coordinates.add(new RadioAlert.Coordinate(halPolygon.coordinates[idx].latitude, + halPolygon.coordinates[idx].longitude)); + } + return new RadioAlert.Polygon(coordinates); + } + + private static RadioAlert.Geocode geocodeFromHalGeocode( + android.hardware.broadcastradio.Geocode geocode) { + return new RadioAlert.Geocode(geocode.valueName, geocode.value); + } + + private static RadioAlert.AlertArea alertAreaFromHalAlertArea( + android.hardware.broadcastradio.AlertArea halAlertArea) { + List<RadioAlert.Polygon> polygonList = new ArrayList<>(); + for (int idx = 0; idx < halAlertArea.polygons.length; idx++) { + RadioAlert.Polygon polygon = polygonFromHalPolygon(halAlertArea.polygons[idx]); + if (polygon != null) { + polygonList.add(polygon); + } + } + List<RadioAlert.Geocode> geocodeList = new ArrayList<>(halAlertArea.geocodes.length); + for (int idx = 0; idx < halAlertArea.geocodes.length; idx++) { + geocodeList.add(geocodeFromHalGeocode(halAlertArea.geocodes[idx])); + } + return new RadioAlert.AlertArea(polygonList, geocodeList); + } + + private static RadioAlert.AlertInfo alertInfoFromHalAlertInfo( + android.hardware.broadcastradio.AlertInfo halAlertInfo) { + int[] categoryArray = new int[halAlertInfo.categoryArray.length]; + for (int idx = 0; idx < halAlertInfo.categoryArray.length; idx++) { + // Integer values in android.hardware.radio.RadioAlert.AlertCategory and + // android.hardware.broadcastradio.AlertCategory match. + categoryArray[idx] = halAlertInfo.categoryArray[idx]; + } + List<RadioAlert.AlertArea> alertAreaList = new ArrayList<>(); + for (int idx = 0; idx < halAlertInfo.areas.length; idx++) { + alertAreaList.add(alertAreaFromHalAlertArea(halAlertInfo.areas[idx])); + } + // Integer values in android.hardware.radio.RadioAlert.AlertUrgency and + // android.hardware.broadcastradio.AlertUrgency match. + // Integer values in android.hardware.radio.RadioAlert.AlertSeverity and + // android.hardware.broadcastradio.AlertSeverity match. + // Integer values in android.hardware.radio.RadioAlert.AlertCertainty and + // android.hardware.broadcastradio.AlertCertainty match. + return new RadioAlert.AlertInfo(categoryArray, halAlertInfo.urgency, halAlertInfo.severity, + halAlertInfo.certainty, halAlertInfo.description, alertAreaList, + halAlertInfo.language); + } + + @VisibleForTesting + @Nullable static RadioAlert radioAlertFromHalAlert(Alert halAlert) { + if (halAlert == null) { + return null; + } + List<RadioAlert.AlertInfo> alertInfo = new ArrayList<>(halAlert.infoArray.length); + for (int idx = 0; idx < halAlert.infoArray.length; idx++) { + alertInfo.add(alertInfoFromHalAlertInfo(halAlert.infoArray[idx])); + } + // Integer values in android.hardware.radio.RadioAlert.AlertStatus and + // android.hardware.broadcastradio.AlertStatus match. + // Integer values in android.hardware.radio.RadioAlert.AlertMessageType and + // android.hardware.broadcastradio.AlertMessageType match. + return new RadioAlert(halAlert.status, halAlert.messageType, alertInfo); + } + private static boolean isValidLogicallyTunedTo(ProgramIdentifier id) { return id.type == IdentifierType.AMFM_FREQUENCY_KHZ || id.type == IdentifierType.RDS_PI || id.type == IdentifierType.HD_STATION_ID_EXT @@ -605,7 +687,18 @@ final class ConversionUtils { } } } - + if (!Flags.hdRadioEmergencyAlertSystem()) { + return new RadioManager.ProgramInfo( + Objects.requireNonNull(programSelectorFromHalProgramSelector(info.selector)), + identifierFromHalProgramIdentifier(info.logicallyTunedTo), + identifierFromHalProgramIdentifier(info.physicallyTunedTo), + relatedContent, + info.infoFlags, + info.signalQuality, + radioMetadataFromHalMetadata(info.metadata), + vendorInfoFromHalVendorKeyValues(info.vendorInfo) + ); + } return new RadioManager.ProgramInfo( Objects.requireNonNull(programSelectorFromHalProgramSelector(info.selector)), identifierFromHalProgramIdentifier(info.logicallyTunedTo), @@ -614,7 +707,8 @@ final class ConversionUtils { info.infoFlags, info.signalQuality, radioMetadataFromHalMetadata(info.metadata), - vendorInfoFromHalVendorKeyValues(info.vendorInfo) + vendorInfoFromHalVendorKeyValues(info.vendorInfo), + radioAlertFromHalAlert(info.emergencyAlert) ); } diff --git a/services/core/java/com/android/server/display/DisplayDeviceInfo.java b/services/core/java/com/android/server/display/DisplayDeviceInfo.java index 0807c70d9922..4ad7c10a1444 100644 --- a/services/core/java/com/android/server/display/DisplayDeviceInfo.java +++ b/services/core/java/com/android/server/display/DisplayDeviceInfo.java @@ -26,6 +26,7 @@ import android.view.DisplayAddress; import android.view.DisplayCutout; import android.view.DisplayEventReceiver; import android.view.DisplayShape; +import android.view.FrameRateCategoryRate; import android.view.RoundedCorners; import android.view.Surface; @@ -300,6 +301,11 @@ final class DisplayDeviceInfo { public boolean hasArrSupport; /** + * Represents frame rate for the FrameRateCategory Normal and High. + * @see android.view.Display#getSuggestedFrameRate(int) for more details. + */ + public FrameRateCategoryRate frameRateCategoryRate; + /** * The default mode of the display. */ public int defaultModeId; @@ -548,7 +554,8 @@ final class DisplayDeviceInfo { || !Objects.equals(roundedCorners, other.roundedCorners) || installOrientation != other.installOrientation || !Objects.equals(displayShape, other.displayShape) - || hasArrSupport != other.hasArrSupport) { + || hasArrSupport != other.hasArrSupport + || !Objects.equals(frameRateCategoryRate, other.frameRateCategoryRate)) { diff |= DIFF_OTHER; } return diff; @@ -567,6 +574,7 @@ final class DisplayDeviceInfo { modeId = other.modeId; renderFrameRate = other.renderFrameRate; hasArrSupport = other.hasArrSupport; + frameRateCategoryRate = other.frameRateCategoryRate; defaultModeId = other.defaultModeId; userPreferredModeId = other.userPreferredModeId; supportedModes = other.supportedModes; @@ -612,6 +620,7 @@ final class DisplayDeviceInfo { sb.append(", modeId ").append(modeId); sb.append(", renderFrameRate ").append(renderFrameRate); sb.append(", hasArrSupport ").append(hasArrSupport); + sb.append(", frameRateCategoryRate ").append(frameRateCategoryRate); sb.append(", defaultModeId ").append(defaultModeId); sb.append(", userPreferredModeId ").append(userPreferredModeId); sb.append(", supportedModes ").append(Arrays.toString(supportedModes)); diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index 3603cdbd775f..f5a75c7d1c38 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -25,7 +25,7 @@ import static android.Manifest.permission.MANAGE_DISPLAYS; import static android.Manifest.permission.RESTRICT_DISPLAY_MODES; import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_CACHED; import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_GONE; -import static android.hardware.display.DisplayManager.EventsMask; +import static android.hardware.display.DisplayManager.EventFlag; import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED; import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR; import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_CAN_SHOW_WITH_INSECURE_KEYGUARD; @@ -1390,16 +1390,16 @@ public final class DisplayManagerService extends SystemService { } private void registerCallbackInternal(IDisplayManagerCallback callback, int callingPid, - int callingUid, @EventsMask long eventsMask) { + int callingUid, @EventFlag long eventFlagsMask) { synchronized (mSyncRoot) { CallbackRecord record = mCallbacks.get(callingPid); if (record != null) { - record.updateEventsMask(eventsMask); + record.updateEventFlagsMask(eventFlagsMask); return; } - record = new CallbackRecord(callingPid, callingUid, callback, eventsMask); + record = new CallbackRecord(callingPid, callingUid, callback, eventFlagsMask); try { IBinder binder = callback.asBinder(); binder.linkToDeath(record, 0); @@ -1889,6 +1889,7 @@ public final class DisplayManagerService extends SystemService { final String displayUniqueId = VirtualDisplayAdapter.generateDisplayUniqueId( packageName, callingUid, virtualDisplayConfig); + boolean shouldClearDisplayWindowSettings = false; if (virtualDisplayConfig.isHomeSupported()) { if ((flags & VIRTUAL_DISPLAY_FLAG_TRUSTED) == 0) { Slog.w(TAG, "Display created with home support but lacks " @@ -1900,6 +1901,18 @@ public final class DisplayManagerService extends SystemService { } else { mWindowManagerInternal.setHomeSupportedOnDisplay(displayUniqueId, Display.TYPE_VIRTUAL, true); + shouldClearDisplayWindowSettings = true; + } + } + + if (virtualDisplayConfig.isIgnoreActivitySizeRestrictions()) { + if ((flags & VIRTUAL_DISPLAY_FLAG_TRUSTED) == 0) { + Slog.w(TAG, "Display created to ignore activity size restrictions, " + + "but lacks VIRTUAL_DISPLAY_FLAG_TRUSTED, ignoring the request."); + } else { + mWindowManagerInternal.setIgnoreActivitySizeRestrictionsOnDisplay( + displayUniqueId, Display.TYPE_VIRTUAL, true); + shouldClearDisplayWindowSettings = true; } } @@ -1922,8 +1935,7 @@ public final class DisplayManagerService extends SystemService { } } - if (displayId == Display.INVALID_DISPLAY && virtualDisplayConfig.isHomeSupported() - && (flags & VIRTUAL_DISPLAY_FLAG_TRUSTED) != 0) { + if (displayId == Display.INVALID_DISPLAY && shouldClearDisplayWindowSettings) { // Failed to create the virtual display, so we should clean up the WM settings // because it won't receive the onDisplayRemoved callback. mWindowManagerInternal.clearDisplaySettings(displayUniqueId, Display.TYPE_VIRTUAL); @@ -3997,7 +4009,7 @@ public final class DisplayManagerService extends SystemService { public final int mPid; public final int mUid; private final IDisplayManagerCallback mCallback; - private @EventsMask AtomicLong mEventsMask; + private @DisplayManager.EventFlag AtomicLong mEventFlagsMask; private final String mPackageName; public boolean mWifiDisplayScanRequested; @@ -4018,11 +4030,11 @@ public final class DisplayManagerService extends SystemService { private boolean mFrozen; CallbackRecord(int pid, int uid, @NonNull IDisplayManagerCallback callback, - @EventsMask long eventsMask) { + @EventFlag long eventFlagsMask) { mPid = pid; mUid = uid; mCallback = callback; - mEventsMask = new AtomicLong(eventsMask); + mEventFlagsMask = new AtomicLong(eventFlagsMask); mCached = false; mFrozen = false; @@ -4044,8 +4056,8 @@ public final class DisplayManagerService extends SystemService { mPackageName = packageNames == null ? null : packageNames[0]; } - public void updateEventsMask(@EventsMask long eventsMask) { - mEventsMask.set(eventsMask); + public void updateEventFlagsMask(@EventFlag long eventFlag) { + mEventFlagsMask.set(eventFlag); } /** @@ -4109,12 +4121,13 @@ public final class DisplayManagerService extends SystemService { if (!shouldSendEvent(event)) { if (extraLogging(mPackageName)) { Slog.i(TAG, - "Not sending displayEvent: " + event + " due to mask:" + mEventsMask); + "Not sending displayEvent: " + event + " due to flag:" + + mEventFlagsMask); } if (Trace.isTagEnabled(Trace.TRACE_TAG_POWER)) { Trace.instant(Trace.TRACE_TAG_POWER, - "notifyDisplayEventAsync#notSendingEvent=" + event + ",mEventsMask=" - + mEventsMask); + "notifyDisplayEventAsync#notSendingEvent=" + event + ",mEventsFlag=" + + mEventFlagsMask); } // The client is not interested in this event, so do nothing. return true; @@ -4160,22 +4173,22 @@ public final class DisplayManagerService extends SystemService { * Return true if the client is interested in this event. */ private boolean shouldSendEvent(@DisplayEvent int event) { - final long mask = mEventsMask.get(); + final long flag = mEventFlagsMask.get(); switch (event) { case DisplayManagerGlobal.EVENT_DISPLAY_ADDED: - return (mask & DisplayManager.EVENT_FLAG_DISPLAY_ADDED) != 0; + return (flag & DisplayManager.EVENT_FLAG_DISPLAY_ADDED) != 0; case DisplayManagerGlobal.EVENT_DISPLAY_CHANGED: - return (mask & DisplayManager.EVENT_FLAG_DISPLAY_CHANGED) != 0; + return (flag & DisplayManager.EVENT_FLAG_DISPLAY_CHANGED) != 0; case DisplayManagerGlobal.EVENT_DISPLAY_BRIGHTNESS_CHANGED: - return (mask & DisplayManager.EVENT_FLAG_DISPLAY_BRIGHTNESS) != 0; + return (flag & DisplayManager.EVENT_FLAG_DISPLAY_BRIGHTNESS) != 0; case DisplayManagerGlobal.EVENT_DISPLAY_REMOVED: - return (mask & DisplayManager.EVENT_FLAG_DISPLAY_REMOVED) != 0; + return (flag & DisplayManager.EVENT_FLAG_DISPLAY_REMOVED) != 0; case DisplayManagerGlobal.EVENT_DISPLAY_HDR_SDR_RATIO_CHANGED: - return (mask & DisplayManager.EVENT_FLAG_HDR_SDR_RATIO_CHANGED) != 0; + return (flag & DisplayManager.EVENT_FLAG_HDR_SDR_RATIO_CHANGED) != 0; case DisplayManagerGlobal.EVENT_DISPLAY_CONNECTED: // fallthrough case DisplayManagerGlobal.EVENT_DISPLAY_DISCONNECTED: - return (mask & DisplayManager.EVENT_FLAG_DISPLAY_CONNECTION_CHANGED) != 0; + return (flag & DisplayManager.EVENT_FLAG_DISPLAY_CONNECTION_CHANGED) != 0; default: // This should never happen. Slog.e(TAG, "Unknown display event " + event); @@ -4369,7 +4382,7 @@ public final class DisplayManagerService extends SystemService { @Override // Binder call @SuppressLint("AndroidFrameworkRequiresPermission") // Permission only required sometimes public void registerCallbackWithEventMask(IDisplayManagerCallback callback, - @EventsMask long eventsMask) { + @EventFlag long eventFlagsMask) { if (callback == null) { throw new IllegalArgumentException("listener must not be null"); } @@ -4378,7 +4391,7 @@ public final class DisplayManagerService extends SystemService { final int callingUid = Binder.getCallingUid(); if (mFlags.isConnectedDisplayManagementEnabled()) { - if ((eventsMask & DisplayManager.EVENT_FLAG_DISPLAY_CONNECTION_CHANGED) != 0) { + if ((eventFlagsMask & DisplayManager.EVENT_FLAG_DISPLAY_CONNECTION_CHANGED) != 0) { mContext.enforceCallingOrSelfPermission(MANAGE_DISPLAYS, "Permission required to get signals about connection events."); } @@ -4386,7 +4399,7 @@ public final class DisplayManagerService extends SystemService { final long token = Binder.clearCallingIdentity(); try { - registerCallbackInternal(callback, callingPid, callingUid, eventsMask); + registerCallbackInternal(callback, callingPid, callingUid, eventFlagsMask); } finally { Binder.restoreCallingIdentity(token); } diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java index f9c3a46828b9..a4bb8c348d3f 100644 --- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java +++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java @@ -45,6 +45,7 @@ import android.view.DisplayAddress; import android.view.DisplayCutout; import android.view.DisplayEventReceiver; import android.view.DisplayShape; +import android.view.FrameRateCategoryRate; import android.view.RoundedCorners; import android.view.SurfaceControl; @@ -247,6 +248,7 @@ final class LocalDisplayAdapter extends DisplayAdapter { private boolean mDisplayModeSpecsInvalid; private int mActiveColorMode; private boolean mHasArrSupport; + private FrameRateCategoryRate mFrameRateCategoryRate; private Display.HdrCapabilities mHdrCapabilities; private boolean mAllmSupported; private boolean mGameContentTypeSupported; @@ -313,6 +315,7 @@ final class LocalDisplayAdapter extends DisplayAdapter { changed |= updateAllmSupport(dynamicInfo.autoLowLatencyModeSupported); changed |= updateGameContentTypeSupport(dynamicInfo.gameContentTypeSupported); changed |= updateHasArrSupportLocked(dynamicInfo.hasArrSupport); + changed |= updateFrameRateCategoryRatesLocked(dynamicInfo.frameRateCategoryRate); if (changed) { mHavePendingChanges = true; @@ -604,6 +607,15 @@ final class LocalDisplayAdapter extends DisplayAdapter { return true; } + private boolean updateFrameRateCategoryRatesLocked( + FrameRateCategoryRate newFrameRateCategoryRate) { + if (Objects.equals(mFrameRateCategoryRate, newFrameRateCategoryRate)) { + return false; + } + mFrameRateCategoryRate = newFrameRateCategoryRate; + return true; + } + private boolean updateHasArrSupportLocked(boolean newHasArrSupport) { if (mHasArrSupport == newHasArrSupport) { return false; @@ -695,6 +707,7 @@ final class LocalDisplayAdapter extends DisplayAdapter { } mInfo.hdrCapabilities = mHdrCapabilities; mInfo.hasArrSupport = mHasArrSupport; + mInfo.frameRateCategoryRate = mFrameRateCategoryRate; mInfo.appVsyncOffsetNanos = mActiveSfDisplayMode.appVsyncOffsetNanos; mInfo.presentationDeadlineNanos = mActiveSfDisplayMode.presentationDeadlineNanos; mInfo.state = mState; diff --git a/services/core/java/com/android/server/display/LogicalDisplay.java b/services/core/java/com/android/server/display/LogicalDisplay.java index 074a4d851aef..7cfdcafcb610 100644 --- a/services/core/java/com/android/server/display/LogicalDisplay.java +++ b/services/core/java/com/android/server/display/LogicalDisplay.java @@ -507,6 +507,7 @@ final class LogicalDisplay { mBaseDisplayInfo.modeId = deviceInfo.modeId; mBaseDisplayInfo.renderFrameRate = deviceInfo.renderFrameRate; mBaseDisplayInfo.hasArrSupport = deviceInfo.hasArrSupport; + mBaseDisplayInfo.frameRateCategoryRate = deviceInfo.frameRateCategoryRate; mBaseDisplayInfo.defaultModeId = deviceInfo.defaultModeId; mBaseDisplayInfo.userPreferredModeId = deviceInfo.userPreferredModeId; mBaseDisplayInfo.supportedModes = Arrays.copyOf( diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig index 36cadf5271c4..a9bdccef2300 100644 --- a/services/core/java/com/android/server/display/feature/display_flags.aconfig +++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig @@ -430,3 +430,19 @@ flag { bug: "350617205" is_fixed_read_only: true } + +flag { + name: "enable_get_suggested_frame_rate" + namespace: "core_graphics" + description: "Flag for an API to get suggested frame rates" + bug: "361433796" + is_fixed_read_only: true +} + +flag { + name: "enable_get_supported_refresh_rates" + namespace: "core_graphics" + description: "Flag to use the surfaceflinger rates for getSupportedRefreshRates" + bug: "365163968" + is_fixed_read_only: true +} diff --git a/services/core/java/com/android/server/display/state/DisplayStateController.java b/services/core/java/com/android/server/display/state/DisplayStateController.java index dba687413496..0b46e0fc3268 100644 --- a/services/core/java/com/android/server/display/state/DisplayStateController.java +++ b/services/core/java/com/android/server/display/state/DisplayStateController.java @@ -61,7 +61,7 @@ public class DisplayStateController { // We might override this below based on other factors. // Initialise brightness as invalid. int state; - int reason = Display.STATE_REASON_DEFAULT_POLICY; + int reason = displayPowerRequest.policyReason; switch (displayPowerRequest.policy) { case DisplayManagerInternal.DisplayPowerRequest.POLICY_OFF: state = Display.STATE_OFF; diff --git a/services/core/java/com/android/server/hdmi/HdmiControlService.java b/services/core/java/com/android/server/hdmi/HdmiControlService.java index 0766c3ae0f2b..132d6fa377eb 100644 --- a/services/core/java/com/android/server/hdmi/HdmiControlService.java +++ b/services/core/java/com/android/server/hdmi/HdmiControlService.java @@ -3178,6 +3178,10 @@ public class HdmiControlService extends SystemService { HdmiCecLocalDeviceSource source = playback(); if (source == null) { source = audioSystem(); + } else { + // Cancel an existing timer to send the device to sleep since OTP was triggered. + playback().mDelayedStandbyOnActiveSourceLostHandler + .removeCallbacksAndMessages(null); } if (source == null) { diff --git a/services/core/java/com/android/server/input/InputGestureManager.java b/services/core/java/com/android/server/input/InputGestureManager.java new file mode 100644 index 000000000000..f4bd402e63a2 --- /dev/null +++ b/services/core/java/com/android/server/input/InputGestureManager.java @@ -0,0 +1,141 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.input; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.UserIdInt; +import android.hardware.input.InputGestureData; +import android.hardware.input.InputManager; +import android.util.IndentingPrintWriter; +import android.util.SparseArray; +import android.view.KeyEvent; + +import com.android.internal.annotations.GuardedBy; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +/** + * A thread-safe component of {@link InputManagerService} responsible for managing pre-defined input + * gestures and custom gestures defined by other system components using Input APIs. + * + * TODO(b/365064144): Add implementation to persist data, identify clashes with existing shortcuts. + * + */ +final class InputGestureManager { + private static final String TAG = "InputGestureManager"; + + private static final int KEY_GESTURE_META_MASK = + KeyEvent.META_CTRL_ON | KeyEvent.META_ALT_ON | KeyEvent.META_SHIFT_ON + | KeyEvent.META_META_ON; + + @GuardedBy("mCustomInputGestures") + private final SparseArray<Map<InputGestureData.Trigger, InputGestureData>> + mCustomInputGestures = new SparseArray<>(); + + @InputManager.CustomInputGestureResult + public int addCustomInputGesture(int userId, InputGestureData newGesture) { + synchronized (mCustomInputGestures) { + if (!mCustomInputGestures.contains(userId)) { + mCustomInputGestures.put(userId, new HashMap<>()); + } + Map<InputGestureData.Trigger, InputGestureData> customGestures = + mCustomInputGestures.get(userId); + if (customGestures.containsKey(newGesture.getTrigger())) { + return InputManager.CUSTOM_INPUT_GESTURE_RESULT_ERROR_ALREADY_EXISTS; + } + customGestures.put(newGesture.getTrigger(), newGesture); + return InputManager.CUSTOM_INPUT_GESTURE_RESULT_SUCCESS; + } + } + + @InputManager.CustomInputGestureResult + public int removeCustomInputGesture(int userId, InputGestureData data) { + synchronized (mCustomInputGestures) { + if (!mCustomInputGestures.contains(userId)) { + return InputManager.CUSTOM_INPUT_GESTURE_RESULT_ERROR_DOES_NOT_EXIST; + } + Map<InputGestureData.Trigger, InputGestureData> customGestures = + mCustomInputGestures.get(userId); + InputGestureData customGesture = customGestures.get(data.getTrigger()); + if (!Objects.equals(data, customGesture)) { + return InputManager.CUSTOM_INPUT_GESTURE_RESULT_ERROR_DOES_NOT_EXIST; + } + customGestures.remove(data.getTrigger()); + if (customGestures.size() == 0) { + mCustomInputGestures.remove(userId); + } + return InputManager.CUSTOM_INPUT_GESTURE_RESULT_SUCCESS; + } + } + + public void removeAllCustomInputGestures(int userId) { + synchronized (mCustomInputGestures) { + mCustomInputGestures.remove(userId); + } + } + + @NonNull + public List<InputGestureData> getCustomInputGestures(int userId) { + synchronized (mCustomInputGestures) { + if (!mCustomInputGestures.contains(userId)) { + return List.of(); + } + return new ArrayList<>(mCustomInputGestures.get(userId).values()); + } + } + + @Nullable + public InputGestureData getCustomGestureForKeyEvent(@UserIdInt int userId, KeyEvent event) { + final int keyCode = event.getKeyCode(); + if (keyCode == KeyEvent.KEYCODE_UNKNOWN) { + return null; + } + synchronized (mCustomInputGestures) { + Map<InputGestureData.Trigger, InputGestureData> customGestures = + mCustomInputGestures.get(userId); + if (customGestures == null) { + return null; + } + int modifierState = event.getMetaState() & KEY_GESTURE_META_MASK; + return customGestures.get(InputGestureData.createKeyTrigger(keyCode, modifierState)); + } + } + + public void dump(IndentingPrintWriter ipw) { + ipw.println("InputGestureManager:"); + ipw.increaseIndent(); + synchronized (mCustomInputGestures) { + int size = mCustomInputGestures.size(); + for (int i = 0; i < size; i++) { + Map<InputGestureData.Trigger, InputGestureData> customGestures = + mCustomInputGestures.valueAt(i); + ipw.println("UserId = " + mCustomInputGestures.keyAt(i)); + ipw.increaseIndent(); + for (InputGestureData customGesture : customGestures.values()) { + ipw.println(customGesture); + } + ipw.decreaseIndent(); + } + } + ipw.decreaseIndent(); + } +} diff --git a/services/core/java/com/android/server/input/InputManagerService.java b/services/core/java/com/android/server/input/InputManagerService.java index a421d044507a..26929f5999eb 100644 --- a/services/core/java/com/android/server/input/InputManagerService.java +++ b/services/core/java/com/android/server/input/InputManagerService.java @@ -51,6 +51,7 @@ import android.hardware.SensorPrivacyManager.Sensors; import android.hardware.SensorPrivacyManagerInternal; import android.hardware.display.DisplayManagerInternal; import android.hardware.display.DisplayViewport; +import android.hardware.input.AidlInputGestureData; import android.hardware.input.HostUsiVersion; import android.hardware.input.IInputDeviceBatteryListener; import android.hardware.input.IInputDeviceBatteryState; @@ -63,6 +64,7 @@ import android.hardware.input.IKeyboardBacklightListener; import android.hardware.input.IStickyModifierStateListener; import android.hardware.input.ITabletModeChangedListener; import android.hardware.input.InputDeviceIdentifier; +import android.hardware.input.InputGestureData; import android.hardware.input.InputManager; import android.hardware.input.InputSensorInfo; import android.hardware.input.InputSettings; @@ -2985,8 +2987,42 @@ public class InputManagerService extends IInputManager.Stub mKeyGestureController.unregisterKeyGestureHandler(handler, Binder.getCallingPid()); } + @Override + @PermissionManuallyEnforced + public int addCustomInputGesture(@NonNull AidlInputGestureData inputGestureData) { + enforceManageKeyGesturePermission(); + + Objects.requireNonNull(inputGestureData); + return mKeyGestureController.addCustomInputGesture(UserHandle.getCallingUserId(), + inputGestureData); + } + + @Override + @PermissionManuallyEnforced + public int removeCustomInputGesture(@NonNull AidlInputGestureData inputGestureData) { + enforceManageKeyGesturePermission(); + + Objects.requireNonNull(inputGestureData); + return mKeyGestureController.removeCustomInputGesture(UserHandle.getCallingUserId(), + inputGestureData); + } + + @Override + @PermissionManuallyEnforced + public void removeAllCustomInputGestures() { + enforceManageKeyGesturePermission(); + + mKeyGestureController.removeAllCustomInputGestures(UserHandle.getCallingUserId()); + } + + @Override + public AidlInputGestureData[] getCustomInputGestures() { + return mKeyGestureController.getCustomInputGestures(UserHandle.getCallingUserId()); + } + private void handleCurrentUserChanged(@UserIdInt int userId) { mCurrentUserId = userId; + mKeyGestureController.setCurrentUserId(userId); } /** diff --git a/services/core/java/com/android/server/input/InputShellCommand.java b/services/core/java/com/android/server/input/InputShellCommand.java index 4c5a3c27e156..98330163a693 100644 --- a/services/core/java/com/android/server/input/InputShellCommand.java +++ b/services/core/java/com/android/server/input/InputShellCommand.java @@ -89,6 +89,9 @@ public class InputShellCommand extends ShellCommand { private static final int DEFAULT_FLAGS = 0; private static final boolean INJECT_ASYNC = true; private static final boolean INJECT_SYNC = false; + private static final long SECOND_IN_MILLISECONDS = 1000; + + public static final int SWIPE_EVENT_HZ_DEFAULT = 120; /** Modifier key to meta state */ private static final Map<Integer, Integer> MODIFIER; @@ -519,11 +522,30 @@ public class InputShellCommand extends ShellCommand { } long now = SystemClock.uptimeMillis(); final long endTime = down + duration; + final float swipeEventPeriodMillis = + (float) SECOND_IN_MILLISECONDS / SWIPE_EVENT_HZ_DEFAULT; + int injected = 1; while (now < endTime) { - final long elapsedTime = now - down; + // Ensure that we inject at most at the frequency of SWIPE_EVENT_HZ_DEFAULT + // by waiting an additional delta between the actual time and expected time. + long elapsedTime = now - down; + final long errorMillis = + (long) Math.floor(injected * swipeEventPeriodMillis - elapsedTime); + if (errorMillis > 0) { + // Make sure not to exceed the duration and inject an extra event. + if (errorMillis > endTime - now) { + sleep(endTime - now); + break; + } + sleep(errorMillis); + } + + now = SystemClock.uptimeMillis(); + elapsedTime = now - down; final float alpha = (float) elapsedTime / duration; injectMotionEvent(inputSource, MotionEvent.ACTION_MOVE, down, now, lerp(x1, x2, alpha), lerp(y1, y2, alpha), 1.0f, displayId); + injected++; now = SystemClock.uptimeMillis(); } injectMotionEvent(inputSource, MotionEvent.ACTION_UP, down, now, x2, y2, 0.0f, diff --git a/services/core/java/com/android/server/input/KeyGestureController.java b/services/core/java/com/android/server/input/KeyGestureController.java index 7ee811656dc1..ebeef651c19b 100644 --- a/services/core/java/com/android/server/input/KeyGestureController.java +++ b/services/core/java/com/android/server/input/KeyGestureController.java @@ -25,19 +25,25 @@ import static com.android.hardware.input.Flags.useKeyGestureEventHandler; import static com.android.hardware.input.Flags.useKeyGestureEventHandlerMultiPressGestures; import static com.android.server.flags.Flags.newBugreportKeyboardShortcut; import static com.android.window.flags.Flags.enableMoveToNextDisplayShortcut; +import static com.android.window.flags.Flags.enableTaskResizingKeyboardShortcuts; import android.annotation.BinderThread; import android.annotation.MainThread; +import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; +import android.annotation.UserIdInt; import android.content.ContentResolver; import android.content.Context; import android.content.pm.PackageManager; import android.content.res.Resources; import android.database.ContentObserver; +import android.hardware.input.AidlInputGestureData; import android.hardware.input.AidlKeyGestureEvent; +import android.hardware.input.AppLaunchData; import android.hardware.input.IKeyGestureEventListener; import android.hardware.input.IKeyGestureHandler; +import android.hardware.input.InputGestureData; import android.hardware.input.InputManager; import android.hardware.input.InputSettings; import android.hardware.input.KeyGestureEvent; @@ -67,6 +73,7 @@ import com.android.server.policy.KeyCombinationManager; import java.util.ArrayDeque; import java.util.HashSet; +import java.util.List; import java.util.Objects; import java.util.Set; import java.util.TreeMap; @@ -85,6 +92,9 @@ final class KeyGestureController { // Maximum key gesture events that are tracked and will be available in input dump. private static final int MAX_TRACKED_EVENTS = 10; + private static final int SHORTCUT_META_MASK = + KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON | KeyEvent.META_ALT_ON + | KeyEvent.META_SHIFT_ON; private static final int MSG_NOTIFY_KEY_GESTURE_EVENT = 1; @@ -109,6 +119,11 @@ final class KeyGestureController { private final int mSystemPid; private final KeyCombinationManager mKeyCombinationManager; private final SettingsObserver mSettingsObserver; + private final InputGestureManager mInputGestureManager = new InputGestureManager(); + private static final Object mUserLock = new Object(); + @UserIdInt + @GuardedBy("mUserLock") + private int mCurrentUserId = UserHandle.USER_SYSTEM; // Pending actions private boolean mPendingMetaAction; @@ -222,7 +237,7 @@ final class KeyGestureController { @Override public void execute() { - handleKeyGesture( + handleMultiKeyGesture( new int[]{KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_POWER}, KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD, KeyGestureEvent.ACTION_GESTURE_START, 0); @@ -230,7 +245,7 @@ final class KeyGestureController { @Override public void cancel() { - handleKeyGesture( + handleMultiKeyGesture( new int[]{KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_POWER}, KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD, KeyGestureEvent.ACTION_GESTURE_COMPLETE, @@ -250,14 +265,14 @@ final class KeyGestureController { @Override public void execute() { - handleKeyGesture(new int[]{KeyEvent.KEYCODE_POWER, + handleMultiKeyGesture(new int[]{KeyEvent.KEYCODE_POWER, KeyEvent.KEYCODE_STEM_PRIMARY}, KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD, KeyGestureEvent.ACTION_GESTURE_START, 0); } @Override public void cancel() { - handleKeyGesture(new int[]{KeyEvent.KEYCODE_POWER, + handleMultiKeyGesture(new int[]{KeyEvent.KEYCODE_POWER, KeyEvent.KEYCODE_STEM_PRIMARY}, KeyGestureEvent.KEY_GESTURE_TYPE_SCREENSHOT_CHORD, KeyGestureEvent.ACTION_GESTURE_COMPLETE, @@ -278,7 +293,7 @@ final class KeyGestureController { @Override public void execute() { - handleKeyGesture( + handleMultiKeyGesture( new int[]{KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_VOLUME_UP}, KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD, KeyGestureEvent.ACTION_GESTURE_START, 0); @@ -286,7 +301,7 @@ final class KeyGestureController { @Override public void cancel() { - handleKeyGesture( + handleMultiKeyGesture( new int[]{KeyEvent.KEYCODE_VOLUME_DOWN, KeyEvent.KEYCODE_VOLUME_UP}, KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT_CHORD, KeyGestureEvent.ACTION_GESTURE_COMPLETE, @@ -318,7 +333,7 @@ final class KeyGestureController { if (gestureType == KeyGestureEvent.KEY_GESTURE_TYPE_UNSPECIFIED) { return; } - handleKeyGesture( + handleMultiKeyGesture( new int[]{KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_POWER}, gestureType, KeyGestureEvent.ACTION_GESTURE_START, 0); } @@ -328,7 +343,7 @@ final class KeyGestureController { if (gestureType == KeyGestureEvent.KEY_GESTURE_TYPE_UNSPECIFIED) { return; } - handleKeyGesture( + handleMultiKeyGesture( new int[]{KeyEvent.KEYCODE_VOLUME_UP, KeyEvent.KEYCODE_POWER}, gestureType, KeyGestureEvent.ACTION_GESTURE_COMPLETE, KeyGestureEvent.FLAG_CANCELLED); @@ -362,7 +377,7 @@ final class KeyGestureController { @Override public void execute() { - handleKeyGesture( + handleMultiKeyGesture( new int[]{KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN}, KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD, KeyGestureEvent.ACTION_GESTURE_START, 0); @@ -370,7 +385,7 @@ final class KeyGestureController { @Override public void cancel() { - handleKeyGesture( + handleMultiKeyGesture( new int[]{KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_DOWN}, KeyGestureEvent.KEY_GESTURE_TYPE_TV_ACCESSIBILITY_SHORTCUT_CHORD, KeyGestureEvent.ACTION_GESTURE_COMPLETE, @@ -399,14 +414,14 @@ final class KeyGestureController { @Override public void execute() { - handleKeyGesture( + handleMultiKeyGesture( new int[]{KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_CENTER}, KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT, KeyGestureEvent.ACTION_GESTURE_START, 0); } @Override public void cancel() { - handleKeyGesture( + handleMultiKeyGesture( new int[]{KeyEvent.KEYCODE_BACK, KeyEvent.KEYCODE_DPAD_CENTER}, KeyGestureEvent.KEY_GESTURE_TYPE_TV_TRIGGER_BUG_REPORT, KeyGestureEvent.ACTION_GESTURE_COMPLETE, @@ -481,7 +496,7 @@ final class KeyGestureController { private boolean interceptSystemKeysAndShortcuts(IBinder focusedToken, KeyEvent event) { final int keyCode = event.getKeyCode(); final int repeatCount = event.getRepeatCount(); - final int metaState = event.getMetaState(); + final int metaState = event.getMetaState() & SHORTCUT_META_MASK; final boolean down = event.getAction() == KeyEvent.ACTION_DOWN; final boolean canceled = event.isCanceled(); final int displayId = event.getDisplayId(); @@ -504,7 +519,7 @@ final class KeyGestureController { return handleKeyGesture(deviceId, new int[]{keyCode}, KeyEvent.META_META_ON, KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_ASSISTANT, KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, - focusedToken, /* flags = */0); + focusedToken, /* flags = */0, /* appLaunchData = */null); } break; case KeyEvent.KEYCODE_RECENT_APPS: @@ -512,7 +527,7 @@ final class KeyGestureController { handleKeyGesture(deviceId, new int[]{keyCode}, /* modifierState = */0, KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS, KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, - focusedToken, /* flags = */0); + focusedToken, /* flags = */0, /* appLaunchData = */null); } return true; case KeyEvent.KEYCODE_APP_SWITCH: @@ -520,12 +535,13 @@ final class KeyGestureController { handleKeyGesture(deviceId, new int[]{keyCode}, /* modifierState = */0, KeyGestureEvent.KEY_GESTURE_TYPE_APP_SWITCH, KeyGestureEvent.ACTION_GESTURE_START, displayId, - focusedToken, /* flags = */0); + focusedToken, /* flags = */0, /* appLaunchData = */null); } else if (!down) { handleKeyGesture(deviceId, new int[]{keyCode}, /* modifierState = */0, KeyGestureEvent.KEY_GESTURE_TYPE_APP_SWITCH, KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, - focusedToken, canceled ? KeyGestureEvent.FLAG_CANCELLED : 0); + focusedToken, canceled ? KeyGestureEvent.FLAG_CANCELLED : 0, + /* appLaunchData = */null); } return true; case KeyEvent.KEYCODE_H: @@ -534,7 +550,7 @@ final class KeyGestureController { return handleKeyGesture(deviceId, new int[]{keyCode}, KeyEvent.META_META_ON, KeyGestureEvent.KEY_GESTURE_TYPE_HOME, KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, - focusedToken, /* flags = */0); + focusedToken, /* flags = */0, /* appLaunchData = */null); } break; case KeyEvent.KEYCODE_I: @@ -542,7 +558,7 @@ final class KeyGestureController { return handleKeyGesture(deviceId, new int[]{keyCode}, KeyEvent.META_META_ON, KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS, KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, - focusedToken, /* flags = */0); + focusedToken, /* flags = */0, /* appLaunchData = */null); } break; case KeyEvent.KEYCODE_L: @@ -550,7 +566,7 @@ final class KeyGestureController { return handleKeyGesture(deviceId, new int[]{keyCode}, KeyEvent.META_META_ON, KeyGestureEvent.KEY_GESTURE_TYPE_LOCK_SCREEN, KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, - focusedToken, /* flags = */0); + focusedToken, /* flags = */0, /* appLaunchData = */null); } break; case KeyEvent.KEYCODE_N: @@ -560,13 +576,13 @@ final class KeyGestureController { KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON, KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_NOTES, KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, - focusedToken, /* flags = */0); + focusedToken, /* flags = */0, /* appLaunchData = */null); } else { return handleKeyGesture(deviceId, new int[]{keyCode}, KeyEvent.META_META_ON, KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL, KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, - focusedToken, /* flags = */0); + focusedToken, /* flags = */0, /* appLaunchData = */null); } } break; @@ -576,7 +592,7 @@ final class KeyGestureController { KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON, KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT, KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, - focusedToken, /* flags = */0); + focusedToken, /* flags = */0, /* appLaunchData = */null); } break; case KeyEvent.KEYCODE_T: @@ -586,7 +602,7 @@ final class KeyGestureController { KeyEvent.META_META_ON | KeyEvent.META_ALT_ON, KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_TALKBACK, KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, - focusedToken, /* flags = */0); + focusedToken, /* flags = */0, /* appLaunchData = */null); } } break; @@ -598,7 +614,7 @@ final class KeyGestureController { KeyEvent.META_META_ON | KeyEvent.META_ALT_ON, KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_BOUNCE_KEYS, KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, - focusedToken, /* flags = */0); + focusedToken, /* flags = */0, /* appLaunchData = */null); } } break; @@ -610,7 +626,7 @@ final class KeyGestureController { KeyEvent.META_META_ON | KeyEvent.META_ALT_ON, KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MOUSE_KEYS, KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, - focusedToken, /* flags = */0); + focusedToken, /* flags = */0, /* appLaunchData = */null); } } break; @@ -622,7 +638,7 @@ final class KeyGestureController { KeyEvent.META_META_ON | KeyEvent.META_ALT_ON, KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_STICKY_KEYS, KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, - focusedToken, /* flags = */0); + focusedToken, /* flags = */0, /* appLaunchData = */null); } } break; @@ -634,7 +650,7 @@ final class KeyGestureController { KeyEvent.META_META_ON | KeyEvent.META_ALT_ON, KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_SLOW_KEYS, KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, - focusedToken, /* flags = */0); + focusedToken, /* flags = */0, /* appLaunchData = */null); } } break; @@ -646,7 +662,7 @@ final class KeyGestureController { KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON, KeyGestureEvent.KEY_GESTURE_TYPE_TRIGGER_BUG_REPORT, KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, - focusedToken, /* flags = */0); + focusedToken, /* flags = */0, /* appLaunchData = */null); } } // fall through @@ -655,7 +671,7 @@ final class KeyGestureController { return handleKeyGesture(deviceId, new int[]{keyCode}, KeyEvent.META_META_ON, KeyGestureEvent.KEY_GESTURE_TYPE_BACK, KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, - focusedToken, /* flags = */0); + focusedToken, /* flags = */0, /* appLaunchData = */null); } break; case KeyEvent.KEYCODE_DPAD_UP: @@ -664,7 +680,7 @@ final class KeyGestureController { KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON, KeyGestureEvent.KEY_GESTURE_TYPE_MULTI_WINDOW_NAVIGATION, KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, - focusedToken, /* flags = */0); + focusedToken, /* flags = */0, /* appLaunchData = */null); } break; case KeyEvent.KEYCODE_DPAD_DOWN: @@ -673,7 +689,7 @@ final class KeyGestureController { KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON, KeyGestureEvent.KEY_GESTURE_TYPE_DESKTOP_MODE, KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, - focusedToken, /* flags = */0); + focusedToken, /* flags = */0, /* appLaunchData = */null); } break; case KeyEvent.KEYCODE_DPAD_LEFT: @@ -683,19 +699,19 @@ final class KeyGestureController { KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON, KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_LEFT, KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, - focusedToken, /* flags = */0); + focusedToken, /* flags = */0, /* appLaunchData = */null); } else if (event.isAltPressed()) { return handleKeyGesture(deviceId, new int[]{keyCode}, KeyEvent.META_META_ON | KeyEvent.META_ALT_ON, KeyGestureEvent.KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_LEFT, KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, - focusedToken, /* flags = */0); + focusedToken, /* flags = */0, /* appLaunchData = */null); } else { return handleKeyGesture(deviceId, new int[]{keyCode}, KeyEvent.META_META_ON, KeyGestureEvent.KEY_GESTURE_TYPE_BACK, KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, - focusedToken, /* flags = */0); + focusedToken, /* flags = */0, /* appLaunchData = */null); } } break; @@ -706,13 +722,13 @@ final class KeyGestureController { KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON, KeyGestureEvent.KEY_GESTURE_TYPE_SPLIT_SCREEN_NAVIGATION_RIGHT, KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, - focusedToken, /* flags = */0); + focusedToken, /* flags = */0, /* appLaunchData = */null); } else if (event.isAltPressed()) { return handleKeyGesture(deviceId, new int[]{keyCode}, KeyEvent.META_META_ON | KeyEvent.META_ALT_ON, KeyGestureEvent.KEY_GESTURE_TYPE_CHANGE_SPLITSCREEN_FOCUS_RIGHT, KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, - focusedToken, /* flags = */0); + focusedToken, /* flags = */0, /* appLaunchData = */null); } } break; @@ -723,8 +739,51 @@ final class KeyGestureController { KeyEvent.META_META_ON | KeyEvent.META_CTRL_ON, KeyGestureEvent.KEY_GESTURE_TYPE_MOVE_TO_NEXT_DISPLAY, KeyGestureEvent.ACTION_GESTURE_COMPLETE, - displayId, - focusedToken, /* flags = */0); + displayId, focusedToken, /* flags = */0, /* appLaunchData = */null); + } + } + break; + case KeyEvent.KEYCODE_LEFT_BRACKET: + if (enableTaskResizingKeyboardShortcuts()) { + if (firstDown && event.isAltPressed()) { + return handleKeyGesture(deviceId, new int[]{keyCode}, + KeyEvent.META_ALT_ON, + KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_LEFT_FREEFORM_WINDOW, + KeyGestureEvent.ACTION_GESTURE_COMPLETE, + displayId, focusedToken, /* flags = */0, /* appLaunchData = */null); + } + } + break; + case KeyEvent.KEYCODE_RIGHT_BRACKET: + if (enableTaskResizingKeyboardShortcuts()) { + if (firstDown && event.isAltPressed()) { + return handleKeyGesture(deviceId, new int[]{keyCode}, + KeyEvent.META_ALT_ON, + KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW, + KeyGestureEvent.ACTION_GESTURE_COMPLETE, + displayId, focusedToken, /* flags = */0, /* appLaunchData = */null); + } + } + break; + case KeyEvent.KEYCODE_EQUALS: + if (enableTaskResizingKeyboardShortcuts()) { + if (firstDown && event.isAltPressed()) { + return handleKeyGesture(deviceId, new int[]{keyCode}, + KeyEvent.META_ALT_ON, + KeyGestureEvent.KEY_GESTURE_TYPE_MAXIMIZE_FREEFORM_WINDOW, + KeyGestureEvent.ACTION_GESTURE_COMPLETE, + displayId, focusedToken, /* flags = */0, /* appLaunchData = */null); + } + } + break; + case KeyEvent.KEYCODE_MINUS: + if (enableTaskResizingKeyboardShortcuts()) { + if (firstDown && event.isAltPressed()) { + return handleKeyGesture(deviceId, new int[]{keyCode}, + KeyEvent.META_ALT_ON, + KeyGestureEvent.KEY_GESTURE_TYPE_RESTORE_FREEFORM_WINDOW_SIZE, + KeyGestureEvent.ACTION_GESTURE_COMPLETE, + displayId, focusedToken, /* flags = */0, /* appLaunchData = */null); } } break; @@ -733,7 +792,7 @@ final class KeyGestureController { return handleKeyGesture(deviceId, new int[]{keyCode}, KeyEvent.META_META_ON, KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_SHORTCUT_HELPER, KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, - focusedToken, /* flags = */0); + focusedToken, /* flags = */0, /* appLaunchData = */null); } break; case KeyEvent.KEYCODE_BRIGHTNESS_UP: @@ -744,7 +803,7 @@ final class KeyGestureController { ? KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_UP : KeyGestureEvent.KEY_GESTURE_TYPE_BRIGHTNESS_DOWN, KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, - focusedToken, /* flags = */0); + focusedToken, /* flags = */0, /* appLaunchData = */null); } return true; case KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_DOWN: @@ -752,7 +811,7 @@ final class KeyGestureController { handleKeyGesture(deviceId, new int[]{keyCode}, /* modifierState = */0, KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_DOWN, KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, - focusedToken, /* flags = */0); + focusedToken, /* flags = */0, /* appLaunchData = */null); } return true; case KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_UP: @@ -760,7 +819,7 @@ final class KeyGestureController { handleKeyGesture(deviceId, new int[]{keyCode}, /* modifierState = */0, KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_UP, KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, - focusedToken, /* flags = */0); + focusedToken, /* flags = */0, /* appLaunchData = */null); } return true; case KeyEvent.KEYCODE_KEYBOARD_BACKLIGHT_TOGGLE: @@ -769,7 +828,7 @@ final class KeyGestureController { handleKeyGesture(deviceId, new int[]{keyCode}, /* modifierState = */0, KeyGestureEvent.KEY_GESTURE_TYPE_KEYBOARD_BACKLIGHT_TOGGLE, KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, - focusedToken, /* flags = */0); + focusedToken, /* flags = */0, /* appLaunchData = */null); } return true; case KeyEvent.KEYCODE_ALL_APPS: @@ -777,7 +836,7 @@ final class KeyGestureController { handleKeyGesture(deviceId, new int[]{keyCode}, /* modifierState = */0, KeyGestureEvent.KEY_GESTURE_TYPE_ALL_APPS, KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, - focusedToken, /* flags = */0); + focusedToken, /* flags = */0, /* appLaunchData = */null); } return true; case KeyEvent.KEYCODE_NOTIFICATION: @@ -785,7 +844,7 @@ final class KeyGestureController { handleKeyGesture(deviceId, new int[]{keyCode}, /* modifierState = */0, KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL, KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, - focusedToken, /* flags = */0); + focusedToken, /* flags = */0, /* appLaunchData = */null); } return true; case KeyEvent.KEYCODE_SEARCH: @@ -793,7 +852,7 @@ final class KeyGestureController { return handleKeyGesture(deviceId, new int[]{keyCode}, /* modifierState = */0, KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SEARCH, KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, - focusedToken, /* flags = */0); + focusedToken, /* flags = */0, /* appLaunchData = */null); } break; @@ -804,13 +863,13 @@ final class KeyGestureController { new int[]{keyCode}, /* modifierState = */0, KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_SYSTEM_SETTINGS, KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, - focusedToken, /* flags = */0); + focusedToken, /* flags = */0, /* appLaunchData = */null); } else if (mSettingsKeyBehavior == SETTINGS_KEY_BEHAVIOR_NOTIFICATION_PANEL) { handleKeyGesture(deviceId, new int[]{keyCode}, /* modifierState = */0, KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_NOTIFICATION_PANEL, KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, - focusedToken, /* flags = */0); + focusedToken, /* flags = */0, /* appLaunchData = */null); } } return true; @@ -820,7 +879,7 @@ final class KeyGestureController { event.isShiftPressed() ? KeyEvent.META_SHIFT_ON : 0, KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH, KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, - focusedToken, /* flags = */0); + focusedToken, /* flags = */0, /* appLaunchData = */null); } return true; case KeyEvent.KEYCODE_CAPS_LOCK: @@ -830,7 +889,8 @@ final class KeyGestureController { AidlKeyGestureEvent eventToNotify = createKeyGestureEvent(deviceId, new int[]{keyCode}, metaState, KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK, - KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, /* flags = */0); + KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, /* flags = */0, + /* appLaunchData = */null); Message msg = Message.obtain(mHandler, MSG_NOTIFY_KEY_GESTURE_EVENT, eventToNotify); mHandler.sendMessage(msg); @@ -841,7 +901,7 @@ final class KeyGestureController { handleKeyGesture(deviceId, new int[]{keyCode}, /* modifierState = */0, KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT, KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, - focusedToken, /* flags = */0); + focusedToken, /* flags = */0, /* appLaunchData = */null); } return true; case KeyEvent.KEYCODE_META_LEFT: @@ -862,7 +922,7 @@ final class KeyGestureController { KeyEvent.KEYCODE_ALT_LEFT}, /* modifierState = */0, KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK, KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, - focusedToken, /* flags = */0); + focusedToken, /* flags = */0, /* appLaunchData = */null); } else if (mPendingMetaAction) { mPendingMetaAction = false; @@ -871,7 +931,7 @@ final class KeyGestureController { /* modifierState = */0, KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_ALL_APPS, KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, - focusedToken, /* flags = */0); + focusedToken, /* flags = */0, /* appLaunchData = */null); } } } @@ -882,7 +942,7 @@ final class KeyGestureController { return handleKeyGesture(deviceId, new int[]{keyCode}, KeyEvent.META_META_ON, KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS, KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, - focusedToken, /* flags = */0); + focusedToken, /* flags = */0, /* appLaunchData = */null); } else if (!mPendingHideRecentSwitcher) { final int shiftlessModifiers = event.getModifiers() & ~KeyEvent.META_SHIFT_MASK; @@ -893,7 +953,7 @@ final class KeyGestureController { KeyEvent.META_ALT_ON, KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER, KeyGestureEvent.ACTION_GESTURE_START, displayId, - focusedToken, /* flags = */0); + focusedToken, /* flags = */0, /* appLaunchData = */null); } } } @@ -914,7 +974,7 @@ final class KeyGestureController { KeyEvent.META_ALT_ON, KeyGestureEvent.KEY_GESTURE_TYPE_RECENT_APPS_SWITCHER, KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, - focusedToken, /* flags = */0); + focusedToken, /* flags = */0, /* appLaunchData = */null); } // Toggle Caps Lock on META-ALT. @@ -924,7 +984,7 @@ final class KeyGestureController { KeyEvent.KEYCODE_ALT_LEFT}, /* modifierState = */0, KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_CAPS_LOCK, KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, - focusedToken, /* flags = */0); + focusedToken, /* flags = */0, /* appLaunchData = */null); } } break; @@ -943,6 +1003,22 @@ final class KeyGestureController { + " interceptKeyBeforeQueueing"); return true; } + + if (firstDown) { + InputGestureData customGesture; + synchronized (mUserLock) { + customGesture = mInputGestureManager.getCustomGestureForKeyEvent(mCurrentUserId, + event); + } + if (customGesture == null) { + return false; + } + return handleKeyGesture(deviceId, new int[]{keyCode}, metaState, + customGesture.getAction().keyGestureType(), + KeyGestureEvent.ACTION_GESTURE_COMPLETE, + displayId, focusedToken, /* flags = */0, + customGesture.getAction().appLaunchData()); + } return false; } @@ -965,7 +1041,7 @@ final class KeyGestureController { ? KeyEvent.META_SHIFT_ON : 0), KeyGestureEvent.KEY_GESTURE_TYPE_LANGUAGE_SWITCH, KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, - focusedToken, /* flags = */0); + focusedToken, /* flags = */0, /* appLaunchData = */null); } } break; @@ -977,7 +1053,7 @@ final class KeyGestureController { KeyEvent.META_CTRL_ON | KeyEvent.META_ALT_ON, KeyGestureEvent.KEY_GESTURE_TYPE_ACCESSIBILITY_SHORTCUT, KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, - focusedToken, /* flags = */0); + focusedToken, /* flags = */0, /* appLaunchData = */null); } break; case KeyEvent.KEYCODE_SYSRQ: @@ -985,7 +1061,7 @@ final class KeyGestureController { return handleKeyGesture(deviceId, new int[]{keyCode}, /* modifierState = */0, KeyGestureEvent.KEY_GESTURE_TYPE_TAKE_SCREENSHOT, KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, - focusedToken, /* flags = */0); + focusedToken, /* flags = */0, /* appLaunchData = */null); } break; case KeyEvent.KEYCODE_ESCAPE: @@ -993,7 +1069,7 @@ final class KeyGestureController { return handleKeyGesture(deviceId, new int[]{keyCode}, /* modifierState = */0, KeyGestureEvent.KEY_GESTURE_TYPE_CLOSE_ALL_DIALOGS, KeyGestureEvent.ACTION_GESTURE_COMPLETE, displayId, - focusedToken, /* flags = */0); + focusedToken, /* flags = */0, /* appLaunchData = */null); } break; } @@ -1001,18 +1077,19 @@ final class KeyGestureController { return false; } - private boolean handleKeyGesture(int[] keycodes, + private void handleMultiKeyGesture(int[] keycodes, @KeyGestureEvent.KeyGestureType int gestureType, int action, int flags) { - return handleKeyGesture(KeyCharacterMap.VIRTUAL_KEYBOARD, keycodes, /* modifierState= */0, - gestureType, action, Display.DEFAULT_DISPLAY, /* focusedToken = */null, flags); + handleKeyGesture(KeyCharacterMap.VIRTUAL_KEYBOARD, keycodes, /* modifierState= */0, + gestureType, action, Display.DEFAULT_DISPLAY, /* focusedToken = */null, flags, + /* appLaunchData = */null); } @VisibleForTesting boolean handleKeyGesture(int deviceId, int[] keycodes, int modifierState, @KeyGestureEvent.KeyGestureType int gestureType, int action, int displayId, - @Nullable IBinder focusedToken, int flags) { + @Nullable IBinder focusedToken, int flags, @Nullable AppLaunchData appLaunchData) { return handleKeyGesture(createKeyGestureEvent(deviceId, keycodes, - modifierState, gestureType, action, displayId, flags), focusedToken); + modifierState, gestureType, action, displayId, flags, appLaunchData), focusedToken); } private boolean handleKeyGesture(AidlKeyGestureEvent event, @Nullable IBinder focusedToken) { @@ -1044,18 +1121,27 @@ final class KeyGestureController { // TODO(b/358569822): Once we move the gesture detection logic to IMS, we ideally // should not rely on PWM to tell us about the gesture start and end. AidlKeyGestureEvent event = createKeyGestureEvent(deviceId, keycodes, modifierState, - gestureType, KeyGestureEvent.ACTION_GESTURE_COMPLETE, Display.DEFAULT_DISPLAY, 0); + gestureType, KeyGestureEvent.ACTION_GESTURE_COMPLETE, Display.DEFAULT_DISPLAY, + /* flags = */0, /* appLaunchData = */null); mHandler.obtainMessage(MSG_NOTIFY_KEY_GESTURE_EVENT, event).sendToTarget(); } public void handleKeyGesture(int deviceId, int[] keycodes, int modifierState, @KeyGestureEvent.KeyGestureType int gestureType) { AidlKeyGestureEvent event = createKeyGestureEvent(deviceId, keycodes, modifierState, - gestureType, KeyGestureEvent.ACTION_GESTURE_COMPLETE, Display.DEFAULT_DISPLAY, 0); + gestureType, KeyGestureEvent.ACTION_GESTURE_COMPLETE, Display.DEFAULT_DISPLAY, + /* flags = */0, /* appLaunchData = */null); handleKeyGesture(event, null /*focusedToken*/); } @MainThread + public void setCurrentUserId(@UserIdInt int userId) { + synchronized (mUserLock) { + mCurrentUserId = userId; + } + } + + @MainThread private void notifyKeyGestureEvent(AidlKeyGestureEvent event) { InputDevice device = getInputDevice(event.deviceId); if (device == null) { @@ -1134,6 +1220,37 @@ final class KeyGestureController { } } + @BinderThread + @InputManager.CustomInputGestureResult + public int addCustomInputGesture(@UserIdInt int userId, + @NonNull AidlInputGestureData inputGestureData) { + return mInputGestureManager.addCustomInputGesture(userId, + new InputGestureData(inputGestureData)); + } + + @BinderThread + @InputManager.CustomInputGestureResult + public int removeCustomInputGesture(@UserIdInt int userId, + @NonNull AidlInputGestureData inputGestureData) { + return mInputGestureManager.removeCustomInputGesture(userId, + new InputGestureData(inputGestureData)); + } + + @BinderThread + public void removeAllCustomInputGestures(@UserIdInt int userId) { + mInputGestureManager.removeAllCustomInputGestures(userId); + } + + @BinderThread + public AidlInputGestureData[] getCustomInputGestures(@UserIdInt int userId) { + List<InputGestureData> customGestures = mInputGestureManager.getCustomInputGestures(userId); + AidlInputGestureData[] result = new AidlInputGestureData[customGestures.size()]; + for (int i = 0; i < customGestures.size(); i++) { + result[i] = customGestures.get(i).getAidlData(); + } + return result; + } + private void onKeyGestureEventListenerDied(int pid) { synchronized (mKeyGestureEventListenerRecords) { mKeyGestureEventListenerRecords.remove(pid); @@ -1281,7 +1398,7 @@ final class KeyGestureController { private AidlKeyGestureEvent createKeyGestureEvent(int deviceId, int[] keycodes, int modifierState, @KeyGestureEvent.KeyGestureType int gestureType, int action, - int displayId, int flags) { + int displayId, int flags, @Nullable AppLaunchData appLaunchData) { AidlKeyGestureEvent event = new AidlKeyGestureEvent(); event.deviceId = deviceId; event.keycodes = keycodes; @@ -1290,6 +1407,18 @@ final class KeyGestureController { event.action = action; event.displayId = displayId; event.flags = flags; + if (appLaunchData != null) { + if (appLaunchData instanceof AppLaunchData.CategoryData categoryData) { + event.appLaunchCategory = categoryData.getCategory(); + } else if (appLaunchData instanceof AppLaunchData.RoleData roleData) { + event.appLaunchRole = roleData.getRole(); + } else if (appLaunchData instanceof AppLaunchData.ComponentData componentData) { + event.appLaunchPackageName = componentData.getPackageName(); + event.appLaunchClassName = componentData.getClassName(); + } else { + throw new IllegalArgumentException("AppLaunchData type is invalid!"); + } + } return event; } @@ -1335,5 +1464,6 @@ final class KeyGestureController { } ipw.decreaseIndent(); mKeyCombinationManager.dump("", ipw); + mInputGestureManager.dump(ipw); } } diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 15d76a27490e..010437337ba1 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -1082,6 +1082,19 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. AdditionalSubtypeMapRepository.remove(userId); InputMethodSettingsRepository.remove(userId); mService.mUserDataRepository.remove(userId); + synchronized (ImfLock.class) { + final int nextOrCurrentUser = mService.mUserSwitchHandlerTask != null + ? mService.mUserSwitchHandlerTask.mToUserId : mService.mCurrentImeUserId; + if (!mService.mConcurrentMultiUserModeEnabled && userId == nextOrCurrentUser) { + // The current user was removed without an ongoing switch, or the user targeted + // by the ongoing switch was removed. Switch to the current non-profile user + // to allow starting input on it or one of its profile users later. + // Note: non-profile users cannot be removed while they are the current user. + final int currentUserId = mService.mActivityManagerInternal.getCurrentUserId(); + mService.scheduleSwitchUserTaskLocked(currentUserId, + null /* clientToBeReset */); + } + } } @Override @@ -1332,7 +1345,7 @@ public final class InputMethodManagerService implements IInputMethodManagerImpl. + " prevUserId=" + prevUserId); } - // Clean up stuff for mCurrentUserId, which soon becomes the previous user. + // Clean up stuff for mCurrentImeUserId, which soon becomes the previous user. // TODO(b/338461930): Check if this is still necessary or not. onUnbindCurrentMethodByReset(prevUserId); diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java index bbdac5636fa4..c314ab06fbad 100644 --- a/services/core/java/com/android/server/locksettings/LockSettingsService.java +++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java @@ -253,10 +253,10 @@ public class LockSettingsService extends ILockSettings.Stub { private static final String MIGRATED_FRP2 = "migrated_frp2"; private static final String MIGRATED_KEYSTORE_NS = "migrated_keystore_namespace"; - private static final String MIGRATED_SP_CE_ONLY = "migrated_all_users_to_sp_and_bound_ce"; private static final String MIGRATED_SP_FULL = "migrated_all_users_to_sp_and_bound_keys"; private static final String MIGRATED_WEAVER_DISABLED_ON_UNSECURED_USERS = "migrated_weaver_disabled_on_unsecured_users"; + // Note: some other migrated_* strings used to be used and may exist in the database already. // Duration that LockSettingsService will store the gatekeeper password for. This allows // multiple biometric enrollments without prompting the user to enter their password via @@ -347,6 +347,8 @@ public class LockSettingsService extends ILockSettings.Stub { private final StorageManagerInternal mStorageManagerInternal; + private final Object mGcWorkToken = new Object(); + // This class manages life cycle events for encrypted users on File Based Encryption (FBE) // devices. The most basic of these is to show/hide notifications about missing features until // the user unlocks the account and credential-encrypted storage is available. @@ -1183,9 +1185,7 @@ public class LockSettingsService extends ILockSettings.Stub { // If config_disableWeaverOnUnsecuredUsers=true, then the Weaver HAL may be buggy and // need multiple retries before it works here to unwrap the SP, if the SP was already - // protected by Weaver. Note that the problematic HAL can also deadlock if called with - // the ActivityManagerService lock held, but that should not be a problem here since - // that lock isn't held here, unlike unlockUserKeyIfUnsecured() where it is. + // protected by Weaver. for (int i = 0; i < 12 && sp == null; i++) { Slog.e(TAG, "Failed to unwrap synthetic password. Waiting 5 seconds to retry."); SystemClock.sleep(5000); @@ -1221,21 +1221,16 @@ public class LockSettingsService extends ILockSettings.Stub { Slog.i(TAG, "Synthetic password is already not protected by Weaver"); } } else if (sp == null) { - Slogf.wtf(TAG, "Failed to unwrap synthetic password for unsecured user %d", userId); - return; + throw new IllegalStateException( + "Failed to unwrap synthetic password for unsecured user " + userId); } // Call setCeStorageProtection(), to re-encrypt the CE key with the SP if it's currently - // encrypted by an empty secret. Skip this if it was definitely already done as part of the - // upgrade to Android 14, since while setCeStorageProtection() is idempotent it does log - // some error messages when called again. Do not skip this if - // config_disableWeaverOnUnsecuredUsers=true, since in that case we'd like to recover from - // the case where an earlier upgrade to Android 14 incorrectly skipped this step. - if (getString(MIGRATED_SP_CE_ONLY, null, 0) == null - || isWeaverDisabledOnUnsecuredUsers()) { - Slogf.i(TAG, "Encrypting CE key of user %d with synthetic password", userId); - setCeStorageProtection(userId, sp); - } + // encrypted by an empty secret. If the CE key is already encrypted by the SP, then this is + // a no-op except for some log messages. + Slogf.i(TAG, "Encrypting CE key of user %d with synthetic password", userId); + setCeStorageProtection(userId, sp); + Slogf.i(TAG, "Initializing Keystore super keys for user %d", userId); initKeystoreSuperKeys(userId, sp, /* allowExisting= */ true); } @@ -3639,11 +3634,19 @@ public class LockSettingsService extends ILockSettings.Stub { * release references to the argument. */ private void scheduleGc() { + // Cancel any existing GC request first, so that GC requests don't pile up if lockscreen + // credential operations are happening very quickly, e.g. as sometimes happens during tests. + // + // This delays the already-requested GC, but that is fine in practice where lockscreen + // operations don't happen very quickly. And the precise time that the sanitization happens + // isn't very important; doing it within a minute can be fine, for example. + mHandler.removeCallbacksAndMessages(mGcWorkToken); + mHandler.postDelayed(() -> { System.gc(); System.runFinalization(); System.gc(); - }, 2000); + }, mGcWorkToken, 2000); } private class DeviceProvisionedObserver extends ContentObserver { diff --git a/services/core/java/com/android/server/media/AudioManagerRouteController.java b/services/core/java/com/android/server/media/AudioManagerRouteController.java index 25e1c94aa689..6bc40988c097 100644 --- a/services/core/java/com/android/server/media/AudioManagerRouteController.java +++ b/services/core/java/com/android/server/media/AudioManagerRouteController.java @@ -32,6 +32,7 @@ import android.media.AudioDeviceCallback; import android.media.AudioDeviceInfo; import android.media.AudioManager; import android.media.MediaRoute2Info; +import android.media.audio.Flags; import android.media.audiopolicy.AudioProductStrategy; import android.os.Handler; import android.os.HandlerExecutor; @@ -711,5 +712,13 @@ import java.util.Objects; MediaRoute2Info.TYPE_DOCK, /* defaultRouteId= */ "ROUTE_ID_DOCK_ANALOG", /* nameResource= */ R.string.default_audio_route_name_dock_speakers)); + if (Flags.enableMultichannelGroupDevice()) { + AUDIO_DEVICE_INFO_TYPE_TO_ROUTE_INFO.put( + AudioDeviceInfo.TYPE_MULTICHANNEL_GROUP, + new SystemRouteInfo( + MediaRoute2Info.TYPE_MULTICHANNEL_SPEAKER_GROUP, + /* defaultRouteId= */ "ROUTE_ID_MULTICHANNEL_SPEAKER_GROUP", + /* nameResource= */ R.string.default_audio_route_name_external_device)); + } } } 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 40a27e8ce739..a45ea1d8369d 100644 --- a/services/core/java/com/android/server/media/quality/MediaQualityService.java +++ b/services/core/java/com/android/server/media/quality/MediaQualityService.java @@ -17,10 +17,20 @@ package com.android.server.media.quality; import android.content.Context; +import android.media.quality.AmbientBacklightSettings; +import android.media.quality.IAmbientBacklightCallback; import android.media.quality.IMediaQualityManager; +import android.media.quality.IPictureProfileCallback; +import android.media.quality.ISoundProfileCallback; +import android.media.quality.ParamCapability; +import android.media.quality.PictureProfile; +import android.media.quality.SoundProfile; import com.android.server.SystemService; +import java.util.ArrayList; +import java.util.List; + /** * This service manage picture profile and sound profile for TV setting. Also communicates with the * database to save, update the profiles. @@ -43,5 +53,122 @@ public class MediaQualityService extends SystemService { // TODO: Add additional APIs. b/373951081 private final class BinderService extends IMediaQualityManager.Stub { + @Override + public PictureProfile createPictureProfile(PictureProfile pp) { + // TODO: implement + return pp; + } + @Override + public void updatePictureProfile(long id, PictureProfile pp) { + // TODO: implement + } + @Override + public void removePictureProfile(long id) { + // TODO: implement + } + @Override + public PictureProfile getPictureProfileById(long id) { + return null; + } + @Override + public List<PictureProfile> getPictureProfilesByPackage(String packageName) { + return new ArrayList<>(); + } + @Override + public List<PictureProfile> getAvailablePictureProfiles() { + return new ArrayList<>(); + } + @Override + public List<PictureProfile> getAllPictureProfiles() { + return new ArrayList<>(); + } + + @Override + public SoundProfile createSoundProfile(SoundProfile pp) { + // TODO: implement + return pp; + } + @Override + public void updateSoundProfile(long id, SoundProfile pp) { + // TODO: implement + } + @Override + public void removeSoundProfile(long id) { + // TODO: implement + } + @Override + public SoundProfile getSoundProfileById(long id) { + return null; + } + @Override + public List<SoundProfile> getSoundProfilesByPackage(String packageName) { + return new ArrayList<>(); + } + @Override + public List<SoundProfile> getAvailableSoundProfiles() { + return new ArrayList<>(); + } + @Override + public List<SoundProfile> getAllSoundProfiles() { + return new ArrayList<>(); + } + + + @Override + public void registerPictureProfileCallback(final IPictureProfileCallback callback) { + } + @Override + public void registerSoundProfileCallback(final ISoundProfileCallback callback) { + } + + @Override + public void registerAmbientBacklightCallback(IAmbientBacklightCallback callback) { + } + + @Override + public void setAmbientBacklightSettings(AmbientBacklightSettings settings) { + } + + @Override + public void setAmbientBacklightEnabled(boolean enabled) { + } + + @Override + public List<ParamCapability> getParamCapabilities(List<String> names) { + return new ArrayList<>(); + } + + + @Override + public boolean isSupported() { + return false; + } + + @Override + public void setAutoPictureQualityEnabled(boolean enabled) { + } + + @Override + public boolean isAutoPictureQualityEnabled() { + return false; + } + + @Override + public void setSuperResolutionEnabled(boolean enabled) { + } + + @Override + public boolean isSuperResolutionEnabled() { + return false; + } + + @Override + public void setAutoSoundQualityEnabled(boolean enabled) { + } + + @Override + public boolean isAutoSoundQualityEnabled() { + return false; + } } } diff --git a/services/core/java/com/android/server/notification/NotificationBackupHelper.java b/services/core/java/com/android/server/notification/NotificationBackupHelper.java index ee9ec159a5e0..9df44a41877f 100644 --- a/services/core/java/com/android/server/notification/NotificationBackupHelper.java +++ b/services/core/java/com/android/server/notification/NotificationBackupHelper.java @@ -16,6 +16,8 @@ package com.android.server.notification; +import static android.app.backup.NotificationLoggingConstants.KEY_NOTIFICATIONS; + import android.app.INotificationManager; import android.app.backup.BlobBackupHelper; import android.os.ServiceManager; @@ -31,9 +33,6 @@ public class NotificationBackupHelper extends BlobBackupHelper { // Current version of the blob schema static final int BLOB_VERSION = 1; - // Key under which the payload blob is stored - static final String KEY_NOTIFICATIONS = "notifications"; - private final int mUserId; private final NotificationManagerInternal mNm; diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index abf3da45d0cb..2c45fc89ff0b 100644 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -105,6 +105,7 @@ import static android.os.UserHandle.USER_SYSTEM; import static android.service.notification.Adjustment.KEY_TYPE; import static android.service.notification.Adjustment.TYPE_CONTENT_RECOMMENDATION; import static android.service.notification.Adjustment.TYPE_PROMOTION; +import static android.service.notification.Flags.FLAG_NOTIFICATION_CONVERSATION_CHANNEL_MANAGEMENT; import static android.service.notification.Flags.callstyleCallbackApi; import static android.service.notification.Flags.notificationClassification; import static android.service.notification.Flags.notificationForceGrouping; @@ -161,6 +162,8 @@ import static com.android.internal.util.Preconditions.checkNotNull; import static com.android.server.am.PendingIntentRecord.FLAG_ACTIVITY_SENDER; import static com.android.server.am.PendingIntentRecord.FLAG_BROADCAST_SENDER; import static com.android.server.am.PendingIntentRecord.FLAG_SERVICE_SENDER; +import static android.app.backup.NotificationLoggingConstants.DATA_TYPE_ZEN_CONFIG; +import static android.app.backup.NotificationLoggingConstants.ERROR_XML_PARSING; import static com.android.server.notification.Flags.expireBitmaps; import static com.android.server.policy.PhoneWindowManager.TOAST_WINDOW_ANIM_BUFFER; import static com.android.server.policy.PhoneWindowManager.TOAST_WINDOW_TIMEOUT; @@ -1098,7 +1101,7 @@ public class NotificationManagerService extends SystemService { } void readPolicyXml(InputStream stream, boolean forRestore, int userId, - BackupRestoreEventLogger logger) + @Nullable BackupRestoreEventLogger logger) throws XmlPullParserException, NumberFormatException, IOException { final TypedXmlPullParser parser; if (forRestore) { @@ -1114,7 +1117,27 @@ public class NotificationManagerService extends SystemService { int outerDepth = parser.getDepth(); while (XmlUtils.nextElementWithin(parser, outerDepth)) { if (ZenModeConfig.ZEN_TAG.equals(parser.getName())) { - mZenModeHelper.readXml(parser, forRestore, userId); + int successfulReads = 0; + int unsuccessfulReads = 0; + try { + boolean loadedCorrectly = + mZenModeHelper.readXml(parser, forRestore, userId, logger); + if (loadedCorrectly) + successfulReads++; + else + unsuccessfulReads++; + } catch (Exception e) { + Slog.wtf(TAG, "failed to read config", e); + unsuccessfulReads++; + } + if (logger != null) { + logger.logItemsRestored(DATA_TYPE_ZEN_CONFIG, successfulReads); + if (unsuccessfulReads > 0) { + logger.logItemsRestoreFailed( + DATA_TYPE_ZEN_CONFIG, unsuccessfulReads, ERROR_XML_PARSING); + } + } + } else if (PreferencesHelper.TAG_RANKING.equals(parser.getName())){ mPreferencesHelper.readXml(parser, forRestore, userId); } @@ -1246,7 +1269,7 @@ public class NotificationManagerService extends SystemService { } } - private void writePolicyXml(OutputStream stream, boolean forBackup, int userId, + void writePolicyXml(OutputStream stream, boolean forBackup, int userId, BackupRestoreEventLogger logger) throws IOException { final TypedXmlSerializer out; if (forBackup) { @@ -1258,7 +1281,7 @@ public class NotificationManagerService extends SystemService { out.startDocument(null, true); out.startTag(null, TAG_NOTIFICATION_POLICY); out.attributeInt(null, ATTR_VERSION, DB_VERSION); - mZenModeHelper.writeXml(out, forBackup, null, userId); + mZenModeHelper.writeXml(out, forBackup, null, userId, logger); mPreferencesHelper.writeXml(out, forBackup, userId); mListeners.writeXml(out, forBackup, userId); mAssistants.writeXml(out, forBackup, userId); @@ -5683,14 +5706,16 @@ public class NotificationManagerService extends SystemService { final int zenMode = zenModeFromInterruptionFilter(interruptionFilter, -1); if (zenMode == -1) return; + + UserHandle zenUser = getCallingZenUser(); if (!canManageGlobalZenPolicy(info.component.getPackageName(), callingUid)) { mZenModeHelper.applyGlobalZenModeAsImplicitZenRule( - info.component.getPackageName(), callingUid, zenMode); + zenUser, info.component.getPackageName(), callingUid, zenMode); } else { int origin = computeZenOrigin(/* fromUser= */ false); Binder.withCleanCallingIdentity(() -> { - mZenModeHelper.setManualZenMode(zenMode, /* conditionId= */ null, origin, - "listener:" + info.component.flattenToShortString(), + mZenModeHelper.setManualZenMode(zenUser, zenMode, /* conditionId= */ null, + origin, "listener:" + info.component.flattenToShortString(), /* caller= */ info.component.getPackageName(), callingUid); }); @@ -5745,12 +5770,13 @@ public class NotificationManagerService extends SystemService { public void setZenMode(int mode, Uri conditionId, String reason, boolean fromUser) { enforceSystemOrSystemUI("INotificationManager.setZenMode"); enforceUserOriginOnlyFromSystem(fromUser, "setZenMode"); + UserHandle zenUser = getCallingZenUser(); final int callingUid = Binder.getCallingUid(); final long identity = Binder.clearCallingIdentity(); try { - mZenModeHelper.setManualZenMode(mode, conditionId, computeZenOrigin(fromUser), - reason, /* caller= */ null, callingUid); + mZenModeHelper.setManualZenMode(zenUser, mode, conditionId, + computeZenOrigin(fromUser), reason, /* caller= */ null, callingUid); } finally { Binder.restoreCallingIdentity(identity); } @@ -5760,7 +5786,7 @@ public class NotificationManagerService extends SystemService { @Override public List<ZenModeConfig.ZenRule> getZenRules() throws RemoteException { enforcePolicyAccess(Binder.getCallingUid(), "getZenRules"); - return mZenModeHelper.getZenRules(); + return mZenModeHelper.getZenRules(getCallingZenUser()); } @Override @@ -5769,14 +5795,14 @@ public class NotificationManagerService extends SystemService { throw new IllegalStateException("getAutomaticZenRules called with flag off!"); } enforcePolicyAccess(Binder.getCallingUid(), "getAutomaticZenRules"); - return mZenModeHelper.getAutomaticZenRules(); + return mZenModeHelper.getAutomaticZenRules(getCallingZenUser()); } @Override public AutomaticZenRule getAutomaticZenRule(String id) throws RemoteException { Objects.requireNonNull(id, "Id is null"); enforcePolicyAccess(Binder.getCallingUid(), "getAutomaticZenRule"); - return mZenModeHelper.getAutomaticZenRule(id); + return mZenModeHelper.getAutomaticZenRule(getCallingZenUser(), id); } @Override @@ -5791,6 +5817,7 @@ public class NotificationManagerService extends SystemService { } enforcePolicyAccess(Binder.getCallingUid(), "addAutomaticZenRule"); enforceUserOriginOnlyFromSystem(fromUser, "addAutomaticZenRule"); + UserHandle zenUser = getCallingZenUser(); // If the calling app is the system (from any user), take the package name from the // rule's owner rather than from the caller's package. @@ -5801,16 +5828,18 @@ public class NotificationManagerService extends SystemService { } } - return mZenModeHelper.addAutomaticZenRule(rulePkg, automaticZenRule, + return mZenModeHelper.addAutomaticZenRule(zenUser, rulePkg, automaticZenRule, computeZenOrigin(fromUser), "addAutomaticZenRule", Binder.getCallingUid()); } @Override public void setManualZenRuleDeviceEffects(ZenDeviceEffects effects) throws RemoteException { checkCallerIsSystem(); + UserHandle zenUser = getCallingZenUser(); - mZenModeHelper.setManualZenRuleDeviceEffects(effects, computeZenOrigin(true), - "Update manual mode non-policy settings", Binder.getCallingUid()); + mZenModeHelper.setManualZenRuleDeviceEffects(zenUser, effects, + computeZenOrigin(true), "Update manual mode non-policy settings", + Binder.getCallingUid()); } @Override @@ -5819,8 +5848,9 @@ public class NotificationManagerService extends SystemService { validateAutomaticZenRule(id, automaticZenRule); enforcePolicyAccess(Binder.getCallingUid(), "updateAutomaticZenRule"); enforceUserOriginOnlyFromSystem(fromUser, "updateAutomaticZenRule"); + UserHandle zenUser = getCallingZenUser(); - return mZenModeHelper.updateAutomaticZenRule(id, automaticZenRule, + return mZenModeHelper.updateAutomaticZenRule(zenUser, id, automaticZenRule, computeZenOrigin(fromUser), "updateAutomaticZenRule", Binder.getCallingUid()); } @@ -5886,8 +5916,9 @@ public class NotificationManagerService extends SystemService { // Verify that they can modify zen rules. enforcePolicyAccess(Binder.getCallingUid(), "removeAutomaticZenRule"); enforceUserOriginOnlyFromSystem(fromUser, "removeAutomaticZenRule"); + UserHandle zenUser = getCallingZenUser(); - return mZenModeHelper.removeAutomaticZenRule(id, computeZenOrigin(fromUser), + return mZenModeHelper.removeAutomaticZenRule(zenUser, id, computeZenOrigin(fromUser), "removeAutomaticZenRule", Binder.getCallingUid()); } @@ -5897,9 +5928,11 @@ public class NotificationManagerService extends SystemService { Objects.requireNonNull(packageName, "Package name is null"); enforceSystemOrSystemUI("removeAutomaticZenRules"); enforceUserOriginOnlyFromSystem(fromUser, "removeAutomaticZenRules"); + UserHandle zenUser = getCallingZenUser(); - return mZenModeHelper.removeAutomaticZenRules(packageName, computeZenOrigin(fromUser), - packageName + "|removeAutomaticZenRules", Binder.getCallingUid()); + return mZenModeHelper.removeAutomaticZenRules(zenUser, packageName, + computeZenOrigin(fromUser), packageName + "|removeAutomaticZenRules", + Binder.getCallingUid()); } @Override @@ -5907,7 +5940,7 @@ public class NotificationManagerService extends SystemService { Objects.requireNonNull(owner, "Owner is null"); enforceSystemOrSystemUI("getRuleInstanceCount"); - return mZenModeHelper.getCurrentInstanceCount(owner); + return mZenModeHelper.getCurrentInstanceCount(getCallingZenUser(), owner); } @Override @@ -5915,7 +5948,7 @@ public class NotificationManagerService extends SystemService { public int getAutomaticZenRuleState(@NonNull String id) { Objects.requireNonNull(id, "id is null"); enforcePolicyAccess(Binder.getCallingUid(), "getAutomaticZenRuleState"); - return mZenModeHelper.getAutomaticZenRuleState(id); + return mZenModeHelper.getAutomaticZenRuleState(getCallingZenUser(), id); } @Override @@ -5926,9 +5959,30 @@ public class NotificationManagerService extends SystemService { enforcePolicyAccess(Binder.getCallingUid(), "setAutomaticZenRuleState"); boolean fromUser = (condition.source == Condition.SOURCE_USER_ACTION); + UserHandle zenUser = getCallingZenUser(); - mZenModeHelper.setAutomaticZenRuleState(id, condition, computeZenOrigin(fromUser), - Binder.getCallingUid()); + mZenModeHelper.setAutomaticZenRuleState(zenUser, id, condition, + computeZenOrigin(fromUser), Binder.getCallingUid()); + } + + /** + * Returns the {@link UserHandle} corresponding to the caller that is performing a + * zen-related operation (such as {@link #setInterruptionFilter}, + * {@link #addAutomaticZenRule}, {@link #setAutomaticZenRuleState}, etc). The user is + * {@link UserHandle#USER_CURRENT} if the caller is the system or SystemUI (assuming + * that all interactions in SystemUI are for the "current" user); otherwise it's the user + * associated to the binder call. + */ + private UserHandle getCallingZenUser() { + if (android.app.Flags.modesMultiuser()) { + if (isCallerSystemOrSystemUiOrShell()) { + return UserHandle.CURRENT; + } else { + return Binder.getCallingUserHandle(); + } + } else { + return UserHandle.CURRENT; + } } @ZenModeConfig.ConfigOrigin @@ -5964,15 +6018,16 @@ public class NotificationManagerService extends SystemService { if (zen == -1) throw new IllegalArgumentException("Invalid filter: " + filter); final int callingUid = Binder.getCallingUid(); enforceUserOriginOnlyFromSystem(fromUser, "setInterruptionFilter"); + UserHandle zenUser = getCallingZenUser(); if (android.app.Flags.modesApi() && !canManageGlobalZenPolicy(pkg, callingUid)) { - mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(pkg, callingUid, zen); + mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(zenUser, pkg, callingUid, zen); return; } final long identity = Binder.clearCallingIdentity(); try { - mZenModeHelper.setManualZenMode(zen, null, computeZenOrigin(fromUser), + mZenModeHelper.setManualZenMode(zenUser, zen, null, computeZenOrigin(fromUser), /* reason= */ "setInterruptionFilter", /* caller= */ pkg, callingUid); } finally { @@ -6268,12 +6323,13 @@ public class NotificationManagerService extends SystemService { @Override public Policy getNotificationPolicy(String pkg) { final int callingUid = Binder.getCallingUid(); + UserHandle zenUser = getCallingZenUser(); if (android.app.Flags.modesApi() && !canManageGlobalZenPolicy(pkg, callingUid)) { - return mZenModeHelper.getNotificationPolicyFromImplicitZenRule(pkg); + return mZenModeHelper.getNotificationPolicyFromImplicitZenRule(zenUser, pkg); } final long identity = Binder.clearCallingIdentity(); try { - return mZenModeHelper.getNotificationPolicy(); + return mZenModeHelper.getNotificationPolicy(zenUser); } finally { Binder.restoreCallingIdentity(identity); } @@ -6302,6 +6358,7 @@ public class NotificationManagerService extends SystemService { enforceUserOriginOnlyFromSystem(fromUser, "setNotificationPolicy"); int callingUid = Binder.getCallingUid(); @ZenModeConfig.ConfigOrigin int origin = computeZenOrigin(fromUser); + UserHandle zenUser = getCallingZenUser(); boolean isSystemCaller = isCallerSystemOrSystemUiOrShell(); boolean shouldApplyAsImplicitRule = android.app.Flags.modesApi() @@ -6311,7 +6368,7 @@ public class NotificationManagerService extends SystemService { try { final ApplicationInfo applicationInfo = mPackageManager.getApplicationInfo(pkg, 0, UserHandle.getUserId(callingUid)); - Policy currPolicy = mZenModeHelper.getNotificationPolicy(); + Policy currPolicy = mZenModeHelper.getNotificationPolicy(zenUser); if (applicationInfo.targetSdkVersion < Build.VERSION_CODES.P) { int priorityCategories = policy.priorityCategories; @@ -6369,11 +6426,12 @@ public class NotificationManagerService extends SystemService { } if (shouldApplyAsImplicitRule) { - mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(pkg, callingUid, policy); + mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(zenUser, pkg, callingUid, + policy); } else { ZenLog.traceSetNotificationPolicy(pkg, applicationInfo.targetSdkVersion, policy); - mZenModeHelper.setNotificationPolicy(policy, origin, callingUid); + mZenModeHelper.setNotificationPolicy(zenUser, policy, origin, callingUid); } } catch (RemoteException e) { Slog.e(TAG, "Failed to set notification policy", e); @@ -6624,6 +6682,33 @@ public class NotificationManagerService extends SystemService { } @Override + @FlaggedApi(FLAG_NOTIFICATION_CONVERSATION_CHANNEL_MANAGEMENT) + public NotificationChannel createConversationNotificationChannelForPackageFromPrivilegedListener( + INotificationListener token, String pkg, UserHandle user, + String parentId, String conversationId) throws RemoteException { + Objects.requireNonNull(pkg); + Objects.requireNonNull(user); + Objects.requireNonNull(parentId); + Objects.requireNonNull(conversationId); + + verifyPrivilegedListener(token, user, true); + + int uid = getUidForPackageAndUser(pkg, user); + NotificationChannel conversationChannel = + mPreferencesHelper.getNotificationChannel(pkg, uid, parentId, false).copy(); + String conversationChannelId = String.format( + CONVERSATION_CHANNEL_ID_FORMAT, parentId, conversationId); + conversationChannel.setId(conversationChannelId); + conversationChannel.setConversationId(parentId, conversationId); + createNotificationChannelsImpl( + pkg, uid, new ParceledListSlice(Arrays.asList(conversationChannel))); + handleSavePolicyFile(); + + return mPreferencesHelper.getConversationNotificationChannel( + pkg, uid, parentId, conversationId, false, false).copy(); + } + + @Override public void updateNotificationChannelGroupFromPrivilegedListener( INotificationListener token, String pkg, UserHandle user, NotificationChannelGroup group) throws RemoteException { @@ -6641,7 +6726,7 @@ public class NotificationManagerService extends SystemService { Objects.requireNonNull(pkg); Objects.requireNonNull(user); - verifyPrivilegedListener(token, user, false); + verifyPrivilegedListener(token, user, true); final NotificationChannel originalChannel = mPreferencesHelper.getNotificationChannel( pkg, getUidForPackageAndUser(pkg, user), channel.getId(), true); @@ -9278,10 +9363,15 @@ public class NotificationManagerService extends SystemService { // a group summary or children (complete a group) mHandler.postDelayed(() -> { synchronized (mNotificationLock) { - mGroupHelper.onNotificationPostedWithDelay( - r, mNotificationList, mSummaryByGroupKey); + NotificationRecord record = + mNotificationsByKey.get(key); + if (record != null) { + mGroupHelper.onNotificationPostedWithDelay( + record, mNotificationList, + mSummaryByGroupKey); + } } - }, r.getKey(), DELAY_FORCE_REGROUP_TIME); + }, key, DELAY_FORCE_REGROUP_TIME); } } @@ -9327,10 +9417,15 @@ public class NotificationManagerService extends SystemService { if (notificationForceGrouping()) { mHandler.postDelayed(() -> { synchronized (mNotificationLock) { - mGroupHelper.onNotificationPostedWithDelay(r, - mNotificationList, mSummaryByGroupKey); + NotificationRecord record = + mNotificationsByKey.get(key); + if (record != null) { + mGroupHelper.onNotificationPostedWithDelay( + record, mNotificationList, + mSummaryByGroupKey); + } } - }, r.getKey(), DELAY_FORCE_REGROUP_TIME); + }, key, DELAY_FORCE_REGROUP_TIME); } } } @@ -10310,7 +10405,7 @@ public class NotificationManagerService extends SystemService { } mListeners.notifyRemovedLocked(r, reason, r.getStats()); if (notificationForceGrouping()) { - mHandler.removeCallbacksAndMessages(r.getKey()); + mHandler.removeCallbacksAndEqualMessages(r.getKey()); mHandler.post(() -> { synchronized (NotificationManagerService.this.mNotificationLock) { mGroupHelper.onNotificationRemoved(r, mNotificationList); diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java index d26a5aa5491f..9f0b4b0b6299 100644 --- a/services/core/java/com/android/server/notification/PreferencesHelper.java +++ b/services/core/java/com/android/server/notification/PreferencesHelper.java @@ -2028,8 +2028,9 @@ public class PreferencesHelper implements RankingConfig { * </ul> */ void syncChannelsBypassingDnd() { - mCurrentUserHasChannelsBypassingDnd = (mZenModeHelper.getNotificationPolicy().state - & NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND) != 0; + mCurrentUserHasChannelsBypassingDnd = + (mZenModeHelper.getNotificationPolicy(UserHandle.CURRENT).state + & NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND) != 0; updateCurrentUserHasChannelsBypassingDnd(/* callingUid= */ Process.SYSTEM_UID, /* fromSystemOrSystemUi= */ true); @@ -2072,7 +2073,8 @@ public class PreferencesHelper implements RankingConfig { if (mCurrentUserHasChannelsBypassingDnd != haveBypassingApps) { mCurrentUserHasChannelsBypassingDnd = haveBypassingApps; if (android.app.Flags.modesUi()) { - mZenModeHelper.updateHasPriorityChannels(mCurrentUserHasChannelsBypassingDnd); + mZenModeHelper.updateHasPriorityChannels(UserHandle.CURRENT, + mCurrentUserHasChannelsBypassingDnd); } else { updateZenPolicy(mCurrentUserHasChannelsBypassingDnd, callingUid, fromSystemOrSystemUi); @@ -2099,8 +2101,10 @@ public class PreferencesHelper implements RankingConfig { // PreferencesHelper should otherwise not need to modify actual policy public void updateZenPolicy(boolean areChannelsBypassingDnd, int callingUid, boolean fromSystemOrSystemUi) { - NotificationManager.Policy policy = mZenModeHelper.getNotificationPolicy(); + NotificationManager.Policy policy = mZenModeHelper.getNotificationPolicy( + UserHandle.CURRENT); mZenModeHelper.setNotificationPolicy( + UserHandle.CURRENT, new NotificationManager.Policy( policy.priorityCategories, policy.priorityCallSenders, policy.priorityMessageSenders, policy.suppressedVisualEffects, diff --git a/services/core/java/com/android/server/notification/ZenModeConditions.java b/services/core/java/com/android/server/notification/ZenModeConditions.java index b1f010c38d4e..52d0c41614d5 100644 --- a/services/core/java/com/android/server/notification/ZenModeConditions.java +++ b/services/core/java/com/android/server/notification/ZenModeConditions.java @@ -20,6 +20,7 @@ import android.content.ComponentName; import android.net.Uri; import android.os.Binder; import android.os.Process; +import android.os.UserHandle; import android.service.notification.Condition; import android.service.notification.IConditionProvider; import android.service.notification.ZenModeConfig; @@ -117,7 +118,10 @@ public class ZenModeConditions implements ConditionProviders.Callback { ZenModeConfig config = mHelper.getConfig(); if (config == null) return; final int callingUid = Binder.getCallingUid(); - mHelper.setAutomaticZenRuleState(id, condition, + + // This change is known to be for UserHandle.CURRENT because ConditionProviders for + // background users are not bound. + mHelper.setAutomaticZenRuleState(UserHandle.CURRENT, id, condition, callingUid == Process.SYSTEM_UID ? ZenModeConfig.ORIGIN_SYSTEM : ZenModeConfig.ORIGIN_APP, callingUid); diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java index 5547bd3b34ea..d5f13a8ff495 100644 --- a/services/core/java/com/android/server/notification/ZenModeHelper.java +++ b/services/core/java/com/android/server/notification/ZenModeHelper.java @@ -40,9 +40,12 @@ import static android.service.notification.ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI import static android.service.notification.ZenModeConfig.ZenRule.OVERRIDE_ACTIVATE; import static android.service.notification.ZenModeConfig.ZenRule.OVERRIDE_DEACTIVATE; import static android.service.notification.ZenModeConfig.implicitRuleId; +import static android.service.notification.ZenModeConfig.isImplicitRuleId; import static com.android.internal.util.FrameworkStatsLog.DND_MODE_RULE; import static com.android.internal.util.Preconditions.checkArgument; +import static android.app.backup.NotificationLoggingConstants.DATA_TYPE_ZEN_CONFIG; +import static android.app.backup.NotificationLoggingConstants.ERROR_XML_PARSING; import static java.util.Objects.requireNonNull; @@ -56,6 +59,7 @@ import android.app.AutomaticZenRule; import android.app.Flags; import android.app.NotificationManager; import android.app.NotificationManager.Policy; +import android.app.backup.BackupRestoreEventLogger; import android.app.compat.CompatChanges; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledSince; @@ -167,15 +171,13 @@ public class ZenModeHelper { private final Clock mClock; private final SettingsObserver mSettingsObserver; private final AppOpsManager mAppOps; - private final NotificationManager mNotificationManager; private final ZenModeConfig mDefaultConfig; private final ArrayList<Callback> mCallbacks = new ArrayList<Callback>(); private final ZenModeFiltering mFiltering; private final RingerModeDelegate mRingerModeDelegate = new RingerModeDelegate(); @VisibleForTesting protected final ZenModeConditions mConditions; - private final Object mConfigsArrayLock = new Object(); - @GuardedBy("mConfigsArrayLock") + @GuardedBy("mConfigLock") @VisibleForTesting final SparseArray<ZenModeConfig> mConfigs = new SparseArray<>(); private final Metrics mMetrics = new Metrics(); private final ConditionProviders.Config mServiceConfig; @@ -215,15 +217,14 @@ public class ZenModeHelper { mClock = clock; addCallback(mMetrics); mAppOps = context.getSystemService(AppOpsManager.class); - mNotificationManager = context.getSystemService(NotificationManager.class); mDefaultConfig = Flags.modesUi() ? ZenModeConfig.getDefaultConfig() : readDefaultConfig(mContext.getResources()); updateDefaultConfig(mContext, mDefaultConfig); - mConfig = mDefaultConfig.copy(); - synchronized (mConfigsArrayLock) { + synchronized (mConfigLock) { + mConfig = mDefaultConfig.copy(); mConfigs.put(UserHandle.USER_SYSTEM, mConfig); } mConsolidatedPolicy = mConfig.toNotificationPolicy(); @@ -237,10 +238,6 @@ public class ZenModeHelper { mZenModeEventLogger = zenModeEventLogger; } - public Looper getLooper() { - return mHandler.getLooper(); - } - @Override public String toString() { return TAG; @@ -331,7 +328,7 @@ public class ZenModeHelper { public void onUserRemoved(int user) { if (user < UserHandle.USER_SYSTEM) return; if (DEBUG) Log.d(TAG, "onUserRemoved u=" + user); - synchronized (mConfigsArrayLock) { + synchronized (mConfigLock) { mConfigs.remove(user); } } @@ -350,7 +347,7 @@ public class ZenModeHelper { mUser = user; if (DEBUG) Log.d(TAG, reason + " u=" + user); ZenModeConfig config = null; - synchronized (mConfigsArrayLock) { + synchronized (mConfigLock) { if (mConfigs.get(user) != null) { config = mConfigs.get(user).copy(); } @@ -376,7 +373,9 @@ public class ZenModeHelper { boolean fromSystemOrSystemUi) { final int newZen = NotificationManager.zenModeFromInterruptionFilter(filter, -1); if (newZen != -1) { - setManualZenMode(newZen, null, + // This change is known to be for UserHandle.CURRENT because NLSes for + // background users are unbound. + setManualZenMode(UserHandle.CURRENT, newZen, null, fromSystemOrSystemUi ? ORIGIN_SYSTEM : ORIGIN_APP, /* reason= */ "listener:" + (name != null ? name.flattenToShortString() : null), /* caller= */ name != null ? name.getPackageName() : null, @@ -399,11 +398,12 @@ public class ZenModeHelper { } // TODO: b/310620812 - Make private (or inline) when MODES_API is inlined. - public List<ZenRule> getZenRules() { + public List<ZenRule> getZenRules(UserHandle user) { List<ZenRule> rules = new ArrayList<>(); synchronized (mConfigLock) { - if (mConfig == null) return rules; - for (ZenRule rule : mConfig.automaticRules.values()) { + ZenModeConfig config = getConfigLocked(user); + if (config == null) return rules; + for (ZenRule rule : config.automaticRules.values()) { if (canManageAutomaticZenRule(rule)) { rules.add(rule); } @@ -417,8 +417,8 @@ public class ZenModeHelper { * (which means the owned rules for a regular app, and every rule for system callers) together * with their ids. */ - Map<String, AutomaticZenRule> getAutomaticZenRules() { - List<ZenRule> ruleList = getZenRules(); + Map<String, AutomaticZenRule> getAutomaticZenRules(UserHandle user) { + List<ZenRule> ruleList = getZenRules(user); HashMap<String, AutomaticZenRule> rules = new HashMap<>(ruleList.size()); for (ZenRule rule : ruleList) { rules.put(rule.id, zenRuleToAutomaticZenRule(rule)); @@ -426,11 +426,12 @@ public class ZenModeHelper { return rules; } - public AutomaticZenRule getAutomaticZenRule(String id) { + public AutomaticZenRule getAutomaticZenRule(UserHandle user, String id) { ZenRule rule; synchronized (mConfigLock) { - if (mConfig == null) return null; - rule = mConfig.automaticRules.get(id); + ZenModeConfig config = getConfigLocked(user); + if (config == null) return null; + rule = config.automaticRules.get(id); } if (rule == null) return null; if (canManageAutomaticZenRule(rule)) { @@ -439,8 +440,9 @@ public class ZenModeHelper { return null; } - public String addAutomaticZenRule(String pkg, AutomaticZenRule automaticZenRule, - @ConfigOrigin int origin, String reason, int callingUid) { + public String addAutomaticZenRule(UserHandle user, String pkg, + AutomaticZenRule automaticZenRule, @ConfigOrigin int origin, String reason, + int callingUid) { checkManageRuleOrigin("addAutomaticZenRule", origin); if (!ZenModeConfig.SYSTEM_AUTHORITY.equals(pkg)) { PackageItemInfo component = getServiceInfo(automaticZenRule.getOwner()); @@ -455,10 +457,10 @@ public class ZenModeHelper { ruleInstanceLimit = component.metaData.getInt( ConditionProviderService.META_DATA_RULE_INSTANCE_LIMIT, -1); } - int newRuleInstanceCount = getCurrentInstanceCount(automaticZenRule.getOwner()) - + getCurrentInstanceCount(automaticZenRule.getConfigurationActivity()) + int newRuleInstanceCount = getCurrentInstanceCount(user, automaticZenRule.getOwner()) + + getCurrentInstanceCount(user, automaticZenRule.getConfigurationActivity()) + 1; - int newPackageRuleCount = getPackageRuleCount(pkg) + 1; + int newPackageRuleCount = getPackageRuleCount(user, pkg) + 1; if (newPackageRuleCount > RULE_LIMIT_PER_PACKAGE || (ruleInstanceLimit > 0 && ruleInstanceLimit < newRuleInstanceCount)) { throw new IllegalArgumentException("Rule instance limit exceeded"); @@ -467,15 +469,16 @@ public class ZenModeHelper { ZenModeConfig newConfig; synchronized (mConfigLock) { - if (mConfig == null) { + ZenModeConfig config = getConfigLocked(user); + if (config == null) { throw new AndroidRuntimeException("Could not create rule"); } if (DEBUG) { Log.d(TAG, "addAutomaticZenRule rule= " + automaticZenRule + " reason=" + reason); } - newConfig = mConfig.copy(); + newConfig = config.copy(); ZenRule rule = new ZenRule(); - populateZenRule(pkg, automaticZenRule, rule, origin, /* isNew= */ true); + populateZenRule(pkg, automaticZenRule, newConfig, rule, origin, /* isNew= */ true); rule = maybeRestoreRemovedRule(newConfig, pkg, rule, automaticZenRule, origin); newConfig.automaticRules.put(rule.id, rule); maybeReplaceDefaultRule(newConfig, null, automaticZenRule); @@ -524,7 +527,7 @@ public class ZenModeHelper { // "Preserve" the previous rule by considering the azrToAdd an update instead. // Only app-modifiable fields will actually be modified. - populateZenRule(pkg, azrToAdd, ruleToRestore, origin, /* isNew= */ false); + populateZenRule(pkg, azrToAdd, config, ruleToRestore, origin, /* isNew= */ false); return ruleToRestore; } @@ -558,35 +561,37 @@ public class ZenModeHelper { } } - public boolean updateAutomaticZenRule(String ruleId, AutomaticZenRule automaticZenRule, - @ConfigOrigin int origin, String reason, int callingUid) { + public boolean updateAutomaticZenRule(UserHandle user, String ruleId, + AutomaticZenRule automaticZenRule, @ConfigOrigin int origin, String reason, + int callingUid) { checkManageRuleOrigin("updateAutomaticZenRule", origin); if (ruleId == null) { throw new IllegalArgumentException("ruleId cannot be null"); } synchronized (mConfigLock) { - if (mConfig == null) return false; + ZenModeConfig config = getConfigLocked(user); + if (config == null) return false; if (DEBUG) { Log.d(TAG, "updateAutomaticZenRule zenRule=" + automaticZenRule + " reason=" + reason); } - ZenModeConfig.ZenRule oldRule = mConfig.automaticRules.get(ruleId); + ZenModeConfig.ZenRule oldRule = config.automaticRules.get(ruleId); if (oldRule == null || !canManageAutomaticZenRule(oldRule)) { throw new SecurityException( "Cannot update rules not owned by your condition provider"); } - ZenModeConfig newConfig = mConfig.copy(); + ZenModeConfig newConfig = config.copy(); ZenModeConfig.ZenRule newRule = requireNonNull(newConfig.automaticRules.get(ruleId)); if (!Flags.modesApi()) { if (newRule.enabled != automaticZenRule.isEnabled()) { - dispatchOnAutomaticRuleStatusChanged(mConfig.user, newRule.getPkg(), ruleId, + dispatchOnAutomaticRuleStatusChanged(config.user, newRule.getPkg(), ruleId, automaticZenRule.isEnabled() ? AUTOMATIC_RULE_STATUS_ENABLED : AUTOMATIC_RULE_STATUS_DISABLED); } } - boolean updated = populateZenRule(newRule.pkg, automaticZenRule, newRule, + boolean updated = populateZenRule(newRule.pkg, automaticZenRule, newConfig, newRule, origin, /* isNew= */ false); if (Flags.modesApi() && !updated) { // Bail out so we don't have the side effects of updating a rule (i.e. dropping @@ -619,16 +624,18 @@ public class ZenModeHelper { * * @param zenMode one of the {@code Global#ZEN_MODE_x} values */ - void applyGlobalZenModeAsImplicitZenRule(String callingPkg, int callingUid, int zenMode) { + void applyGlobalZenModeAsImplicitZenRule(UserHandle user, String callingPkg, int callingUid, + int zenMode) { if (!android.app.Flags.modesApi()) { Log.wtf(TAG, "applyGlobalZenModeAsImplicitZenRule called with flag off!"); return; } synchronized (mConfigLock) { - if (mConfig == null) { + ZenModeConfig config = getConfigLocked(user); + if (config == null) { return; } - ZenModeConfig newConfig = mConfig.copy(); + ZenModeConfig newConfig = config.copy(); ZenRule rule = newConfig.automaticRules.get(implicitRuleId(callingPkg)); if (zenMode == Global.ZEN_MODE_OFF) { // Deactivate implicit rule if it exists and is active; otherwise ignore. @@ -650,9 +657,14 @@ public class ZenModeHelper { // would apply if changing the global interruption filter. We only do this // for newly created rules, as existing rules have a pre-existing policy // (whether initialized here or set via app or user). - rule.zenPolicy = mConfig.getZenPolicy().copy(); + rule.zenPolicy = config.getZenPolicy().copy(); newConfig.automaticRules.put(rule.id, rule); + } else { + if (Flags.modesUi()) { + updateImplicitZenRuleNameAndDescription(rule); + } } + // If the user has changed the rule's *zenMode*, then don't let app overwrite it. // We allow the update if the user has only changed other aspects of the rule. if ((rule.userModifiedFields & AutomaticZenRule.FIELD_INTERRUPTION_FILTER) == 0) { @@ -680,17 +692,18 @@ public class ZenModeHelper { * {@link AutomaticZenRule#configurationActivity}. Its zen mode will be set to * {@link Global#ZEN_MODE_IMPORTANT_INTERRUPTIONS}. */ - void applyGlobalPolicyAsImplicitZenRule(String callingPkg, int callingUid, + void applyGlobalPolicyAsImplicitZenRule(UserHandle user, String callingPkg, int callingUid, NotificationManager.Policy policy) { if (!android.app.Flags.modesApi()) { Log.wtf(TAG, "applyGlobalPolicyAsImplicitZenRule called with flag off!"); return; } synchronized (mConfigLock) { - if (mConfig == null) { + ZenModeConfig config = getConfigLocked(user); + if (config == null) { return; } - ZenModeConfig newConfig = mConfig.copy(); + ZenModeConfig newConfig = config.copy(); boolean isNew = false; ZenRule rule = newConfig.automaticRules.get(implicitRuleId(callingPkg)); if (rule == null) { @@ -698,7 +711,12 @@ public class ZenModeHelper { rule = newImplicitZenRule(callingPkg); rule.zenMode = Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; newConfig.automaticRules.put(rule.id, rule); + } else { + if (Flags.modesUi()) { + updateImplicitZenRuleNameAndDescription(rule); + } } + // If the user has changed the rule's *ZenPolicy*, then don't let app overwrite it. // We allow the update if the user has only changed other aspects of the rule. if (rule.zenPolicyUserModifiedFields == 0) { @@ -709,9 +727,10 @@ public class ZenModeHelper { // would take effect if changing the global policy. // Note that NotificationManager.Policy cannot have any unset priority // categories, but *can* have unset visual effects, which is why we do this. - newZenPolicy = mConfig.getZenPolicy().overwrittenWith(newZenPolicy); + newZenPolicy = config.getZenPolicy().overwrittenWith(newZenPolicy); } updatePolicy( + newConfig, rule, newZenPolicy, /* updateBitmask= */ false, @@ -734,25 +753,26 @@ public class ZenModeHelper { * <p>Any unset values in the {@link ZenPolicy} will be mapped to their current defaults. */ @Nullable - Policy getNotificationPolicyFromImplicitZenRule(String callingPkg) { + Policy getNotificationPolicyFromImplicitZenRule(UserHandle user, String callingPkg) { if (!android.app.Flags.modesApi()) { Log.wtf(TAG, "getNotificationPolicyFromImplicitZenRule called with flag off!"); - return getNotificationPolicy(); + return getNotificationPolicy(user); } synchronized (mConfigLock) { - if (mConfig == null) { + ZenModeConfig config = getConfigLocked(user); + if (config == null) { return null; } - ZenRule implicitRule = mConfig.automaticRules.get(implicitRuleId(callingPkg)); + ZenRule implicitRule = config.automaticRules.get(implicitRuleId(callingPkg)); if (implicitRule != null && implicitRule.zenPolicy != null) { - // toNotificationPolicy takes defaults from mConfig, and technically, those are not + // toNotificationPolicy takes defaults from config, and technically those are not // the defaults that would apply if any fields were unset. However, all rules should // have all fields set in their ZenPolicy objects upon rule creation, so in // practice, this is only filling in the areChannelsBypassingDnd field, which is a // state rather than a part of the policy. - return mConfig.toNotificationPolicy(implicitRule.zenPolicy); + return config.toNotificationPolicy(implicitRule.zenPolicy); } else { - return getNotificationPolicy(); + return getNotificationPolicy(user); } } } @@ -766,24 +786,8 @@ public class ZenModeHelper { rule.id = implicitRuleId(pkg); rule.pkg = pkg; rule.creationTime = mClock.millis(); - - Binder.withCleanCallingIdentity(() -> { - try { - ApplicationInfo applicationInfo = mPm.getApplicationInfo(pkg, 0); - rule.name = applicationInfo.loadLabel(mPm).toString(); - if (!Flags.modesUi()) { - rule.iconResName = drawableResIdToResName(pkg, applicationInfo.icon); - } - } catch (PackageManager.NameNotFoundException e) { - // Should not happen, since it's the app calling us (?) - Log.w(TAG, "Package not found for creating implicit zen rule"); - rule.name = "Unknown"; - } - }); - + updateImplicitZenRuleNameAndDescription(rule); rule.type = AutomaticZenRule.TYPE_OTHER; - rule.triggerDescription = mContext.getString(R.string.zen_mode_implicit_trigger_description, - rule.name); rule.condition = null; rule.conditionId = new Uri.Builder() .scheme(Condition.SCHEME) @@ -798,13 +802,46 @@ public class ZenModeHelper { return rule; } - boolean removeAutomaticZenRule(String id, @ConfigOrigin int origin, String reason, - int callingUid) { + private void updateImplicitZenRuleNameAndDescription(ZenRule rule) { + checkArgument(isImplicitRuleId(rule.id)); + requireNonNull(rule.pkg, "Implicit rule is not associated to package yet!"); + + String pkgAppName = Binder.withCleanCallingIdentity(() -> { + try { + ApplicationInfo applicationInfo = mPm.getApplicationInfo(rule.pkg, 0); + return applicationInfo.loadLabel(mPm).toString(); + } catch (PackageManager.NameNotFoundException e) { + // Should not happen. When creating it's the app calling us, and when updating + // the rule would've been deleted if the package was removed. + Slog.e(TAG, "Package not found when updating implicit zen rule name", e); + return null; + } + }); + + if (pkgAppName != null) { + if ((rule.userModifiedFields & AutomaticZenRule.FIELD_NAME) == 0) { + if (Flags.modesUi()) { + rule.name = mContext.getString(R.string.zen_mode_implicit_name, pkgAppName); + } else { + rule.name = pkgAppName; + } + } + rule.triggerDescription = mContext.getString( + R.string.zen_mode_implicit_trigger_description, pkgAppName); + } else if (rule.name == null) { + // We must give a new rule SOME name. But this path should never be hit. + rule.name = "Unknown"; + } + } + + boolean removeAutomaticZenRule(UserHandle user, String id, @ConfigOrigin int origin, + String reason, int callingUid) { checkManageRuleOrigin("removeAutomaticZenRule", origin); ZenModeConfig newConfig; synchronized (mConfigLock) { - if (mConfig == null) return false; - newConfig = mConfig.copy(); + ZenModeConfig config = getConfigLocked(user); + if (config == null) return false; + newConfig = config.copy(); ZenRule ruleToRemove = newConfig.automaticRules.get(id); if (ruleToRemove == null) return false; if (canManageAutomaticZenRule(ruleToRemove)) { @@ -826,18 +863,19 @@ public class ZenModeHelper { "Cannot delete rules not owned by your condition provider"); } dispatchOnAutomaticRuleStatusChanged( - mConfig.user, ruleToRemove.getPkg(), id, AUTOMATIC_RULE_STATUS_REMOVED); + config.user, ruleToRemove.getPkg(), id, AUTOMATIC_RULE_STATUS_REMOVED); return setConfigLocked(newConfig, origin, reason, null, true, callingUid); } } - boolean removeAutomaticZenRules(String packageName, @ConfigOrigin int origin, + boolean removeAutomaticZenRules(UserHandle user, String packageName, @ConfigOrigin int origin, String reason, int callingUid) { checkManageRuleOrigin("removeAutomaticZenRules", origin); ZenModeConfig newConfig; synchronized (mConfigLock) { - if (mConfig == null) return false; - newConfig = mConfig.copy(); + ZenModeConfig config = getConfigLocked(user); + if (config == null) return false; + newConfig = config.copy(); for (int i = newConfig.automaticRules.size() - 1; i >= 0; i--) { ZenRule rule = newConfig.automaticRules.get(newConfig.automaticRules.keyAt(i)); if (Objects.equals(rule.getPkg(), packageName) && canManageAutomaticZenRule(rule)) { @@ -885,12 +923,13 @@ public class ZenModeHelper { } @Condition.State - int getAutomaticZenRuleState(String id) { + int getAutomaticZenRuleState(UserHandle user, String id) { synchronized (mConfigLock) { - if (mConfig == null) { + ZenModeConfig config = getConfigLocked(user); + if (config == null) { return Condition.STATE_UNKNOWN; } - ZenRule rule = mConfig.automaticRules.get(id); + ZenRule rule = config.automaticRules.get(id); if (rule == null || !canManageAutomaticZenRule(rule)) { return Condition.STATE_UNKNOWN; } @@ -903,14 +942,15 @@ public class ZenModeHelper { } } - void setAutomaticZenRuleState(String id, Condition condition, @ConfigOrigin int origin, - int callingUid) { + void setAutomaticZenRuleState(UserHandle user, String id, Condition condition, + @ConfigOrigin int origin, int callingUid) { checkSetRuleStateOrigin("setAutomaticZenRuleState(String id)", origin); ZenModeConfig newConfig; synchronized (mConfigLock) { - if (mConfig == null) return; + ZenModeConfig config = getConfigLocked(user); + if (config == null) return; - newConfig = mConfig.copy(); + newConfig = config.copy(); ZenRule rule = newConfig.automaticRules.get(id); if (Flags.modesApi()) { if (rule != null && canManageAutomaticZenRule(rule)) { @@ -925,13 +965,14 @@ public class ZenModeHelper { } } - void setAutomaticZenRuleState(Uri ruleDefinition, Condition condition, + void setAutomaticZenRuleState(UserHandle user, Uri ruleDefinition, Condition condition, @ConfigOrigin int origin, int callingUid) { checkSetRuleStateOrigin("setAutomaticZenRuleState(Uri ruleDefinition)", origin); ZenModeConfig newConfig; synchronized (mConfigLock) { - if (mConfig == null) return; - newConfig = mConfig.copy(); + ZenModeConfig config = getConfigLocked(user); + if (config == null) return; + newConfig = config.copy(); List<ZenRule> matchingRules = findMatchingRules(newConfig, ruleDefinition, condition); if (Flags.modesApi()) { @@ -1025,13 +1066,16 @@ public class ZenModeHelper { return true; } - public int getCurrentInstanceCount(ComponentName cn) { + public int getCurrentInstanceCount(UserHandle user, ComponentName cn) { if (cn == null) { return 0; } int count = 0; synchronized (mConfigLock) { - for (ZenRule rule : mConfig.automaticRules.values()) { + ZenModeConfig config = getConfigLocked(user); + if (config == null) return 0; + + for (ZenRule rule : config.automaticRules.values()) { if (cn.equals(rule.component) || cn.equals(rule.configurationActivity)) { count++; } @@ -1042,13 +1086,16 @@ public class ZenModeHelper { // Equivalent method to getCurrentInstanceCount, but for all rules associated with a specific // package rather than a condition provider service or activity. - private int getPackageRuleCount(String pkg) { + private int getPackageRuleCount(UserHandle user, String pkg) { if (pkg == null) { return 0; } int count = 0; synchronized (mConfigLock) { - for (ZenRule rule : mConfig.automaticRules.values()) { + ZenModeConfig config = getConfigLocked(user); + if (config == null) return 0; + + for (ZenRule rule : config.automaticRules.values()) { if (pkg.equals(rule.getPkg())) { count++; } @@ -1081,13 +1128,15 @@ public class ZenModeHelper { void updateZenRulesOnLocaleChange() { updateRuleStringsForCurrentLocale(mContext, mDefaultConfig); synchronized (mConfigLock) { - if (mConfig == null) { + ZenModeConfig config = getConfigLocked(UserHandle.CURRENT); + if (config == null) { return; } - ZenModeConfig config = mConfig.copy(); + + ZenModeConfig newConfig = config.copy(); boolean updated = false; for (ZenRule defaultRule : mDefaultConfig.automaticRules.values()) { - ZenRule currRule = config.automaticRules.get(defaultRule.id); + ZenRule currRule = newConfig.automaticRules.get(defaultRule.id); // if default rule wasn't user-modified use localized name // instead of previous system name if (currRule != null @@ -1103,14 +1152,16 @@ public class ZenModeHelper { } } if (Flags.modesApi() && Flags.modesUi()) { - for (ZenRule rule : config.automaticRules.values()) { + for (ZenRule rule : newConfig.automaticRules.values()) { if (SystemZenRules.isSystemOwnedRule(rule)) { updated |= SystemZenRules.updateTriggerDescription(mContext, rule); + } else if (isImplicitRuleId(rule.id)) { + updateImplicitZenRuleNameAndDescription(rule); } } } if (updated) { - setConfigLocked(config, null, ORIGIN_SYSTEM, + setConfigLocked(newConfig, null, ORIGIN_SYSTEM, "updateZenRulesOnLocaleChange", Process.SYSTEM_UID); } } @@ -1170,8 +1221,8 @@ public class ZenModeHelper { * deactivated) unless the update has origin == {@link ZenModeConfig#ORIGIN_USER_IN_SYSTEMUI}. */ @GuardedBy("mConfigLock") - private boolean populateZenRule(String pkg, AutomaticZenRule azr, ZenRule rule, - @ConfigOrigin int origin, boolean isNew) { + private boolean populateZenRule(String pkg, AutomaticZenRule azr, ZenModeConfig config, + ZenRule rule, @ConfigOrigin int origin, boolean isNew) { if (Flags.modesApi()) { boolean modified = false; // These values can always be edited by the app, so we apply changes immediately. @@ -1307,7 +1358,7 @@ public class ZenModeHelper { } // Updates the bitmask and values for all policy fields, based on the origin. - modified |= updatePolicy(rule, azr.getZenPolicy(), updateBitmask, isNew); + modified |= updatePolicy(config, rule, azr.getZenPolicy(), updateBitmask, isNew); // Updates the bitmask and values for all device effect fields, based on the origin. modified |= updateZenDeviceEffects(rule, azr.getDeviceEffects(), @@ -1360,13 +1411,13 @@ public class ZenModeHelper { * <p>Returns {@code true} if the policy of the rule was modified. */ @GuardedBy("mConfigLock") - private boolean updatePolicy(ZenRule zenRule, @Nullable ZenPolicy newPolicy, - boolean updateBitmask, boolean isNew) { + private boolean updatePolicy(ZenModeConfig config, ZenRule zenRule, + @Nullable ZenPolicy newPolicy, boolean updateBitmask, boolean isNew) { if (newPolicy == null) { if (isNew) { // Newly created rule with no provided policy; fill in with the default. zenRule.zenPolicy = - (Flags.modesUi() ? mDefaultConfig.getZenPolicy() : mConfig.getZenPolicy()) + (Flags.modesUi() ? mDefaultConfig.getZenPolicy() : config.getZenPolicy()) .copy(); return true; } @@ -1378,7 +1429,7 @@ public class ZenModeHelper { // fields in the bitmask should be marked as updated. ZenPolicy oldPolicy = zenRule.zenPolicy != null ? zenRule.zenPolicy - : (Flags.modesUi() ? mDefaultConfig.getZenPolicy() : mConfig.getZenPolicy()); + : (Flags.modesUi() ? mDefaultConfig.getZenPolicy() : config.getZenPolicy()); // If this is updating a rule rather than creating a new one, keep any fields from the // old policy if they are unspecified in the new policy. For newly created rules, oldPolicy @@ -1570,17 +1621,20 @@ public class ZenModeHelper { // Update only the hasPriorityChannels state (aka areChannelsBypassingDnd) without modifying // any of the rest of the existing policy. This allows components that only want to modify // this bit (PreferencesHelper) to not have to adjust the rest of the policy. - protected void updateHasPriorityChannels(boolean hasPriorityChannels) { + protected void updateHasPriorityChannels(UserHandle user, boolean hasPriorityChannels) { if (!Flags.modesUi()) { Log.wtf(TAG, "updateHasPriorityChannels called without modes_ui"); } synchronized (mConfigLock) { + ZenModeConfig config = getConfigLocked(user); + if (config == null) return; + // If it already matches, do nothing - if (mConfig.areChannelsBypassingDnd == hasPriorityChannels) { + if (config.areChannelsBypassingDnd == hasPriorityChannels) { return; } - ZenModeConfig newConfig = mConfig.copy(); + ZenModeConfig newConfig = config.copy(); newConfig.areChannelsBypassingDnd = hasPriorityChannels; // The updated calculation of whether there are priority channels is always done by // the system, even if the event causing the calculation had a different origin. @@ -1610,22 +1664,25 @@ public class ZenModeHelper { : AUTOMATIC_RULE_STATUS_DISABLED); } - void setManualZenMode(int zenMode, Uri conditionId, @ConfigOrigin int origin, + void setManualZenMode(UserHandle user, int zenMode, Uri conditionId, @ConfigOrigin int origin, String reason, @Nullable String caller, int callingUid) { - setManualZenMode(zenMode, conditionId, origin, reason, caller, true /*setRingerMode*/, + setManualZenMode(user, zenMode, conditionId, origin, reason, caller, true /*setRingerMode*/, callingUid); } - private void setManualZenMode(int zenMode, Uri conditionId, @ConfigOrigin int origin, - String reason, @Nullable String caller, boolean setRingerMode, int callingUid) { + private void setManualZenMode(UserHandle user, int zenMode, Uri conditionId, + @ConfigOrigin int origin, String reason, @Nullable String caller, boolean setRingerMode, + int callingUid) { ZenModeConfig newConfig; synchronized (mConfigLock) { - if (mConfig == null) return; + ZenModeConfig config = getConfigLocked(user); + if (config == null) return; + if (!Global.isValidZenMode(zenMode)) return; if (DEBUG) Log.d(TAG, "setManualZenMode " + Global.zenModeToString(zenMode) + " conditionId=" + conditionId + " reason=" + reason + " setRingerMode=" + setRingerMode); - newConfig = mConfig.copy(); + newConfig = config.copy(); if (Flags.modesUi()) { newConfig.manualRule.enabler = caller; newConfig.manualRule.conditionId = conditionId != null ? conditionId : Uri.EMPTY; @@ -1668,18 +1725,20 @@ public class ZenModeHelper { } } - public void setManualZenRuleDeviceEffects(ZenDeviceEffects deviceEffects, + public void setManualZenRuleDeviceEffects(UserHandle user, ZenDeviceEffects deviceEffects, @ConfigOrigin int origin, String reason, int callingUid) { if (!Flags.modesUi()) { return; } ZenModeConfig newConfig; synchronized (mConfigLock) { - if (mConfig == null) return; + ZenModeConfig config = getConfigLocked(user); + if (config == null) return; + if (DEBUG) Log.d(TAG, "updateManualRule " + deviceEffects + " reason=" + reason + " callingUid=" + callingUid); - newConfig = mConfig.copy(); + newConfig = config.copy(); newConfig.manualRule.pkg = PACKAGE_ANDROID; newConfig.manualRule.zenDeviceEffects = deviceEffects; @@ -1709,7 +1768,7 @@ public class ZenModeHelper { pw.println(Global.zenModeToString(mZenMode)); pw.print(prefix); pw.println("mConsolidatedPolicy=" + mConsolidatedPolicy.toString()); - synchronized (mConfigsArrayLock) { + synchronized (mConfigLock) { final int N = mConfigs.size(); for (int i = 0; i < N; i++) { dump(pw, prefix, "mConfigs[u=" + mConfigs.keyAt(i) + "]", mConfigs.valueAt(i)); @@ -1730,11 +1789,10 @@ public class ZenModeHelper { pw.println(config); } - public void readXml(TypedXmlPullParser parser, boolean forRestore, int userId) - throws XmlPullParserException, IOException { - ZenModeConfig config = ZenModeConfig.readXml(parser); + public boolean readXml(TypedXmlPullParser parser, boolean forRestore, int userId, + @Nullable BackupRestoreEventLogger logger) throws XmlPullParserException, IOException { + ZenModeConfig config = ZenModeConfig.readXml(parser, logger); String reason = "readXml"; - if (config != null) { if (forRestore) { config.user = userId; @@ -1826,22 +1884,38 @@ public class ZenModeHelper { if (DEBUG) Log.d(TAG, reason); synchronized (mConfigLock) { - setConfigLocked(config, null, + return setConfigLocked(config, null, forRestore ? ORIGIN_RESTORE_BACKUP : ORIGIN_INIT, reason, Process.SYSTEM_UID); } } + return false; } - public void writeXml(TypedXmlSerializer out, boolean forBackup, Integer version, int userId) - throws IOException { - synchronized (mConfigsArrayLock) { + public void writeXml(TypedXmlSerializer out, boolean forBackup, Integer version, int userId, + @Nullable BackupRestoreEventLogger logger) throws IOException { + synchronized (mConfigLock) { + int successfulWrites = 0; + int unsuccessfulWrites = 0; final int n = mConfigs.size(); for (int i = 0; i < n; i++) { if (forBackup && mConfigs.keyAt(i) != userId) { continue; } - mConfigs.valueAt(i).writeXml(out, version, forBackup); + try { + mConfigs.valueAt(i).writeXml(out, version, forBackup, logger); + successfulWrites++; + } catch (Exception e) { + Slog.e(TAG, "failed to write config", e); + unsuccessfulWrites++; + } + } + if (logger != null) { + logger.logItemsBackedUp(DATA_TYPE_ZEN_CONFIG, successfulWrites); + if (unsuccessfulWrites > 0) { + logger.logItemsBackupFailed(DATA_TYPE_ZEN_CONFIG, + unsuccessfulWrites, ERROR_XML_PARSING); + } } } } @@ -1849,24 +1923,29 @@ public class ZenModeHelper { /** * @return user-specified default notification policy for priority only do not disturb */ - public Policy getNotificationPolicy() { + @Nullable + public Policy getNotificationPolicy(UserHandle user) { synchronized (mConfigLock) { - return getNotificationPolicy(mConfig); + return getNotificationPolicy(getConfigLocked(user)); } } - private static Policy getNotificationPolicy(ZenModeConfig config) { + @Nullable + private static Policy getNotificationPolicy(@Nullable ZenModeConfig config) { return config == null ? null : config.toNotificationPolicy(); } /** * Sets the global notification policy used for priority only do not disturb */ - public void setNotificationPolicy(Policy policy, @ConfigOrigin int origin, + public void setNotificationPolicy(UserHandle user, Policy policy, @ConfigOrigin int origin, int callingUid) { synchronized (mConfigLock) { - if (policy == null || mConfig == null) return; - final ZenModeConfig newConfig = mConfig.copy(); + if (policy == null) return; + ZenModeConfig config = getConfigLocked(user); + if (config == null) return; + + final ZenModeConfig newConfig = config.copy(); if (Flags.modesApi() && !Flags.modesUi()) { // Fix for b/337193321 -- propagate changes to notificationPolicy to rules where // the user cannot edit zen policy to emulate the previous "inheritance". @@ -1894,7 +1973,7 @@ public class ZenModeHelper { } /** - * Cleans up obsolete rules: + * Cleans up obsolete rules in the current {@link ZenModeConfig}. * <ul> * <li>Rule instances whose owner is not installed. * <li>Deleted rules that were deleted more than 30 days ago. @@ -1966,6 +2045,27 @@ public class ZenModeHelper { return mDefaultConfig.getZenPolicy(); } + /** + * Returns the {@link ZenModeConfig} corresponding to the supplied {@link UserHandle}. + * The result will be {@link #mConfig} if the user is {@link UserHandle#CURRENT}, or the same + * as {@link #mUser}, otherwise will be the corresponding entry in {@link #mConfigs}. + * + * <p>Remember to continue holding {@link #mConfigLock} while operating on the returned value. + */ + @Nullable + @GuardedBy("mConfigLock") + private ZenModeConfig getConfigLocked(@NonNull UserHandle user) { + if (Flags.modesMultiuser()) { + if (user.getIdentifier() == UserHandle.USER_CURRENT || user.getIdentifier() == mUser) { + return mConfig; + } else { + return mConfigs.get(user.getIdentifier()); + } + } else { + return mConfig; + } + } + @GuardedBy("mConfigLock") private boolean setConfigLocked(ZenModeConfig config, ComponentName triggeringComponent, @ConfigOrigin int origin, String reason, int callingUid) { @@ -1992,7 +2092,7 @@ public class ZenModeHelper { } if (config.user != mUser) { // simply store away for background users - synchronized (mConfigsArrayLock) { + synchronized (mConfigLock) { mConfigs.put(config.user, config); } if (DEBUG) Log.d(TAG, "setConfigLocked: store config for user " + config.user); @@ -2001,7 +2101,7 @@ public class ZenModeHelper { // handle CPS backed conditions - danger! may modify config mConditions.evaluateConfig(config, null, false /*processSubscriptions*/); - synchronized (mConfigsArrayLock) { + synchronized (mConfigLock) { mConfigs.put(config.user, config); } if (DEBUG) Log.d(TAG, "setConfigLocked reason=" + reason, new Throwable()); @@ -2142,7 +2242,8 @@ public class ZenModeHelper { } @GuardedBy("mConfigLock") - private void applyCustomPolicy(ZenPolicy policy, ZenRule rule, boolean useManualConfig) { + private void applyCustomPolicy(ZenModeConfig config, ZenPolicy policy, ZenRule rule, + boolean useManualConfig) { if (rule.zenMode == Global.ZEN_MODE_NO_INTERRUPTIONS) { if (Flags.modesApi() && Flags.modesUi()) { policy.apply(ZenPolicy.getBasePolicyInterruptionFilterNone()); @@ -2168,8 +2269,8 @@ public class ZenModeHelper { } else { if (Flags.modesApi()) { if (useManualConfig) { - // manual rule is configured using the settings stored directly in mConfig - policy.apply(mConfig.getZenPolicy()); + // manual rule is configured using the settings stored directly in ZenModeConfig + policy.apply(config.getZenPolicy()); } else { // under modes_api flag, an active automatic rule with no specified policy // inherits the device default settings as stored in mDefaultConfig. While the @@ -2177,11 +2278,11 @@ public class ZenModeHelper { // catch any that may have fallen through the cracks. Log.wtf(TAG, "active automatic rule found with no specified policy: " + rule); policy.apply(Flags.modesUi() - ? mDefaultConfig.getZenPolicy() : mConfig.getZenPolicy()); + ? mDefaultConfig.getZenPolicy() : config.getZenPolicy()); } } else { // active rule with no specified policy inherits the manual rule config settings - policy.apply(mConfig.getZenPolicy()); + policy.apply(config.getZenPolicy()); } } } @@ -2194,7 +2295,7 @@ public class ZenModeHelper { ZenPolicy policy = new ZenPolicy(); ZenDeviceEffects.Builder deviceEffectsBuilder = new ZenDeviceEffects.Builder(); if (mConfig.isManualActive()) { - applyCustomPolicy(policy, mConfig.manualRule, true); + applyCustomPolicy(mConfig, policy, mConfig.manualRule, true); if (Flags.modesApi()) { deviceEffectsBuilder.add(mConfig.manualRule.zenDeviceEffects); } @@ -2206,7 +2307,7 @@ public class ZenModeHelper { // policy. This is relevant in case some other active rule has a more // restrictive INTERRUPTION_FILTER but a more lenient ZenPolicy! if (!Flags.modesApi() || automaticRule.zenMode != Global.ZEN_MODE_OFF) { - applyCustomPolicy(policy, automaticRule, false); + applyCustomPolicy(mConfig, policy, automaticRule, false); } if (Flags.modesApi()) { deviceEffectsBuilder.add(automaticRule.zenDeviceEffects); @@ -2445,7 +2546,8 @@ public class ZenModeHelper { try { parser = resources.getXml(R.xml.default_zen_mode_config); while (parser.next() != XmlPullParser.END_DOCUMENT) { - final ZenModeConfig config = ZenModeConfig.readXml(XmlUtils.makeTyped(parser)); + final ZenModeConfig config = + ZenModeConfig.readXml(XmlUtils.makeTyped(parser), null); if (config != null) return config; } } catch (Exception e) { @@ -2469,7 +2571,7 @@ public class ZenModeHelper { * Generate pulled atoms about do not disturb configurations. */ public void pullRules(List<StatsEvent> events) { - synchronized (mConfigsArrayLock) { + synchronized (mConfigLock) { final int numConfigs = mConfigs.size(); for (int i = 0; i < numConfigs; i++) { final int user = mConfigs.keyAt(i); @@ -2496,7 +2598,7 @@ public class ZenModeHelper { } } - @GuardedBy("mConfigsArrayLock") + @GuardedBy("mConfigLock") private void ruleToProtoLocked(int user, ZenRule rule, boolean isManualRule, List<StatsEvent> events) { // Make the ID safe. @@ -2601,7 +2703,7 @@ public class ZenModeHelper { } if (newZen != -1) { - setManualZenMode(newZen, null, ORIGIN_SYSTEM, + setManualZenMode(UserHandle.CURRENT, newZen, null, ORIGIN_SYSTEM, "ringerModeInternal", /* caller= */ null, /* setRingerMode= */ false, Process.SYSTEM_UID); } @@ -2646,7 +2748,7 @@ public class ZenModeHelper { break; } if (newZen != -1) { - setManualZenMode(newZen, null, ORIGIN_SYSTEM, + setManualZenMode(UserHandle.CURRENT, newZen, null, ORIGIN_SYSTEM, "ringerModeExternal", caller, false /*setRingerMode*/, Process.SYSTEM_UID); } diff --git a/services/core/java/com/android/server/pm/BackgroundInstallControlCallbackHelper.java b/services/core/java/com/android/server/pm/BackgroundInstallControlCallbackHelper.java index 155361837071..27c4e9dca586 100644 --- a/services/core/java/com/android/server/pm/BackgroundInstallControlCallbackHelper.java +++ b/services/core/java/com/android/server/pm/BackgroundInstallControlCallbackHelper.java @@ -35,6 +35,7 @@ public class BackgroundInstallControlCallbackHelper { @VisibleForTesting static final String FLAGGED_PACKAGE_NAME_KEY = "packageName"; @VisibleForTesting static final String FLAGGED_USER_ID_KEY = "userId"; + @VisibleForTesting static final String INSTALL_EVENT_TYPE_KEY = "installEventType"; private static final String TAG = "BackgroundInstallControlCallbackHelper"; private final Handler mHandler; @@ -74,10 +75,14 @@ public class BackgroundInstallControlCallbackHelper { * Invokes all registered callbacks Callbacks are processed through user provided-threads and * parameters are passed in via {@link BackgroundInstallControlManager} InstallEvent */ - public void notifyAllCallbacks(int userId, String packageName) { + public void notifyAllCallbacks( + int userId, + String packageName, + @BackgroundInstallControlService.InstallEventType int installEventType) { Bundle extras = new Bundle(); extras.putCharSequence(FLAGGED_PACKAGE_NAME_KEY, packageName); extras.putInt(FLAGGED_USER_ID_KEY, userId); + extras.putInt(INSTALL_EVENT_TYPE_KEY, installEventType); synchronized (mCallbacks) { mHandler.post( () -> diff --git a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java index b6daed121057..20cca969dade 100644 --- a/services/core/java/com/android/server/pm/BackgroundInstallControlService.java +++ b/services/core/java/com/android/server/pm/BackgroundInstallControlService.java @@ -19,6 +19,7 @@ package com.android.server.pm; import static android.Manifest.permission.GET_BACKGROUND_INSTALLED_PACKAGES; import static android.content.pm.PackageManager.PERMISSION_GRANTED; +import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.RequiresPermission; import android.app.Flags; @@ -64,6 +65,8 @@ import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; import java.util.Comparator; import java.util.List; @@ -76,11 +79,23 @@ import java.util.TreeSet; * @hide */ public class BackgroundInstallControlService extends SystemService { + public static final int INSTALL_EVENT_TYPE_UNKNOWN = 0; + public static final int INSTALL_EVENT_TYPE_INSTALL = 1; + public static final int INSTALL_EVENT_TYPE_UNINSTALL = 2; + + @IntDef( + value = { + INSTALL_EVENT_TYPE_UNKNOWN, + INSTALL_EVENT_TYPE_INSTALL, + INSTALL_EVENT_TYPE_UNINSTALL, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface InstallEventType {} + private static final String TAG = "BackgroundInstallControlService"; private static final String DISK_FILE_NAME = "states"; private static final String DISK_DIR_NAME = "bic"; - private static final String ENFORCE_PERMISSION_ERROR_MSG = "User is not permitted to call service: "; @@ -313,7 +328,7 @@ public class BackgroundInstallControlService extends SystemService { initBackgroundInstalledPackages(); mBackgroundInstalledPackages.add(userId, packageName); - mCallbackHelper.notifyAllCallbacks(userId, packageName); + mCallbackHelper.notifyAllCallbacks(userId, packageName, INSTALL_EVENT_TYPE_INSTALL); writeBackgroundInstalledPackagesToDisk(); } @@ -391,6 +406,7 @@ public class BackgroundInstallControlService extends SystemService { initBackgroundInstalledPackages(); mBackgroundInstalledPackages.remove(userId, packageName); writeBackgroundInstalledPackagesToDisk(); + mCallbackHelper.notifyAllCallbacks(userId, packageName, INSTALL_EVENT_TYPE_UNINSTALL); } void handleUsageEvent(UsageEvents.Event event, int userId) { diff --git a/services/core/java/com/android/server/pm/BackgroundUserSoundNotifier.java b/services/core/java/com/android/server/pm/BackgroundUserSoundNotifier.java index d1d6ed0f1f99..77572e018dda 100644 --- a/services/core/java/com/android/server/pm/BackgroundUserSoundNotifier.java +++ b/services/core/java/com/android/server/pm/BackgroundUserSoundNotifier.java @@ -179,13 +179,17 @@ public class BackgroundUserSoundNotifier { final String action = intent.getAction().substring(actionIndex); Log.d(LOG_TAG, "Action requested: " + action + ", by userId " + ActivityManager.getCurrentUser() + " for alarm on user " - + UserHandle.getUserHandleForUid(clientUid)); + + UserHandle.getUserHandleForUid(clientUid).getIdentifier()); } if (ACTION_MUTE_SOUND.equals(intent.getAction())) { muteAlarmSounds(clientUid); } else if (ACTION_SWITCH_USER.equals(intent.getAction())) { - activityManager.switchUser(UserHandle.getUserId(clientUid)); + int userId = UserHandle.getUserId(clientUid); + if (mUserManager.isProfile(userId)) { + userId = mUserManager.getProfileParent(userId).id; + } + activityManager.switchUser(userId); } if (Flags.multipleAlarmNotificationsSupport()) { mNotificationClientUids.remove(clientUid); @@ -237,11 +241,12 @@ public class BackgroundUserSoundNotifier { UserHandle.of(ActivityManager.getCurrentUser()), 0); final int userId = UserHandle.getUserId(afi.getClientUid()); final int usage = afi.getAttributes().getUsage(); - UserInfo userInfo = mUserManager.getUserInfo(userId); - + UserInfo userInfo = mUserManager.isProfile(userId) ? mUserManager.getProfileParent(userId) : + mUserManager.getUserInfo(userId); + ActivityManager activityManager = foregroundContext.getSystemService(ActivityManager.class); // Only show notification if the sound is coming from background user and the notification // for this UID is not already shown. - if (userInfo != null && userId != foregroundContext.getUserId() + if (userInfo != null && !activityManager.isProfileForeground(userInfo.getUserHandle()) && !isNotificationShown(afi.getClientUid())) { //TODO: b/349138482 - Add handling of cases when usage == USAGE_NOTIFICATION_RINGTONE if (usage == USAGE_ALARM) { diff --git a/services/core/java/com/android/server/pm/BroadcastHelper.java b/services/core/java/com/android/server/pm/BroadcastHelper.java index 84a5f2b0e8bc..9f4b9f1c1f24 100644 --- a/services/core/java/com/android/server/pm/BroadcastHelper.java +++ b/services/core/java/com/android/server/pm/BroadcastHelper.java @@ -64,6 +64,9 @@ import android.util.Pair; import android.util.Slog; import android.util.SparseArray; +import com.android.internal.pm.pkg.component.ParsedActivity; +import com.android.internal.pm.pkg.component.ParsedProvider; +import com.android.internal.pm.pkg.component.ParsedService; import com.android.internal.util.ArrayUtils; import com.android.internal.util.FrameworkStatsLog; import com.android.server.pm.pkg.AndroidPackage; @@ -80,6 +83,8 @@ import java.util.function.BiFunction; */ public final class BroadcastHelper { private static final boolean DEBUG_BROADCASTS = false; + private static final String PERMISSION_PACKAGE_CHANGED_BROADCAST_ON_COMPONENT_STATE_CHANGED = + "android.permission.INTERNAL_RECEIVE_PACKAGE_CHANGED_BROADCAST_ON_COMPONENT_STATE_CHANGED"; private final UserManagerInternal mUmInternal; private final ActivityManagerInternal mAmInternal; @@ -291,6 +296,57 @@ public final class BroadcastHelper { return bOptions; } + private ArrayList<String> getAllNotExportedComponents(@NonNull AndroidPackage pkg, + @NonNull ArrayList<String> inputComponentNames) { + final ArrayList<String> outputNotExportedComponentNames = new ArrayList<>(); + int remainingComponentCount = inputComponentNames.size(); + for (ParsedActivity component : pkg.getReceivers()) { + if (inputComponentNames.contains(component.getClassName())) { + if (!component.isExported()) { + outputNotExportedComponentNames.add(component.getClassName()); + } + remainingComponentCount--; + if (remainingComponentCount <= 0) { + return outputNotExportedComponentNames; + } + } + } + for (ParsedProvider component : pkg.getProviders()) { + if (inputComponentNames.contains(component.getClassName())) { + if (!component.isExported()) { + outputNotExportedComponentNames.add(component.getClassName()); + } + remainingComponentCount--; + if (remainingComponentCount <= 0) { + return outputNotExportedComponentNames; + } + } + } + for (ParsedService component : pkg.getServices()) { + if (inputComponentNames.contains(component.getClassName())) { + if (!component.isExported()) { + outputNotExportedComponentNames.add(component.getClassName()); + } + remainingComponentCount--; + if (remainingComponentCount <= 0) { + return outputNotExportedComponentNames; + } + } + } + for (ParsedActivity component : pkg.getActivities()) { + if (inputComponentNames.contains(component.getClassName())) { + if (!component.isExported()) { + outputNotExportedComponentNames.add(component.getClassName()); + } + remainingComponentCount--; + if (remainingComponentCount <= 0) { + return outputNotExportedComponentNames; + } + } + } + return outputNotExportedComponentNames; + } + private void sendPackageChangedBroadcastInternal(@NonNull String packageName, boolean dontKillApp, @NonNull ArrayList<String> componentNames, @@ -298,10 +354,48 @@ public final class BroadcastHelper { @Nullable String reason, @Nullable int[] userIds, @Nullable int[] instantUserIds, - @Nullable SparseArray<int[]> broadcastAllowList) { - sendPackageChangedBroadcastWithPermissions(packageName, dontKillApp, componentNames, - packageUid, reason, userIds, instantUserIds, broadcastAllowList, - null /* targetPackageName */, null /* requiredPermissions */); + @Nullable SparseArray<int[]> broadcastAllowList, + @NonNull AndroidPackage pkg) { + final boolean isForWholeApp = componentNames.contains(packageName); + if (isForWholeApp || !android.content.pm.Flags.reduceBroadcastsForComponentStateChanges()) { + sendPackageChangedBroadcastWithPermissions(packageName, dontKillApp, componentNames, + packageUid, reason, userIds, instantUserIds, broadcastAllowList, + null /* targetPackageName */, null /* requiredPermissions */); + return; + } + // Currently only these four components of activity, receiver, provider and service are + // considered to send only the broadcast to the system and the application itself when the + // component is not exported. In order to avoid losing to send the broadcast for other + // components, it gets the not exported components for these four components of activity, + // receiver, provider and service and the others are considered the exported components. + final ArrayList<String> notExportedComponentNames = getAllNotExportedComponents(pkg, + componentNames); + final ArrayList<String> exportedComponentNames = (ArrayList<String>) componentNames.clone(); + exportedComponentNames.removeAll(notExportedComponentNames); + + if (!notExportedComponentNames.isEmpty()) { + // Limit sending of the PACKAGE_CHANGED broadcast to only the system and the + // application itself when the component is not exported. + + // First, send the PACKAGE_CHANGED broadcast to the system. + sendPackageChangedBroadcastWithPermissions(packageName, dontKillApp, + notExportedComponentNames, packageUid, reason, userIds, instantUserIds, + broadcastAllowList, "android" /* targetPackageName */, + new String[]{PERMISSION_PACKAGE_CHANGED_BROADCAST_ON_COMPONENT_STATE_CHANGED}); + + // Second, send the PACKAGE_CHANGED broadcast to the application itself. + sendPackageChangedBroadcastWithPermissions(packageName, dontKillApp, + notExportedComponentNames, packageUid, reason, userIds, instantUserIds, + broadcastAllowList, packageName /* targetPackageName */, + null /* requiredPermissions */); + } + + if (!exportedComponentNames.isEmpty()) { + sendPackageChangedBroadcastWithPermissions(packageName, dontKillApp, + exportedComponentNames, packageUid, reason, userIds, instantUserIds, + broadcastAllowList, null /* targetPackageName */, + null /* requiredPermissions */); + } } private void sendPackageChangedBroadcastWithPermissions(@NonNull String packageName, @@ -830,7 +924,7 @@ public final class BroadcastHelper { @NonNull String reason) { PackageStateInternal setting = snapshot.getPackageStateInternal(packageName, Process.SYSTEM_UID); - if (setting == null) { + if (setting == null || setting.getPkg() == null) { return; } final int userId = UserHandle.getUserId(packageUid); @@ -842,7 +936,7 @@ public final class BroadcastHelper { isInstantApp ? null : snapshot.getVisibilityAllowLists(packageName, userIds); mHandler.post(() -> sendPackageChangedBroadcastInternal( packageName, dontKillApp, componentNames, packageUid, reason, userIds, - instantUserIds, broadcastAllowList)); + instantUserIds, broadcastAllowList, setting.getPkg())); mPackageMonitorCallbackHelper.notifyPackageChanged(packageName, dontKillApp, componentNames, packageUid, reason, userIds, instantUserIds, broadcastAllowList, mHandler); } diff --git a/services/core/java/com/android/server/pm/SaferIntentUtils.java b/services/core/java/com/android/server/pm/SaferIntentUtils.java index 9a7ba0f082ea..bc36fabc7f67 100644 --- a/services/core/java/com/android/server/pm/SaferIntentUtils.java +++ b/services/core/java/com/android/server/pm/SaferIntentUtils.java @@ -26,7 +26,6 @@ import android.annotation.Nullable; import android.app.ActivityManager; import android.app.ActivityManagerInternal; import android.compat.annotation.ChangeId; -import android.compat.annotation.Disabled; import android.compat.annotation.EnabledAfter; import android.compat.annotation.Overridable; import android.content.Intent; @@ -45,6 +44,7 @@ import android.util.Printer; import android.util.Slog; import com.android.internal.pm.pkg.component.ParsedMainComponent; +import com.android.internal.pm.pkg.component.ParsedMainComponentImpl; import com.android.internal.util.FrameworkStatsLog; import com.android.server.IntentResolver; import com.android.server.LocalServices; @@ -88,22 +88,6 @@ public class SaferIntentUtils { @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.TIRAMISU) private static final long IMPLICIT_INTENTS_ONLY_MATCH_EXPORTED_COMPONENTS = 229362273; - /** - * Intents sent from apps enabling this feature will stop resolving to components with - * non matching intent filters, even when explicitly setting a component name, unless the - * target components are in the same app as the calling app. - * <p> - * When an app registers an exported component in its manifest and adds <intent-filter>s, - * the component can be started by any intent - even those that do not match the intent filter. - * This has proven to be something that many developers find counterintuitive. - * Without checking the intent when the component is started, in some circumstances this can - * allow 3P apps to trigger internal-only functionality. - */ - @ChangeId - @Overridable - @Disabled - private static final long ENFORCE_INTENTS_TO_MATCH_INTENT_FILTERS = 161252188; - @Nullable private static ParsedMainComponent infoToComponent( ComponentInfo info, ComponentResolverApi resolver, boolean isReceiver) { @@ -277,11 +261,6 @@ public class SaferIntentUtils { ? new LogPrinter(Log.VERBOSE, TAG, Log.LOG_ID_SYSTEM) : null; - final boolean enforceMatch = Flags.enforceIntentFilterMatch() - && args.isChangeEnabled(ENFORCE_INTENTS_TO_MATCH_INTENT_FILTERS); - final boolean blockNullAction = Flags.blockNullActionIntents() - && args.isChangeEnabled(IntentFilter.BLOCK_NULL_ACTION_INTENTS); - for (int i = resolveInfos.size() - 1; i >= 0; --i) { final ComponentInfo info = resolveInfos.get(i).getComponentInfo(); @@ -296,57 +275,65 @@ public class SaferIntentUtils { continue; } - Boolean match = null; - - if (args.intent.getAction() == null) { - args.reportEvent( - UNSAFE_INTENT_EVENT_REPORTED__EVENT_TYPE__NULL_ACTION_MATCH, - enforceMatch && blockNullAction); - if (blockNullAction) { - // Skip intent filter matching if blocking null action - match = false; + boolean enforceIntentFilter = Flags.enableIntentMatchingFlags(); + boolean allowNullAction = false; + + if (Flags.enableIntentMatchingFlags()) { + int flags = comp.getIntentMatchingFlags(); + if (flags == 0 || (flags & ParsedMainComponentImpl.INTENT_MATCHING_FLAGS_NONE) + == ParsedMainComponentImpl.INTENT_MATCHING_FLAGS_NONE + || (flags + & ParsedMainComponentImpl.INTENT_MATCHING_FLAGS_ENFORCE_INTENT_FILTER) + == 0) { + enforceIntentFilter = false; + } + if ((flags & ParsedMainComponentImpl.INTENT_MATCHING_FLAGS_ALLOW_NULL_ACTION) + == ParsedMainComponentImpl.INTENT_MATCHING_FLAGS_ALLOW_NULL_ACTION) { + allowNullAction = true; } } - if (match == null) { - // Check if any intent filter matches - for (int j = 0, size = comp.getIntents().size(); j < size; ++j) { - IntentFilter intentFilter = comp.getIntents().get(j).getIntentFilter(); - if (IntentResolver.intentMatchesFilter( - intentFilter, args.intent, args.resolvedType)) { - match = true; - break; - } + boolean hasNullAction = args.intent.getAction() == null; + boolean intentMatchesComponent = false; + + for (int j = 0, size = comp.getIntents().size(); j < size; ++j) { + IntentFilter intentFilter = comp.getIntents().get(j).getIntentFilter(); + if (IntentResolver.intentMatchesFilter( + intentFilter, args.intent, args.resolvedType)) { + intentMatchesComponent = true; + break; } } - // At this point, the value `match` has the following states: - // null : Intent does not match any intent filter - // false: Null action intent detected AND blockNullAction == true - // true : The intent matches at least one intent filter + boolean blockIntent = false; + if (enforceIntentFilter) { + if ((hasNullAction && !allowNullAction) || !intentMatchesComponent) { + blockIntent = true; + } + } - if (match == null) { + if (hasNullAction) { + args.reportEvent( + UNSAFE_INTENT_EVENT_REPORTED__EVENT_TYPE__NULL_ACTION_MATCH, blockIntent); + } else if (!intentMatchesComponent) { args.reportEvent( UNSAFE_INTENT_EVENT_REPORTED__EVENT_TYPE__EXPLICIT_INTENT_FILTER_UNMATCH, - enforceMatch); - match = false; + blockIntent); } - if (!match) { - // All non-matching intents has to be marked accordingly - if (Flags.enforceIntentFilterMatch()) { - args.intent.addExtendedFlags(Intent.EXTENDED_FLAG_FILTER_MISMATCH); - } - if (enforceMatch) { - Slog.w(TAG, "Intent does not match component's intent filter: " + args.intent); - Slog.w(TAG, "Access blocked: " + comp.getComponentName()); - if (DEBUG_INTENT_MATCHING) { - Slog.v(TAG, "Component intent filters:"); - comp.getIntents().forEach(f -> f.getIntentFilter().dump(logPrinter, " ")); - Slog.v(TAG, "-----------------------------"); - } - resolveInfos.remove(i); + if (Flags.enforceIntentFilterMatch() && (hasNullAction || !intentMatchesComponent)) { + args.intent.addExtendedFlags(Intent.EXTENDED_FLAG_FILTER_MISMATCH); + } + + if (blockIntent) { + Slog.w(TAG, "Intent does not match component's intent filter: " + args.intent); + Slog.w(TAG, "Access blocked: " + comp.getComponentName()); + if (DEBUG_INTENT_MATCHING) { + Slog.v(TAG, "Component intent filters:"); + comp.getIntents().forEach(f -> f.getIntentFilter().dump(logPrinter, " ")); + Slog.v(TAG, "-----------------------------"); } + resolveInfos.remove(i); } } } diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index 6c03214a2610..7ecfe7f64ffe 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -1222,7 +1222,7 @@ public class UserManagerService extends IUserManager.Stub { // Mark the user for removal. addRemovingUserIdLocked(ui.id); ui.partial = true; - ui.flags |= UserInfo.FLAG_DISABLED; + addUserInfoFlags(ui, UserInfo.FLAG_DISABLED); } /* Prunes out any partially created or partially removed users. */ @@ -1264,7 +1264,7 @@ public class UserManagerService extends IUserManager.Stub { if (ui.preCreated) { preCreatedUsers.add(ui); addRemovingUserIdLocked(ui.id); - ui.flags |= UserInfo.FLAG_DISABLED; + addUserInfoFlags(ui, UserInfo.FLAG_DISABLED); ui.partial = true; } } @@ -2120,7 +2120,7 @@ public class UserManagerService extends IUserManager.Stub { info = getUserInfoLU(userId); if (info != null && !info.isEnabled()) { wasUserDisabled = true; - info.flags ^= UserInfo.FLAG_DISABLED; + removeUserInfoFlags(info, UserInfo.FLAG_DISABLED); writeUserLP(getUserDataLU(info.id)); } } @@ -2130,6 +2130,36 @@ public class UserManagerService extends IUserManager.Stub { } } + /** + * This method is for monitoring flag changes on users flags and invalidate cache relevant to + * the change. The method add flags and invalidateOnUserInfoFlagChange for the flags which + * has changed. + * @param userInfo of existing user in mUsers list + * @param flags to be added to userInfo + */ + private void addUserInfoFlags(UserInfo userInfo, @UserInfoFlag int flags) { + int diff = ~userInfo.flags & flags; + if (diff > 0) { + userInfo.flags |= diff; + UserManager.invalidateOnUserInfoFlagChange(diff); + } + } + + /** + * This method is for monitoring flag changes on users flags and invalidate cache relevant to + * the change. The method remove flags and invalidateOnUserInfoFlagChange for the flags which + * has changed. + * @param userInfo of existing user in mUsers list + * @param flags to be removed from userInfo + */ + private void removeUserInfoFlags(UserInfo userInfo, @UserInfoFlag int flags) { + int diff = userInfo.flags & flags; + if (diff > 0) { + userInfo.flags ^= diff; + UserManager.invalidateOnUserInfoFlagChange(diff); + } + } + @Override public void setUserAdmin(@UserIdInt int userId) { checkManageUserAndAcrossUsersFullPermission("set user admin"); @@ -6245,7 +6275,7 @@ public class UserManagerService extends IUserManager.Stub { userData.info.guestToRemove = true; // Mark it as disabled, so that it isn't returned any more when // profiles are queried. - userData.info.flags |= UserInfo.FLAG_DISABLED; + addUserInfoFlags(userData.info, UserInfo.FLAG_DISABLED); writeUserLP(userData); } } finally { @@ -6390,7 +6420,7 @@ public class UserManagerService extends IUserManager.Stub { } // Mark it as disabled, so that it isn't returned any more when // profiles are queried. - userData.info.flags |= UserInfo.FLAG_DISABLED; + addUserInfoFlags(userData.info, UserInfo.FLAG_DISABLED); writeUserLP(userData); } @@ -7789,7 +7819,7 @@ public class UserManagerService extends IUserManager.Stub { if (userInfo != null && userInfo.isEphemeral()) { // Do not allow switching back to the ephemeral user again as the user is going // to be deleted. - userInfo.flags |= UserInfo.FLAG_DISABLED; + addUserInfoFlags(userInfo, UserInfo.FLAG_DISABLED); if (userInfo.isGuest()) { // Indicate that the guest will be deleted after it stops. userInfo.guestToRemove = true; diff --git a/services/core/java/com/android/server/pm/parsing/PackageCacher.java b/services/core/java/com/android/server/pm/parsing/PackageCacher.java index 2db454aa4c41..db65bf059319 100644 --- a/services/core/java/com/android/server/pm/parsing/PackageCacher.java +++ b/services/core/java/com/android/server/pm/parsing/PackageCacher.java @@ -33,6 +33,7 @@ import com.android.internal.pm.parsing.IPackageCacher; import com.android.internal.pm.parsing.PackageParser2; import com.android.internal.pm.parsing.pkg.PackageImpl; import com.android.internal.pm.parsing.pkg.ParsedPackage; +import com.android.internal.pm.pkg.component.AconfigFlags; import com.android.internal.pm.pkg.parsing.ParsingPackageUtils; import com.android.server.pm.ApexManager; @@ -41,6 +42,8 @@ import libcore.io.IoUtils; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.util.Map; +import java.util.Objects; import java.util.concurrent.atomic.AtomicInteger; public class PackageCacher implements IPackageCacher { @@ -57,6 +60,8 @@ public class PackageCacher implements IPackageCacher { @Nullable private final PackageParser2.Callback mCallback; + private static final AconfigFlags sAconfigFlags = ParsingPackageUtils.getAconfigFlags(); + public PackageCacher(File cacheDir) { this(cacheDir, null); } @@ -136,7 +141,7 @@ public class PackageCacher implements IPackageCacher { * Given a {@code packageFile} and a {@code cacheFile} returns whether the * cache file is up to date based on the mod-time of both files. */ - private static boolean isCacheUpToDate(File packageFile, File cacheFile) { + private static boolean isCacheFileUpToDate(File packageFile, File cacheFile) { try { // In case packageFile is located on one of /apex mount points it's mtime will always be // 0. Instead, we can use mtime of the APEX file backing the corresponding mount point. @@ -185,16 +190,36 @@ public class PackageCacher implements IPackageCacher { try { // If the cache is not up to date, return null. - if (!isCacheUpToDate(packageFile, cacheFile)) { + if (!isCacheFileUpToDate(packageFile, cacheFile)) { return null; } final byte[] bytes = IoUtils.readFileAsByteArray(cacheFile.getAbsolutePath()); - ParsedPackage parsed = fromCacheEntry(bytes); + final ParsedPackage parsed = fromCacheEntry(bytes); if (!packageFile.getAbsolutePath().equals(parsed.getPath())) { // Don't use this cache if the path doesn't match return null; } + + if (!android.content.pm.Flags.includeFeatureFlagsInPackageCacher()) { + return parsed; + } + + final Map<String, Boolean> featureFlagState = + ((PackageImpl) parsed).getFeatureFlagState(); + if (!featureFlagState.isEmpty()) { + Slog.d(TAG, "Feature flags for package " + packageFile + ": " + featureFlagState); + for (var entry : featureFlagState.entrySet()) { + final String flagPackageAndName = entry.getKey(); + if (!Objects.equals(sAconfigFlags.getFlagValue(flagPackageAndName), + entry.getValue())) { + Slog.i(TAG, "Feature flag " + flagPackageAndName + " changed for package " + + packageFile + "; cached result is invalid"); + return null; + } + } + } + return parsed; } catch (Throwable e) { Slog.w(TAG, "Error reading package cache: ", e); diff --git a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java index bc6a40abaee3..07fd1cb544f6 100644 --- a/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java +++ b/services/core/java/com/android/server/pm/permission/DefaultPermissionGrantPolicy.java @@ -1271,6 +1271,7 @@ final class DefaultPermissionGrantPolicy { */ private boolean isFixedOrUserSet(int flags) { return (flags & (PackageManager.FLAG_PERMISSION_USER_SET + | PackageManager.FLAG_PERMISSION_ONE_TIME | PackageManager.FLAG_PERMISSION_USER_FIXED | PackageManager.FLAG_PERMISSION_POLICY_FIXED | PackageManager.FLAG_PERMISSION_SYSTEM_FIXED)) != 0; diff --git a/services/core/java/com/android/server/power/PowerGroup.java b/services/core/java/com/android/server/power/PowerGroup.java index 2e8a0c678f48..a928814c7909 100644 --- a/services/core/java/com/android/server/power/PowerGroup.java +++ b/services/core/java/com/android/server/power/PowerGroup.java @@ -42,6 +42,7 @@ import android.view.Display; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.util.LatencyTracker; +import com.android.server.power.feature.PowerManagerFlags; /** * Used to store power related requests to every display in a @@ -62,6 +63,8 @@ public class PowerGroup { private final DisplayManagerInternal mDisplayManagerInternal; private final boolean mSupportsSandman; private final int mGroupId; + private final PowerManagerFlags mFeatureFlags; + /** True if DisplayManagerService has applied all the latest display states that were requested * for this group. */ private boolean mReady; @@ -82,10 +85,15 @@ public class PowerGroup { private long mLastWakeTime; /** Timestamp (milliseconds since boot) of the last time the power group was put to sleep. */ private long mLastSleepTime; + /** The last reason that woke the power group. */ + private @PowerManager.WakeReason int mLastWakeReason = PowerManager.WAKE_REASON_UNKNOWN; + /** The last reason that put the power group to sleep. */ + private @PowerManager.GoToSleepReason int mLastSleepReason = + PowerManager.GO_TO_SLEEP_REASON_UNKNOWN; PowerGroup(int groupId, PowerGroupListener wakefulnessListener, Notifier notifier, DisplayManagerInternal displayManagerInternal, int wakefulness, boolean ready, - boolean supportsSandman, long eventTime) { + boolean supportsSandman, long eventTime, PowerManagerFlags featureFlags) { mGroupId = groupId; mWakefulnessListener = wakefulnessListener; mNotifier = notifier; @@ -95,10 +103,12 @@ public class PowerGroup { mSupportsSandman = supportsSandman; mLastWakeTime = eventTime; mLastSleepTime = eventTime; + mFeatureFlags = featureFlags; } PowerGroup(int wakefulness, PowerGroupListener wakefulnessListener, Notifier notifier, - DisplayManagerInternal displayManagerInternal, long eventTime) { + DisplayManagerInternal displayManagerInternal, long eventTime, + PowerManagerFlags featureFlags) { mGroupId = Display.DEFAULT_DISPLAY_GROUP; mWakefulnessListener = wakefulnessListener; mNotifier = notifier; @@ -108,6 +118,7 @@ public class PowerGroup { mSupportsSandman = true; mLastWakeTime = eventTime; mLastSleepTime = eventTime; + mFeatureFlags = featureFlags; } long getLastWakeTimeLocked() { @@ -138,8 +149,14 @@ public class PowerGroup { setLastPowerOnTimeLocked(eventTime); setIsPoweringOnLocked(true); mLastWakeTime = eventTime; + if (mFeatureFlags.isPolicyReasonInDisplayPowerRequestEnabled()) { + mLastWakeReason = reason; + } } else if (isInteractive(mWakefulness) && !isInteractive(newWakefulness)) { mLastSleepTime = eventTime; + if (mFeatureFlags.isPolicyReasonInDisplayPowerRequestEnabled()) { + mLastSleepReason = reason; + } } mWakefulness = newWakefulness; mWakefulnessListener.onWakefulnessChangedLocked(mGroupId, mWakefulness, eventTime, @@ -393,37 +410,51 @@ public class PowerGroup { return false; } - @VisibleForTesting - int getDesiredScreenPolicyLocked(boolean quiescent, boolean dozeAfterScreenOff, + // TODO: create and use more specific policy reasons, beyond the ones that correlate to + // interactivity state + private void updateScreenPolicyLocked(boolean quiescent, boolean dozeAfterScreenOff, boolean bootCompleted, boolean screenBrightnessBoostInProgress, boolean brightWhenDozing) { final int wakefulness = getWakefulnessLocked(); final int wakeLockSummary = getWakeLockSummaryLocked(); - if (wakefulness == WAKEFULNESS_ASLEEP || quiescent) { - return DisplayPowerRequest.POLICY_OFF; + int policyReason = Display.STATE_REASON_DEFAULT_POLICY; + int policy = Integer.MAX_VALUE; // do not set to real policy to start with. + if (quiescent) { + policy = DisplayPowerRequest.POLICY_OFF; + } else if (wakefulness == WAKEFULNESS_ASLEEP) { + policy = DisplayPowerRequest.POLICY_OFF; + policyReason = sleepReasonToDisplayStateReason(mLastSleepReason); } else if (wakefulness == WAKEFULNESS_DOZING) { if ((wakeLockSummary & WAKE_LOCK_DOZE) != 0) { - return DisplayPowerRequest.POLICY_DOZE; - } - if (dozeAfterScreenOff) { - return DisplayPowerRequest.POLICY_OFF; - } - if (brightWhenDozing) { - return DisplayPowerRequest.POLICY_BRIGHT; + policy = DisplayPowerRequest.POLICY_DOZE; + } else if (dozeAfterScreenOff) { + policy = DisplayPowerRequest.POLICY_OFF; + } else if (brightWhenDozing) { + policy = DisplayPowerRequest.POLICY_BRIGHT; } // Fall through and preserve the current screen policy if not configured to // bright when dozing or doze after screen off. This causes the screen off transition // to be skipped. } - if ((wakeLockSummary & WAKE_LOCK_SCREEN_BRIGHT) != 0 - || !bootCompleted - || (getUserActivitySummaryLocked() & USER_ACTIVITY_SCREEN_BRIGHT) != 0 - || screenBrightnessBoostInProgress) { - return DisplayPowerRequest.POLICY_BRIGHT; + if (policy == Integer.MAX_VALUE) { // policy is not set yet. + if (isInteractive(wakefulness)) { + policyReason = wakeReasonToDisplayStateReason(mLastWakeReason); + } + if ((wakeLockSummary & WAKE_LOCK_SCREEN_BRIGHT) != 0 + || !bootCompleted + || (getUserActivitySummaryLocked() & USER_ACTIVITY_SCREEN_BRIGHT) != 0 + || screenBrightnessBoostInProgress) { + policy = DisplayPowerRequest.POLICY_BRIGHT; + } else { + policy = DisplayPowerRequest.POLICY_DIM; + } } - return DisplayPowerRequest.POLICY_DIM; + if (mFeatureFlags.isPolicyReasonInDisplayPowerRequestEnabled()) { + mDisplayPowerRequest.policyReason = policyReason; + } + mDisplayPowerRequest.policy = policy; } int getPolicyLocked() { @@ -439,7 +470,7 @@ public class PowerGroup { boolean dozeAfterScreenOff, boolean bootCompleted, boolean screenBrightnessBoostInProgress, boolean waitForNegativeProximity, boolean brightWhenDozing) { - mDisplayPowerRequest.policy = getDesiredScreenPolicyLocked(quiescent, dozeAfterScreenOff, + updateScreenPolicyLocked(quiescent, dozeAfterScreenOff, bootCompleted, screenBrightnessBoostInProgress, brightWhenDozing); mDisplayPowerRequest.screenBrightnessOverride = screenBrightnessOverride; mDisplayPowerRequest.screenBrightnessOverrideTag = overrideTag; @@ -478,6 +509,33 @@ public class PowerGroup { return ready; } + /** Determines the respective display state reason for a given PowerManager WakeReason. */ + private static int wakeReasonToDisplayStateReason(@PowerManager.WakeReason int wakeReason) { + switch (wakeReason) { + case PowerManager.WAKE_REASON_POWER_BUTTON: + case PowerManager.WAKE_REASON_WAKE_KEY: + return Display.STATE_REASON_KEY; + case PowerManager.WAKE_REASON_WAKE_MOTION: + return Display.STATE_REASON_MOTION; + case PowerManager.WAKE_REASON_TILT: + return Display.STATE_REASON_TILT; + default: + return Display.STATE_REASON_DEFAULT_POLICY; + } + } + + /** Determines the respective display state reason for a given PowerManager GoToSleepReason. */ + private static int sleepReasonToDisplayStateReason( + @PowerManager.GoToSleepReason int sleepReason) { + switch (sleepReason) { + case PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON: + case PowerManager.GO_TO_SLEEP_REASON_SLEEP_BUTTON: + return Display.STATE_REASON_KEY; + default: + return Display.STATE_REASON_DEFAULT_POLICY; + } + } + protected interface PowerGroupListener { /** * Informs the recipient about a wakefulness change of a {@link PowerGroup}. diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java index 65f22416a535..3a5afacd0977 100644 --- a/services/core/java/com/android/server/power/PowerManagerService.java +++ b/services/core/java/com/android/server/power/PowerManagerService.java @@ -790,7 +790,8 @@ public final class PowerManagerService extends SystemService WAKEFULNESS_AWAKE, /* ready= */ false, supportsSandman, - mClock.uptimeMillis()); + mClock.uptimeMillis(), + mFeatureFlags); mPowerGroups.append(groupId, powerGroup); onPowerGroupEventLocked(DISPLAY_GROUP_ADDED, powerGroup); } @@ -1375,7 +1376,8 @@ public final class PowerManagerService extends SystemService mPowerGroups.append(Display.DEFAULT_DISPLAY_GROUP, new PowerGroup(WAKEFULNESS_AWAKE, mPowerGroupWakefulnessChangeListener, - mNotifier, mDisplayManagerInternal, mClock.uptimeMillis())); + mNotifier, mDisplayManagerInternal, mClock.uptimeMillis(), + mFeatureFlags)); DisplayGroupPowerChangeListener displayGroupPowerChangeListener = new DisplayGroupPowerChangeListener(); mDisplayManagerInternal.registerDisplayGroupListener(displayGroupPowerChangeListener); @@ -3738,14 +3740,6 @@ public final class PowerManagerService extends SystemService } @VisibleForTesting - @GuardedBy("mLock") - int getDesiredScreenPolicyLocked(int groupId) { - return mPowerGroups.get(groupId).getDesiredScreenPolicyLocked(sQuiescent, - mDozeAfterScreenOff, mBootCompleted, - mScreenBrightnessBoostInProgress, mBrightWhenDozingConfig); - } - - @VisibleForTesting int getDreamsBatteryLevelDrain() { return mDreamsBatteryLevelDrain; } @@ -4588,7 +4582,8 @@ public final class PowerManagerService extends SystemService WAKEFULNESS_AWAKE, /* ready= */ false, /* supportsSandman= */ false, - mClock.uptimeMillis()); + mClock.uptimeMillis(), + mFeatureFlags); mPowerGroups.append(displayGroupId, powerGroup); } mDirty |= DIRTY_DISPLAY_GROUP_WAKEFULNESS; diff --git a/services/core/java/com/android/server/power/feature/PowerManagerFlags.java b/services/core/java/com/android/server/power/feature/PowerManagerFlags.java index db57d11a75c0..4ddf0c02c730 100644 --- a/services/core/java/com/android/server/power/feature/PowerManagerFlags.java +++ b/services/core/java/com/android/server/power/feature/PowerManagerFlags.java @@ -50,6 +50,11 @@ public class PowerManagerFlags { private final FlagState mFrameworkWakelockInfo = new FlagState(Flags.FLAG_FRAMEWORK_WAKELOCK_INFO, Flags::frameworkWakelockInfo); + private final FlagState mPolicyReasonInDisplayPowerRequest = new FlagState( + Flags.FLAG_POLICY_REASON_IN_DISPLAY_POWER_REQUEST, + Flags::policyReasonInDisplayPowerRequest + ); + /** Returns whether early-screen-timeout-detector is enabled on not. */ public boolean isEarlyScreenTimeoutDetectorEnabled() { return mEarlyScreenTimeoutDetectorFlagState.isEnabled(); @@ -77,6 +82,13 @@ public class PowerManagerFlags { } /** + * @return Whether the wakefulness reason is populated in DisplayPowerRequest. + */ + public boolean isPolicyReasonInDisplayPowerRequestEnabled() { + return mPolicyReasonInDisplayPowerRequest.isEnabled(); + } + + /** * dumps all flagstates * @param pw printWriter */ diff --git a/services/core/java/com/android/server/power/feature/power_flags.aconfig b/services/core/java/com/android/server/power/feature/power_flags.aconfig index 8bb69ba0c5de..e27f8bb6fee6 100644 --- a/services/core/java/com/android/server/power/feature/power_flags.aconfig +++ b/services/core/java/com/android/server/power/feature/power_flags.aconfig @@ -33,3 +33,11 @@ flag { description: "Feature flag to enable statsd pulling of FrameworkWakelockInfo atoms" bug: "352602149" } + +flag { + name: "policy_reason_in_display_power_request" + namespace: "wear_frameworks" + description: "Whether the policy reason is populted in DisplayPowerRequest." + bug: "364349703" + is_fixed_read_only: true +} diff --git a/services/core/java/com/android/server/security/advancedprotection/AdvancedProtectionService.java b/services/core/java/com/android/server/security/advancedprotection/AdvancedProtectionService.java index c33ed555a7f9..1260eeec098f 100644 --- a/services/core/java/com/android/server/security/advancedprotection/AdvancedProtectionService.java +++ b/services/core/java/com/android/server/security/advancedprotection/AdvancedProtectionService.java @@ -33,6 +33,7 @@ import android.os.RemoteException; import android.os.ResultReceiver; import android.os.ShellCallback; import android.provider.Settings; +import android.security.advancedprotection.AdvancedProtectionFeature; import android.security.advancedprotection.IAdvancedProtectionCallback; import android.security.advancedprotection.IAdvancedProtectionService; import android.util.ArrayMap; @@ -44,9 +45,11 @@ import com.android.server.LocalServices; import com.android.server.SystemService; import com.android.server.pm.UserManagerInternal; import com.android.server.security.advancedprotection.features.AdvancedProtectionHook; +import com.android.server.security.advancedprotection.features.AdvancedProtectionProvider; import java.io.FileDescriptor; import java.util.ArrayList; +import java.util.List; /** @hide */ public class AdvancedProtectionService extends IAdvancedProtectionService.Stub { @@ -58,10 +61,12 @@ public class AdvancedProtectionService extends IAdvancedProtectionService.Stub private final Handler mHandler; private final AdvancedProtectionStore mStore; - // Features owned by the service - their code will be executed when state changes + // Features living with the service - their code will be executed when state changes private final ArrayList<AdvancedProtectionHook> mHooks = new ArrayList<>(); // External features - they will be called on state change private final ArrayMap<IBinder, IAdvancedProtectionCallback> mCallbacks = new ArrayMap<>(); + // For tracking only - not called on state change + private final ArrayList<AdvancedProtectionProvider> mProviders = new ArrayList<>(); private AdvancedProtectionService(@NonNull Context context) { super(PermissionEnforcer.fromContext(context)); @@ -71,13 +76,17 @@ public class AdvancedProtectionService extends IAdvancedProtectionService.Stub } private void initFeatures(boolean enabled) { + // Empty until features are added. + // Examples: + // mHooks.add(new SideloadingAdvancedProtectionHook(mContext, enabled)); + // mProviders.add(new WifiAdvancedProtectionProvider()); } // Only for tests @VisibleForTesting AdvancedProtectionService(@NonNull Context context, @NonNull AdvancedProtectionStore store, @NonNull Looper looper, @NonNull PermissionEnforcer permissionEnforcer, - @Nullable AdvancedProtectionHook hook) { + @Nullable AdvancedProtectionHook hook, @Nullable AdvancedProtectionProvider provider) { super(permissionEnforcer); mContext = context; mStore = store; @@ -85,6 +94,10 @@ public class AdvancedProtectionService extends IAdvancedProtectionService.Stub if (hook != null) { mHooks.add(hook); } + + if (provider != null) { + mProviders.add(provider); + } } @Override @@ -146,6 +159,25 @@ public class AdvancedProtectionService extends IAdvancedProtectionService.Stub } @Override + @EnforcePermission(Manifest.permission.SET_ADVANCED_PROTECTION_MODE) + public List<AdvancedProtectionFeature> getAdvancedProtectionFeatures() { + getAdvancedProtectionFeatures_enforcePermission(); + List<AdvancedProtectionFeature> features = new ArrayList<>(); + for (int i = 0; i < mProviders.size(); i++) { + features.addAll(mProviders.get(i).getFeatures()); + } + + for (int i = 0; i < mHooks.size(); i++) { + AdvancedProtectionHook hook = mHooks.get(i); + if (hook.isAvailable()) { + features.add(hook.getFeature()); + } + } + + return features; + } + + @Override public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, @NonNull String[] args, ShellCallback callback, @NonNull ResultReceiver resultReceiver) { diff --git a/services/core/java/com/android/server/security/advancedprotection/features/AdvancedProtectionHook.java b/services/core/java/com/android/server/security/advancedprotection/features/AdvancedProtectionHook.java index b2acc511950b..f82db9676c2c 100644 --- a/services/core/java/com/android/server/security/advancedprotection/features/AdvancedProtectionHook.java +++ b/services/core/java/com/android/server/security/advancedprotection/features/AdvancedProtectionHook.java @@ -18,11 +18,15 @@ package com.android.server.security.advancedprotection.features; import android.annotation.NonNull; import android.content.Context; +import android.security.advancedprotection.AdvancedProtectionFeature; /** @hide */ public abstract class AdvancedProtectionHook { /** Called on boot phase PHASE_SYSTEM_SERVICES_READY */ public AdvancedProtectionHook(@NonNull Context context, boolean enabled) {} + /** The feature this hook provides */ + @NonNull + public abstract AdvancedProtectionFeature getFeature(); /** Whether this feature is relevant on this device. If false, onAdvancedProtectionChanged will * not be called, and the feature will not be displayed in the onboarding UX. */ public abstract boolean isAvailable(); diff --git a/services/core/java/com/android/server/security/advancedprotection/features/AdvancedProtectionProvider.java b/services/core/java/com/android/server/security/advancedprotection/features/AdvancedProtectionProvider.java new file mode 100644 index 000000000000..ed451f1e2257 --- /dev/null +++ b/services/core/java/com/android/server/security/advancedprotection/features/AdvancedProtectionProvider.java @@ -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.server.security.advancedprotection.features; + +import android.security.advancedprotection.AdvancedProtectionFeature; + +import java.util.List; + +/** @hide */ +public abstract class AdvancedProtectionProvider { + /** The list of features provided */ + public abstract List<AdvancedProtectionFeature> getFeatures(); +} diff --git a/services/core/java/com/android/server/trust/TrustManagerService.java b/services/core/java/com/android/server/trust/TrustManagerService.java index 457196b74d2e..465ac2f1731d 100644 --- a/services/core/java/com/android/server/trust/TrustManagerService.java +++ b/services/core/java/com/android/server/trust/TrustManagerService.java @@ -21,6 +21,7 @@ import static android.service.trust.GrantTrustResult.STATUS_UNLOCKED_BY_GRANT; import static android.service.trust.TrustAgentService.FLAG_GRANT_TRUST_TEMPORARY_AND_RENEWABLE; import android.Manifest; +import android.annotation.EnforcePermission; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UserIdInt; @@ -1896,8 +1897,11 @@ public class TrustManagerService extends SystemService { } } + @EnforcePermission(Manifest.permission.ACCESS_FINE_LOCATION) @Override public boolean isInSignificantPlace() { + super.isInSignificantPlace_enforcePermission(); + if (android.security.Flags.significantPlaces()) { mSignificantPlaceServiceWatcher.runOnBinder( binder -> ISignificantPlaceProvider.Stub.asInterface(binder) diff --git a/services/core/java/com/android/server/tv/tunerresourcemanager/ClientProfile.java b/services/core/java/com/android/server/tv/tunerresourcemanager/ClientProfile.java index 096231910e6e..38bc026c473a 100644 --- a/services/core/java/com/android/server/tv/tunerresourcemanager/ClientProfile.java +++ b/services/core/java/com/android/server/tv/tunerresourcemanager/ClientProfile.java @@ -115,11 +115,18 @@ public final class ClientProfile { */ private int mPriority; + /** + * If resource holder retains ownership of the resource in a challenge scenario then value is + * true. + */ + private boolean mResourceHolderRetain; + private ClientProfile(Builder builder) { this.mId = builder.mId; this.mTvInputSessionId = builder.mTvInputSessionId; this.mUseCase = builder.mUseCase; this.mProcessId = builder.mProcessId; + this.mResourceHolderRetain = builder.mResourceHolderRetain; } public int getId() { @@ -139,6 +146,14 @@ public final class ClientProfile { } /** + * Returns true when the resource holder retains ownership of the resource in a challenge + * scenario. + */ + public boolean shouldResourceHolderRetain() { + return mResourceHolderRetain; + } + + /** * If the client priority is overwrttien. */ public boolean isPriorityOverwritten() { @@ -180,6 +195,19 @@ public final class ClientProfile { } /** + * Determines whether the resource holder retains ownership of the resource during a challenge + * scenario, when both resource holder and resource challenger have same processId and same + * priority. + * + * @param resourceHolderRetain Set to true to allow the resource holder to retain ownership, or + * false (or resourceHolderRetain not set at all) to allow the resource challenger to + * acquire the resource. If not explicitly set, resourceHolderRetain is set to false. + */ + public void setResourceHolderRetain(boolean resourceHolderRetain) { + mResourceHolderRetain = resourceHolderRetain; + } + + /** * Set when the client starts to use a frontend. * * @param frontendHandle being used. @@ -361,6 +389,7 @@ public final class ClientProfile { private String mTvInputSessionId; private int mUseCase; private int mProcessId; + private boolean mResourceHolderRetain = false; Builder(int id) { this.mId = id; @@ -397,6 +426,18 @@ public final class ClientProfile { } /** + * Builder for {@link ClientProfile}. + * + * @param resourceHolderRetain the determining factor for resource ownership during + * challenger scenario. The default behavior favors the resource challenger and grants + * them ownership of the resource if resourceHolderRetain is not explicitly set to true. + */ + public Builder resourceHolderRetain(boolean resourceHolderRetain) { + this.mResourceHolderRetain = resourceHolderRetain; + return this; + } + + /** * Build a {@link ClientProfile}. * * @return {@link ClientProfile}. diff --git a/services/core/java/com/android/server/tv/tunerresourcemanager/TunerResourceManagerService.java b/services/core/java/com/android/server/tv/tunerresourcemanager/TunerResourceManagerService.java index c5b6bbf30ae1..5ae8c11f1d8f 100644 --- a/services/core/java/com/android/server/tv/tunerresourcemanager/TunerResourceManagerService.java +++ b/services/core/java/com/android/server/tv/tunerresourcemanager/TunerResourceManagerService.java @@ -16,6 +16,8 @@ package com.android.server.tv.tunerresourcemanager; +import static android.media.tv.flags.Flags.setResourceHolderRetain; + import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; @@ -229,6 +231,14 @@ public class TunerResourceManagerService extends SystemService implements IBinde } @Override + public void setResourceHolderRetain(int clientId, boolean resourceHolderRetain) { + enforceTrmAccessPermission("setResourceHolderRetain"); + synchronized (mLock) { + getClientProfile(clientId).setResourceHolderRetain(resourceHolderRetain); + } + } + + @Override public boolean isLowestPriority(int clientId, int frontendType) throws RemoteException { enforceTrmAccessPermission("isLowestPriority"); @@ -1066,8 +1076,10 @@ public class TunerResourceManagerService extends SystemService implements IBinde // request client has higher priority. if (inUseLowestPriorityFrontend != null && ((requestClient.getPriority() > currentLowestPriority) - || ((requestClient.getPriority() == currentLowestPriority) - && isRequestFromSameProcess))) { + || ((requestClient.getPriority() == currentLowestPriority) + && isRequestFromSameProcess + && !(setResourceHolderRetain() + && requestClient.shouldResourceHolderRetain())))) { frontendHandle[0] = inUseLowestPriorityFrontend.getHandle(); reclaimOwnerId[0] = inUseLowestPriorityFrontend.getOwnerClientId(); return true; @@ -1249,9 +1261,11 @@ public class TunerResourceManagerService extends SystemService implements IBinde // When all the resources are occupied, grant the lowest priority resource if the // request client has higher priority. if (inUseLowestPriorityLnb != null - && ((requestClient.getPriority() > currentLowestPriority) || ( - (requestClient.getPriority() == currentLowestPriority) - && isRequestFromSameProcess))) { + && ((requestClient.getPriority() > currentLowestPriority) + || ((requestClient.getPriority() == currentLowestPriority) + && isRequestFromSameProcess + && !(setResourceHolderRetain() + && requestClient.shouldResourceHolderRetain())))) { lnbHandle[0] = inUseLowestPriorityLnb.getHandle(); reclaimOwnerId[0] = inUseLowestPriorityLnb.getOwnerClientId(); return true; @@ -1335,8 +1349,10 @@ public class TunerResourceManagerService extends SystemService implements IBinde // request client has higher priority. if (lowestPriorityOwnerId != INVALID_CLIENT_ID && ((requestClient.getPriority() > currentLowestPriority) - || ((requestClient.getPriority() == currentLowestPriority) - && isRequestFromSameProcess))) { + || ((requestClient.getPriority() == currentLowestPriority) + && isRequestFromSameProcess + && !(setResourceHolderRetain() + && requestClient.shouldResourceHolderRetain())))) { casSessionHandle[0] = cas.getHandle(); reclaimOwnerId[0] = lowestPriorityOwnerId; return true; @@ -1420,8 +1436,10 @@ public class TunerResourceManagerService extends SystemService implements IBinde // request client has higher priority. if (lowestPriorityOwnerId != INVALID_CLIENT_ID && ((requestClient.getPriority() > currentLowestPriority) - || ((requestClient.getPriority() == currentLowestPriority) - && isRequestFromSameProcess))) { + || ((requestClient.getPriority() == currentLowestPriority) + && isRequestFromSameProcess + && !(setResourceHolderRetain() + && requestClient.shouldResourceHolderRetain())))) { ciCamHandle[0] = ciCam.getHandle(); reclaimOwnerId[0] = lowestPriorityOwnerId; return true; @@ -1655,9 +1673,11 @@ public class TunerResourceManagerService extends SystemService implements IBinde // When all the resources are occupied, grant the lowest priority resource if the // request client has higher priority. if (inUseLowestPriorityDemux != null - && ((requestClient.getPriority() > currentLowestPriority) || ( - (requestClient.getPriority() == currentLowestPriority) - && isRequestFromSameProcess))) { + && ((requestClient.getPriority() > currentLowestPriority) + || ((requestClient.getPriority() == currentLowestPriority) + && isRequestFromSameProcess + && !(setResourceHolderRetain() + && requestClient.shouldResourceHolderRetain())))) { demuxHandle[0] = inUseLowestPriorityDemux.getHandle(); reclaimOwnerId[0] = inUseLowestPriorityDemux.getOwnerClientId(); return true; diff --git a/services/core/java/com/android/server/vcn/TelephonySubscriptionTracker.java b/services/core/java/com/android/server/vcn/TelephonySubscriptionTracker.java index baf84cf4aa8b..3392d039109a 100644 --- a/services/core/java/com/android/server/vcn/TelephonySubscriptionTracker.java +++ b/services/core/java/com/android/server/vcn/TelephonySubscriptionTracker.java @@ -436,6 +436,17 @@ public class TelephonySubscriptionTracker extends BroadcastReceiver { return mPrivilegedPackages.keySet(); } + /** Returns all subscription groups */ + @NonNull + public Set<ParcelUuid> getAllSubscriptionGroups() { + final Set<ParcelUuid> subGroups = new ArraySet<>(); + for (SubscriptionInfo subInfo : mSubIdToInfoMap.values()) { + subGroups.add(subInfo.getGroupUuid()); + } + + return subGroups; + } + /** Checks if the provided package is carrier privileged for the specified sub group. */ public boolean packageHasPermissionsForSubscriptionGroup( @NonNull ParcelUuid subGrp, @NonNull String packageName) { diff --git a/services/core/java/com/android/server/vcn/VcnContext.java b/services/core/java/com/android/server/vcn/VcnContext.java index a492a72933d7..6ce868540070 100644 --- a/services/core/java/com/android/server/vcn/VcnContext.java +++ b/services/core/java/com/android/server/vcn/VcnContext.java @@ -18,7 +18,6 @@ package com.android.server.vcn; import android.annotation.NonNull; import android.content.Context; -import android.net.IpSecTransformState; import android.net.vcn.FeatureFlags; import android.net.vcn.FeatureFlagsImpl; import android.os.Looper; @@ -70,19 +69,6 @@ public class VcnContext { return mIsInTestMode; } - public boolean isFlagIpSecTransformStateEnabled() { - // TODO: b/328844044: Ideally this code should gate the behavior by checking the - // android.net.platform.flags.ipsec_transform_state flag but that flag is not accessible - // right now. We should either update the code when the flag is accessible or remove the - // legacy behavior after VIC SDK finalization - try { - new IpSecTransformState.Builder(); - return true; - } catch (Exception e) { - return false; - } - } - @NonNull public FeatureFlags getFeatureFlags() { return mFeatureFlags; diff --git a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java index 189b6089186e..2d3bc84debff 100644 --- a/services/core/java/com/android/server/vcn/VcnGatewayConnection.java +++ b/services/core/java/com/android/server/vcn/VcnGatewayConnection.java @@ -1912,8 +1912,7 @@ public class VcnGatewayConnection extends StateMachine { // Transforms do not need to be persisted; the IkeSession will keep them alive mIpSecManager.applyTunnelModeTransform(tunnelIface, direction, transform); - if (direction == IpSecManager.DIRECTION_IN - && mVcnContext.isFlagIpSecTransformStateEnabled()) { + if (direction == IpSecManager.DIRECTION_IN) { mUnderlyingNetworkController.updateInboundTransform(mUnderlying, transform); } diff --git a/services/core/java/com/android/server/vcn/routeselection/IpSecPacketLossDetector.java b/services/core/java/com/android/server/vcn/routeselection/IpSecPacketLossDetector.java index 6f1e15b5033f..16ab51e8d604 100644 --- a/services/core/java/com/android/server/vcn/routeselection/IpSecPacketLossDetector.java +++ b/services/core/java/com/android/server/vcn/routeselection/IpSecPacketLossDetector.java @@ -148,12 +148,6 @@ public class IpSecPacketLossDetector extends NetworkMetricMonitor { Objects.requireNonNull(deps, "Missing deps"); - if (!vcnContext.isFlagIpSecTransformStateEnabled()) { - // Caller error - logWtf("ipsecTransformState flag disabled"); - throw new IllegalAccessException("ipsecTransformState flag disabled"); - } - mHandler = new Handler(getVcnContext().getLooper()); mPowerManager = getVcnContext().getContext().getSystemService(PowerManager.class); diff --git a/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkController.java b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkController.java index 0b9b677df16a..3eeeece5da46 100644 --- a/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkController.java +++ b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkController.java @@ -204,10 +204,8 @@ public class UnderlyingNetworkController { List<NetworkCallback> oldCellCallbacks = new ArrayList<>(mCellBringupCallbacks); mCellBringupCallbacks.clear(); - if (mVcnContext.isFlagIpSecTransformStateEnabled()) { - for (UnderlyingNetworkEvaluator evaluator : mUnderlyingNetworkRecords.values()) { - evaluator.close(); - } + for (UnderlyingNetworkEvaluator evaluator : mUnderlyingNetworkRecords.values()) { + evaluator.close(); } mUnderlyingNetworkRecords.clear(); @@ -429,10 +427,7 @@ public class UnderlyingNetworkController { if (oldSnapshot .getAllSubIdsInGroup(mSubscriptionGroup) .equals(newSnapshot.getAllSubIdsInGroup(mSubscriptionGroup))) { - - if (mVcnContext.isFlagIpSecTransformStateEnabled()) { - reevaluateNetworks(); - } + reevaluateNetworks(); return; } registerOrUpdateNetworkRequests(); @@ -445,11 +440,6 @@ public class UnderlyingNetworkController { */ public void updateInboundTransform( @NonNull UnderlyingNetworkRecord currentNetwork, @NonNull IpSecTransform transform) { - if (!mVcnContext.isFlagIpSecTransformStateEnabled()) { - logWtf("#updateInboundTransform: unexpected call; flags missing"); - return; - } - Objects.requireNonNull(currentNetwork, "currentNetwork is null"); Objects.requireNonNull(transform, "transform is null"); @@ -572,10 +562,7 @@ public class UnderlyingNetworkController { @Override public void onLost(@NonNull Network network) { - if (mVcnContext.isFlagIpSecTransformStateEnabled()) { - mUnderlyingNetworkRecords.get(network).close(); - } - + mUnderlyingNetworkRecords.get(network).close(); mUnderlyingNetworkRecords.remove(network); reevaluateNetworks(); @@ -648,11 +635,6 @@ public class UnderlyingNetworkController { class NetworkEvaluatorCallbackImpl implements NetworkEvaluatorCallback { @Override public void onEvaluationResultChanged() { - if (!mVcnContext.isFlagIpSecTransformStateEnabled()) { - logWtf("#onEvaluationResultChanged: unexpected call; flags missing"); - return; - } - mVcnContext.ensureRunningOnLooperThread(); reevaluateNetworks(); } diff --git a/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluator.java b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluator.java index 448a7ebfffd8..08be11e29689 100644 --- a/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluator.java +++ b/services/core/java/com/android/server/vcn/routeselection/UnderlyingNetworkEvaluator.java @@ -102,17 +102,15 @@ public class UnderlyingNetworkEvaluator { updatePriorityClass( underlyingNetworkTemplates, subscriptionGroup, lastSnapshot, carrierConfig); - if (isIpSecPacketLossDetectorEnabled()) { - try { - mMetricMonitors.add( - mDependencies.newIpSecPacketLossDetector( - mVcnContext, - mNetworkRecordBuilder.getNetwork(), - carrierConfig, - new MetricMonitorCallbackImpl())); - } catch (IllegalAccessException e) { - // No action. Do not add anything to mMetricMonitors - } + try { + mMetricMonitors.add( + mDependencies.newIpSecPacketLossDetector( + mVcnContext, + mNetworkRecordBuilder.getNetwork(), + carrierConfig, + new MetricMonitorCallbackImpl())); + } catch (IllegalAccessException e) { + // No action. Do not add anything to mMetricMonitors } } @@ -188,22 +186,12 @@ public class UnderlyingNetworkEvaluator { } } - private boolean isIpSecPacketLossDetectorEnabled() { - return isIpSecPacketLossDetectorEnabled(mVcnContext); - } - - private static boolean isIpSecPacketLossDetectorEnabled(VcnContext vcnContext) { - return vcnContext.isFlagIpSecTransformStateEnabled(); - } - /** Get the comparator for UnderlyingNetworkEvaluator */ public static Comparator<UnderlyingNetworkEvaluator> getComparator(VcnContext vcnContext) { return (left, right) -> { - if (isIpSecPacketLossDetectorEnabled(vcnContext)) { - if (left.mIsPenalized != right.mIsPenalized) { - // A penalized network should have lower priority which means a larger index - return left.mIsPenalized ? 1 : -1; - } + if (left.mIsPenalized != right.mIsPenalized) { + // A penalized network should have lower priority which means a larger index + return left.mIsPenalized ? 1 : -1; } final int leftIndex = left.mPriorityClass; diff --git a/services/core/java/com/android/server/vibrator/ComposePwleV2VibratorStep.java b/services/core/java/com/android/server/vibrator/ComposePwleV2VibratorStep.java index d0d6071e9dcb..32a322725692 100644 --- a/services/core/java/com/android/server/vibrator/ComposePwleV2VibratorStep.java +++ b/services/core/java/com/android/server/vibrator/ComposePwleV2VibratorStep.java @@ -20,6 +20,7 @@ import android.annotation.NonNull; import android.os.Trace; import android.os.VibrationEffect; import android.os.vibrator.Flags; +import android.os.vibrator.PwlePoint; import android.os.vibrator.PwleSegment; import android.os.vibrator.VibrationEffectSegment; import android.util.Slog; @@ -57,7 +58,7 @@ final class ComposePwleV2VibratorStep extends AbstractComposedVibratorStep { // Load the next PwleSegments to create a single composePwleV2 call to the vibrator, // limited to the vibrator's maximum envelope effect size. int limit = controller.getVibratorInfo().getMaxEnvelopeEffectSize(); - List<PwleSegment> pwles = unrollPwleSegments(effect, segmentIndex, limit); + List<PwlePoint> pwles = unrollPwleSegments(effect, segmentIndex, limit); if (pwles.isEmpty()) { Slog.w(VibrationThread.TAG, "Ignoring wrong segment for a ComposeEnvelopeStep: " @@ -70,7 +71,7 @@ final class ComposePwleV2VibratorStep extends AbstractComposedVibratorStep { Slog.d(VibrationThread.TAG, "Compose " + pwles + " PWLEs on vibrator " + controller.getVibratorInfo().getId()); } - PwleSegment[] pwlesArray = pwles.toArray(new PwleSegment[pwles.size()]); + PwlePoint[] pwlesArray = pwles.toArray(new PwlePoint[pwles.size()]); long vibratorOnResult = controller.on(pwlesArray, getVibration().id); handleVibratorOnResult(vibratorOnResult); getVibration().stats.reportComposePwle(vibratorOnResult, pwlesArray); @@ -82,9 +83,9 @@ final class ComposePwleV2VibratorStep extends AbstractComposedVibratorStep { } } - private List<PwleSegment> unrollPwleSegments(VibrationEffect.Composed effect, int startIndex, + private List<PwlePoint> unrollPwleSegments(VibrationEffect.Composed effect, int startIndex, int limit) { - List<PwleSegment> segments = new ArrayList<>(limit); + List<PwlePoint> pwlePoints = new ArrayList<>(limit); float bestBreakAmplitude = 1; int bestBreakPosition = limit; // Exclusive index. @@ -93,7 +94,7 @@ final class ComposePwleV2VibratorStep extends AbstractComposedVibratorStep { // Loop once after reaching the limit to see if breaking it will really be necessary, then // apply the best break position found, otherwise return the full list as it fits the limit. - for (int i = startIndex; segments.size() <= limit; i++) { + for (int i = startIndex; pwlePoints.size() < limit; i++) { if (i == segmentCount) { if (repeatIndex >= 0) { i = repeatIndex; @@ -104,12 +105,20 @@ final class ComposePwleV2VibratorStep extends AbstractComposedVibratorStep { } VibrationEffectSegment segment = effect.getSegments().get(i); if (segment instanceof PwleSegment pwleSegment) { - segments.add(pwleSegment); + if (pwlePoints.isEmpty()) { + // The initial state is defined by the starting amplitude and frequency of the + // first PwleSegment. The time parameter is set to zero to indicate this is + // the initial condition without any ramp up time. + pwlePoints.add(new PwlePoint(pwleSegment.getStartAmplitude(), + pwleSegment.getStartFrequencyHz(), /*timeMillis=*/ 0)); + } + pwlePoints.add(new PwlePoint(pwleSegment.getEndAmplitude(), + pwleSegment.getEndFrequencyHz(), (int) pwleSegment.getDuration())); - if (isBetterBreakPosition(segments, bestBreakAmplitude, limit)) { + if (isBetterBreakPosition(pwlePoints, bestBreakAmplitude, limit)) { // Mark this position as the best one so far to break a long waveform. bestBreakAmplitude = pwleSegment.getEndAmplitude(); - bestBreakPosition = segments.size(); // Break after this pwle ends. + bestBreakPosition = pwlePoints.size(); // Break after this pwle ends. } } else { // First non-pwle segment, stop collecting pwles. @@ -117,21 +126,21 @@ final class ComposePwleV2VibratorStep extends AbstractComposedVibratorStep { } } - return segments.size() > limit + return pwlePoints.size() > limit // Remove excessive segments, using the best breaking position recorded. - ? segments.subList(0, bestBreakPosition) + ? pwlePoints.subList(0, bestBreakPosition) // Return all collected pwle segments. - : segments; + : pwlePoints; } /** * Returns true if the current segment list represents a better break position for a PWLE, * given the current amplitude being used for breaking it at a smaller size and the size limit. */ - private boolean isBetterBreakPosition(List<PwleSegment> segments, + private boolean isBetterBreakPosition(List<PwlePoint> segments, float currentBestBreakAmplitude, int limit) { - PwleSegment lastSegment = segments.get(segments.size() - 1); - float breakAmplitudeCandidate = lastSegment.getEndAmplitude(); + PwlePoint lastSegment = segments.get(segments.size() - 1); + float breakAmplitudeCandidate = lastSegment.getAmplitude(); int breakPositionCandidate = segments.size(); if (breakPositionCandidate > limit) { diff --git a/services/core/java/com/android/server/vibrator/VibrationStats.java b/services/core/java/com/android/server/vibrator/VibrationStats.java index bc4dbe75b37e..de423f061b2b 100644 --- a/services/core/java/com/android/server/vibrator/VibrationStats.java +++ b/services/core/java/com/android/server/vibrator/VibrationStats.java @@ -22,7 +22,7 @@ import android.os.CombinedVibration; import android.os.SystemClock; import android.os.vibrator.PrebakedSegment; import android.os.vibrator.PrimitiveSegment; -import android.os.vibrator.PwleSegment; +import android.os.vibrator.PwlePoint; import android.os.vibrator.RampSegment; import android.util.Slog; import android.util.SparseBooleanArray; @@ -294,18 +294,22 @@ final class VibrationStats { } /** Report a call to vibrator method to trigger a vibration as a PWLE. */ - void reportComposePwle(long halResult, PwleSegment[] segments) { + void reportComposePwle(long halResult, PwlePoint[] pwlePoints) { mVibratorComposePwleCount++; - mVibrationPwleTotalSize += segments.length; + mVibrationPwleTotalSize += pwlePoints.length; if (halResult > 0) { // If HAL result is positive then it represents the actual duration of the vibration. // Remove the zero-amplitude segments to update the total time the vibrator was ON. - for (PwleSegment ramp : segments) { - if ((ramp.getStartAmplitude() == 0) && (ramp.getEndAmplitude() == 0)) { - halResult -= ramp.getDuration(); + for (int i = 0; i < pwlePoints.length - 1; i++) { + PwlePoint current = pwlePoints[i]; + PwlePoint next = pwlePoints[i + 1]; + + if (current.getAmplitude() == 0 && next.getAmplitude() == 0) { + halResult -= next.getTimeMillis(); } } + if (halResult > 0) { mVibratorOnTotalDurationMillis += (int) halResult; } diff --git a/services/core/java/com/android/server/vibrator/VibratorController.java b/services/core/java/com/android/server/vibrator/VibratorController.java index f78bff8e229d..acb31ceb4027 100644 --- a/services/core/java/com/android/server/vibrator/VibratorController.java +++ b/services/core/java/com/android/server/vibrator/VibratorController.java @@ -30,7 +30,7 @@ import android.os.VibrationEffect; import android.os.VibratorInfo; import android.os.vibrator.PrebakedSegment; import android.os.vibrator.PrimitiveSegment; -import android.os.vibrator.PwleSegment; +import android.os.vibrator.PwlePoint; import android.os.vibrator.RampSegment; import android.util.IndentingPrintWriter; import android.util.Slog; @@ -415,21 +415,21 @@ final class VibratorController { } /** - * Plays a composition of pwle v2 primitives, using {@code vibrationId} for completion callback + * Plays a composition of pwle v2 points, using {@code vibrationId} for completion callback * to {@link OnVibrationCompleteListener}. * * <p>This will affect the state of {@link #isVibrating()}. * * @return The duration of the effect playing, or 0 if unsupported. */ - public long on(PwleSegment[] primitives, long vibrationId) { + public long on(PwlePoint[] pwlePoints, long vibrationId) { Trace.traceBegin(TRACE_TAG_VIBRATOR, "VibratorController#on (PWLE v2)"); try { if (!mVibratorInfo.hasCapability(IVibrator.CAP_COMPOSE_PWLE_EFFECTS_V2)) { return 0; } synchronized (mLock) { - long duration = mNativeWrapper.composePwleV2(primitives, vibrationId); + long duration = mNativeWrapper.composePwleV2(pwlePoints, vibrationId); if (duration > 0) { mCurrentAmplitude = -1; updateStateAndNotifyListenersLocked(VibratorState.VIBRATING); @@ -562,7 +562,7 @@ final class VibratorController { private static native long performPwleEffect(long nativePtr, RampSegment[] effect, int braking, long vibrationId); - private static native long performPwleV2Effect(long nativePtr, PwleSegment[] effect, + private static native long performPwleV2Effect(long nativePtr, PwlePoint[] effect, long vibrationId); private static native void setExternalControl(long nativePtr, boolean enabled); @@ -631,9 +631,9 @@ final class VibratorController { return performPwleEffect(mNativePtr, primitives, braking, vibrationId); } - /** Turns vibrator on to perform PWLE effect composed of given primitives. */ - public long composePwleV2(PwleSegment[] primitives, long vibrationId) { - return performPwleV2Effect(mNativePtr, primitives, vibrationId); + /** Turns vibrator on to perform PWLE effect composed of given points. */ + public long composePwleV2(PwlePoint[] pwlePoints, long vibrationId) { + return performPwleV2Effect(mNativePtr, pwlePoints, vibrationId); } /** Enabled the device vibrator to be controlled by another service. */ diff --git a/services/core/java/com/android/server/wallpaper/WallpaperData.java b/services/core/java/com/android/server/wallpaper/WallpaperData.java index c8d5a0332a4f..f09b0a1c6e41 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperData.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperData.java @@ -16,6 +16,7 @@ package com.android.server.wallpaper; +import static android.app.Flags.liveWallpaperContentHandling; import static android.app.WallpaperManager.FLAG_LOCK; import static android.app.WallpaperManager.ORIENTATION_UNKNOWN; @@ -25,6 +26,7 @@ import static com.android.server.wallpaper.WallpaperUtils.WALLPAPER_LOCK_CROP; import static com.android.server.wallpaper.WallpaperUtils.WALLPAPER_LOCK_ORIG; import static com.android.server.wallpaper.WallpaperUtils.getWallpaperDir; +import android.annotation.NonNull; import android.app.IWallpaperManagerCallback; import android.app.WallpaperColors; import android.app.WallpaperManager.ScreenOrientation; @@ -149,6 +151,7 @@ class WallpaperData { UNKNOWN, CONNECT_LOCKED, CONNECTION_TRY_TO_REBIND, + FALLBACK_DEFAULT_MISSING, INITIALIZE_FALLBACK, PACKAGE_UPDATE_FINISHED, RESTORE_SETTINGS_LIVE_FAILURE, @@ -183,7 +186,7 @@ class WallpaperData { int mOrientationWhenSet = ORIENTATION_UNKNOWN; /** Description of the current wallpaper */ - private WallpaperDescription mDescription; + private WallpaperDescription mDescription = new WallpaperDescription.Builder().build(); WallpaperData(int userId, @SetWallpaperFlags int wallpaperType) { this.userId = userId; @@ -212,6 +215,9 @@ class WallpaperData { this.primaryColors = source.primaryColors; this.mWallpaperDimAmount = source.mWallpaperDimAmount; this.connection = source.connection; + if (liveWallpaperContentHandling()) { + this.setDescription(source.getDescription()); + } if (this.connection != null) { this.connection.mWallpaper = this; } @@ -236,19 +242,37 @@ class WallpaperData { return result; } - ComponentName getComponent() { - return mWallpaperComponent; + @NonNull ComponentName getComponent() { + if (liveWallpaperContentHandling()) { + return mDescription.getComponent(); + } else { + return mWallpaperComponent; + } } - void setComponent(ComponentName componentName) { + void setComponent(@NonNull ComponentName componentName) { + if (liveWallpaperContentHandling()) { + throw new IllegalStateException( + "Use \"setDescription\" when content handling is enabled"); + } this.mWallpaperComponent = componentName; } - WallpaperDescription getDescription() { + @NonNull WallpaperDescription getDescription() { return mDescription; } - void setDescription(WallpaperDescription description) { + void setDescription(@NonNull WallpaperDescription description) { + if (!liveWallpaperContentHandling()) { + throw new IllegalStateException( + "Use \"setContent\" when content handling is disabled"); + } + if (description == null) { + throw new IllegalArgumentException("WallpaperDescription must not be null"); + } + if (description.getComponent() == null) { + throw new IllegalArgumentException("WallpaperDescription component must not be null"); + } this.mDescription = description; } diff --git a/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java b/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java index cf76bf05ab19..17a254ab85e9 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperDataParser.java @@ -180,15 +180,8 @@ public class WallpaperDataParser { success = true; } catch (FileNotFoundException e) { Slog.w(TAG, "no current wallpaper -- first boot?"); - } catch (NullPointerException e) { - Slog.w(TAG, "failed parsing " + file + " " + e); - } catch (NumberFormatException e) { - Slog.w(TAG, "failed parsing " + file + " " + e); - } catch (XmlPullParserException e) { - Slog.w(TAG, "failed parsing " + file + " " + e); - } catch (IOException e) { - Slog.w(TAG, "failed parsing " + file + " " + e); - } catch (IndexOutOfBoundsException e) { + } catch (NullPointerException | NumberFormatException | XmlPullParserException + | IOException | IndexOutOfBoundsException e) { Slog.w(TAG, "failed parsing " + file + " " + e); } IoUtils.closeQuietly(stream); @@ -256,12 +249,13 @@ public class WallpaperDataParser { } ComponentName comp = parseComponentName(parser); - if (removeNextWallpaperComponent()) { - wallpaperToParse.setComponent(comp); - } else { - wallpaperToParse.nextWallpaperComponent = comp; + if (!liveWallpaperContentHandling()) { + if (removeNextWallpaperComponent()) { + wallpaperToParse.setComponent(comp); + } else { + wallpaperToParse.nextWallpaperComponent = comp; + } } - if (multiCrop()) { parseWallpaperAttributes(parser, wallpaperToParse, keepDimensionHints); } @@ -745,9 +739,9 @@ public class WallpaperDataParser { private static List<Pair<Integer, String>> screenDimensionPairs() { return List.of( - new Pair<>(WallpaperManager.PORTRAIT, "Portrait"), - new Pair<>(WallpaperManager.LANDSCAPE, "Landscape"), - new Pair<>(WallpaperManager.SQUARE_PORTRAIT, "SquarePortrait"), - new Pair<>(WallpaperManager.SQUARE_LANDSCAPE, "SquareLandscape")); + new Pair<>(WallpaperManager.ORIENTATION_PORTRAIT, "Portrait"), + new Pair<>(WallpaperManager.ORIENTATION_LANDSCAPE, "Landscape"), + new Pair<>(WallpaperManager.ORIENTATION_SQUARE_PORTRAIT, "SquarePortrait"), + new Pair<>(WallpaperManager.ORIENTATION_SQUARE_LANDSCAPE, "SquareLandscape")); } } diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java index da9a67640f77..5cff37a36656 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java @@ -21,6 +21,7 @@ import static android.Manifest.permission.MANAGE_EXTERNAL_STORAGE; import static android.Manifest.permission.READ_WALLPAPER_INTERNAL; import static android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND; import static android.app.Flags.fixWallpaperChanged; +import static android.app.Flags.liveWallpaperContentHandling; import static android.app.Flags.removeNextWallpaperComponent; import static android.app.WallpaperManager.COMMAND_REAPPLY; import static android.app.WallpaperManager.FLAG_LOCK; @@ -66,6 +67,8 @@ import android.app.WallpaperInfo; import android.app.WallpaperManager; import android.app.WallpaperManager.SetWallpaperFlags; import android.app.admin.DevicePolicyManagerInternal; +import android.app.wallpaper.WallpaperDescription; +import android.app.wallpaper.WallpaperInstance; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; @@ -849,16 +852,24 @@ public class WallpaperManagerService extends IWallpaperManager.Stub final DisplayData wpdData = mWallpaperDisplayHelper.getDisplayDataOrCreate(mDisplayId); try { - connection.mService.attach(connection, mToken, TYPE_WALLPAPER, false, - wpdData.mWidth, wpdData.mHeight, - wpdData.mPadding, mDisplayId, wallpaper.mWhich, connection.mInfo); + if (liveWallpaperContentHandling()) { + connection.mService.attach(connection, mToken, TYPE_WALLPAPER, false, + wpdData.mWidth, wpdData.mHeight, + wpdData.mPadding, mDisplayId, wallpaper.mWhich, connection.mInfo, + wallpaper.getDescription()); + } else { + connection.mService.attach(connection, mToken, TYPE_WALLPAPER, false, + wpdData.mWidth, wpdData.mHeight, + wpdData.mPadding, mDisplayId, wallpaper.mWhich, connection.mInfo, + /* description= */ null); + } } catch (RemoteException e) { Slog.w(TAG, "Failed attaching wallpaper on display", e); if (wallpaper != null && !wallpaper.wallpaperUpdating && connection.getConnectedEngineSize() == 0) { wallpaper.mBindSource = BindSource.CONNECT_LOCKED; - bindWallpaperComponentLocked(null /* componentName */, false /* force */, - false /* fromUser */, wallpaper, null /* reply */); + bindWallpaperComponentLocked(null, false /* force */, false /* fromUser */, + wallpaper, null /* reply */); } } t.traceEnd(); @@ -1324,7 +1335,11 @@ public class WallpaperManagerService extends IWallpaperManager.Stub if (DEBUG) { Slog.v(TAG, "static system+lock to system success"); } - lockWp.setComponent(mOriginalSystem.getComponent()); + if (liveWallpaperContentHandling()) { + lockWp.setDescription(mOriginalSystem.getDescription()); + } else { + lockWp.setComponent(mOriginalSystem.getComponent()); + } lockWp.connection = mOriginalSystem.connection; lockWp.connection.mWallpaper = lockWp; mOriginalSystem.mWhich = FLAG_LOCK; @@ -1391,7 +1406,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub Slog.i(TAG, "Wallpaper " + wpService + " update has finished"); } wallpaper.wallpaperUpdating = false; - clearWallpaperComponentLocked(wallpaper); + detachWallpaperLocked(wallpaper); wallpaper.mBindSource = BindSource.PACKAGE_UPDATE_FINISHED; if (!bindWallpaperComponentLocked(wpService, false, false, wallpaper, null)) { @@ -1905,6 +1920,23 @@ public class WallpaperManagerService extends IWallpaperManager.Stub if ((wallpaper.mWhich & FLAG_SYSTEM) != 0) mHomeWallpaperWaitingForUnlock = false; if ((wallpaper.mWhich & FLAG_LOCK) != 0) mLockWallpaperWaitingForUnlock = false; + if (liveWallpaperContentHandling()) { + final WallpaperDescription description = wallpaper.getDescription(); + if (!bindWallpaperDescriptionLocked(description, true, false, wallpaper, reply)) { + // We failed to bind the desired wallpaper, but that might + // happen if the wallpaper isn't direct-boot aware + ServiceInfo si = null; + try { + si = mIPackageManager.getServiceInfo(description.getComponent(), + PackageManager.MATCH_DIRECT_BOOT_UNAWARE, wallpaper.userId); + } catch (RemoteException e) { + Slog.w(TAG, "Failure starting previous wallpaper; clearing", e); + } + onSwitchWallpaperFailLocked(wallpaper, reply, si); + } + return; + } + final ComponentName cname; if (removeNextWallpaperComponent()) { cname = wallpaper.getComponent(); @@ -2001,9 +2033,6 @@ public class WallpaperManagerService extends IWallpaperManager.Stub try { if (userId != mCurrentUserId && !hasCrossUserPermission()) return; - final ComponentName component; - final int finalWhich; - // Clear any previous ImageWallpaper related fields List<WallpaperData> toClear = new ArrayList<>(); if ((which & FLAG_LOCK) > 0 && lockWallpaper != null) toClear.add(lockWallpaper); @@ -2017,19 +2046,34 @@ public class WallpaperManagerService extends IWallpaperManager.Stub } } - // lock only case: set the system wallpaper component to both screens - if (which == FLAG_LOCK) { - component = wallpaper.getComponent(); - finalWhich = FLAG_LOCK | FLAG_SYSTEM; + final WallpaperDescription description; + final int finalWhich; + + if (liveWallpaperContentHandling()) { + if (which == FLAG_LOCK) { + // lock only case: set the system wallpaper component to both screens + description = wallpaper.getDescription(); + finalWhich = FLAG_LOCK | FLAG_SYSTEM; + } else { + description = new WallpaperDescription.Builder().build(); + finalWhich = which; + } } else { - component = null; - finalWhich = which; + if (which == FLAG_LOCK) { + // lock only case: set the system wallpaper component to both screens + description = new WallpaperDescription.Builder().setComponent( + wallpaper.getComponent()).build(); + finalWhich = FLAG_LOCK | FLAG_SYSTEM; + } else { + description = new WallpaperDescription.Builder().build(); + finalWhich = which; + } } // except for the lock case (for which we keep the system wallpaper as-is), force rebind boolean force = which != FLAG_LOCK; - boolean success = withCleanCallingIdentity(() -> setWallpaperComponentInternal( - component, finalWhich, userId, force, fromForeground, reply)); + boolean success = withCleanCallingIdentity(() -> setWallpaperDescriptionInternal( + description, finalWhich, userId, force, fromForeground, reply)); if (success) return; } catch (IllegalArgumentException e1) { e = e1; @@ -2040,7 +2084,10 @@ public class WallpaperManagerService extends IWallpaperManager.Stub // let's not let it crash the system and just live with no // wallpaper. Slog.e(TAG, "Default wallpaper component not found!", e); - withCleanCallingIdentity(() -> clearWallpaperComponentLocked(wallpaper)); + withCleanCallingIdentity(() -> { + wallpaper.mBindSource = BindSource.FALLBACK_DEFAULT_MISSING; + bindWallpaperComponentLocked(mImageWallpaper, true, false, wallpaper, reply); + }); if (reply != null) { try { reply.sendResult(null); @@ -2412,6 +2459,9 @@ public class WallpaperManagerService extends IWallpaperManager.Stub @Override public WallpaperInfo getWallpaperInfoWithFlags(@SetWallpaperFlags int which, int userId) { + if (liveWallpaperContentHandling()) { + return getWallpaperInstance(which, userId, false).getInfo(); + } userId = ActivityManager.handleIncomingUser(Binder.getCallingPid(), Binder.getCallingUid(), userId, false, true, "getWallpaperInfo", null); @@ -2425,13 +2475,45 @@ public class WallpaperManagerService extends IWallpaperManager.Stub WallpaperInfo info = wallpaper.connection.mInfo; if (hasPermission(READ_WALLPAPER_INTERNAL) || mPackageManagerInternal.canQueryPackage( - Binder.getCallingUid(), info.getComponent().getPackageName())) { + Binder.getCallingUid(), info.getComponent().getPackageName())) { return info; } } return null; } + @NonNull + @Override + public WallpaperInstance getWallpaperInstance(@SetWallpaperFlags int which, int userId) { + return getWallpaperInstance(which, userId, true); + } + + private WallpaperInstance getWallpaperInstance(@SetWallpaperFlags int which, int userId, + boolean requireReadWallpaper) { + final WallpaperInstance defaultInstance = new WallpaperInstance(null, + new WallpaperDescription.Builder().build()); + userId = ActivityManager.handleIncomingUser(Binder.getCallingPid(), + Binder.getCallingUid(), userId, false, true, "getWallpaperInfo", null); + synchronized (mLock) { + WallpaperData wallpaper = (which == FLAG_LOCK) ? mLockWallpaperMap.get(userId) + : mWallpaperMap.get(userId); + if (wallpaper == null + || wallpaper.connection == null + || wallpaper.connection.mInfo == null) { + return defaultInstance; + } + + WallpaperInfo info = wallpaper.connection.mInfo; + boolean canQueryPackage = mPackageManagerInternal.canQueryPackage( + Binder.getCallingUid(), info.getComponent().getPackageName()); + if (hasPermission(READ_WALLPAPER_INTERNAL) + || (canQueryPackage && !requireReadWallpaper)) { + return new WallpaperInstance(info, wallpaper.getDescription()); + } + } + return defaultInstance; + } + @Override public ParcelFileDescriptor getWallpaperInfoFile(int userId) { synchronized (mLock) { @@ -3159,11 +3241,11 @@ public class WallpaperManagerService extends IWallpaperManager.Stub } @Override - public void setWallpaperComponentChecked(ComponentName name, String callingPackage, - @SetWallpaperFlags int which, int userId) { + public void setWallpaperComponentChecked(WallpaperDescription description, + String callingPackage, @SetWallpaperFlags int which, int userId) { if (isWallpaperSupported(callingPackage) && isSetWallpaperAllowed(callingPackage)) { - setWallpaperComponent(name, callingPackage, which, userId); + setWallpaperDescription(description, callingPackage, which, userId); } } @@ -3176,12 +3258,23 @@ public class WallpaperManagerService extends IWallpaperManager.Stub @VisibleForTesting boolean setWallpaperComponent(ComponentName name, String callingPackage, @SetWallpaperFlags int which, int userId) { + return setWallpaperDescription( + new WallpaperDescription.Builder().setComponent(name).build(), callingPackage, + which, userId); + } + + @VisibleForTesting + boolean setWallpaperDescription(WallpaperDescription description, String callingPackage, + @SetWallpaperFlags int which, int userId) { boolean fromForeground = isFromForegroundApp(callingPackage); - return setWallpaperComponentInternal(name, which, userId, false, fromForeground, null); + return setWallpaperDescriptionInternal(description, which, userId, false, fromForeground, + null); } - private boolean setWallpaperComponentInternal(ComponentName name, @SetWallpaperFlags int which, - int userIdIn, boolean force, boolean fromForeground, IRemoteCallback reply) { + private boolean setWallpaperDescriptionInternal(@NonNull WallpaperDescription description, + @SetWallpaperFlags int which, int userIdIn, boolean force, boolean fromForeground, + IRemoteCallback reply) { + ComponentName name = description.getComponent(); if (DEBUG) { Slog.v(TAG, "Setting new live wallpaper: which=" + which + ", component: " + name); } @@ -3191,11 +3284,11 @@ public class WallpaperManagerService extends IWallpaperManager.Stub checkPermission(android.Manifest.permission.SET_WALLPAPER_COMPONENT); boolean shouldNotifyColors = false; + final boolean bindSuccess; // If the lockscreen wallpaper is set to the same as the home screen, notify that the // lockscreen wallpaper colors changed, even if we don't bind a new wallpaper engine. boolean shouldNotifyLockscreenColors = false; - boolean bindSuccess; final WallpaperData newWallpaper; synchronized (mLock) { @@ -3226,8 +3319,14 @@ public class WallpaperManagerService extends IWallpaperManager.Stub final WallpaperDestinationChangeHandler liveSync = new WallpaperDestinationChangeHandler( newWallpaper); - boolean same = changingToSame(name, newWallpaper.connection, - newWallpaper.getComponent()); + boolean same; + if (liveWallpaperContentHandling()) { + same = changingToSame(description, newWallpaper.connection, + newWallpaper.getDescription()); + } else { + same = changingToSame(name, newWallpaper.connection, + newWallpaper.getComponent()); + } /* * If we have a shared system+lock wallpaper, and we reapply the same wallpaper @@ -3238,8 +3337,13 @@ public class WallpaperManagerService extends IWallpaperManager.Stub newWallpaper.mBindSource = (name == null) ? BindSource.SET_LIVE_TO_CLEAR : BindSource.SET_LIVE; - bindSuccess = bindWallpaperComponentLocked(name, /* force */ - forceRebind, /* fromUser */ true, newWallpaper, reply); + if (liveWallpaperContentHandling()) { + bindSuccess = bindWallpaperDescriptionLocked(description, forceRebind, + /* fromUser */ true, newWallpaper, reply); + } else { + bindSuccess = bindWallpaperComponentLocked(name, forceRebind, + /* fromUser */ true, newWallpaper, reply); + } if (bindSuccess) { if (!same || (offloadColorExtraction() && forceRebind)) { newWallpaper.primaryColors = null; @@ -3335,6 +3439,24 @@ public class WallpaperManagerService extends IWallpaperManager.Stub return false; } + private boolean changingToSame(WallpaperDescription newDescription, + WallpaperConnection currentConnection, WallpaperDescription currentDescription) { + if (currentConnection == null) { + return false; + } + if (isDefaultComponent(newDescription.getComponent()) && isDefaultComponent( + currentDescription.getComponent())) { + if (DEBUG) Slog.v(TAG, "changingToSame: still using default"); + // Still using default wallpaper. + return true; + } else if (newDescription.equals(currentDescription)) { + // Changing to same wallpaper. + if (DEBUG) Slog.v(TAG, "same wallpaper"); + return true; + } + return false; + } + /* * Attempt to bind the wallpaper given by `componentName`, returning true on success otherwise * false. @@ -3352,12 +3474,29 @@ public class WallpaperManagerService extends IWallpaperManager.Stub */ boolean bindWallpaperComponentLocked(ComponentName componentName, boolean force, boolean fromUser, WallpaperData wallpaper, IRemoteCallback reply) { + return bindWallpaperDescriptionLocked( + new WallpaperDescription.Builder().setComponent(componentName).build(), force, + fromUser, wallpaper, reply); + } + + // When `liveWallpaperContentHandling()` is false this acts exactly like the version which takes + // a ComponentName argument did: it uses the ComponentName from `description`, it binds the + // service given by that component, and updates WallpaperData with that component on success. + boolean bindWallpaperDescriptionLocked(WallpaperDescription description, boolean force, + boolean fromUser, WallpaperData wallpaper, IRemoteCallback reply) { + ComponentName componentName = description.getComponent(); if (DEBUG_LIVE) { Slog.v(TAG, "bindWallpaperComponentLocked: componentName=" + componentName); } - // Has the component changed? - if (!force && changingToSame(componentName, wallpaper.connection, - wallpaper.getComponent())) { + boolean skipBinding; + if (liveWallpaperContentHandling()) { + skipBinding = !force && changingToSame(description, wallpaper.connection, + wallpaper.getDescription()); + } else { + skipBinding = !force && changingToSame(componentName, wallpaper.connection, + wallpaper.getComponent()); + } + if (skipBinding) { try { if (DEBUG_LIVE) { Slog.v(TAG, "Changing to the same component, ignoring"); @@ -3374,6 +3513,9 @@ public class WallpaperManagerService extends IWallpaperManager.Stub try { if (componentName == null) { componentName = mDefaultWallpaperComponent; + if (liveWallpaperContentHandling()) { + description = description.toBuilder().setComponent(componentName).build(); + } } int serviceUserId = wallpaper.userId; ServiceInfo si = mIPackageManager.getServiceInfo(componentName, @@ -3494,7 +3636,11 @@ public class WallpaperManagerService extends IWallpaperManager.Stub return false; } maybeDetachLastWallpapers(wallpaper); - wallpaper.setComponent(componentName); + if (liveWallpaperContentHandling()) { + wallpaper.setDescription(description); + } else { + wallpaper.setComponent(componentName); + } wallpaper.connection = newConn; newConn.mReply = reply; updateCurrentWallpapers(wallpaper); @@ -3618,11 +3764,6 @@ public class WallpaperManagerService extends IWallpaperManager.Stub }); } - private void clearWallpaperComponentLocked(WallpaperData wallpaper) { - wallpaper.setComponent(null); - detachWallpaperLocked(wallpaper); - } - private void attachServiceLocked(WallpaperConnection conn, WallpaperData wallpaper) { TimingsTraceAndSlog t = new TimingsTraceAndSlog(TAG); t.traceBegin("WPMS.attachServiceLocked"); @@ -3858,6 +3999,7 @@ public class WallpaperManagerService extends IWallpaperManager.Stub } // Called by SystemBackupAgent after files are restored to disk. + // TODO(b/373875373) Remove this method public void settingsRestored() { // Verify caller is the system if (Binder.getCallingUid() != android.os.Process.SYSTEM_UID) { @@ -3876,6 +4018,14 @@ public class WallpaperManagerService extends IWallpaperManager.Stub ComponentName componentName = removeNextWallpaperComponent() ? wallpaper.getComponent() : wallpaper.nextWallpaperComponent; + + if (liveWallpaperContentHandling()) { + // Per b/373875373 this method should be removed, so we just set wallpapers to + // default. + bindWallpaperDescriptionLocked(new WallpaperDescription.Builder().build(), false, + false, wallpaper, null); + return; + } if (componentName != null && !componentName.equals(mImageWallpaper)) { wallpaper.mBindSource = BindSource.RESTORE_SETTINGS_LIVE_SUCCESS; if (!bindWallpaperComponentLocked(componentName, false, false, @@ -3894,15 +4044,20 @@ public class WallpaperManagerService extends IWallpaperManager.Stub if (DEBUG) Slog.v(TAG, "settingsRestored: name is empty"); success = true; } else { - if (DEBUG) Slog.v(TAG, "settingsRestored: attempting to restore named resource"); + if (DEBUG) { + Slog.v(TAG, "settingsRestored: attempting to restore named resource"); + } success = mWallpaperDataParser.restoreNamedResourceLocked(wallpaper); } - if (DEBUG) Slog.v(TAG, "settingsRestored: success=" + success - + " id=" + wallpaper.wallpaperId); + if (DEBUG) { + Slog.v(TAG, "settingsRestored: success=" + success + " id=" + + wallpaper.wallpaperId); + } if (success) { - mWallpaperCropper.generateCrop(wallpaper); // based on the new image + metadata + mWallpaperCropper.generateCrop( + wallpaper); // based on the new image + metadata wallpaper.mBindSource = BindSource.RESTORE_SETTINGS_STATIC; - bindWallpaperComponentLocked(componentName, true, false, wallpaper, null); + bindWallpaperComponentLocked(null, true, false, wallpaper, null); } } } diff --git a/services/core/java/com/android/server/webkit/SystemImpl.java b/services/core/java/com/android/server/webkit/SystemImpl.java index ab5316f46d78..92ce251c4293 100644 --- a/services/core/java/com/android/server/webkit/SystemImpl.java +++ b/services/core/java/com/android/server/webkit/SystemImpl.java @@ -16,8 +16,6 @@ package com.android.server.webkit; -import static android.webkit.Flags.updateServiceV2; - import android.app.ActivityManager; import android.content.Context; import android.content.pm.ApplicationInfo; @@ -253,46 +251,11 @@ public class SystemImpl implements SystemInterface { } @Override - public int getMultiProcessSetting() { - if (updateServiceV2()) { - throw new IllegalStateException( - "getMultiProcessSetting shouldn't be called if update_service_v2 flag is set."); - } - return Settings.Global.getInt( - mContext.getContentResolver(), Settings.Global.WEBVIEW_MULTIPROCESS, 0); - } - - @Override - public void setMultiProcessSetting(int value) { - if (updateServiceV2()) { - throw new IllegalStateException( - "setMultiProcessSetting shouldn't be called if update_service_v2 flag is set."); - } - Settings.Global.putInt( - mContext.getContentResolver(), Settings.Global.WEBVIEW_MULTIPROCESS, value); - } - - @Override - public void notifyZygote(boolean enableMultiProcess) { - if (updateServiceV2()) { - throw new IllegalStateException( - "notifyZygote shouldn't be called if update_service_v2 flag is set."); - } - WebViewZygote.setMultiprocessEnabled(enableMultiProcess); - } - - @Override public void ensureZygoteStarted() { WebViewZygote.getProcess(); } @Override - public boolean isMultiProcessDefaultEnabled() { - // Multiprocess is enabled by default for all devices. - return true; - } - - @Override public void pinWebviewIfRequired(ApplicationInfo appInfo) { PinnerService pinnerService = LocalServices.getService(PinnerService.class); int webviewPinQuota = pinnerService.getWebviewPinQuota(); diff --git a/services/core/java/com/android/server/webkit/SystemInterface.java b/services/core/java/com/android/server/webkit/SystemInterface.java index 3b77d07412ce..67105542e6ef 100644 --- a/services/core/java/com/android/server/webkit/SystemInterface.java +++ b/services/core/java/com/android/server/webkit/SystemInterface.java @@ -55,12 +55,8 @@ public interface SystemInterface { */ List<UserPackage> getPackageInfoForProviderAllUsers(WebViewProviderInfo configInfo); - int getMultiProcessSetting(); - void setMultiProcessSetting(int value); - void notifyZygote(boolean enableMultiProcess); /** Start the zygote if it's not already running. */ void ensureZygoteStarted(); - boolean isMultiProcessDefaultEnabled(); void pinWebviewIfRequired(ApplicationInfo appInfo); } diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateService.java b/services/core/java/com/android/server/webkit/WebViewUpdateService.java index 7acb864cbb40..e1fa8848b0e1 100644 --- a/services/core/java/com/android/server/webkit/WebViewUpdateService.java +++ b/services/core/java/com/android/server/webkit/WebViewUpdateService.java @@ -16,8 +16,6 @@ package com.android.server.webkit; -import static android.webkit.Flags.updateServiceV2; - import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; @@ -63,7 +61,7 @@ public class WebViewUpdateService extends SystemService { new Histogram.ScaledRangeOptions(20, 0, 1, 1.4f)); private BroadcastReceiver mWebViewUpdatedReceiver; - private WebViewUpdateServiceInterface mImpl; + private WebViewUpdateServiceImpl2 mImpl; static final int PACKAGE_CHANGED = 0; static final int PACKAGE_ADDED = 1; @@ -72,11 +70,7 @@ public class WebViewUpdateService extends SystemService { public WebViewUpdateService(Context context) { super(context); - if (updateServiceV2()) { - mImpl = new WebViewUpdateServiceImpl2(new SystemImpl(context)); - } else { - mImpl = new WebViewUpdateServiceImpl(new SystemImpl(context)); - } + mImpl = new WebViewUpdateServiceImpl2(new SystemImpl(context)); } @Override @@ -84,8 +78,13 @@ public class WebViewUpdateService extends SystemService { mWebViewUpdatedReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + if (action == null) { + return; + } + int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL); - switch (intent.getAction()) { + switch (action) { case Intent.ACTION_PACKAGE_REMOVED: // When a package is replaced we will receive two intents, one // representing the removal of the old package and one representing the @@ -170,13 +169,8 @@ public class WebViewUpdateService extends SystemService { public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, String[] args, ShellCallback callback, ResultReceiver resultReceiver) { - if (updateServiceV2()) { - (new WebViewUpdateServiceShellCommand2(this)) - .exec(this, in, out, err, args, callback, resultReceiver); - } else { - (new WebViewUpdateServiceShellCommand(this)) - .exec(this, in, out, err, args, callback, resultReceiver); - } + (new WebViewUpdateServiceShellCommand2(this)) + .exec(this, in, out, err, args, callback, resultReceiver); } @@ -300,45 +294,6 @@ public class WebViewUpdateService extends SystemService { return currentWebViewPackage; } - @Override // Binder call - public boolean isMultiProcessEnabled() { - if (updateServiceV2()) { - throw new IllegalStateException( - "isMultiProcessEnabled shouldn't be called if update_service_v2 flag is" - + " set."); - } - return WebViewUpdateService.this.mImpl.isMultiProcessEnabled(); - } - - @Override // Binder call - public void enableMultiProcess(boolean enable) { - if (updateServiceV2()) { - throw new IllegalStateException( - "enableMultiProcess shouldn't be called if update_service_v2 flag is set."); - } - if (getContext() - .checkCallingPermission( - android.Manifest.permission.WRITE_SECURE_SETTINGS) - != PackageManager.PERMISSION_GRANTED) { - String msg = - "Permission Denial: enableMultiProcess() from pid=" - + Binder.getCallingPid() - + ", uid=" - + Binder.getCallingUid() - + " requires " - + android.Manifest.permission.WRITE_SECURE_SETTINGS; - Slog.w(TAG, msg); - throw new SecurityException(msg); - } - - final long callingId = Binder.clearCallingIdentity(); - try { - WebViewUpdateService.this.mImpl.enableMultiProcess(enable); - } finally { - Binder.restoreCallingIdentity(callingId); - } - } - @Override protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { if (!DumpUtils.checkDumpPermission(getContext(), TAG, pw)) return; diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java deleted file mode 100644 index b9be4a2deef2..000000000000 --- a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl.java +++ /dev/null @@ -1,768 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT 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.webkit; - -import android.annotation.Nullable; -import android.content.pm.PackageInfo; -import android.content.pm.PackageManager.NameNotFoundException; -import android.content.pm.Signature; -import android.os.AsyncTask; -import android.os.Trace; -import android.os.UserHandle; -import android.util.AndroidRuntimeException; -import android.util.Slog; -import android.webkit.UserPackage; -import android.webkit.WebViewFactory; -import android.webkit.WebViewProviderInfo; -import android.webkit.WebViewProviderResponse; - -import com.android.modules.expresslog.Counter; - -import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.List; - -/** - * Implementation of the WebViewUpdateService. - * This class doesn't depend on the android system like the actual Service does and can be used - * directly by tests (as long as they implement a SystemInterface). - * - * This class keeps track of and prepares the current WebView implementation, and needs to keep - * track of a couple of different things such as what package is used as WebView implementation. - * - * The package-visible methods in this class are accessed from WebViewUpdateService either on the UI - * thread or on one of multiple Binder threads. The WebView preparation code shares state between - * threads meaning that code that chooses a new WebView implementation or checks which - * implementation is being used needs to hold a lock. - * - * The WebViewUpdateService can be accessed in a couple of different ways. - * 1. It is started from the SystemServer at boot - at that point we just initiate some state such - * as the WebView preparation class. - * 2. The SystemServer calls WebViewUpdateService.prepareWebViewInSystemServer. This happens at boot - * and the WebViewUpdateService should not have been accessed before this call. In this call we - * choose WebView implementation for the first time. - * 3. The update service listens for Intents related to package installs and removals. These intents - * are received and processed on the UI thread. Each intent can result in changing WebView - * implementation. - * 4. The update service can be reached through Binder calls which are handled on specific binder - * threads. These calls can be made from any process. Generally they are used for changing WebView - * implementation (from Settings), getting information about the current WebView implementation (for - * loading WebView into an app process), or notifying the service about Relro creation being - * completed. - * - * @hide - */ -class WebViewUpdateServiceImpl implements WebViewUpdateServiceInterface { - private static final String TAG = WebViewUpdateServiceImpl.class.getSimpleName(); - - private static class WebViewPackageMissingException extends Exception { - WebViewPackageMissingException(String message) { - super(message); - } - - WebViewPackageMissingException(Exception e) { - super(e); - } - } - - private static final int WAIT_TIMEOUT_MS = 1000; // KEY_DISPATCHING_TIMEOUT is 5000. - private static final long NS_PER_MS = 1000000; - - private static final int VALIDITY_OK = 0; - private static final int VALIDITY_INCORRECT_SDK_VERSION = 1; - private static final int VALIDITY_INCORRECT_VERSION_CODE = 2; - private static final int VALIDITY_INCORRECT_SIGNATURE = 3; - private static final int VALIDITY_NO_LIBRARY_FLAG = 4; - - private static final int MULTIPROCESS_SETTING_ON_VALUE = Integer.MAX_VALUE; - private static final int MULTIPROCESS_SETTING_OFF_VALUE = Integer.MIN_VALUE; - - private final SystemInterface mSystemInterface; - - private long mMinimumVersionCode = -1; - - // Keeps track of the number of running relro creations - private int mNumRelroCreationsStarted = 0; - private int mNumRelroCreationsFinished = 0; - // Implies that we need to rerun relro creation because we are using an out-of-date package - private boolean mWebViewPackageDirty = false; - private boolean mAnyWebViewInstalled = false; - - private static final int NUMBER_OF_RELROS_UNKNOWN = Integer.MAX_VALUE; - - // The WebView package currently in use (or the one we are preparing). - private PackageInfo mCurrentWebViewPackage = null; - - private final Object mLock = new Object(); - - WebViewUpdateServiceImpl(SystemInterface systemInterface) { - mSystemInterface = systemInterface; - } - - @Override - public void packageStateChanged(String packageName, int changedState, int userId) { - // We don't early out here in different cases where we could potentially early-out (e.g. if - // we receive PACKAGE_CHANGED for another user than the system user) since that would - // complicate this logic further and open up for more edge cases. - for (WebViewProviderInfo provider : mSystemInterface.getWebViewPackages()) { - String webviewPackage = provider.packageName; - - if (webviewPackage.equals(packageName)) { - boolean updateWebView = false; - boolean removedOrChangedOldPackage = false; - String oldProviderName = null; - PackageInfo newPackage = null; - synchronized (mLock) { - try { - newPackage = findPreferredWebViewPackage(); - if (mCurrentWebViewPackage != null) { - oldProviderName = mCurrentWebViewPackage.packageName; - } - // Only trigger update actions if the updated package is the one - // that will be used, or the one that was in use before the - // update, or if we haven't seen a valid WebView package before. - updateWebView = - provider.packageName.equals(newPackage.packageName) - || provider.packageName.equals(oldProviderName) - || mCurrentWebViewPackage == null; - // We removed the old package if we received an intent to remove - // or replace the old package. - removedOrChangedOldPackage = - provider.packageName.equals(oldProviderName); - if (updateWebView) { - onWebViewProviderChanged(newPackage); - } - } catch (WebViewPackageMissingException e) { - mCurrentWebViewPackage = null; - Slog.e(TAG, "Could not find valid WebView package to create relro with " - + e); - } - } - if (updateWebView && !removedOrChangedOldPackage - && oldProviderName != null) { - // If the provider change is the result of adding or replacing a - // package that was not the previous provider then we must kill - // packages dependent on the old package ourselves. The framework - // only kills dependents of packages that are being removed. - mSystemInterface.killPackageDependents(oldProviderName); - } - return; - } - } - } - - @Override - public void prepareWebViewInSystemServer() { - mSystemInterface.notifyZygote(isMultiProcessEnabled()); - try { - synchronized (mLock) { - mCurrentWebViewPackage = findPreferredWebViewPackage(); - String userSetting = mSystemInterface.getUserChosenWebViewProvider(); - if (userSetting != null - && !userSetting.equals(mCurrentWebViewPackage.packageName)) { - // Don't persist the user-chosen setting across boots if the package being - // chosen is not used (could be disabled or uninstalled) so that the user won't - // be surprised by the device switching to using a certain webview package, - // that was uninstalled/disabled a long time ago, if it is installed/enabled - // again. - mSystemInterface.updateUserSetting(mCurrentWebViewPackage.packageName); - } - onWebViewProviderChanged(mCurrentWebViewPackage); - } - } catch (WebViewPackageMissingException e) { - Slog.e(TAG, "Could not find valid WebView package to create relro with", e); - } catch (Throwable t) { - // We don't know a case when this should happen but we log and discard errors at this - // stage as we must not crash the system server. - Slog.wtf(TAG, "error preparing webview provider from system server", t); - } - - if (getCurrentWebViewPackage() == null) { - // We didn't find a valid WebView implementation. Try explicitly re-enabling the - // fallback package for all users in case it was disabled, even if we already did the - // one-time migration before. If this actually changes the state, we will see the - // PackageManager broadcast shortly and try again. - WebViewProviderInfo[] webviewProviders = mSystemInterface.getWebViewPackages(); - WebViewProviderInfo fallbackProvider = getFallbackProvider(webviewProviders); - if (fallbackProvider != null) { - Slog.w(TAG, "No valid provider, trying to enable " + fallbackProvider.packageName); - mSystemInterface.enablePackageForAllUsers(fallbackProvider.packageName, true); - } else { - Slog.e(TAG, "No valid provider and no fallback available."); - } - } - } - - private void startZygoteWhenReady() { - // Wait on a background thread for RELRO creation to be done. We ignore the return value - // because even if RELRO creation failed we still want to start the zygote. - waitForAndGetProvider(); - mSystemInterface.ensureZygoteStarted(); - } - - @Override - public void handleNewUser(int userId) { - // The system user is always started at boot, and by that point we have already run one - // round of the package-changing logic (through prepareWebViewInSystemServer()), so early - // out here. - if (userId == UserHandle.USER_SYSTEM) return; - handleUserChange(); - } - - @Override - public void handleUserRemoved(int userId) { - handleUserChange(); - } - - /** - * Called when a user was added or removed to ensure WebView preparation is triggered. - * This has to be done since the WebView package we use depends on the enabled-state - * of packages for all users (so adding or removing a user might cause us to change package). - */ - private void handleUserChange() { - // Potentially trigger package-changing logic. - updateCurrentWebViewPackage(null); - } - - @Override - public void notifyRelroCreationCompleted() { - synchronized (mLock) { - mNumRelroCreationsFinished++; - checkIfRelrosDoneLocked(); - } - } - - @Override - public WebViewProviderResponse waitForAndGetProvider() { - PackageInfo webViewPackage = null; - final long timeoutTimeMs = System.nanoTime() / NS_PER_MS + WAIT_TIMEOUT_MS; - boolean webViewReady = false; - int webViewStatus = WebViewFactory.LIBLOAD_SUCCESS; - synchronized (mLock) { - webViewReady = webViewIsReadyLocked(); - while (!webViewReady) { - final long timeNowMs = System.nanoTime() / NS_PER_MS; - if (timeNowMs >= timeoutTimeMs) break; - try { - mLock.wait(timeoutTimeMs - timeNowMs); - } catch (InterruptedException e) { - // ignore - } - webViewReady = webViewIsReadyLocked(); - } - // Make sure we return the provider that was used to create the relro file - webViewPackage = mCurrentWebViewPackage; - if (webViewReady) { - // success - } else if (!mAnyWebViewInstalled) { - webViewStatus = WebViewFactory.LIBLOAD_FAILED_LISTING_WEBVIEW_PACKAGES; - } else { - // Either the current relro creation isn't done yet, or the new relro creatioin - // hasn't kicked off yet (the last relro creation used an out-of-date WebView). - webViewStatus = WebViewFactory.LIBLOAD_FAILED_WAITING_FOR_RELRO; - String timeoutError = "Timed out waiting for relro creation, relros started " - + mNumRelroCreationsStarted - + " relros finished " + mNumRelroCreationsFinished - + " package dirty? " + mWebViewPackageDirty; - Slog.e(TAG, timeoutError); - Trace.instant(Trace.TRACE_TAG_ACTIVITY_MANAGER, timeoutError); - } - } - if (!webViewReady) Slog.w(TAG, "creating relro file timed out"); - return new WebViewProviderResponse(webViewPackage, webViewStatus); - } - - /** - * Change WebView provider and provider setting and kill packages using the old provider. - * Return the new provider (in case we are in the middle of creating relro files, or - * replacing that provider it will not be in use directly, but will be used when the relros - * or the replacement are done). - */ - @Override - public String changeProviderAndSetting(String newProviderName) { - PackageInfo newPackage = updateCurrentWebViewPackage(newProviderName); - if (newPackage == null) return ""; - return newPackage.packageName; - } - - /** - * Update the current WebView package. - * @param newProviderName the package to switch to, null if no package has been explicitly - * chosen. - */ - private PackageInfo updateCurrentWebViewPackage(@Nullable String newProviderName) { - PackageInfo oldPackage = null; - PackageInfo newPackage = null; - boolean providerChanged = false; - synchronized (mLock) { - oldPackage = mCurrentWebViewPackage; - - if (newProviderName != null) { - mSystemInterface.updateUserSetting(newProviderName); - } - - try { - newPackage = findPreferredWebViewPackage(); - providerChanged = (oldPackage == null) - || !newPackage.packageName.equals(oldPackage.packageName); - } catch (WebViewPackageMissingException e) { - // If updated the Setting but don't have an installed WebView package, the - // Setting will be used when a package is available. - mCurrentWebViewPackage = null; - Slog.e(TAG, "Couldn't find WebView package to use " + e); - return null; - } - // Perform the provider change if we chose a new provider - if (providerChanged) { - onWebViewProviderChanged(newPackage); - } - } - // Kill apps using the old provider only if we changed provider - if (providerChanged && oldPackage != null) { - mSystemInterface.killPackageDependents(oldPackage.packageName); - } - // Return the new provider, this is not necessarily the one we were asked to switch to, - // but the persistent setting will now be pointing to the provider we were asked to - // switch to anyway. - return newPackage; - } - - /** - * This is called when we change WebView provider, either when the current provider is - * updated or a new provider is chosen / takes precedence. - */ - private void onWebViewProviderChanged(PackageInfo newPackage) { - synchronized (mLock) { - mAnyWebViewInstalled = true; - if (mNumRelroCreationsStarted == mNumRelroCreationsFinished) { - mSystemInterface.pinWebviewIfRequired(newPackage.applicationInfo); - mCurrentWebViewPackage = newPackage; - - // The relro creations might 'finish' (not start at all) before - // WebViewFactory.onWebViewProviderChanged which means we might not know the - // number of started creations before they finish. - mNumRelroCreationsStarted = NUMBER_OF_RELROS_UNKNOWN; - mNumRelroCreationsFinished = 0; - mNumRelroCreationsStarted = - mSystemInterface.onWebViewProviderChanged(newPackage); - Counter.logIncrement("webview.value_on_webview_provider_changed_counter"); - if (newPackage.packageName.equals(getDefaultWebViewPackage().packageName)) { - Counter.logIncrement( - "webview.value_on_webview_provider_changed_" - + "with_default_package_counter"); - } - // If the relro creations finish before we know the number of started creations - // we will have to do any cleanup/notifying here. - checkIfRelrosDoneLocked(); - } else { - mWebViewPackageDirty = true; - } - } - - // Once we've notified the system that the provider has changed and started RELRO creation, - // try to restart the zygote so that it will be ready when apps use it. - if (isMultiProcessEnabled()) { - AsyncTask.THREAD_POOL_EXECUTOR.execute(this::startZygoteWhenReady); - } - } - - /** - * Fetch only the currently valid WebView packages. - **/ - @Override - public WebViewProviderInfo[] getValidWebViewPackages() { - ProviderAndPackageInfo[] providersAndPackageInfos = getValidWebViewPackagesAndInfos(); - WebViewProviderInfo[] providers = - new WebViewProviderInfo[providersAndPackageInfos.length]; - for (int n = 0; n < providersAndPackageInfos.length; n++) { - providers[n] = providersAndPackageInfos[n].provider; - } - return providers; - } - - @Override - public WebViewProviderInfo getDefaultWebViewPackage() { - for (WebViewProviderInfo provider : getWebViewPackages()) { - if (provider.availableByDefault) { - return provider; - } - } - - // This should be unreachable because the config parser enforces that there is at least - // one availableByDefault provider. - throw new AndroidRuntimeException("No available by default WebView Provider."); - } - - private static class ProviderAndPackageInfo { - public final WebViewProviderInfo provider; - public final PackageInfo packageInfo; - - ProviderAndPackageInfo(WebViewProviderInfo provider, PackageInfo packageInfo) { - this.provider = provider; - this.packageInfo = packageInfo; - } - } - - private ProviderAndPackageInfo[] getValidWebViewPackagesAndInfos() { - WebViewProviderInfo[] allProviders = mSystemInterface.getWebViewPackages(); - List<ProviderAndPackageInfo> providers = new ArrayList<>(); - for (int n = 0; n < allProviders.length; n++) { - try { - PackageInfo packageInfo = - mSystemInterface.getPackageInfoForProvider(allProviders[n]); - if (validityResult(allProviders[n], packageInfo) == VALIDITY_OK) { - providers.add(new ProviderAndPackageInfo(allProviders[n], packageInfo)); - } - } catch (NameNotFoundException e) { - // Don't add non-existent packages - } - } - return providers.toArray(new ProviderAndPackageInfo[providers.size()]); - } - - /** - * Returns either the package info of the WebView provider determined in the following way: - * If the user has chosen a provider then use that if it is valid, - * otherwise use the first package in the webview priority list that is valid. - * - */ - private PackageInfo findPreferredWebViewPackage() throws WebViewPackageMissingException { - ProviderAndPackageInfo[] providers = getValidWebViewPackagesAndInfos(); - - String userChosenProvider = mSystemInterface.getUserChosenWebViewProvider(); - - // If the user has chosen provider, use that (if it's installed and enabled for all - // users). - for (ProviderAndPackageInfo providerAndPackage : providers) { - if (providerAndPackage.provider.packageName.equals(userChosenProvider)) { - // userPackages can contain null objects. - List<UserPackage> userPackages = - mSystemInterface.getPackageInfoForProviderAllUsers( - providerAndPackage.provider); - if (isInstalledAndEnabledForAllUsers(userPackages)) { - return providerAndPackage.packageInfo; - } - } - } - - // User did not choose, or the choice failed; use the most stable provider that is - // installed and enabled for all users, and available by default (not through - // user choice). - for (ProviderAndPackageInfo providerAndPackage : providers) { - if (providerAndPackage.provider.availableByDefault) { - // userPackages can contain null objects. - List<UserPackage> userPackages = - mSystemInterface.getPackageInfoForProviderAllUsers( - providerAndPackage.provider); - if (isInstalledAndEnabledForAllUsers(userPackages)) { - return providerAndPackage.packageInfo; - } - } - } - - // This should never happen during normal operation (only with modified system images). - mAnyWebViewInstalled = false; - throw new WebViewPackageMissingException("Could not find a loadable WebView package"); - } - - /** - * Return true iff {@param packageInfos} point to only installed and enabled packages. - * The given packages {@param packageInfos} should all be pointing to the same package, but each - * PackageInfo representing a different user's package. - */ - private static boolean isInstalledAndEnabledForAllUsers( - List<UserPackage> userPackages) { - for (UserPackage userPackage : userPackages) { - if (!userPackage.isInstalledPackage() || !userPackage.isEnabledPackage()) { - return false; - } - } - return true; - } - - @Override - public WebViewProviderInfo[] getWebViewPackages() { - return mSystemInterface.getWebViewPackages(); - } - - @Override - public PackageInfo getCurrentWebViewPackage() { - synchronized (mLock) { - return mCurrentWebViewPackage; - } - } - - /** - * Returns whether WebView is ready and is not going to go through its preparation phase - * again directly. - */ - private boolean webViewIsReadyLocked() { - return !mWebViewPackageDirty - && (mNumRelroCreationsStarted == mNumRelroCreationsFinished) - // The current package might be replaced though we haven't received an intent - // declaring this yet, the following flag makes anyone loading WebView to wait in - // this case. - && mAnyWebViewInstalled; - } - - private void checkIfRelrosDoneLocked() { - if (mNumRelroCreationsStarted == mNumRelroCreationsFinished) { - if (mWebViewPackageDirty) { - mWebViewPackageDirty = false; - // If we have changed provider since we started the relro creation we need to - // redo the whole process using the new package instead. - try { - PackageInfo newPackage = findPreferredWebViewPackage(); - onWebViewProviderChanged(newPackage); - } catch (WebViewPackageMissingException e) { - mCurrentWebViewPackage = null; - // If we can't find any valid WebView package we are now in a state where - // mAnyWebViewInstalled is false, so loading WebView will be blocked and we - // should simply wait until we receive an intent declaring a new package was - // installed. - } - } else { - mLock.notifyAll(); - } - } - } - - private int validityResult(WebViewProviderInfo configInfo, PackageInfo packageInfo) { - // Ensure the provider targets this framework release (or a later one). - if (!UserPackage.hasCorrectTargetSdkVersion(packageInfo)) { - return VALIDITY_INCORRECT_SDK_VERSION; - } - if (!versionCodeGE(packageInfo.getLongVersionCode(), getMinimumVersionCode()) - && !mSystemInterface.systemIsDebuggable()) { - // Webview providers may be downgraded arbitrarily low, prevent that by enforcing - // minimum version code. This check is only enforced for user builds. - return VALIDITY_INCORRECT_VERSION_CODE; - } - if (!providerHasValidSignature(configInfo, packageInfo, mSystemInterface)) { - return VALIDITY_INCORRECT_SIGNATURE; - } - if (WebViewFactory.getWebViewLibrary(packageInfo.applicationInfo) == null) { - return VALIDITY_NO_LIBRARY_FLAG; - } - return VALIDITY_OK; - } - - /** - * Both versionCodes should be from a WebView provider package implemented by Chromium. - * VersionCodes from other kinds of packages won't make any sense in this method. - * - * An introduction to Chromium versionCode scheme: - * "BBBBPPPXX" - * BBBB: 4 digit branch number. It monotonically increases over time. - * PPP: patch number in the branch. It is padded with zeroes to the left. These three digits - * may change their meaning in the future. - * XX: Digits to differentiate different APK builds of the same source version. - * - * This method takes the "BBBB" of versionCodes and compare them. - * - * https://www.chromium.org/developers/version-numbers describes general Chromium versioning; - * https://source.chromium.org/chromium/chromium/src/+/master:build/util/android_chrome_version.py - * is the canonical source for how Chromium versionCodes are calculated. - * - * @return true if versionCode1 is higher than or equal to versionCode2. - */ - private static boolean versionCodeGE(long versionCode1, long versionCode2) { - long v1 = versionCode1 / 100000; - long v2 = versionCode2 / 100000; - - return v1 >= v2; - } - - /** - * Gets the minimum version code allowed for a valid provider. It is the minimum versionCode - * of all available-by-default WebView provider packages. If there is no such WebView provider - * package on the system, then return -1, which means all positive versionCode WebView packages - * are accepted. - * - * Note that this is a private method that handles a variable (mMinimumVersionCode) which is - * shared between threads. Furthermore, this method does not hold mLock meaning that we must - * take extra care to ensure this method is thread-safe. - */ - private long getMinimumVersionCode() { - if (mMinimumVersionCode > 0) { - return mMinimumVersionCode; - } - - long minimumVersionCode = -1; - for (WebViewProviderInfo provider : mSystemInterface.getWebViewPackages()) { - if (provider.availableByDefault) { - try { - long versionCode = - mSystemInterface.getFactoryPackageVersion(provider.packageName); - if (minimumVersionCode < 0 || versionCode < minimumVersionCode) { - minimumVersionCode = versionCode; - } - } catch (NameNotFoundException e) { - // Safe to ignore. - } - } - } - - mMinimumVersionCode = minimumVersionCode; - return mMinimumVersionCode; - } - - private static boolean providerHasValidSignature(WebViewProviderInfo provider, - PackageInfo packageInfo, SystemInterface systemInterface) { - // Skip checking signatures on debuggable builds, for development purposes. - if (systemInterface.systemIsDebuggable()) return true; - - // Allow system apps to be valid providers regardless of signature. - if (packageInfo.applicationInfo.isSystemApp()) return true; - - // We don't support packages with multiple signatures. - if (packageInfo.signatures.length != 1) return false; - - // If any of the declared signatures match the package signature, it's valid. - for (Signature signature : provider.signatures) { - if (signature.equals(packageInfo.signatures[0])) return true; - } - - return false; - } - - /** - * Returns the only fallback provider in the set of given packages, or null if there is none. - */ - private static WebViewProviderInfo getFallbackProvider(WebViewProviderInfo[] webviewPackages) { - for (WebViewProviderInfo provider : webviewPackages) { - if (provider.isFallback) { - return provider; - } - } - return null; - } - - @Override - public boolean isMultiProcessEnabled() { - int settingValue = mSystemInterface.getMultiProcessSetting(); - if (mSystemInterface.isMultiProcessDefaultEnabled()) { - // Multiprocess should be enabled unless the user has turned it off manually. - return settingValue > MULTIPROCESS_SETTING_OFF_VALUE; - } else { - // Multiprocess should not be enabled, unless the user has turned it on manually. - return settingValue >= MULTIPROCESS_SETTING_ON_VALUE; - } - } - - @Override - public void enableMultiProcess(boolean enable) { - PackageInfo current = getCurrentWebViewPackage(); - mSystemInterface.setMultiProcessSetting( - enable ? MULTIPROCESS_SETTING_ON_VALUE : MULTIPROCESS_SETTING_OFF_VALUE); - mSystemInterface.notifyZygote(enable); - if (current != null) { - mSystemInterface.killPackageDependents(current.packageName); - } - } - - /** - * Dump the state of this Service. - */ - @Override - public void dumpState(PrintWriter pw) { - pw.println("Current WebView Update Service state"); - pw.println(String.format(" Multiprocess enabled: %b", isMultiProcessEnabled())); - synchronized (mLock) { - if (mCurrentWebViewPackage == null) { - pw.println(" Current WebView package is null"); - } else { - pw.println(String.format(" Current WebView package (name, version): (%s, %s)", - mCurrentWebViewPackage.packageName, - mCurrentWebViewPackage.versionName)); - } - pw.println(String.format(" Minimum targetSdkVersion: %d", - UserPackage.MINIMUM_SUPPORTED_SDK)); - pw.println(String.format(" Minimum WebView version code: %d", - mMinimumVersionCode)); - pw.println(String.format(" Number of relros started: %d", - mNumRelroCreationsStarted)); - pw.println(String.format(" Number of relros finished: %d", - mNumRelroCreationsFinished)); - pw.println(String.format(" WebView package dirty: %b", mWebViewPackageDirty)); - pw.println(String.format(" Any WebView package installed: %b", - mAnyWebViewInstalled)); - - try { - PackageInfo preferredWebViewPackage = findPreferredWebViewPackage(); - pw.println(String.format( - " Preferred WebView package (name, version): (%s, %s)", - preferredWebViewPackage.packageName, - preferredWebViewPackage.versionName)); - } catch (WebViewPackageMissingException e) { - pw.println(String.format(" Preferred WebView package: none")); - } - - dumpAllPackageInformationLocked(pw); - } - } - - private void dumpAllPackageInformationLocked(PrintWriter pw) { - WebViewProviderInfo[] allProviders = mSystemInterface.getWebViewPackages(); - pw.println(" WebView packages:"); - for (WebViewProviderInfo provider : allProviders) { - List<UserPackage> userPackages = - mSystemInterface.getPackageInfoForProviderAllUsers(provider); - PackageInfo systemUserPackageInfo = - userPackages.get(UserHandle.USER_SYSTEM).getPackageInfo(); - if (systemUserPackageInfo == null) { - pw.println(String.format(" %s is NOT installed.", provider.packageName)); - continue; - } - - int validity = validityResult(provider, systemUserPackageInfo); - String packageDetails = String.format( - "versionName: %s, versionCode: %d, targetSdkVersion: %d", - systemUserPackageInfo.versionName, - systemUserPackageInfo.getLongVersionCode(), - systemUserPackageInfo.applicationInfo.targetSdkVersion); - if (validity == VALIDITY_OK) { - boolean installedForAllUsers = isInstalledAndEnabledForAllUsers( - mSystemInterface.getPackageInfoForProviderAllUsers(provider)); - pw.println(String.format( - " Valid package %s (%s) is %s installed/enabled for all users", - systemUserPackageInfo.packageName, - packageDetails, - installedForAllUsers ? "" : "NOT")); - } else { - pw.println(String.format(" Invalid package %s (%s), reason: %s", - systemUserPackageInfo.packageName, - packageDetails, - getInvalidityReason(validity))); - } - } - } - - private static String getInvalidityReason(int invalidityReason) { - switch (invalidityReason) { - case VALIDITY_INCORRECT_SDK_VERSION: - return "SDK version too low"; - case VALIDITY_INCORRECT_VERSION_CODE: - return "Version code too low"; - case VALIDITY_INCORRECT_SIGNATURE: - return "Incorrect signature"; - case VALIDITY_NO_LIBRARY_FLAG: - return "No WebView-library manifest flag"; - default: - return "Unexcepted validity-reason"; - } - } -} diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java index 307c15b72c76..a5a02cdedf97 100644 --- a/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java +++ b/services/core/java/com/android/server/webkit/WebViewUpdateServiceImpl2.java @@ -66,7 +66,7 @@ import java.util.List; * * @hide */ -class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { +class WebViewUpdateServiceImpl2 { private static final String TAG = WebViewUpdateServiceImpl2.class.getSimpleName(); private static class WebViewPackageMissingException extends Exception { @@ -125,7 +125,6 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { mDefaultProvider = defaultProvider; } - @Override public void packageStateChanged(String packageName, int changedState, int userId) { // We don't early out here in different cases where we could potentially early-out (e.g. if // we receive PACKAGE_CHANGED for another user than the system user) since that would @@ -216,7 +215,6 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { mSystemInterface.enablePackageForAllUsers(mDefaultProvider.packageName, true); } - @Override public void prepareWebViewInSystemServer() { try { boolean repairNeeded = true; @@ -256,7 +254,6 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { mSystemInterface.ensureZygoteStarted(); } - @Override public void handleNewUser(int userId) { // The system user is always started at boot, and by that point we have already run one // round of the package-changing logic (through prepareWebViewInSystemServer()), so early @@ -265,7 +262,6 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { handleUserChange(); } - @Override public void handleUserRemoved(int userId) { handleUserChange(); } @@ -280,7 +276,6 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { updateCurrentWebViewPackage(null); } - @Override public void notifyRelroCreationCompleted() { synchronized (mLock) { mNumRelroCreationsFinished++; @@ -288,7 +283,6 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { } } - @Override public WebViewProviderResponse waitForAndGetProvider() { PackageInfo webViewPackage = null; final long timeoutTimeMs = System.nanoTime() / NS_PER_MS + WAIT_TIMEOUT_MS; @@ -334,7 +328,6 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { * replacing that provider it will not be in use directly, but will be used when the relros * or the replacement are done). */ - @Override public String changeProviderAndSetting(String newProviderName) { PackageInfo newPackage = updateCurrentWebViewPackage(newProviderName); if (newPackage == null) return ""; @@ -430,7 +423,6 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { } /** Fetch only the currently valid WebView packages. */ - @Override public WebViewProviderInfo[] getValidWebViewPackages() { ProviderAndPackageInfo[] providersAndPackageInfos = getValidWebViewPackagesAndInfos(); WebViewProviderInfo[] providers = @@ -445,7 +437,6 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { * Returns the default WebView provider which should be first availableByDefault option in the * system config. */ - @Override public WebViewProviderInfo getDefaultWebViewPackage() { return mDefaultProvider; } @@ -550,12 +541,10 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { return true; } - @Override public WebViewProviderInfo[] getWebViewPackages() { return mSystemInterface.getWebViewPackages(); } - @Override public PackageInfo getCurrentWebViewPackage() { synchronized (mLock) { return mCurrentWebViewPackage; @@ -708,20 +697,7 @@ class WebViewUpdateServiceImpl2 implements WebViewUpdateServiceInterface { return null; } - @Override - public boolean isMultiProcessEnabled() { - throw new IllegalStateException( - "isMultiProcessEnabled shouldn't be called if update_service_v2 flag is set."); - } - - @Override - public void enableMultiProcess(boolean enable) { - throw new IllegalStateException( - "enableMultiProcess shouldn't be called if update_service_v2 flag is set."); - } - /** Dump the state of this Service. */ - @Override public void dumpState(PrintWriter pw) { pw.println("Current WebView Update Service state"); synchronized (mLock) { diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateServiceInterface.java b/services/core/java/com/android/server/webkit/WebViewUpdateServiceInterface.java deleted file mode 100644 index 1772ef9c7405..000000000000 --- a/services/core/java/com/android/server/webkit/WebViewUpdateServiceInterface.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.android.server.webkit; - -import android.content.pm.PackageInfo; -import android.webkit.WebViewProviderInfo; -import android.webkit.WebViewProviderResponse; - -import java.io.PrintWriter; - -interface WebViewUpdateServiceInterface { - void packageStateChanged(String packageName, int changedState, int userId); - - void handleNewUser(int userId); - - void handleUserRemoved(int userId); - - WebViewProviderInfo[] getWebViewPackages(); - - void prepareWebViewInSystemServer(); - - void notifyRelroCreationCompleted(); - - WebViewProviderResponse waitForAndGetProvider(); - - String changeProviderAndSetting(String newProviderName); - - WebViewProviderInfo[] getValidWebViewPackages(); - - WebViewProviderInfo getDefaultWebViewPackage(); - - PackageInfo getCurrentWebViewPackage(); - - boolean isMultiProcessEnabled(); - - void enableMultiProcess(boolean enable); - - void dumpState(PrintWriter pw); -} diff --git a/services/core/java/com/android/server/webkit/WebViewUpdateServiceShellCommand.java b/services/core/java/com/android/server/webkit/WebViewUpdateServiceShellCommand.java deleted file mode 100644 index 7529c4157101..000000000000 --- a/services/core/java/com/android/server/webkit/WebViewUpdateServiceShellCommand.java +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT 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.webkit; - -import android.os.RemoteException; -import android.os.ShellCommand; -import android.webkit.IWebViewUpdateService; - -import java.io.PrintWriter; - -class WebViewUpdateServiceShellCommand extends ShellCommand { - final IWebViewUpdateService mInterface; - - WebViewUpdateServiceShellCommand(IWebViewUpdateService service) { - mInterface = service; - } - - @Override - public int onCommand(String cmd) { - if (cmd == null) { - return handleDefaultCommands(cmd); - } - - final PrintWriter pw = getOutPrintWriter(); - try { - switch(cmd) { - case "set-webview-implementation": - return setWebViewImplementation(); - case "enable-multiprocess": - return enableMultiProcess(true); - case "disable-multiprocess": - return enableMultiProcess(false); - default: - return handleDefaultCommands(cmd); - } - } catch (RemoteException e) { - pw.println("Remote exception: " + e); - } - return -1; - } - - private int setWebViewImplementation() throws RemoteException { - final PrintWriter pw = getOutPrintWriter(); - String shellChosenPackage = getNextArg(); - if (shellChosenPackage == null) { - pw.println("Failed to switch, no PACKAGE provided."); - pw.println(""); - helpSetWebViewImplementation(); - return 1; - } - String newPackage = mInterface.changeProviderAndSetting(shellChosenPackage); - if (shellChosenPackage.equals(newPackage)) { - pw.println("Success"); - return 0; - } else { - pw.println(String.format( - "Failed to switch to %s, the WebView implementation is now provided by %s.", - shellChosenPackage, newPackage)); - return 1; - } - } - - private int enableMultiProcess(boolean enable) throws RemoteException { - final PrintWriter pw = getOutPrintWriter(); - mInterface.enableMultiProcess(enable); - pw.println("Success"); - return 0; - } - - public void helpSetWebViewImplementation() { - PrintWriter pw = getOutPrintWriter(); - pw.println(" set-webview-implementation PACKAGE"); - pw.println(" Set the WebView implementation to the specified package."); - } - - @Override - public void onHelp() { - PrintWriter pw = getOutPrintWriter(); - pw.println("WebView updater commands:"); - pw.println(" help"); - pw.println(" Print this help text."); - pw.println(""); - helpSetWebViewImplementation(); - pw.println(" enable-multiprocess"); - pw.println(" Enable multi-process mode for WebView"); - pw.println(" disable-multiprocess"); - pw.println(" Disable multi-process mode for WebView"); - pw.println(); - } -} diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index a71b10fc77dc..69643002750b 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -629,6 +629,9 @@ 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; + // Whether the activity was launched from a bubble. private boolean mLaunchedFromBubble; @@ -7300,6 +7303,16 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A return mLocusId; } + void setLimitSystemEducationDialogs(boolean limitSystemEducationDialogs) { + if (mShouldLimitSystemEducationDialogs == limitSystemEducationDialogs) return; + mShouldLimitSystemEducationDialogs = limitSystemEducationDialogs; + final Task task = getTask(); + if (task != null) { + final boolean force = isVisibleRequested() && this == task.getTopNonFinishingActivity(); + getTask().dispatchTaskInfoChangedIfNeeded(force); + } + } + public void reportScreenCaptured() { if (mCaptureCallbacks != null) { final int n = mCaptureCallbacks.beginBroadcast(); diff --git a/services/core/java/com/android/server/wm/ActivityRecordInputSink.java b/services/core/java/com/android/server/wm/ActivityRecordInputSink.java index fa5beca31ec1..ea6506a03a80 100644 --- a/services/core/java/com/android/server/wm/ActivityRecordInputSink.java +++ b/services/core/java/com/android/server/wm/ActivityRecordInputSink.java @@ -40,7 +40,6 @@ class ActivityRecordInputSink { @ChangeId static final long ENABLE_TOUCH_OPAQUE_ACTIVITIES = 194480991L; - // TODO(b/369605358) Update EnabledSince when SDK 36 version code is available. /** * If the app's target SDK is 36+, pass-through touches from a cross-uid overlaying activity is * blocked by default. The activity may opt in to receive pass-through touches using @@ -52,7 +51,7 @@ class ActivityRecordInputSink { * @see ActivityOptions#setAllowPassThroughOnTouchOutside */ @ChangeId - @EnabledSince(targetSdkVersion = Build.VERSION_CODES.CUR_DEVELOPMENT) + @EnabledSince(targetSdkVersion = Build.VERSION_CODES.BAKLAVA) static final long ENABLE_OVERLAY_TOUCH_PASS_THROUGH_OPT_IN_ENFORCEMENT = 358129114L; private final ActivityRecord mActivityRecord; diff --git a/services/core/java/com/android/server/wm/ActivityStartController.java b/services/core/java/com/android/server/wm/ActivityStartController.java index 6e97f8a943dd..3f24da9d89f2 100644 --- a/services/core/java/com/android/server/wm/ActivityStartController.java +++ b/services/core/java/com/android/server/wm/ActivityStartController.java @@ -478,7 +478,7 @@ public class ActivityStartController { intentGrants.merge(creatorIntentGrants); } } catch (SecurityException securityException) { - ActivityStarter.logAndThrowExceptionForIntentRedirect( + ActivityStarter.logAndThrowExceptionForIntentRedirect(mService.mContext, "Creator URI Grant Caused Exception.", intent, creatorUid, creatorPackage, filterCallingUid, callingPackage, securityException); diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index e15968db9b3d..05a96d9fcf5e 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -55,6 +55,7 @@ import static android.content.pm.ActivityInfo.LAUNCH_SINGLE_TOP; import static android.content.pm.ActivityInfo.launchModeToString; import static android.os.Process.INVALID_UID; import static android.security.Flags.preventIntentRedirectAbortOrThrowException; +import static android.security.Flags.preventIntentRedirectShowToast; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.WindowManager.TRANSIT_NONE; import static android.view.WindowManager.TRANSIT_OPEN; @@ -105,6 +106,7 @@ import android.compat.annotation.ChangeId; import android.compat.annotation.Disabled; import android.compat.annotation.EnabledSince; import android.compat.annotation.Overridable; +import android.content.Context; import android.content.IIntentSender; import android.content.Intent; import android.content.IntentSender; @@ -128,12 +130,14 @@ import android.service.voice.IVoiceInteractionSession; import android.text.TextUtils; import android.util.Pools.SynchronizedPool; import android.util.Slog; +import android.widget.Toast; import android.window.RemoteTransition; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.HeavyWeightSwitcherActivity; import com.android.internal.app.IVoiceInteractor; import com.android.internal.protolog.ProtoLog; +import com.android.server.UiThread; import com.android.server.am.ActivityManagerService.IntentCreatorToken; import com.android.server.am.PendingIntentRecord; import com.android.server.pm.InstantAppResolver; @@ -614,7 +618,7 @@ class ActivityStarter { // Check if the Intent was redirected if ((intent.getExtendedFlags() & Intent.EXTENDED_FLAG_MISSING_CREATOR_OR_INVALID_TOKEN) != 0) { - ActivityStarter.logAndThrowExceptionForIntentRedirect( + logAndThrowExceptionForIntentRedirect(supervisor.mService.mContext, "Unparceled intent does not have a creator token set.", intent, intentCreatorUid, intentCreatorPackage, resolvedCallingUid, resolvedCallingPackage, null); @@ -650,7 +654,7 @@ class ActivityStarter { intentGrants.merge(creatorIntentGrants); } } catch (SecurityException securityException) { - ActivityStarter.logAndThrowExceptionForIntentRedirect( + logAndThrowExceptionForIntentRedirect(supervisor.mService.mContext, "Creator URI Grant Caused Exception.", intent, intentCreatorUid, intentCreatorPackage, resolvedCallingUid, resolvedCallingPackage, securityException); @@ -674,7 +678,7 @@ class ActivityStarter { intentGrants.merge(creatorIntentGrants); } } catch (SecurityException securityException) { - ActivityStarter.logAndThrowExceptionForIntentRedirect( + logAndThrowExceptionForIntentRedirect(supervisor.mService.mContext, "Creator URI Grant Caused Exception.", intent, intentCreatorUid, intentCreatorPackage, resolvedCallingUid, resolvedCallingPackage, securityException); @@ -1250,27 +1254,27 @@ class ActivityStarter { requestCode, 0, intentCreatorUid, intentCreatorPackage, "", request.ignoreTargetSecurity, inTask != null, null, resultRecord, resultRootTask)) { - abort = logAndAbortForIntentRedirect( + abort = logAndAbortForIntentRedirect(mService.mContext, "Creator checkStartAnyActivityPermission Caused abortion.", intent, intentCreatorUid, intentCreatorPackage, callingUid, callingPackage); } } catch (SecurityException e) { - logAndThrowExceptionForIntentRedirect( + logAndThrowExceptionForIntentRedirect(mService.mContext, "Creator checkStartAnyActivityPermission Caused Exception.", intent, intentCreatorUid, intentCreatorPackage, callingUid, callingPackage, e); } if (!mService.mIntentFirewall.checkStartActivity(intent, intentCreatorUid, 0, resolvedType, aInfo.applicationInfo)) { - abort = logAndAbortForIntentRedirect( + abort = logAndAbortForIntentRedirect(mService.mContext, "Creator IntentFirewall.checkStartActivity Caused abortion.", intent, intentCreatorUid, intentCreatorPackage, callingUid, callingPackage); } if (!mService.getPermissionPolicyInternal().checkStartActivity(intent, intentCreatorUid, intentCreatorPackage)) { - abort = logAndAbortForIntentRedirect( + abort = logAndAbortForIntentRedirect(mService.mContext, "Creator PermissionPolicyService.checkStartActivity Caused abortion.", intent, intentCreatorUid, intentCreatorPackage, callingUid, callingPackage); } @@ -3596,25 +3600,38 @@ class ActivityStarter { pw.println(mInTaskFragment); } - static void logAndThrowExceptionForIntentRedirect(@NonNull String message, - @NonNull Intent intent, int intentCreatorUid, @Nullable String intentCreatorPackage, - int callingUid, @Nullable String callingPackage, + static void logAndThrowExceptionForIntentRedirect(@NonNull Context context, + @NonNull String message, @NonNull Intent intent, int intentCreatorUid, + @Nullable String intentCreatorPackage, int callingUid, @Nullable String callingPackage, @Nullable SecurityException originalException) { String msg = getIntentRedirectPreventedLogMessage(message, intent, intentCreatorUid, intentCreatorPackage, callingUid, callingPackage); Slog.wtf(TAG, msg); + if (preventIntentRedirectShowToast()) { + UiThread.getHandler().post( + () -> Toast.makeText(context, + "Activity launch blocked. go/report-bug-intentRedir to report a bug", + Toast.LENGTH_LONG).show()); + } if (preventIntentRedirectAbortOrThrowException() && CompatChanges.isChangeEnabled( ENABLE_PREVENT_INTENT_REDIRECT_TAKE_ACTION, callingUid)) { throw new SecurityException(msg, originalException); } } - private static boolean logAndAbortForIntentRedirect(@NonNull String message, - @NonNull Intent intent, int intentCreatorUid, @Nullable String intentCreatorPackage, - int callingUid, @Nullable String callingPackage) { + private static boolean logAndAbortForIntentRedirect(@NonNull Context context, + @NonNull String message, @NonNull Intent intent, int intentCreatorUid, + @Nullable String intentCreatorPackage, int callingUid, + @Nullable String callingPackage) { String msg = getIntentRedirectPreventedLogMessage(message, intent, intentCreatorUid, intentCreatorPackage, callingUid, callingPackage); Slog.wtf(TAG, msg); + if (preventIntentRedirectShowToast()) { + UiThread.getHandler().post( + () -> Toast.makeText(context, + "Activity launch blocked. go/report-bug-intentRedir to report a bug", + Toast.LENGTH_LONG).show()); + } return preventIntentRedirectAbortOrThrowException() && CompatChanges.isChangeEnabled( ENABLE_PREVENT_INTENT_REDIRECT_TAKE_ACTION, callingUid); } diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index 0745fecdcd76..198e14a16500 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -1854,6 +1854,8 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { } assertPackageMatchesCallingUid(callingPackage); + mAmInternal.addCreatorToken(intent, callingPackage); + final ActivityOptions activityOptions = ActivityOptions.makeBasic(); activityOptions.setLaunchTaskId(taskId); // Pass in the system UID to allow setting launch taskId with MANAGE_GAME_ACTIVITY. @@ -3913,6 +3915,17 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { } @Override + public void setLimitSystemEducationDialogs( + IBinder appToken, boolean limitSystemEducationDialogs) { + synchronized (mGlobalLock) { + final ActivityRecord r = ActivityRecord.isInRootTaskLocked(appToken); + if (r != null) { + r.setLimitSystemEducationDialogs(limitSystemEducationDialogs); + } + } + } + + @Override public boolean updateConfiguration(Configuration values) { mAmInternal.enforceCallingPermission(CHANGE_CONFIGURATION, "updateConfiguration()"); diff --git a/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java b/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java index f1dd41e7d74a..90c0866d7dff 100644 --- a/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java +++ b/services/core/java/com/android/server/wm/AppCompatAspectRatioOverrides.java @@ -37,6 +37,7 @@ import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLAS import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_POSITION_MULTIPLIER_CENTER; import static com.android.server.wm.AppCompatConfiguration.MIN_FIXED_ORIENTATION_LETTERBOX_ASPECT_RATIO; import static com.android.server.wm.AppCompatUtils.isChangeEnabled; +import static com.android.server.wm.AppCompatUtils.isDisplayIgnoreActivitySizeRestrictions; import android.annotation.NonNull; import android.content.pm.IPackageManager; @@ -175,8 +176,17 @@ class AppCompatAspectRatioOverrides { && mActivityRecord.mDisplayContent.getIgnoreOrientationRequest(); } + /** + * Whether to ignore fixed orientation, aspect ratio and resizability of activity. + */ boolean hasFullscreenOverride() { - return shouldApplyUserFullscreenOverride() || isSystemOverrideToFullscreenEnabled(); + return shouldApplyUserFullscreenOverride() || isSystemOverrideToFullscreenEnabled() + || shouldIgnoreActivitySizeRestrictionsForDisplay(); + } + + boolean shouldIgnoreActivitySizeRestrictionsForDisplay() { + return isDisplayIgnoreActivitySizeRestrictions(mActivityRecord) + && !mAllowOrientationOverrideOptProp.isFalse(); } float getUserMinAspectRatio() { diff --git a/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java b/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java index e3a9d672c17f..7aed33d94223 100644 --- a/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java +++ b/services/core/java/com/android/server/wm/AppCompatOrientationPolicy.java @@ -53,11 +53,16 @@ class AppCompatOrientationPolicy { @ActivityInfo.ScreenOrientation int overrideOrientationIfNeeded(@ActivityInfo.ScreenOrientation int candidate) { + final AppCompatAspectRatioOverrides aspectRatioOverrides = + mAppCompatOverrides.getAppCompatAspectRatioOverrides(); + // Ignore all orientation requests of activities for eligible virtual displays. + if (aspectRatioOverrides.shouldIgnoreActivitySizeRestrictionsForDisplay()) { + return SCREEN_ORIENTATION_USER; + } final DisplayContent displayContent = mActivityRecord.mDisplayContent; final boolean isIgnoreOrientationRequestEnabled = displayContent != null && displayContent.getIgnoreOrientationRequest(); - final boolean hasFullscreenOverride = mAppCompatOverrides - .getAppCompatAspectRatioOverrides().hasFullscreenOverride(); + final boolean hasFullscreenOverride = aspectRatioOverrides.hasFullscreenOverride(); final boolean shouldCameraCompatControlOrientation = AppCompatCameraPolicy.shouldCameraCompatControlOrientation(mActivityRecord); if (hasFullscreenOverride && isIgnoreOrientationRequestEnabled @@ -76,8 +81,8 @@ class AppCompatOrientationPolicy { // In some cases (e.g. Kids app) we need to map the candidate orientation to some other // orientation. candidate = mActivityRecord.mWmService.mapOrientationRequest(candidate); - final boolean shouldApplyUserMinAspectRatioOverride = mAppCompatOverrides - .getAppCompatAspectRatioOverrides().shouldApplyUserMinAspectRatioOverride(); + final boolean shouldApplyUserMinAspectRatioOverride = aspectRatioOverrides + .shouldApplyUserMinAspectRatioOverride(); if (shouldApplyUserMinAspectRatioOverride && (!isFixedOrientation(candidate) || candidate == SCREEN_ORIENTATION_LOCKED)) { Slog.v(TAG, "Requested orientation " + screenOrientationToString(candidate) diff --git a/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java b/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java index f069dcdbc86b..d0d3d4321a0a 100644 --- a/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java +++ b/services/core/java/com/android/server/wm/AppCompatSizeCompatModePolicy.java @@ -149,7 +149,7 @@ class AppCompatSizeCompatModePolicy { @NonNull Configuration newParentConfig) { mSizeCompatScale = mActivityRecord.mAppCompatController.getTransparentPolicy() .findOpaqueNotFinishingActivityBelow() - .map(activityRecord -> mSizeCompatScale) + .map(ar -> Math.min(1.0f, ar.getCompatScale())) .orElseGet(() -> calculateSizeCompatScale( resolvedAppBounds, containerAppBounds, newParentConfig)); } diff --git a/services/core/java/com/android/server/wm/AppCompatUtils.java b/services/core/java/com/android/server/wm/AppCompatUtils.java index 8d8424820990..db76eb9ac5d9 100644 --- a/services/core/java/com/android/server/wm/AppCompatUtils.java +++ b/services/core/java/com/android/server/wm/AppCompatUtils.java @@ -32,6 +32,8 @@ import android.view.InsetsSource; import android.view.InsetsState; import android.view.WindowInsets; +import com.android.window.flags.Flags; + import java.util.function.BooleanSupplier; /** @@ -86,10 +88,22 @@ final class AppCompatUtils { /** * @param activityRecord The {@link ActivityRecord} for the app package. * @param overrideChangeId The per-app override identifier. - * @return {@code true} if the per-app override is enable for the given activity. + * @return {@code true} if the per-app override is enable for the given activity and the + * display does not ignore fixed orientation, aspect ratio and resizability of activity. */ static boolean isChangeEnabled(@NonNull ActivityRecord activityRecord, long overrideChangeId) { - return activityRecord.info.isChangeEnabled(overrideChangeId); + return activityRecord.info.isChangeEnabled(overrideChangeId) + && !isDisplayIgnoreActivitySizeRestrictions(activityRecord); + } + + /** + * Whether the display ignores fixed orientation, aspect ratio and resizability of activities. + */ + static boolean isDisplayIgnoreActivitySizeRestrictions( + @NonNull ActivityRecord activityRecord) { + final DisplayContent dc = activityRecord.mDisplayContent; + return Flags.vdmForceAppUniversalResizableApi() && dc != null + && dc.isDisplayIgnoreActivitySizeRestrictions(); } /** diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java index 5c53c2aeba4b..1a70e6896d18 100644 --- a/services/core/java/com/android/server/wm/BackNavigationController.java +++ b/services/core/java/com/android/server/wm/BackNavigationController.java @@ -26,6 +26,8 @@ import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_OLD_NONE; import static android.view.WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION; import static android.view.WindowManager.TRANSIT_TO_BACK; +import static android.window.SystemOverrideOnBackInvokedCallback.OVERRIDE_FINISH_AND_REMOVE_TASK; +import static android.window.SystemOverrideOnBackInvokedCallback.OVERRIDE_UNDEFINED; import static com.android.internal.protolog.WmProtoLogGroups.WM_DEBUG_BACK_PREVIEW; import static com.android.server.wm.BackNavigationProto.ANIMATION_IN_PROGRESS; @@ -60,6 +62,7 @@ import android.window.BackNavigationInfo; import android.window.IBackAnimationFinishedCallback; import android.window.IWindowlessStartingSurfaceCallback; import android.window.OnBackInvokedCallbackInfo; +import android.window.SystemOverrideOnBackInvokedCallback; import android.window.TaskSnapshot; import com.android.internal.annotations.VisibleForTesting; @@ -107,6 +110,12 @@ class BackNavigationController { mNavigationMonitor.onFocusWindowChanged(newFocus); } + void onEmbeddedWindowGestureTransferred(@NonNull WindowState host) { + if (Flags.disallowAppProgressEmbeddedWindow()) { + mNavigationMonitor.onEmbeddedWindowGestureTransferred(host); + } + } + /** * Set up the necessary leashes and build a {@link BackNavigationInfo} instance for an upcoming * back gesture animation. @@ -178,6 +187,9 @@ class BackNavigationController { return null; } + final ArrayList<EmbeddedWindowController.EmbeddedWindow> embeddedWindows = wmService + .mEmbeddedWindowController.getByHostWindow(window); + currentActivity = window.mActivityRecord; currentTask = window.getTask(); if ((currentTask != null && !currentTask.isVisibleRequested()) @@ -199,17 +211,37 @@ class BackNavigationController { infoBuilder.setOnBackInvokedCallback(callbackInfo.getCallback()); infoBuilder.setAnimationCallback(callbackInfo.isAnimationCallback()); infoBuilder.setTouchableRegion(window.getFrame()); - infoBuilder.setAppProgressAllowed((window.getAttrs().privateFlags - & PRIVATE_FLAG_APP_PROGRESS_GENERATION_ALLOWED) != 0); if (currentTask != null) { infoBuilder.setFocusedTaskId(currentTask.mTaskId); } + boolean transferGestureToEmbedded = false; + if (Flags.disallowAppProgressEmbeddedWindow() && embeddedWindows != null) { + for (int i = embeddedWindows.size() - 1; i >= 0; --i) { + if (embeddedWindows.get(i).mGestureToEmbedded) { + transferGestureToEmbedded = true; + break; + } + } + } + final boolean canInterruptInView = (window.getAttrs().privateFlags + & PRIVATE_FLAG_APP_PROGRESS_GENERATION_ALLOWED) != 0; + infoBuilder.setAppProgressAllowed(canInterruptInView && !transferGestureToEmbedded + && callbackInfo.isAnimationCallback()); mNavigationMonitor.startMonitor(window, navigationObserver); + int requestOverride = callbackInfo.getOverrideBehavior(); ProtoLog.d(WM_DEBUG_BACK_PREVIEW, "startBackNavigation currentTask=%s, " + "topRunningActivity=%s, callbackInfo=%s, currentFocus=%s", currentTask, currentActivity, callbackInfo, window); - + if (requestOverride == OVERRIDE_FINISH_AND_REMOVE_TASK) { + final ActivityRecord rootR = currentTask != null ? currentTask.getRootActivity() + : null; + if (currentActivity != null && rootR != currentActivity) { + // The top activity is not root activity, the activity cannot remove task when + // finishAndRemoveTask called. + requestOverride = OVERRIDE_UNDEFINED; + } + } // Clear the pointer down outside focus if any. mWindowManagerService.clearPointerDownOutsideFocusRunnable(); @@ -254,7 +286,8 @@ class BackNavigationController { } else if (hasTranslucentActivity(currentActivity, prevActivities)) { // skip if one of participant activity is translucent backType = BackNavigationInfo.TYPE_CALLBACK; - } else if (prevActivities.size() > 0) { + } else if (prevActivities.size() > 0 + && requestOverride == SystemOverrideOnBackInvokedCallback.OVERRIDE_UNDEFINED) { if ((!isOccluded || isAllActivitiesCanShowWhenLocked(prevActivities)) && isAllActivitiesCreated(prevActivities)) { // We have another Activity in the same currentTask to go to @@ -742,6 +775,20 @@ class BackNavigationController { } /** + * Notify focus window has transferred touch gesture to embedded window. Shell should pilfer + * pointers so embedded process won't receive motion event. + * + */ + void onEmbeddedWindowGestureTransferred(@NonNull WindowState host) { + if (!isMonitorForRemote() || host != mNavigatingWindow) { + return; + } + final Bundle result = new Bundle(); + result.putBoolean(BackNavigationInfo.KEY_TOUCH_GESTURE_TRANSFERRED, true); + mObserver.sendResult(result); + } + + /** * Notify an unexpected transition has happened during back navigation. */ private void onTransitionReadyWhileNavigate(ArrayList<WindowContainer> opening, @@ -1817,7 +1864,7 @@ class BackNavigationController { tc.requestStartTransition(prepareOpen, null /*startTask */, null /* remoteTransition */, null /* displayChange */); - prepareOpen.setReady(makeVisibles.get(0), true); + prepareOpen.setReady(mCloseTarget, true); return prepareOpen; } else if (mSnapshot == null) { return setLaunchBehind(visibleOpenActivities); diff --git a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java index a462c67a19f4..ec171c5e5766 100644 --- a/services/core/java/com/android/server/wm/BackgroundActivityStartController.java +++ b/services/core/java/com/android/server/wm/BackgroundActivityStartController.java @@ -49,10 +49,9 @@ import static com.android.window.flags.Flags.balDontBringExistingBackgroundTaskS import static com.android.window.flags.Flags.balImproveRealCallerVisibilityCheck; import static com.android.window.flags.Flags.balImprovedMetrics; import static com.android.window.flags.Flags.balRequireOptInByPendingIntentCreator; -import static com.android.window.flags.Flags.balRequireOptInSameUid; import static com.android.window.flags.Flags.balRespectAppSwitchStateWhenCheckBoundByForegroundUid; import static com.android.window.flags.Flags.balShowToastsBlocked; -import static com.android.window.flags.Flags.balStrictMode; +import static com.android.window.flags.Flags.balStrictModeRo; import static java.lang.annotation.RetentionPolicy.SOURCE; import static java.util.Objects.requireNonNull; @@ -359,7 +358,7 @@ public class BackgroundActivityStartController { } else if (mIsCallForResult) { mAutoOptInReason = AUTO_OPT_IN_CALL_FOR_RESULT; mAutoOptInCaller = false; - } else if (callingUid == realCallingUid && !balRequireOptInSameUid()) { + } else if (callingUid == realCallingUid) { mAutoOptInReason = AUTO_OPT_IN_SAME_UID; mAutoOptInCaller = false; } else if (realCallerBackgroundActivityStartMode @@ -604,7 +603,6 @@ public class BackgroundActivityStartController { .append(balImproveRealCallerVisibilityCheck()); sb.append("; balRequireOptInByPendingIntentCreator: ") .append(balRequireOptInByPendingIntentCreator()); - sb.append("; balRequireOptInSameUid: ").append(balRequireOptInSameUid()); sb.append("; balRespectAppSwitchStateWhenCheckBoundByForegroundUid: ") .append(balRespectAppSwitchStateWhenCheckBoundByForegroundUid()); sb.append("; balDontBringExistingBackgroundTaskStackToFg: ") @@ -851,7 +849,7 @@ public class BackgroundActivityStartController { showToast("BAL blocked. goo.gle/android-bal"); } BalVerdict verdict = statsLog(BalVerdict.BLOCK, state); - if (balStrictMode()) { + if (balStrictModeRo()) { String abortDebugMessage; if (state.isPendingIntent()) { abortDebugMessage = @@ -1164,8 +1162,9 @@ public class BackgroundActivityStartController { * or {@link #BAL_BLOCK} if the launch should be blocked */ BalVerdict checkBackgroundActivityStartAllowedByRealCallerInBackground(BalState state) { - if (state.mCheckedOptions.getPendingIntentBackgroundActivityStartMode() - == MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS + boolean allowAlways = state.mCheckedOptions.getPendingIntentBackgroundActivityStartMode() + == MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS; + if (allowAlways && hasBalPermission(state.mRealCallingUid, state.mRealCallingPid)) { return new BalVerdict(BAL_ALLOW_PERMISSION, /*background*/ false, @@ -1177,8 +1176,7 @@ public class BackgroundActivityStartController { + state.mRealCallingPid + ", " + state.mRealCallingPackage + ") " + balStartModeToString( state.mCheckedOptions.getPendingIntentBackgroundActivityStartMode())); - if (state.mCheckedOptions.getPendingIntentBackgroundActivityStartMode() - == MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS + if (allowAlways && mService.hasSystemAlertWindowPermission(state.mRealCallingUid, state.mRealCallingPid, state.mRealCallingPackage)) { Slog.w( @@ -1192,7 +1190,7 @@ public class BackgroundActivityStartController { // if the realCallingUid is a persistent system process, abort if the IntentSender // wasn't allowed to start an activity - if (state.mAllowBalExemptionForSystemProcess + if ((allowAlways || state.mAllowBalExemptionForSystemProcess) && state.mIsRealCallingUidPersistentSystemProcess) { return new BalVerdict(BAL_ALLOW_ALLOWLISTED_UID, /*background*/ false, diff --git a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java index cbe3d79f10fc..6f8c17a9ac75 100644 --- a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java +++ b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java @@ -402,6 +402,7 @@ class DeferredDisplayUpdater { || first.modeId != second.modeId || first.renderFrameRate != second.renderFrameRate || first.hasArrSupport != second.hasArrSupport + || !Objects.equals(first.frameRateCategoryRate, second.frameRateCategoryRate) || first.defaultModeId != second.defaultModeId || first.userPreferredModeId != second.userPreferredModeId || !Arrays.equals(first.supportedModes, second.supportedModes) diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java index aac756f4e559..e827f44cb2a2 100644 --- a/services/core/java/com/android/server/wm/DisplayContent.java +++ b/services/core/java/com/android/server/wm/DisplayContent.java @@ -5789,6 +5789,17 @@ class DisplayContent extends RootDisplayArea implements WindowManagerPolicy.Disp } /** + * Checks if this display is allowed to ignore fixed orientation, aspect ratio, + * and resizability of apps. + * + * <p>This can be set via + * {@link VirtualDisplayConfig.Builder#setIgnoreActivitySizeRestrictions}.</p> + */ + boolean isDisplayIgnoreActivitySizeRestrictions() { + return mWmService.mDisplayWindowSettings.isIgnoreActivitySizeRestrictionsLocked(this); + } + + /** * The direct child layer of the display to put all non-overlay windows. This is also used for * screen rotation animation so that there is a parent layer to put the animation leash. */ diff --git a/services/core/java/com/android/server/wm/DisplayWindowSettings.java b/services/core/java/com/android/server/wm/DisplayWindowSettings.java index e585efa8a3cc..c87b811b4231 100644 --- a/services/core/java/com/android/server/wm/DisplayWindowSettings.java +++ b/services/core/java/com/android/server/wm/DisplayWindowSettings.java @@ -275,6 +275,24 @@ class DisplayWindowSettings { mSettingsProvider.updateOverrideSettings(displayInfo, overrideSettings); } + boolean isIgnoreActivitySizeRestrictionsLocked(@NonNull DisplayContent dc) { + final DisplayInfo displayInfo = dc.getDisplayInfo(); + final SettingsProvider.SettingsEntry settings = mSettingsProvider.getSettings(displayInfo); + return settings.mIgnoreActivitySizeRestrictions != null + && settings.mIgnoreActivitySizeRestrictions; + } + + void setIgnoreActivitySizeRestrictionsOnDisplayLocked(@NonNull String displayUniqueId, + int displayType, boolean enabled) { + final DisplayInfo displayInfo = new DisplayInfo(); + displayInfo.uniqueId = displayUniqueId; + displayInfo.type = displayType; + final SettingsProvider.SettingsEntry overrideSettings = + mSettingsProvider.getOverrideSettings(displayInfo); + overrideSettings.mIgnoreActivitySizeRestrictions = enabled; + mSettingsProvider.updateOverrideSettings(displayInfo, overrideSettings); + } + void clearDisplaySettings(@NonNull String displayUniqueId, int displayType) { final DisplayInfo displayInfo = new DisplayInfo(); displayInfo.uniqueId = displayUniqueId; @@ -474,6 +492,8 @@ class DisplayWindowSettings { Boolean mIgnoreDisplayCutout; @Nullable Boolean mDontMoveToTop; + @Nullable + Boolean mIgnoreActivitySizeRestrictions; SettingsEntry() {} @@ -557,6 +577,11 @@ class DisplayWindowSettings { mDontMoveToTop = other.mDontMoveToTop; changed = true; } + if (!Objects.equals(other.mIgnoreActivitySizeRestrictions, + mIgnoreActivitySizeRestrictions)) { + mIgnoreActivitySizeRestrictions = other.mIgnoreActivitySizeRestrictions; + changed = true; + } return changed; } @@ -649,6 +674,11 @@ class DisplayWindowSettings { mDontMoveToTop = delta.mDontMoveToTop; changed = true; } + if (delta.mIgnoreActivitySizeRestrictions != null && !Objects.equals( + delta.mIgnoreActivitySizeRestrictions, mIgnoreActivitySizeRestrictions)) { + mIgnoreActivitySizeRestrictions = delta.mIgnoreActivitySizeRestrictions; + changed = true; + } return changed; } @@ -667,7 +697,8 @@ class DisplayWindowSettings { && mFixedToUserRotation == null && mIgnoreOrientationRequest == null && mIgnoreDisplayCutout == null - && mDontMoveToTop == null; + && mDontMoveToTop == null + && mIgnoreActivitySizeRestrictions == null; } @Override @@ -691,7 +722,9 @@ class DisplayWindowSettings { && Objects.equals(mFixedToUserRotation, that.mFixedToUserRotation) && Objects.equals(mIgnoreOrientationRequest, that.mIgnoreOrientationRequest) && Objects.equals(mIgnoreDisplayCutout, that.mIgnoreDisplayCutout) - && Objects.equals(mDontMoveToTop, that.mDontMoveToTop); + && Objects.equals(mDontMoveToTop, that.mDontMoveToTop) + && Objects.equals(mIgnoreActivitySizeRestrictions, + that.mIgnoreActivitySizeRestrictions); } @Override @@ -700,7 +733,7 @@ class DisplayWindowSettings { mForcedHeight, mForcedDensity, mForcedScalingMode, mRemoveContentMode, mShouldShowWithInsecureKeyguard, mShouldShowSystemDecors, mIsHomeSupported, mImePolicy, mFixedToUserRotation, mIgnoreOrientationRequest, - mIgnoreDisplayCutout, mDontMoveToTop); + mIgnoreDisplayCutout, mDontMoveToTop, mIgnoreActivitySizeRestrictions); } @Override @@ -722,6 +755,7 @@ class DisplayWindowSettings { + ", mIgnoreOrientationRequest=" + mIgnoreOrientationRequest + ", mIgnoreDisplayCutout=" + mIgnoreDisplayCutout + ", mDontMoveToTop=" + mDontMoveToTop + + ", mForceAppsUniversalResizable=" + mIgnoreActivitySizeRestrictions + '}'; } } diff --git a/services/core/java/com/android/server/wm/EmbeddedWindowController.java b/services/core/java/com/android/server/wm/EmbeddedWindowController.java index 5ac4cf8b6483..907d0dc2e183 100644 --- a/services/core/java/com/android/server/wm/EmbeddedWindowController.java +++ b/services/core/java/com/android/server/wm/EmbeddedWindowController.java @@ -39,6 +39,8 @@ import android.window.InputTransferToken; import com.android.internal.protolog.ProtoLog; import com.android.server.input.InputManagerService; +import java.util.ArrayList; + /** * Keeps track of embedded windows. * @@ -146,6 +148,20 @@ class EmbeddedWindowController { return mWindowsByWindowToken.get(windowToken); } + @Nullable ArrayList<EmbeddedWindow> getByHostWindow(WindowState host) { + ArrayList<EmbeddedWindow> windows = null; + for (int i = mWindows.size() - 1; i >= 0; i--) { + final EmbeddedWindow ew = mWindows.valueAt(i); + if (ew.mHostWindowState == host) { + if (windows == null) { + windows = new ArrayList<>(); + } + windows.add(ew); + } + } + return windows; + } + private boolean isValidTouchGestureParams(WindowState hostWindowState, EmbeddedWindow embeddedWindow) { if (embeddedWindow == null) { @@ -191,8 +207,12 @@ class EmbeddedWindowController { throw new SecurityException( "Transfer request must originate from owner of transferFromToken"); } - return mInputManagerService.transferTouchGesture(ew.getInputChannelToken(), - transferToHostWindowState.mInputChannelToken); + final boolean didTransfer = mInputManagerService.transferTouchGesture( + ew.getInputChannelToken(), transferToHostWindowState.mInputChannelToken); + if (didTransfer) { + ew.mGestureToEmbedded = false; + } + return didTransfer; } boolean transferToEmbedded(int callingUid, WindowState hostWindowState, @@ -205,8 +225,15 @@ class EmbeddedWindowController { throw new SecurityException( "Transfer request must originate from owner of transferFromToken"); } - return mInputManagerService.transferTouchGesture(hostWindowState.mInputChannelToken, + final boolean didTransfer = mInputManagerService.transferTouchGesture( + hostWindowState.mInputChannelToken, ew.getInputChannelToken()); + if (didTransfer) { + ew.mGestureToEmbedded = true; + mAtmService.mBackNavigationController.onEmbeddedWindowGestureTransferred( + hostWindowState); + } + return didTransfer; } static class EmbeddedWindow implements InputTarget { @@ -235,6 +262,9 @@ class EmbeddedWindowController { // the host window. private @WindowInsets.Type.InsetsType int mRequestedVisibleTypes = 0; + /** Whether the gesture is transferred to embedded window. */ + boolean mGestureToEmbedded = false; + /** * @param session calling session to check ownership of the window * @param clientToken client token used to clean up the map if the embedding process dies diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index b14dd3f7f17c..7473accb8eee 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -3423,6 +3423,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; final Task parentTask = getParent() != null ? getParent().asTask() : null; info.parentTaskId = parentTask != null && parentTask.mCreatedByOrganizer ? parentTask.mTaskId @@ -3464,7 +3466,8 @@ class Task extends TaskFragment { return null; } final Rect windowFrame = mainWindow.getFrame(); - if (top.getBounds().equals(windowFrame)) { + final Rect parentFrame = mainWindow.getParentFrame(); + if (parentFrame.equals(windowFrame)) { return null; } return windowFrame; @@ -3915,9 +3918,13 @@ class Task extends TaskFragment { sb.append(" aI="); sb.append(affinityIntent.getComponent().flattenToShortString()); } - sb.append(" isResizeable=").append(isResizeable()); - sb.append(" minWidth=").append(mMinWidth); - sb.append(" minHeight=").append(mMinHeight); + if (!isResizeable()) { + sb.append(" nonResizable"); + } + if (mMinWidth != INVALID_MIN_SIZE || mMinHeight != INVALID_MIN_SIZE) { + sb.append(" minWidth=").append(mMinWidth); + sb.append(" minHeight=").append(mMinHeight); + } sb.append('}'); return stringName = sb.toString(); } diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java index fc4660061c55..454e43120ede 100644 --- a/services/core/java/com/android/server/wm/Transition.java +++ b/services/core/java/com/android/server/wm/Transition.java @@ -237,15 +237,16 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { private final ArraySet<WindowToken> mVisibleAtTransitionEndTokens = new ArraySet<>(); /** - * Set of transient activities (lifecycle initially tied to this transition) and their + * Map of transient activities (lifecycle initially tied to this transition) to their * restore-below tasks. */ private ArrayMap<ActivityRecord, Task> mTransientLaunches = null; /** * The tasks that may be occluded by the transient activity. Assume the task stack is - * [Home, A(opaque), B(opaque), C(translucent)] (bottom to top), then A is the restore-below - * task, and [B, C] are the transient-hide tasks. + * [Home, A(opaque), B(opaque), C(translucent)] (bottom to top), and Home is started in a + * transient-launch activity, then A is the restore-below task, and [B, C] are the + * transient-hide tasks. */ private ArrayList<Task> mTransientHideTasks; @@ -401,18 +402,26 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { mTransientLaunches.put(activity, restoreBelow); setTransientLaunchToChanges(activity); - final Task transientRootTask = activity.getRootTask(); + final int restoreBelowTaskId = restoreBelow != null ? restoreBelow.mTaskId : -1; + ProtoLog.v(WmProtoLogGroups.WM_DEBUG_WINDOW_TRANSITIONS, "Transition %d: Set %s as " + + "transient-launch restoreBelowTaskId=%d", mSyncId, activity, restoreBelowTaskId); + + final Task transientLaunchRootTask = activity.getRootTask(); final WindowContainer<?> parent = restoreBelow != null ? restoreBelow.getParent() - : (transientRootTask != null ? transientRootTask.getParent() : null); + : (transientLaunchRootTask != null ? transientLaunchRootTask.getParent() : null); if (parent != null) { // Collect all visible tasks which can be occluded by the transient activity to // make sure they are in the participants so their visibilities can be updated when // finishing transition. + // Note: This currently assumes that the parent is a DA containing the full set of + // visible tasks parent.forAllTasks(t -> { // Skip transient-launch task - if (t == transientRootTask) return false; + if (t == transientLaunchRootTask) return false; if (t.isVisibleRequested() && !t.isAlwaysOnTop()) { if (t.isRootTask()) { + ProtoLog.v(WmProtoLogGroups.WM_DEBUG_WINDOW_TRANSITIONS, + " transient hide: taskId=%d", t.mTaskId); mTransientHideTasks.add(t); } if (t.isLeafTask()) { @@ -442,9 +451,6 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { // the gesture threshold. activity.getTask().setCanAffectSystemUiFlags(false); } - - ProtoLog.v(WmProtoLogGroups.WM_DEBUG_WINDOW_TRANSITIONS, "Transition %d: Set %s as " - + "transient-launch", mSyncId, activity); } /** @return whether `wc` is a descendent of a transient-hide window. */ @@ -462,6 +468,8 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { /** Returns {@code true} if the task should keep visible if this is a transient transition. */ boolean isTransientVisible(@NonNull Task task) { if (mTransientLaunches == null) return false; + + // Check if all the transient-launch activities are occluded int occludedCount = 0; final int numTransient = mTransientLaunches.size(); for (int i = numTransient - 1; i >= 0; --i) { @@ -486,6 +494,8 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { // Let transient-hide activities pause before transition is finished. return false; } + + // If this task is currently transient-hide, then keep it visible return isInTransientHide(task); } @@ -608,7 +618,8 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { } /** - * Only set flag to the parent tasks and activity itself. + * Sets the FLAG_TRANSIENT_LAUNCH flag to all changes associated with the given activity + * container and parent tasks. */ private void setTransientLaunchToChanges(@NonNull WindowContainer wc) { for (WindowContainer curr = wc; curr != null && mChanges.containsKey(curr); @@ -774,7 +785,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { // Only look at tasks, taskfragments, or activities if (wc.asTaskFragment() == null && wc.asActivityRecord() == null) return; if (!isInTransientHide(wc)) return; - info.mFlags |= ChangeInfo.FLAG_ABOVE_TRANSIENT_LAUNCH; + info.mFlags |= ChangeInfo.FLAG_TRANSIENT_HIDE; } private void recordDisplay(DisplayContent dc) { @@ -2448,6 +2459,13 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { sb.append(" type=" + transitTypeToString(mType)); sb.append(" flags=0x" + Integer.toHexString(mFlags)); sb.append(" overrideAnimOptions=" + mOverrideOptions); + if (!mChanges.isEmpty()) { + sb.append(" c=["); + for (int i = 0; i < mChanges.size(); i++) { + sb.append("\n").append(" ").append(mChanges.valueAt(i).toString()); + } + sb.append("\n]\n"); + } sb.append('}'); return sb.toString(); } @@ -3396,8 +3414,14 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { * seamless rotation. This is currently only used by DisplayContent during fixed-rotation. */ private static final int FLAG_SEAMLESS_ROTATION = 1; + /** + * Identifies the associated WindowContainer as a transient-launch task or activity. + */ private static final int FLAG_TRANSIENT_LAUNCH = 2; - private static final int FLAG_ABOVE_TRANSIENT_LAUNCH = 4; + /** + * Identifies the associated WindowContainer as a transient-hide task or activity. + */ + private static final int FLAG_TRANSIENT_HIDE = 4; /** This container explicitly requested no-animation (usually Activity level). */ private static final int FLAG_CHANGE_NO_ANIMATION = 0x8; @@ -3429,7 +3453,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { FLAG_NONE, FLAG_SEAMLESS_ROTATION, FLAG_TRANSIENT_LAUNCH, - FLAG_ABOVE_TRANSIENT_LAUNCH, + FLAG_TRANSIENT_HIDE, FLAG_CHANGE_NO_ANIMATION, FLAG_CHANGE_YES_ANIMATION, FLAG_CHANGE_MOVED_TO_TOP, @@ -3438,7 +3462,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { FLAG_BELOW_BACK_GESTURE_ANIMATION }) @Retention(RetentionPolicy.SOURCE) - @interface Flag {} + @interface ChangeInfoFlag {} @NonNull final WindowContainer mContainer; /** @@ -3467,7 +3491,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { @ActivityInfo.Config int mKnownConfigChanges; /** Extra information about this change. */ - @Flag int mFlags = FLAG_NONE; + @ChangeInfoFlag int mFlags = FLAG_NONE; /** Snapshot surface and luma, if relevant. */ SurfaceControl mSnapshot; @@ -3502,14 +3526,20 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { @Override public String toString() { - return mContainer.toString(); + StringBuilder sb = new StringBuilder(64); + sb.append("ChangeInfo{"); + sb.append(Integer.toHexString(System.identityHashCode(this))); + sb.append(" container=").append(mContainer); + sb.append(" flags=0x").append(Integer.toHexString(mFlags)); + sb.append('}'); + return sb.toString(); } boolean hasChanged() { final boolean currVisible = mContainer.isVisibleRequested(); // the task including transient launch must promote to root task if (currVisible && ((mFlags & ChangeInfo.FLAG_TRANSIENT_LAUNCH) != 0 - || (mFlags & ChangeInfo.FLAG_ABOVE_TRANSIENT_LAUNCH) != 0) + || (mFlags & ChangeInfo.FLAG_TRANSIENT_HIDE) != 0) || (mFlags & ChangeInfo.FLAG_BACK_GESTURE_ANIMATION) != 0 || (mFlags & ChangeInfo.FLAG_BELOW_BACK_GESTURE_ANIMATION) != 0) { return true; @@ -3530,7 +3560,7 @@ class Transition implements BLASTSyncEngine.TransactionReadyListener { @TransitionInfo.TransitionMode int getTransitMode(@NonNull WindowContainer wc) { - if ((mFlags & ChangeInfo.FLAG_ABOVE_TRANSIENT_LAUNCH) != 0) { + if ((mFlags & ChangeInfo.FLAG_TRANSIENT_HIDE) != 0) { return mExistenceChanged ? TRANSIT_CLOSE : TRANSIT_TO_BACK; } if ((mFlags & ChangeInfo.FLAG_BELOW_BACK_GESTURE_ANIMATION) != 0) { diff --git a/services/core/java/com/android/server/wm/TransparentPolicy.java b/services/core/java/com/android/server/wm/TransparentPolicy.java index 85a118db36eb..edd99243c3ef 100644 --- a/services/core/java/com/android/server/wm/TransparentPolicy.java +++ b/services/core/java/com/android/server/wm/TransparentPolicy.java @@ -196,10 +196,11 @@ class TransparentPolicy { // We evaluate the case when the policy should not be applied. private boolean shouldSkipTransparentPolicy(@Nullable ActivityRecord opaqueActivity) { - if (opaqueActivity == null || opaqueActivity.isEmbedded()) { + if (opaqueActivity == null || opaqueActivity.isEmbedded() + || !opaqueActivity.areBoundsLetterboxed()) { // We skip letterboxing if the translucent activity doesn't have any // opaque activities beneath or the activity below is embedded which - // never has letterbox. + // never has letterbox or the activity is not letterboxed at all. return true; } final AppCompatSizeCompatModePolicy scmPolicy = mActivityRecord.mAppCompatController diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index 6c92ae6bb3e7..e0c473de0f33 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -1117,7 +1117,9 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< */ void onDisplayChanged(DisplayContent dc) { if (mDisplayContent != null && mDisplayContent != dc) { - mTransitionController.collect(this); + if (asWindowState() == null) { + mTransitionController.collect(this); + } // Cancel any change transition queued-up for this container on the old display when // this container is moved from the old display. mDisplayContent.mClosingChangingContainers.remove(this); @@ -2119,7 +2121,8 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< } /** - * For all tasks at or below this container call the callback. + * Calls the given {@param callback} for all tasks in depth-first top-down z-order at or below + * this container. * * @param callback Calls the {@link ToBooleanFunction#apply} method for each task found and * stops the search if {@link ToBooleanFunction#apply} returns {@code true}. diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java index 82d39a39d188..ce032b4f7f9a 100644 --- a/services/core/java/com/android/server/wm/WindowManagerInternal.java +++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java @@ -859,6 +859,18 @@ public abstract class WindowManagerInternal { public abstract boolean isHomeSupportedOnDisplay(int displayId); /** + * Sets whether the relevant display content ignores fixed orientation, aspect ratio + * and resizability of apps. + * + * @param displayUniqueId The unique ID of the display. Note that the display may not yet be + * created, but whenever it is, this property will be applied. + * @param displayType The type of the display, e.g. {@link Display#TYPE_VIRTUAL}. + * @param enabled Whether app is universal resizable on this display. + */ + public abstract void setIgnoreActivitySizeRestrictionsOnDisplay( + @NonNull String displayUniqueId, int displayType, boolean enabled); + + /** * Removes any settings relevant to the given display. * * <p>This may be used when a property is set for a display unique ID before the display diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 97bf587cd10f..88b2d229e083 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -8384,6 +8384,20 @@ public class WindowManagerService extends IWindowManager.Stub } @Override + public void setIgnoreActivitySizeRestrictionsOnDisplay(@NonNull String displayUniqueId, + int displayType, boolean enabled) { + final long origId = Binder.clearCallingIdentity(); + try { + synchronized (mGlobalLock) { + mDisplayWindowSettings.setIgnoreActivitySizeRestrictionsOnDisplayLocked( + displayUniqueId, displayType, enabled); + } + } finally { + Binder.restoreCallingIdentity(origId); + } + } + + @Override public void clearDisplaySettings(String displayUniqueId, int displayType) { final long origId = Binder.clearCallingIdentity(); try { diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java index 585e61795e7d..d01e29b2fd5e 100644 --- a/services/core/java/com/android/server/wm/WindowState.java +++ b/services/core/java/com/android/server/wm/WindowState.java @@ -258,6 +258,7 @@ import com.android.server.policy.WindowManagerPolicy; import com.android.server.wm.LocalAnimationAdapter.AnimationSpec; import com.android.server.wm.RefreshRatePolicy.FrameRateVote; import com.android.server.wm.SurfaceAnimator.AnimationType; +import com.android.server.wm.utils.RegionUtils; import com.android.window.flags.Flags; import dalvik.annotation.optimization.NeverCompile; @@ -2327,7 +2328,8 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP if (mStartingData != null && mStartingData.mAssociatedTask == null && mTempConfiguration.windowConfiguration.getRotation() == selfConfiguration.windowConfiguration.getRotation() - && !mTempConfiguration.windowConfiguration.getBounds().equals(getBounds())) { + && !RegionUtils.sizeEquals( + mTempConfiguration.windowConfiguration.getBounds(), getBounds())) { mStartingData.mResizedFromTransfer = true; // Lock the starting window to task, so it won't resize from transfer anymore. mActivityRecord.associateStartingWindowWithTaskIfNeeded(); diff --git a/services/core/java/com/android/server/wm/utils/RegionUtils.java b/services/core/java/com/android/server/wm/utils/RegionUtils.java index ff23145fc6b9..6c5da175644b 100644 --- a/services/core/java/com/android/server/wm/utils/RegionUtils.java +++ b/services/core/java/com/android/server/wm/utils/RegionUtils.java @@ -92,4 +92,9 @@ public class RegionUtils { } return area; } + + /** Returns whether the sizes between the two Rects are equal. */ + public static boolean sizeEquals(Rect a, Rect b) { + return a.width() == b.width() && a.height() == b.height(); + } } diff --git a/services/core/jni/com_android_server_vibrator_VibratorController.cpp b/services/core/jni/com_android_server_vibrator_VibratorController.cpp index 59dbf28b3d1c..0ecc0a8a9524 100644 --- a/services/core/jni/com_android_server_vibrator_VibratorController.cpp +++ b/services/core/jni/com_android_server_vibrator_VibratorController.cpp @@ -74,12 +74,10 @@ static struct { jfieldID duration; } sRampClassInfo; static struct { - jfieldID startAmplitude; - jfieldID endAmplitude; - jfieldID startFrequencyHz; - jfieldID endFrequencyHz; - jfieldID duration; -} sPwleClassInfo; + jfieldID amplitude; + jfieldID frequencyHz; + jfieldID timeMillis; +} sPwlePointClassInfo; static_assert(static_cast<uint8_t>(V1_0::EffectStrength::LIGHT) == static_cast<uint8_t>(Aidl::EffectStrength::LIGHT)); @@ -191,10 +189,11 @@ static Aidl::ActivePwle activePwleFromJavaPrimitive(JNIEnv* env, jobject ramp) { static Aidl::PwleV2Primitive pwleV2PrimitiveFromJavaPrimitive(JNIEnv* env, jobject pwleObj) { Aidl::PwleV2Primitive pwle; - pwle.amplitude = static_cast<float>(env->GetFloatField(pwleObj, sPwleClassInfo.endAmplitude)); + pwle.amplitude = static_cast<float>(env->GetFloatField(pwleObj, sPwlePointClassInfo.amplitude)); pwle.frequencyHz = - static_cast<float>(env->GetFloatField(pwleObj, sPwleClassInfo.endFrequencyHz)); - pwle.timeMillis = static_cast<int32_t>(env->GetIntField(pwleObj, sPwleClassInfo.duration)); + static_cast<float>(env->GetFloatField(pwleObj, sPwlePointClassInfo.frequencyHz)); + pwle.timeMillis = + static_cast<int32_t>(env->GetIntField(pwleObj, sPwlePointClassInfo.timeMillis)); return pwle; } @@ -620,7 +619,7 @@ static const JNINativeMethod method_table[] = { (void*)vibratorPerformComposedEffect}, {"performPwleEffect", "(J[Landroid/os/vibrator/RampSegment;IJ)J", (void*)vibratorPerformPwleEffect}, - {"performPwleV2Effect", "(J[Landroid/os/vibrator/PwleSegment;J)J", + {"performPwleV2Effect", "(J[Landroid/os/vibrator/PwlePoint;J)J", (void*)vibratorPerformPwleV2Effect}, {"setExternalControl", "(JZ)V", (void*)vibratorSetExternalControl}, {"alwaysOnEnable", "(JJJJ)V", (void*)vibratorAlwaysOnEnable}, @@ -647,12 +646,10 @@ int register_android_server_vibrator_VibratorController(JavaVM* jvm, JNIEnv* env sRampClassInfo.endFrequencyHz = GetFieldIDOrDie(env, rampClass, "mEndFrequencyHz", "F"); sRampClassInfo.duration = GetFieldIDOrDie(env, rampClass, "mDuration", "I"); - jclass pwleClass = FindClassOrDie(env, "android/os/vibrator/PwleSegment"); - sPwleClassInfo.startAmplitude = GetFieldIDOrDie(env, pwleClass, "mStartAmplitude", "F"); - sPwleClassInfo.endAmplitude = GetFieldIDOrDie(env, pwleClass, "mEndAmplitude", "F"); - sPwleClassInfo.startFrequencyHz = GetFieldIDOrDie(env, pwleClass, "mStartFrequencyHz", "F"); - sPwleClassInfo.endFrequencyHz = GetFieldIDOrDie(env, pwleClass, "mEndFrequencyHz", "F"); - sPwleClassInfo.duration = GetFieldIDOrDie(env, pwleClass, "mDuration", "I"); + jclass pwlePointClass = FindClassOrDie(env, "android/os/vibrator/PwlePoint"); + sPwlePointClassInfo.amplitude = GetFieldIDOrDie(env, pwlePointClass, "mAmplitude", "F"); + sPwlePointClassInfo.frequencyHz = GetFieldIDOrDie(env, pwlePointClass, "mFrequencyHz", "F"); + sPwlePointClassInfo.timeMillis = GetFieldIDOrDie(env, pwlePointClass, "mTimeMillis", "I"); jclass frequencyProfileLegacyClass = FindClassOrDie(env, "android/os/VibratorInfo$FrequencyProfileLegacy"); diff --git a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionUpgrade.kt b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionUpgrade.kt index 15c9b9f7a13d..a4546aebef21 100644 --- a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionUpgrade.kt +++ b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionUpgrade.kt @@ -297,6 +297,7 @@ class AppIdPermissionUpgrade(private val policy: AppIdPermissionPolicy) { private const val MASK_ANY_FIXED = PermissionFlags.USER_SET or + PermissionFlags.ONE_TIME or PermissionFlags.USER_FIXED or PermissionFlags.POLICY_FIXED or PermissionFlags.SYSTEM_FIXED diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt index d4b57f191ecd..69714f3ecb5b 100644 --- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt +++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/AndroidPackageTest.kt @@ -26,6 +26,8 @@ import android.content.pm.SigningDetails import android.net.Uri import android.os.Bundle import android.os.Parcelable +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule import android.util.ArraySet import android.util.SparseArray import android.util.SparseIntArray @@ -47,14 +49,19 @@ import com.android.internal.pm.pkg.component.ParsedUsesPermissionImpl import com.android.server.pm.pkg.AndroidPackage import com.android.server.testutils.mockThrowOnUnmocked import com.android.server.testutils.whenever +import org.junit.Rule import java.security.KeyPairGenerator import java.security.PublicKey import java.util.UUID import kotlin.contracts.ExperimentalContracts @ExperimentalContracts +@EnableFlags(android.content.pm.Flags.FLAG_INCLUDE_FEATURE_FLAGS_IN_PACKAGE_CACHER) class AndroidPackageTest : ParcelableComponentTest(AndroidPackage::class, PackageImpl::class) { + @get:Rule + val setFlagsRule: SetFlagsRule = SetFlagsRule() + companion object { private val TEST_UUID = UUID.fromString("57554103-df3e-4475-ae7a-8feba49353ac") } @@ -93,6 +100,8 @@ class AndroidPackageTest : ParcelableComponentTest(AndroidPackage::class, Packag "getUsesOptionalLibrariesSorted", "getUsesSdkLibrariesSorted", "getUsesStaticLibrariesSorted", + "readFeatureFlagState", + "writeFeatureFlagState", // Tested through setting minor/major manually "setLongVersionCode", "getLongVersionCode", @@ -149,6 +158,10 @@ class AndroidPackageTest : ParcelableComponentTest(AndroidPackage::class, Packag "isSystem", "isSystemExt", "isVendor", + + // Tested through addFeatureFlag + "addFeatureFlag", + "getFeatureFlagState", ) override val baseParams = listOf( @@ -275,6 +288,7 @@ class AndroidPackageTest : ParcelableComponentTest(AndroidPackage::class, Packag AndroidPackage::isUpdatableSystem, AndroidPackage::getEmergencyInstaller, AndroidPackage::isAllowCrossUidActivitySwitchFromBelow, + AndroidPackage::getIntentMatchingFlags, ) override fun extraParams() = listOf( @@ -613,6 +627,9 @@ class AndroidPackageTest : ParcelableComponentTest(AndroidPackage::class, Packag .setSplitClassLoaderName(1, "testSplitClassLoaderNameOne") .addUsesSdkLibrary("testSdk", 2L, arrayOf("testCertDigest1"), true) .addUsesStaticLibrary("testStatic", 3L, arrayOf("testCertDigest2")) + .addFeatureFlag("testFlag1", null) + .addFeatureFlag("testFlag2", true) + .addFeatureFlag("testFlag3", false) override fun finalizeObject(parcelable: Parcelable) { (parcelable as PackageImpl).hideAsParsed().hideAsFinal() @@ -673,6 +690,12 @@ class AndroidPackageTest : ParcelableComponentTest(AndroidPackage::class, Packag .containsExactly("testCertDigest2") expect.that(after.storageUuid).isEqualTo(TEST_UUID) + + expect.that(after.featureFlagState).containsExactlyEntriesIn(mapOf( + "testFlag1" to null, + "testFlag2" to true, + "testFlag3" to false, + )) } private fun testKey() = KeyPairGenerator.getInstance("RSA") diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/ParsedActivityTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/ParsedActivityTest.kt index 349b83167793..8782b1ad1f39 100644 --- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/ParsedActivityTest.kt +++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/ParsedActivityTest.kt @@ -55,7 +55,8 @@ class ParsedActivityTest : ParsedMainComponentTest( ParsedActivity::getUiOptions, ParsedActivity::isSupportsSizeChanges, ParsedActivity::getRequiredDisplayCategory, - ParsedActivity::getRequireContentUriPermissionFromCaller + ParsedActivity::getRequireContentUriPermissionFromCaller, + ParsedActivity::getIntentMatchingFlags, ) override fun mainComponentSubclassExtraParams() = listOf( diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/ParsedProviderTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/ParsedProviderTest.kt index 1e844705fe3c..3e349795c60a 100644 --- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/ParsedProviderTest.kt +++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/ParsedProviderTest.kt @@ -38,6 +38,7 @@ class ParsedProviderTest : ParsedMainComponentTest(ParsedProvider::class, Parsed ParsedProvider::isForceUriPermissions, ParsedProvider::isMultiProcess, ParsedProvider::getInitOrder, + ParsedProvider::getIntentMatchingFlags, ) override fun mainComponentSubclassExtraParams() = listOf( diff --git a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/ParsedServiceTest.kt b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/ParsedServiceTest.kt index 79d5a4f7030a..694db47140c7 100644 --- a/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/ParsedServiceTest.kt +++ b/services/tests/PackageManagerServiceTests/unit/src/com/android/server/pm/test/parsing/parcelling/ParsedServiceTest.kt @@ -30,5 +30,6 @@ class ParsedServiceTest : ParsedMainComponentTest(ParsedService::class, ParsedSe override val mainComponentSubclassBaseParams = listOf( ParsedService::getForegroundServiceType, ParsedService::getPermission, + ParsedService::getIntentMatchingFlags, ) } diff --git a/services/tests/displayservicetests/src/com/android/server/display/state/DisplayStateControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/state/DisplayStateControllerTest.java index de53266dc14e..fc4cc25243c1 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/state/DisplayStateControllerTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/state/DisplayStateControllerTest.java @@ -63,11 +63,12 @@ public final class DisplayStateControllerTest { DisplayManagerInternal.DisplayPowerRequest.class); displayPowerRequest.policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_OFF; + displayPowerRequest.policyReason = Display.STATE_REASON_KEY; Pair<Integer, Integer> stateAndReason = mDisplayStateController.updateDisplayState( displayPowerRequest, DISPLAY_ENABLED, !DISPLAY_IN_TRANSITION); assertTrue(Display.STATE_OFF == stateAndReason.first); - assertTrue(Display.STATE_REASON_DEFAULT_POLICY == stateAndReason.second); + assertTrue(Display.STATE_REASON_KEY == stateAndReason.second); verify(mDisplayPowerProximityStateController).updateProximityState(displayPowerRequest, Display.STATE_OFF); assertEquals(true, mDisplayStateController.shouldPerformScreenOffTransition()); @@ -105,11 +106,12 @@ public final class DisplayStateControllerTest { DisplayManagerInternal.DisplayPowerRequest.class); displayPowerRequest.policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_BRIGHT; + displayPowerRequest.policyReason = Display.STATE_REASON_KEY; Pair<Integer, Integer> stateAndReason = mDisplayStateController.updateDisplayState( displayPowerRequest, !DISPLAY_ENABLED, !DISPLAY_IN_TRANSITION); assertTrue(Display.STATE_OFF == stateAndReason.first); - assertTrue(Display.STATE_REASON_DEFAULT_POLICY == stateAndReason.second); + assertTrue(Display.STATE_REASON_KEY == stateAndReason.second); verify(mDisplayPowerProximityStateController).updateProximityState(displayPowerRequest, Display.STATE_ON); assertEquals(false, mDisplayStateController.shouldPerformScreenOffTransition()); @@ -123,11 +125,12 @@ public final class DisplayStateControllerTest { DisplayManagerInternal.DisplayPowerRequest.class); displayPowerRequest.policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_BRIGHT; + displayPowerRequest.policyReason = Display.STATE_REASON_MOTION; Pair<Integer, Integer> stateAndReason = mDisplayStateController.updateDisplayState( displayPowerRequest, DISPLAY_ENABLED, DISPLAY_IN_TRANSITION); assertTrue(Display.STATE_OFF == stateAndReason.first); - assertTrue(Display.STATE_REASON_DEFAULT_POLICY == stateAndReason.second); + assertTrue(Display.STATE_REASON_MOTION == stateAndReason.second); verify(mDisplayPowerProximityStateController).updateProximityState(displayPowerRequest, Display.STATE_ON); assertEquals(false, mDisplayStateController.shouldPerformScreenOffTransition()); @@ -141,6 +144,7 @@ public final class DisplayStateControllerTest { DisplayManagerInternal.DisplayPowerRequest.class); displayPowerRequest.policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_BRIGHT; + displayPowerRequest.policyReason = Display.STATE_REASON_DEFAULT_POLICY; Pair<Integer, Integer> stateAndReason = mDisplayStateController.updateDisplayState( displayPowerRequest, DISPLAY_ENABLED, !DISPLAY_IN_TRANSITION); @@ -156,6 +160,7 @@ public final class DisplayStateControllerTest { DisplayManagerInternal.DisplayPowerRequest displayPowerRequest = new DisplayManagerInternal.DisplayPowerRequest(); displayPowerRequest.policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_DOZE; + displayPowerRequest.policyReason = Display.STATE_REASON_DRAW_WAKE_LOCK; mDisplayStateController.overrideDozeScreenState( Display.STATE_DOZE_SUSPEND, Display.STATE_REASON_OFFLOAD); @@ -172,8 +177,9 @@ public final class DisplayStateControllerTest { DisplayManagerInternal.DisplayPowerRequest displayPowerRequest = new DisplayManagerInternal.DisplayPowerRequest(); displayPowerRequest.policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_OFF; + displayPowerRequest.policyReason = Display.STATE_REASON_DEFAULT_POLICY; mDisplayStateController.overrideDozeScreenState( - Display.STATE_DOZE_SUSPEND, Display.STATE_REASON_DEFAULT_POLICY); + Display.STATE_DOZE_SUSPEND, Display.STATE_REASON_DRAW_WAKE_LOCK); Pair<Integer, Integer> stateAndReason = mDisplayStateController.updateDisplayState( @@ -183,6 +189,53 @@ public final class DisplayStateControllerTest { assertTrue(Display.STATE_REASON_DEFAULT_POLICY == stateAndReason.second); } + @Test + public void policyOff_usespolicyReasonFromRequest() { + DisplayManagerInternal.DisplayPowerRequest displayPowerRequest = + new DisplayManagerInternal.DisplayPowerRequest(); + displayPowerRequest.policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_OFF; + displayPowerRequest.policyReason = Display.STATE_REASON_KEY; + + Pair<Integer, Integer> stateAndReason = + mDisplayStateController.updateDisplayState( + displayPowerRequest, DISPLAY_ENABLED, !DISPLAY_IN_TRANSITION); + + assertTrue(Display.STATE_OFF == stateAndReason.first); + assertTrue(Display.STATE_REASON_KEY == stateAndReason.second); + } + + @Test + public void policyBright_usespolicyReasonFromRequest() { + DisplayManagerInternal.DisplayPowerRequest displayPowerRequest = + new DisplayManagerInternal.DisplayPowerRequest(); + displayPowerRequest.policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_BRIGHT; + displayPowerRequest.policyReason = Display.STATE_REASON_DREAM_MANAGER; + + Pair<Integer, Integer> stateAndReason = + mDisplayStateController.updateDisplayState( + displayPowerRequest, DISPLAY_ENABLED, !DISPLAY_IN_TRANSITION); + + assertTrue(Display.STATE_ON == stateAndReason.first); + assertTrue(Display.STATE_REASON_DREAM_MANAGER == stateAndReason.second); + } + + @Test + public void policyRequestHasDozeScreenState_usesPolicyDozeScreenStateReason() { + DisplayManagerInternal.DisplayPowerRequest displayPowerRequest = + new DisplayManagerInternal.DisplayPowerRequest(); + displayPowerRequest.policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_DOZE; + displayPowerRequest.policyReason = Display.STATE_REASON_MOTION; + displayPowerRequest.dozeScreenState = Display.STATE_ON; + displayPowerRequest.dozeScreenStateReason = Display.STATE_REASON_OFFLOAD; + + Pair<Integer, Integer> stateAndReason = + mDisplayStateController.updateDisplayState( + displayPowerRequest, DISPLAY_ENABLED, !DISPLAY_IN_TRANSITION); + + assertTrue(Display.STATE_ON == stateAndReason.first); + assertTrue(Display.STATE_REASON_OFFLOAD == stateAndReason.second); + } + private void validDisplayState(int policy, int displayState, boolean isEnabled, boolean isInTransition) { DisplayManagerInternal.DisplayPowerRequest displayPowerRequest = mock( diff --git a/services/tests/mockingservicestests/src/com/android/server/am/OWNERS b/services/tests/mockingservicestests/src/com/android/server/am/OWNERS index 4fac647c4ceb..809b7bbfd3a0 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/OWNERS +++ b/services/tests/mockingservicestests/src/com/android/server/am/OWNERS @@ -2,3 +2,4 @@ include /services/core/java/com/android/server/am/OWNERS per-file ApplicationStartInfoTest.java = yforta@google.com, carmenjackson@google.com, jji@google.com per-file CachedAppOptimizerTest.java = file:/PERFORMANCE_OWNERS +per-file BaseBroadcastQueueTest.java = file:/BROADCASTS_OWNERS diff --git a/services/tests/mockingservicestests/src/com/android/server/job/JobServiceContextTest.java b/services/tests/mockingservicestests/src/com/android/server/job/JobServiceContextTest.java new file mode 100644 index 000000000000..8c66fd0e684a --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/job/JobServiceContextTest.java @@ -0,0 +1,270 @@ +/* + * 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.job; + +import static android.app.job.Flags.FLAG_HANDLE_ABANDONED_JOBS; +import static android.text.format.DateUtils.MINUTE_IN_MILLIS; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; +import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import android.app.AppGlobals; +import android.app.job.JobParameters; +import android.content.Context; +import android.os.Looper; +import android.os.PowerManager; +import android.os.SystemClock; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; + +import com.android.internal.app.IBatteryStats; +import com.android.server.job.JobServiceContext.JobCallback; +import com.android.server.job.controllers.JobStatus; + +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoSession; +import org.mockito.quality.Strictness; + +import java.time.Clock; +import java.time.Duration; +import java.time.ZoneOffset; + +public class JobServiceContextTest { + private static final String TAG = JobServiceContextTest.class.getSimpleName(); + @ClassRule + public static final SetFlagsRule.ClassRule mSetFlagsClassRule = new SetFlagsRule.ClassRule(); + @Rule + public final SetFlagsRule mSetFlagsRule = mSetFlagsClassRule.createSetFlagsRule(); + @Mock + private JobSchedulerService mMockJobSchedulerService; + @Mock + private JobConcurrencyManager mMockConcurrencyManager; + @Mock + private JobNotificationCoordinator mMockNotificationCoordinator; + @Mock + private IBatteryStats.Stub mMockBatteryStats; + @Mock + private JobPackageTracker mMockJobPackageTracker; + @Mock + private Looper mMockLooper; + @Mock + private Context mMockContext; + @Mock + private JobStatus mMockJobStatus; + @Mock + private JobParameters mMockJobParameters; + @Mock + private JobCallback mMockJobCallback; + private MockitoSession mMockingSession; + private JobServiceContext mJobServiceContext; + private Object mLock; + + @Before + public void setUp() throws Exception { + mMockingSession = + mockitoSession() + .initMocks(this) + .mockStatic(AppGlobals.class) + .strictness(Strictness.LENIENT) + .startMocking(); + JobSchedulerService.sElapsedRealtimeClock = + Clock.fixed(SystemClock.elapsedRealtimeClock().instant(), ZoneOffset.UTC); + doReturn(mock(PowerManager.class)).when(mMockContext).getSystemService(PowerManager.class); + doReturn(mMockContext).when(mMockJobSchedulerService).getContext(); + mLock = new Object(); + doReturn(mLock).when(mMockJobSchedulerService).getLock(); + mJobServiceContext = + new JobServiceContext( + mMockJobSchedulerService, + mMockConcurrencyManager, + mMockNotificationCoordinator, + mMockBatteryStats, + mMockJobPackageTracker, + mMockLooper); + spyOn(mJobServiceContext); + mJobServiceContext.setJobParamsLockedForTest(mMockJobParameters); + } + + @After + public void tearDown() throws Exception { + if (mMockingSession != null) { + mMockingSession.finishMocking(); + } + } + + private Clock getAdvancedClock(Clock clock, long incrementMs) { + return Clock.offset(clock, Duration.ofMillis(incrementMs)); + } + + private void advanceElapsedClock(long incrementMs) { + JobSchedulerService.sElapsedRealtimeClock = + getAdvancedClock(JobSchedulerService.sElapsedRealtimeClock, incrementMs); + } + + /** + * Test that Abandoned jobs that are timed out are stopped with the correct stop reason + */ + @Test + @EnableFlags(FLAG_HANDLE_ABANDONED_JOBS) + public void testJobServiceContext_TimeoutAbandonedJob() { + mJobServiceContext.mVerb = JobServiceContext.VERB_EXECUTING; + ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class); + doNothing().when(mJobServiceContext).sendStopMessageLocked(captor.capture()); + + advanceElapsedClock(30 * MINUTE_IN_MILLIS); // 30 minutes + mJobServiceContext.setPendingStopReasonLockedForTest(JobParameters.STOP_REASON_UNDEFINED); + + mJobServiceContext.setRunningJobLockedForTest(mMockJobStatus); + doReturn(true).when(mMockJobStatus).isAbandoned(); + mJobServiceContext.mVerb = JobServiceContext.VERB_EXECUTING; + + mJobServiceContext.handleOpTimeoutLocked(); + + String stopMessage = captor.getValue(); + assertEquals("timeout while executing and maybe abandoned", stopMessage); + verify(mMockJobParameters) + .setStopReason( + JobParameters.STOP_REASON_TIMEOUT_ABANDONED, + JobParameters.INTERNAL_STOP_REASON_TIMEOUT_ABANDONED, + "client timed out and maybe abandoned"); + } + + /** + * Test that non-abandoned jobs that are timed out are stopped with the correct stop reason + */ + @Test + @EnableFlags(FLAG_HANDLE_ABANDONED_JOBS) + public void testJobServiceContext_TimeoutNoAbandonedJob() { + mJobServiceContext.mVerb = JobServiceContext.VERB_EXECUTING; + ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class); + doNothing().when(mJobServiceContext).sendStopMessageLocked(captor.capture()); + + advanceElapsedClock(30 * MINUTE_IN_MILLIS); // 30 minutes + mJobServiceContext.setPendingStopReasonLockedForTest(JobParameters.STOP_REASON_UNDEFINED); + + mJobServiceContext.setRunningJobLockedForTest(mMockJobStatus); + doReturn(false).when(mMockJobStatus).isAbandoned(); + mJobServiceContext.mVerb = JobServiceContext.VERB_EXECUTING; + + mJobServiceContext.handleOpTimeoutLocked(); + + String stopMessage = captor.getValue(); + assertEquals("timeout while executing", stopMessage); + verify(mMockJobParameters) + .setStopReason( + JobParameters.STOP_REASON_TIMEOUT, + JobParameters.INTERNAL_STOP_REASON_TIMEOUT, + "client timed out"); + } + + /** + * Test that abandoned jobs that are timed out while the flag is disabled + * are stopped with the correct stop reason + */ + @Test + @DisableFlags(FLAG_HANDLE_ABANDONED_JOBS) + public void testJobServiceContext_TimeoutAbandonedJob_flagHandleAbandonedJobsDisabled() { + mJobServiceContext.mVerb = JobServiceContext.VERB_EXECUTING; + ArgumentCaptor<String> captor = ArgumentCaptor.forClass(String.class); + doNothing().when(mJobServiceContext).sendStopMessageLocked(captor.capture()); + + advanceElapsedClock(30 * MINUTE_IN_MILLIS); // 30 minutes + mJobServiceContext.setPendingStopReasonLockedForTest(JobParameters.STOP_REASON_UNDEFINED); + + mJobServiceContext.setRunningJobLockedForTest(mMockJobStatus); + doReturn(true).when(mMockJobStatus).isAbandoned(); + mJobServiceContext.mVerb = JobServiceContext.VERB_EXECUTING; + + mJobServiceContext.handleOpTimeoutLocked(); + + String stopMessage = captor.getValue(); + assertEquals("timeout while executing", stopMessage); + verify(mMockJobParameters) + .setStopReason( + JobParameters.STOP_REASON_TIMEOUT, + JobParameters.INTERNAL_STOP_REASON_TIMEOUT, + "client timed out"); + } + + /** + * Test that the JobStatus is marked as abandoned when the JobServiceContext + * receives a MSG_HANDLE_ABANDONED_JOB message + */ + @Test + @EnableFlags(FLAG_HANDLE_ABANDONED_JOBS) + public void testJobServiceContext_HandleAbandonedJob() { + final int jobId = 123; + mJobServiceContext.setRunningJobLockedForTest(mMockJobStatus); + mJobServiceContext.setRunningCallbackLockedForTest(mMockJobCallback); + doReturn(jobId).when(mMockJobStatus).getJobId(); + + mJobServiceContext.doHandleAbandonedJob(mMockJobCallback, jobId); + + verify(mMockJobStatus).setAbandoned(true); + } + + /** + * Test that the JobStatus is not marked as abandoned when the + * JobServiceContext receives a MSG_HANDLE_ABANDONED_JOB message and the + * JobServiceContext is not running a job + */ + @Test + @EnableFlags(FLAG_HANDLE_ABANDONED_JOBS) + public void testJobServiceContext_HandleAbandonedJob_notRunningJob() { + final int jobId = 123; + mJobServiceContext.setRunningJobLockedForTest(null); + mJobServiceContext.setRunningCallbackLockedForTest(mMockJobCallback); + + mJobServiceContext.doHandleAbandonedJob(mMockJobCallback, jobId); + + verify(mMockJobStatus, never()).setAbandoned(true); + } + + /** + * Test that the JobStatus is not marked as abandoned when the + * JobServiceContext receives a MSG_HANDLE_ABANDONED_JOB message and the + * JobServiceContext is running a job with a different jobId + */ + @Test + @EnableFlags(FLAG_HANDLE_ABANDONED_JOBS) + public void testJobServiceContext_HandleAbandonedJob_differentJobId() { + final int jobId = 123; + final int differentJobId = 456; + mJobServiceContext.setRunningJobLockedForTest(mMockJobStatus); + mJobServiceContext.setRunningCallbackLockedForTest(mMockJobCallback); + doReturn(differentJobId).when(mMockJobStatus).getJobId(); + + mJobServiceContext.doHandleAbandonedJob(mMockJobCallback, jobId); + + verify(mMockJobStatus, never()).setAbandoned(true); + } + +} diff --git a/services/tests/mockingservicestests/src/com/android/server/job/restrictions/ThermalStatusRestrictionTest.java b/services/tests/mockingservicestests/src/com/android/server/job/restrictions/ThermalStatusRestrictionTest.java index c2c67e615228..e04aeecd8b5d 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/restrictions/ThermalStatusRestrictionTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/restrictions/ThermalStatusRestrictionTest.java @@ -44,10 +44,13 @@ import android.app.job.JobInfo; import android.content.ComponentName; import android.content.Context; import android.os.PowerManager; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; 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 android.platform.test.flag.junit.SetFlagsRule; import android.provider.DeviceConfig; import android.util.DebugUtils; @@ -74,6 +77,8 @@ public class ThermalStatusRestrictionTest { private static final String SOURCE_PACKAGE = "com.android.frameworks.mockingservicestests"; private static final int SOURCE_USER_ID = 0; + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + private ThermalStatusRestriction mThermalStatusRestriction; private PowerManager.OnThermalStatusChangedListener mStatusChangedListener; @@ -427,6 +432,7 @@ public class ThermalStatusRestrictionTest { */ @Test @RequiresFlagsEnabled(FLAG_THERMAL_RESTRICTIONS_TO_FGS_JOBS) + @DisableFlags(android.app.job.Flags.FLAG_IGNORE_IMPORTANT_WHILE_FOREGROUND) public void testIsJobRestrictedBiasFgs_flagThermalRestrictionsToFgsJobsEnabled() { JobStatusContainer jc = new JobStatusContainer("testIsJobRestrictedBiasFgs", mJobSchedulerService); @@ -508,6 +514,91 @@ public class ThermalStatusRestrictionTest { } } + @Test + @RequiresFlagsEnabled(FLAG_THERMAL_RESTRICTIONS_TO_FGS_JOBS) + @EnableFlags(android.app.job.Flags.FLAG_IGNORE_IMPORTANT_WHILE_FOREGROUND) + public void testIsJobRestrictedBiasFgs_flagThermalRestrictionsToFgsJobsEnabled_ignoreIWF() { + JobStatusContainer jc = + new JobStatusContainer("testIsJobRestrictedBiasFgs", mJobSchedulerService); + int jobBias = JobInfo.BIAS_FOREGROUND_SERVICE; + for (int thermalStatus : jc.thermalStatuses) { + String msg = debugTag(jobBias, thermalStatus); + mStatusChangedListener.onThermalStatusChanged(thermalStatus); + if (thermalStatus >= THERMAL_STATUS_SEVERE) { + // Full restrictions on all jobs + assertTrue(msg, isJobRestricted(jc.jobMinPriority, jobBias)); + assertTrue(msg, isJobRestricted(jc.jobLowPriority, jobBias)); + assertTrue(msg, isJobRestricted(jc.jobLowPriorityRunning, jobBias)); + assertTrue(msg, isJobRestricted(jc.jobLowPriorityRunningLong, jobBias)); + assertTrue(msg, isJobRestricted(jc.jobDefaultPriority, jobBias)); + assertTrue(msg, isJobRestricted(jc.jobHighPriority, jobBias)); + assertTrue(msg, isJobRestricted(jc.jobHighPriorityRunning, jobBias)); + assertTrue(msg, isJobRestricted(jc.jobHighPriorityRunningLong, jobBias)); + assertTrue(msg, isJobRestricted(jc.ej, jobBias)); + assertTrue(msg, isJobRestricted(jc.ejDowngraded, jobBias)); + assertTrue(msg, isJobRestricted(jc.ejRetried, jobBias)); + assertTrue(msg, isJobRestricted(jc.ejRunning, jobBias)); + assertTrue(msg, isJobRestricted(jc.ejRunningLong, jobBias)); + assertTrue(msg, isJobRestricted(jc.ui, jobBias)); + assertTrue(msg, isJobRestricted(jc.uiRetried, jobBias)); + assertTrue(msg, isJobRestricted(jc.uiRunning, jobBias)); + assertTrue(msg, isJobRestricted(jc.uiRunningLong, jobBias)); + } else if (thermalStatus >= THERMAL_STATUS_MODERATE) { + // No restrictions on user related jobs + assertFalse(msg, isJobRestricted(jc.ui, jobBias)); + assertFalse(msg, isJobRestricted(jc.uiRetried, jobBias)); + assertFalse(msg, isJobRestricted(jc.uiRunning, jobBias)); + assertFalse(msg, isJobRestricted(jc.uiRunningLong, jobBias)); + // Some restrictions on expedited jobs + assertFalse(msg, isJobRestricted(jc.ej, jobBias)); + assertTrue(msg, isJobRestricted(jc.ejDowngraded, jobBias)); + assertTrue(msg, isJobRestricted(jc.ejRetried, jobBias)); + assertFalse(msg, isJobRestricted(jc.ejRunning, jobBias)); + assertTrue(msg, isJobRestricted(jc.ejRunningLong, jobBias)); + // Some restrictions on high priority jobs + assertTrue(msg, isJobRestricted(jc.jobHighPriority, jobBias)); + assertFalse(msg, isJobRestricted(jc.jobHighPriorityRunning, jobBias)); + assertTrue(msg, isJobRestricted(jc.jobHighPriorityRunningLong, jobBias)); + // Full restructions on important while foreground jobs as + // the important while foreground flag is ignored. + assertTrue(isJobRestricted(jc.importantWhileForeground, jobBias)); + assertTrue(isJobRestricted(jc.importantWhileForegroundRunning, jobBias)); + assertTrue(isJobRestricted(jc.importantWhileForegroundRunningLong, jobBias)); + // Full restriction on default priority jobs + assertTrue(msg, isJobRestricted(jc.jobDefaultPriority, jobBias)); + // Full restriction on low priority jobs + assertTrue(msg, isJobRestricted(jc.jobLowPriority, jobBias)); + assertTrue(msg, isJobRestricted(jc.jobLowPriorityRunning, jobBias)); + assertTrue(msg, isJobRestricted(jc.jobLowPriorityRunningLong, jobBias)); + // Full restriction on min priority jobs + assertTrue(msg, isJobRestricted(jc.jobMinPriority, jobBias)); + } else { + // thermalStatus < THERMAL_STATUS_MODERATE + // No restrictions on any job type + assertFalse(msg, isJobRestricted(jc.ui, jobBias)); + assertFalse(msg, isJobRestricted(jc.uiRetried, jobBias)); + assertFalse(msg, isJobRestricted(jc.uiRunning, jobBias)); + assertFalse(msg, isJobRestricted(jc.uiRunningLong, jobBias)); + assertFalse(msg, isJobRestricted(jc.ej, jobBias)); + assertFalse(msg, isJobRestricted(jc.ejDowngraded, jobBias)); + assertFalse(msg, isJobRestricted(jc.ejRetried, jobBias)); + assertFalse(msg, isJobRestricted(jc.ejRunning, jobBias)); + assertFalse(msg, isJobRestricted(jc.ejRunningLong, jobBias)); + assertFalse(msg, isJobRestricted(jc.jobHighPriority, jobBias)); + assertFalse(msg, isJobRestricted(jc.jobHighPriorityRunning, jobBias)); + assertFalse(msg, isJobRestricted(jc.jobHighPriorityRunningLong, jobBias)); + assertFalse(msg, isJobRestricted(jc.importantWhileForeground, jobBias)); + assertFalse(msg, isJobRestricted(jc.importantWhileForegroundRunning, jobBias)); + assertFalse(msg, isJobRestricted(jc.importantWhileForegroundRunningLong, jobBias)); + assertFalse(msg, isJobRestricted(jc.jobDefaultPriority, jobBias)); + assertFalse(msg, isJobRestricted(jc.jobLowPriority, jobBias)); + assertFalse(msg, isJobRestricted(jc.jobLowPriorityRunning, jobBias)); + assertFalse(msg, isJobRestricted(jc.jobLowPriorityRunningLong, jobBias)); + assertFalse(msg, isJobRestricted(jc.jobMinPriority, jobBias)); + } + } + } + /** * Test {@link JobSchedulerService#isJobRestricted(JobStatus)} when Job Bias is less than * Foreground Service and all Thermal states. diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundInstallControlCallbackHelperTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundInstallControlCallbackHelperTest.java index 574f3699edb8..ae404cff7171 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundInstallControlCallbackHelperTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundInstallControlCallbackHelperTest.java @@ -18,6 +18,7 @@ package com.android.server.pm; import static com.android.server.pm.BackgroundInstallControlCallbackHelper.FLAGGED_PACKAGE_NAME_KEY; import static com.android.server.pm.BackgroundInstallControlCallbackHelper.FLAGGED_USER_ID_KEY; +import static com.android.server.pm.BackgroundInstallControlCallbackHelper.INSTALL_EVENT_TYPE_KEY; import static org.junit.Assert.assertEquals; import static org.mockito.Mockito.after; @@ -84,12 +85,18 @@ public class BackgroundInstallControlCallbackHelperTest { int testUserId = 1; mCallbackHelper.registerBackgroundInstallCallback(mCallback); - mCallbackHelper.notifyAllCallbacks(testUserId, testPackageName); + mCallbackHelper.notifyAllCallbacks( + testUserId, + testPackageName, + BackgroundInstallControlService.INSTALL_EVENT_TYPE_INSTALL); ArgumentCaptor<Bundle> bundleCaptor = ArgumentCaptor.forClass(Bundle.class); verify(mCallback, after(1000).times(1)).sendResult(bundleCaptor.capture()); Bundle receivedBundle = bundleCaptor.getValue(); assertEquals(testPackageName, receivedBundle.getString(FLAGGED_PACKAGE_NAME_KEY)); assertEquals(testUserId, receivedBundle.getInt(FLAGGED_USER_ID_KEY)); + assertEquals( + BackgroundInstallControlService.INSTALL_EVENT_TYPE_INSTALL, + receivedBundle.getInt(INSTALL_EVENT_TYPE_KEY)); } } diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundUserSoundNotifierTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundUserSoundNotifierTest.java index bf946a1258fd..3d0c63780ef3 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundUserSoundNotifierTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/BackgroundUserSoundNotifierTest.java @@ -103,6 +103,7 @@ public class BackgroundUserSoundNotifierTest { assumeTrue(UserManager.supportsMultipleUsers()); AudioAttributes aa = new AudioAttributes.Builder().setUsage(USAGE_ALARM).build(); UserInfo user = createUser("User", UserManager.USER_TYPE_FULL_SECONDARY, 0); + final int fgUserId = mSpiedContext.getUserId(); final int bgUserUid = user.id * 100000; doReturn(UserHandle.of(fgUserId)).when(mSpiedContext).getUser(); @@ -209,6 +210,28 @@ public class BackgroundUserSoundNotifierTest { eq(UserHandle.of(fgUserId))); } + @Test + public void testOnAudioFocusGrant_alarmOnProfileOfForegroundUser_foregroundUserNotNotified() { + assumeTrue(UserManager.supportsMultipleUsers()); + final int fgUserId = mSpiedContext.getUserId(); + UserInfo fgUserProfile = createProfileForUser("Background profile", + UserManager.USER_TYPE_PROFILE_MANAGED, fgUserId, null); + assumeTrue("Cannot add a profile", fgUserProfile != null); + int fgUserProfileUid = fgUserProfile.id * 100_000; + + AudioAttributes aa = new AudioAttributes.Builder().setUsage(USAGE_ALARM).build(); + AudioFocusInfo afi = new AudioFocusInfo(aa, fgUserProfileUid, "", "", + AudioManager.AUDIOFOCUS_GAIN, 0, 0, Build.VERSION.SDK_INT); + + mBackgroundUserSoundNotifier.getAudioPolicyFocusListener() + .onAudioFocusGrant(afi, AudioManager.AUDIOFOCUS_REQUEST_GRANTED); + + verify(mNotificationManager, never()) + .notifyAsUser(eq(BackgroundUserSoundNotifier.class.getSimpleName()), + eq(afi.getClientUid()), any(Notification.class), + eq(UserHandle.of(fgUserId))); + } + @Test public void testCreateNotification_UserSwitcherEnabled_bothActionsAvailable() { @@ -327,6 +350,17 @@ public class BackgroundUserSoundNotifierTest { } return user; } + + private UserInfo createProfileForUser(String name, String userType, int userHandle, + String[] disallowedPackages) { + UserInfo profile = mUserManager.createProfileForUser( + name, userType, 0, userHandle, disallowedPackages); + if (profile != null) { + mUsersToRemove.add(profile.id); + } + return profile; + } + private void removeUser(int userId) { mUserManager.removeUser(userId); } diff --git a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperCropperTest.java b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperCropperTest.java index f1bf86f2f57c..b53f6fbee183 100644 --- a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperCropperTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperCropperTest.java @@ -16,11 +16,11 @@ package com.android.server.wallpaper; -import static android.app.WallpaperManager.LANDSCAPE; +import static android.app.WallpaperManager.ORIENTATION_LANDSCAPE; import static android.app.WallpaperManager.ORIENTATION_UNKNOWN; -import static android.app.WallpaperManager.PORTRAIT; -import static android.app.WallpaperManager.SQUARE_LANDSCAPE; -import static android.app.WallpaperManager.SQUARE_PORTRAIT; +import static android.app.WallpaperManager.ORIENTATION_PORTRAIT; +import static android.app.WallpaperManager.ORIENTATION_SQUARE_LANDSCAPE; +import static android.app.WallpaperManager.ORIENTATION_SQUARE_PORTRAIT; import static android.app.WallpaperManager.getOrientation; import static android.app.WallpaperManager.getRotatedOrientation; @@ -406,15 +406,16 @@ public class WallpaperCropperTest { setUpWithDisplays(STANDARD_DISPLAY); Point bitmapSize = new Point(800, 1000); SparseArray<Rect> suggestedCrops = new SparseArray<>(); - suggestedCrops.put(PORTRAIT, new Rect(0, 0, 400, 800)); - for (int otherOrientation: List.of(LANDSCAPE, SQUARE_LANDSCAPE, SQUARE_PORTRAIT)) { + suggestedCrops.put(ORIENTATION_PORTRAIT, new Rect(0, 0, 400, 800)); + for (int otherOrientation: List.of(ORIENTATION_LANDSCAPE, ORIENTATION_SQUARE_LANDSCAPE, + ORIENTATION_SQUARE_PORTRAIT)) { suggestedCrops.put(otherOrientation, new Rect(0, 0, 10, 10)); } for (boolean rtl : List.of(false, true)) { assertThat(mWallpaperCropper.getCrop( new Point(300, 800), bitmapSize, suggestedCrops, rtl)) - .isEqualTo(suggestedCrops.get(PORTRAIT)); + .isEqualTo(suggestedCrops.get(ORIENTATION_PORTRAIT)); assertThat(mWallpaperCropper.getCrop( new Point(500, 800), bitmapSize, suggestedCrops, rtl)) .isEqualTo(new Rect(0, 0, 500, 800)); @@ -440,8 +441,8 @@ public class WallpaperCropperTest { Point landscape = new Point(PORTRAIT_ONE.y, PORTRAIT_ONE.x); Point squarePortrait = SQUARE_PORTRAIT_ONE; Point squareLandscape = new Point(SQUARE_PORTRAIT_ONE.y, SQUARE_PORTRAIT_ONE.y); - suggestedCrops.put(PORTRAIT, centerOf(bitmapRect, portrait)); - suggestedCrops.put(SQUARE_LANDSCAPE, centerOf(bitmapRect, squareLandscape)); + suggestedCrops.put(ORIENTATION_PORTRAIT, centerOf(bitmapRect, portrait)); + suggestedCrops.put(ORIENTATION_SQUARE_LANDSCAPE, centerOf(bitmapRect, squareLandscape)); for (boolean rtl : List.of(false, true)) { assertThat(mWallpaperCropper.getCrop( landscape, bitmapSize, suggestedCrops, rtl)) diff --git a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java index 1ea36748eb5d..db323f1b68e7 100644 --- a/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java +++ b/services/tests/mockingservicestests/src/com/android/server/wallpaper/WallpaperManagerServiceTests.java @@ -46,6 +46,7 @@ import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; @@ -535,7 +536,8 @@ public class WallpaperManagerServiceTests { doReturn(wallpaper).when(mService).getWallpaperSafeLocked(wallpaper.userId, FLAG_SYSTEM); doNothing().when(mService).switchWallpaper(any(), any()); doReturn(true).when(mService) - .bindWallpaperComponentLocked(any(), anyBoolean(), anyBoolean(), any(), any()); + .bindWallpaperComponentLocked(isA(ComponentName.class), anyBoolean(), anyBoolean(), + any(), any()); doNothing().when(mService).saveSettingsLocked(wallpaper.userId); spyOn(mService.mWallpaperCropper); doNothing().when(mService.mWallpaperCropper).generateCrop(wallpaper); diff --git a/services/tests/powerservicetests/src/com/android/server/power/PowerGroupTest.java b/services/tests/powerservicetests/src/com/android/server/power/PowerGroupTest.java index 473c8c5ce8fc..648da6530eb0 100644 --- a/services/tests/powerservicetests/src/com/android/server/power/PowerGroupTest.java +++ b/services/tests/powerservicetests/src/com/android/server/power/PowerGroupTest.java @@ -27,10 +27,13 @@ import static android.os.PowerManager.GO_TO_SLEEP_REASON_DEVICE_FOLD; import static android.os.PowerManager.GO_TO_SLEEP_REASON_TIMEOUT; import static android.os.PowerManager.WAKE_REASON_GESTURE; import static android.os.PowerManager.WAKE_REASON_PLUGGED_IN; +import static android.os.PowerManager.WAKE_REASON_WAKE_MOTION; import static android.os.PowerManagerInternal.WAKEFULNESS_ASLEEP; import static android.os.PowerManagerInternal.WAKEFULNESS_AWAKE; import static android.os.PowerManagerInternal.WAKEFULNESS_DOZING; import static android.os.PowerManagerInternal.WAKEFULNESS_DREAMING; +import static android.view.Display.STATE_REASON_DEFAULT_POLICY; +import static android.view.Display.STATE_REASON_MOTION; import static com.android.server.power.PowerManagerService.USER_ACTIVITY_SCREEN_BRIGHT; import static com.android.server.power.PowerManagerService.WAKE_LOCK_DOZE; @@ -44,6 +47,7 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import android.hardware.display.DisplayManagerInternal; import android.os.PowerManager; @@ -53,6 +57,7 @@ import android.view.Display; import androidx.test.platform.app.InstrumentationRegistry; import com.android.internal.util.LatencyTracker; +import com.android.server.power.feature.PowerManagerFlags; import org.junit.Before; import org.junit.Test; @@ -79,18 +84,23 @@ public class PowerGroupTest { private static final float BRIGHTNESS = 0.99f; private static final float BRIGHTNESS_DOZE = 0.5f; + private static final LatencyTracker LATENCY_TRACKER = LatencyTracker.getInstance( + InstrumentationRegistry.getInstrumentation().getContext()); + private PowerGroup mPowerGroup; @Mock private PowerGroup.PowerGroupListener mWakefulnessCallbackMock; @Mock private Notifier mNotifier; @Mock private DisplayManagerInternal mDisplayManagerInternal; + @Mock private PowerManagerFlags mFeatureFlags; @Before public void setUp() { MockitoAnnotations.initMocks(this); + when(mFeatureFlags.isPolicyReasonInDisplayPowerRequestEnabled()).thenReturn(true); mPowerGroup = new PowerGroup(GROUP_ID, mWakefulnessCallbackMock, mNotifier, mDisplayManagerInternal, WAKEFULNESS_AWAKE, /* ready= */ true, - /* supportsSandman= */ true, TIMESTAMP_CREATE); + /* supportsSandman= */ true, TIMESTAMP_CREATE, mFeatureFlags); } @Test @@ -101,10 +111,8 @@ public class PowerGroupTest { eq(UID), /* opUid= */anyInt(), /* opPackageName= */ isNull(), /* details= */ isNull()); String details = "wake PowerGroup1"; - LatencyTracker latencyTracker = LatencyTracker.getInstance( - InstrumentationRegistry.getInstrumentation().getContext()); mPowerGroup.wakeUpLocked(TIMESTAMP2, WAKE_REASON_PLUGGED_IN, details, UID, - /* opPackageName= */ null, /* opUid= */ 0, latencyTracker); + /* opPackageName= */ null, /* opUid= */ 0, LATENCY_TRACKER); verify(mWakefulnessCallbackMock).onWakefulnessChangedLocked(eq(GROUP_ID), eq(WAKEFULNESS_AWAKE), eq(TIMESTAMP2), eq(WAKE_REASON_PLUGGED_IN), eq(UID), /* opUid= */ anyInt(), /* opPackageName= */ isNull(), eq(details)); @@ -247,6 +255,10 @@ public class PowerGroupTest { @Test public void testUpdateWhileAwake_UpdatesDisplayPowerRequest() { + mPowerGroup.dozeLocked(TIMESTAMP1, UID, GO_TO_SLEEP_REASON_APPLICATION); + mPowerGroup.wakeUpLocked(TIMESTAMP2, WAKE_REASON_WAKE_MOTION, "details", UID, + /* opPackageName= */ null, /* opUid= */ 0, LATENCY_TRACKER); + final boolean batterySaverEnabled = true; float brightnessFactor = 0.7f; PowerSaveState powerSaveState = new PowerSaveState.Builder() @@ -274,6 +286,7 @@ public class PowerGroupTest { DisplayManagerInternal.DisplayPowerRequest displayPowerRequest = mPowerGroup.mDisplayPowerRequest; assertThat(displayPowerRequest.policy).isEqualTo(POLICY_DIM); + assertThat(displayPowerRequest.policyReason).isEqualTo(STATE_REASON_MOTION); assertThat(displayPowerRequest.screenBrightnessOverride).isWithin(PRECISION).of(BRIGHTNESS); assertThat(displayPowerRequest.screenBrightnessOverrideTag.toString()).isEqualTo(tag); assertThat(displayPowerRequest.useProximitySensor).isEqualTo(false); @@ -288,6 +301,55 @@ public class PowerGroupTest { } @Test + public void testWakefulnessReasonInDisplayPowerRequestDisabled_wakefulnessReasonNotPopulated() { + when(mFeatureFlags.isPolicyReasonInDisplayPowerRequestEnabled()).thenReturn(false); + mPowerGroup.dozeLocked(TIMESTAMP1, UID, GO_TO_SLEEP_REASON_APPLICATION); + mPowerGroup.wakeUpLocked(TIMESTAMP2, WAKE_REASON_WAKE_MOTION, "details", UID, + /* opPackageName= */ null, /* opUid= */ 0, LATENCY_TRACKER); + + mPowerGroup.updateLocked(/* screenBrightnessOverride= */ BRIGHTNESS, + /* overrideTag= */ "my/tag", + /* useProximitySensor= */ false, + /* boostScreenBrightness= */ false, + /* dozeScreenStateOverride= */ Display.STATE_ON, + /* dozeScreenStateReason= */ Display.STATE_REASON_DEFAULT_POLICY, + /* dozeScreenBrightness= */ BRIGHTNESS_DOZE, + /* useNormalBrightnessForDoze= */ false, + /* overrideDrawWakeLock= */ false, + new PowerSaveState.Builder().build(), + /* quiescent= */ false, + /* dozeAfterScreenOff= */ false, + /* bootCompleted= */ true, + /* screenBrightnessBoostInProgress= */ false, + /* waitForNegativeProximity= */ false, + /* brightWhenDozing= */ false); + DisplayManagerInternal.DisplayPowerRequest displayPowerRequest = + mPowerGroup.mDisplayPowerRequest; + assertThat(displayPowerRequest.policyReason).isEqualTo(STATE_REASON_DEFAULT_POLICY); + + mPowerGroup.wakeUpLocked(TIMESTAMP2, WAKE_REASON_PLUGGED_IN, "details", UID, + /* opPackageName= */ null, /* opUid= */ 0, LATENCY_TRACKER); + mPowerGroup.updateLocked(/* screenBrightnessOverride= */ BRIGHTNESS, + /* overrideTag= */ "my/tag", + /* useProximitySensor= */ false, + /* boostScreenBrightness= */ false, + /* dozeScreenStateOverride= */ Display.STATE_ON, + /* dozeScreenStateReason= */ Display.STATE_REASON_DEFAULT_POLICY, + /* dozeScreenBrightness= */ BRIGHTNESS_DOZE, + /* useNormalBrightnessForDoze= */ false, + /* overrideDrawWakeLock= */ false, + new PowerSaveState.Builder().build(), + /* quiescent= */ false, + /* dozeAfterScreenOff= */ false, + /* bootCompleted= */ true, + /* screenBrightnessBoostInProgress= */ false, + /* waitForNegativeProximity= */ false, + /* brightWhenDozing= */ false); + displayPowerRequest = mPowerGroup.mDisplayPowerRequest; + assertThat(displayPowerRequest.policyReason).isEqualTo(STATE_REASON_DEFAULT_POLICY); + } + + @Test public void testUpdateWhileDozing_UpdatesDisplayPowerRequest() { final boolean useNormalBrightnessForDoze = false; final boolean batterySaverEnabled = false; @@ -319,6 +381,7 @@ public class PowerGroupTest { DisplayManagerInternal.DisplayPowerRequest displayPowerRequest = mPowerGroup.mDisplayPowerRequest; assertThat(displayPowerRequest.policy).isEqualTo(POLICY_DOZE); + assertThat(displayPowerRequest.policyReason).isEqualTo(STATE_REASON_DEFAULT_POLICY); assertThat(displayPowerRequest.screenBrightnessOverride).isWithin(PRECISION).of(BRIGHTNESS); assertThat(displayPowerRequest.useProximitySensor).isEqualTo(true); assertThat(displayPowerRequest.boostScreenBrightness).isEqualTo(true); @@ -363,6 +426,7 @@ public class PowerGroupTest { DisplayManagerInternal.DisplayPowerRequest displayPowerRequest = mPowerGroup.mDisplayPowerRequest; assertThat(displayPowerRequest.policy).isEqualTo(POLICY_DOZE); + assertThat(displayPowerRequest.policyReason).isEqualTo(STATE_REASON_DEFAULT_POLICY); assertThat(displayPowerRequest.screenBrightnessOverride).isWithin(PRECISION).of(BRIGHTNESS); assertThat(displayPowerRequest.useProximitySensor).isEqualTo(true); assertThat(displayPowerRequest.boostScreenBrightness).isEqualTo(true); @@ -405,6 +469,7 @@ public class PowerGroupTest { DisplayManagerInternal.DisplayPowerRequest displayPowerRequest = mPowerGroup.mDisplayPowerRequest; assertThat(displayPowerRequest.policy).isEqualTo(POLICY_OFF); + assertThat(displayPowerRequest.policyReason).isEqualTo(STATE_REASON_DEFAULT_POLICY); assertThat(displayPowerRequest.screenBrightnessOverride).isWithin(PRECISION).of(BRIGHTNESS); assertThat(displayPowerRequest.useProximitySensor).isEqualTo(true); assertThat(displayPowerRequest.boostScreenBrightness).isEqualTo(true); @@ -419,6 +484,10 @@ public class PowerGroupTest { @Test public void testUpdateQuiescent() { + mPowerGroup.dozeLocked(TIMESTAMP1, UID, GO_TO_SLEEP_REASON_APPLICATION); + mPowerGroup.wakeUpLocked(TIMESTAMP2, WAKE_REASON_WAKE_MOTION, "details", UID, + /* opPackageName= */ null, /* opUid= */ 0, LATENCY_TRACKER); + final boolean batterySaverEnabled = false; float brightnessFactor = 0.3f; PowerSaveState powerSaveState = new PowerSaveState.Builder() @@ -446,6 +515,11 @@ public class PowerGroupTest { DisplayManagerInternal.DisplayPowerRequest displayPowerRequest = mPowerGroup.mDisplayPowerRequest; assertThat(displayPowerRequest.policy).isEqualTo(POLICY_OFF); + // Note how the reason is STATE_REASON_DEFAULT_POLICY, instead of STATE_REASON_MOTION. + // This is because - although there was a wake up request from a motion, the quiescent state + // preceded and forced the policy to be OFF, so we ignore the reason associated with the + // wake up request. + assertThat(displayPowerRequest.policyReason).isEqualTo(STATE_REASON_DEFAULT_POLICY); assertThat(displayPowerRequest.screenBrightnessOverride).isWithin(PRECISION).of(BRIGHTNESS); assertThat(displayPowerRequest.useProximitySensor).isEqualTo(true); assertThat(displayPowerRequest.boostScreenBrightness).isEqualTo(true); @@ -487,6 +561,7 @@ public class PowerGroupTest { DisplayManagerInternal.DisplayPowerRequest displayPowerRequest = mPowerGroup.mDisplayPowerRequest; assertThat(displayPowerRequest.policy).isEqualTo(POLICY_OFF); + assertThat(displayPowerRequest.policyReason).isEqualTo(STATE_REASON_DEFAULT_POLICY); assertThat(displayPowerRequest.screenBrightnessOverride).isWithin(PRECISION).of(BRIGHTNESS); assertThat(displayPowerRequest.useProximitySensor).isEqualTo(true); assertThat(displayPowerRequest.boostScreenBrightness).isEqualTo(true); @@ -529,6 +604,7 @@ public class PowerGroupTest { DisplayManagerInternal.DisplayPowerRequest displayPowerRequest = mPowerGroup.mDisplayPowerRequest; assertThat(displayPowerRequest.policy).isEqualTo(POLICY_BRIGHT); + assertThat(displayPowerRequest.policyReason).isEqualTo(STATE_REASON_DEFAULT_POLICY); assertThat(displayPowerRequest.screenBrightnessOverride).isWithin(PRECISION).of(BRIGHTNESS); assertThat(displayPowerRequest.useProximitySensor).isEqualTo(true); assertThat(displayPowerRequest.boostScreenBrightness).isEqualTo(true); @@ -569,6 +645,7 @@ public class PowerGroupTest { DisplayManagerInternal.DisplayPowerRequest displayPowerRequest = mPowerGroup.mDisplayPowerRequest; assertThat(displayPowerRequest.policy).isEqualTo(POLICY_BRIGHT); + assertThat(displayPowerRequest.policyReason).isEqualTo(STATE_REASON_DEFAULT_POLICY); assertThat(displayPowerRequest.screenBrightnessOverride).isWithin(PRECISION).of(BRIGHTNESS); assertThat(displayPowerRequest.useProximitySensor).isEqualTo(true); assertThat(displayPowerRequest.boostScreenBrightness).isEqualTo(true); @@ -610,6 +687,7 @@ public class PowerGroupTest { DisplayManagerInternal.DisplayPowerRequest displayPowerRequest = mPowerGroup.mDisplayPowerRequest; assertThat(displayPowerRequest.policy).isEqualTo(POLICY_BRIGHT); + assertThat(displayPowerRequest.policyReason).isEqualTo(STATE_REASON_DEFAULT_POLICY); assertThat(displayPowerRequest.screenBrightnessOverride).isWithin(PRECISION).of(BRIGHTNESS); assertThat(displayPowerRequest.useProximitySensor).isEqualTo(true); assertThat(displayPowerRequest.boostScreenBrightness).isEqualTo(true); @@ -650,6 +728,7 @@ public class PowerGroupTest { DisplayManagerInternal.DisplayPowerRequest displayPowerRequest = mPowerGroup.mDisplayPowerRequest; assertThat(displayPowerRequest.policy).isEqualTo(POLICY_BRIGHT); + assertThat(displayPowerRequest.policyReason).isEqualTo(STATE_REASON_DEFAULT_POLICY); assertThat(displayPowerRequest.screenBrightnessOverride).isWithin(PRECISION).of(BRIGHTNESS); assertThat(displayPowerRequest.useProximitySensor).isEqualTo(true); assertThat(displayPowerRequest.boostScreenBrightness).isEqualTo(true); diff --git a/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java b/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java index e9e21dee16a8..b10200daeeb4 100644 --- a/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java +++ b/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java @@ -70,7 +70,6 @@ import android.hardware.devicestate.DeviceStateManager; import android.hardware.devicestate.DeviceStateManager.DeviceStateCallback; import android.hardware.display.AmbientDisplayConfiguration; import android.hardware.display.DisplayManagerInternal; -import android.hardware.display.DisplayManagerInternal.DisplayPowerRequest; import android.hardware.power.Boost; import android.hardware.power.Mode; import android.os.BatteryManager; @@ -1894,15 +1893,6 @@ public class PowerManagerServiceTest { } @Test - public void testBoot_DesiredScreenPolicyShouldBeBright() { - createService(); - startSystem(); - - assertThat(mService.getDesiredScreenPolicyLocked(Display.DEFAULT_DISPLAY)).isEqualTo( - DisplayPowerRequest.POLICY_BRIGHT); - } - - @Test public void testQuiescentBoot_ShouldBeAsleep() { when(mSystemPropertiesMock.get(eq(SYSTEM_PROPERTY_QUIESCENT), any())).thenReturn("1"); createService(); @@ -1914,40 +1904,6 @@ public class PowerManagerServiceTest { } @Test - public void testQuiescentBoot_DesiredScreenPolicyShouldBeOff() { - when(mSystemPropertiesMock.get(eq(SYSTEM_PROPERTY_QUIESCENT), any())).thenReturn("1"); - createService(); - startSystem(); - assertThat(mService.getDesiredScreenPolicyLocked(Display.DEFAULT_DISPLAY)).isEqualTo( - DisplayPowerRequest.POLICY_OFF); - } - - @Test - public void testQuiescentBoot_WakeUp_DesiredScreenPolicyShouldBeBright() { - when(mSystemPropertiesMock.get(eq(SYSTEM_PROPERTY_QUIESCENT), any())).thenReturn("1"); - createService(); - startSystem(); - forceAwake(); - assertThat(mService.getDesiredScreenPolicyLocked(Display.DEFAULT_DISPLAY)).isEqualTo( - DisplayPowerRequest.POLICY_BRIGHT); - } - - @Test - public void testQuiescentBoot_WakeKeyBeforeBootCompleted_AwakeAfterBootCompleted() { - when(mSystemPropertiesMock.get(eq(SYSTEM_PROPERTY_QUIESCENT), any())).thenReturn("1"); - createService(); - startSystem(); - - mService.getBinderServiceInstance().wakeUp(mClock.now(), - PowerManager.WAKE_REASON_UNKNOWN, "testing IPowerManager.wakeUp()", "pkg.name"); - - mService.onBootPhase(SystemService.PHASE_BOOT_COMPLETED); - assertThat(mService.getGlobalWakefulnessLocked()).isEqualTo(WAKEFULNESS_AWAKE); - assertThat(mService.getDesiredScreenPolicyLocked(Display.DEFAULT_DISPLAY_GROUP)).isEqualTo( - DisplayPowerRequest.POLICY_BRIGHT); - } - - @Test public void testIsAmbientDisplayAvailable_available() { createService(); when(mAmbientDisplayConfigurationMock.ambientDisplayAvailable()).thenReturn(true); diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java index 7481fc8ec46d..2edde9b74d0a 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java @@ -1615,7 +1615,8 @@ public class AccessibilityManagerServiceTest { List.of(tile) ); - assertThat(mA11yms.getCurrentUserState().getA11yQsTargets()).doesNotContain(tile); + assertThat(mA11yms.getCurrentUserState() + .getShortcutTargetsLocked(QUICK_SETTINGS)).doesNotContain(tile.flattenToString()); } @Test @@ -1636,7 +1637,7 @@ public class AccessibilityManagerServiceTest { List.of(tile) ); - assertThat(mA11yms.getCurrentUserState().getA11yQsTargets()) + assertThat(mA11yms.getCurrentUserState().getShortcutTargetsLocked(QUICK_SETTINGS)) .contains(TARGET_ALWAYS_ON_A11Y_SERVICE.flattenToString()); } @@ -1656,7 +1657,7 @@ public class AccessibilityManagerServiceTest { ); assertThat( - mA11yms.getCurrentUserState().getA11yQsTargets() + mA11yms.getCurrentUserState().getShortcutTargetsLocked(QUICK_SETTINGS) ).containsExactlyElementsIn(List.of( AccessibilityShortcutController.DALTONIZER_COMPONENT_NAME.flattenToString(), AccessibilityShortcutController.COLOR_INVERSION_COMPONENT_NAME.flattenToString()) @@ -1676,7 +1677,7 @@ public class AccessibilityManagerServiceTest { ); assertThat( - mA11yms.getCurrentUserState().getA11yQsTargets() + mA11yms.getCurrentUserState().getShortcutTargetsLocked(QUICK_SETTINGS) ).doesNotContain( AccessibilityShortcutController.DALTONIZER_COMPONENT_NAME.flattenToString()); } diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java index 627b5e39a20a..8c35925debff 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityUserStateTest.java @@ -29,6 +29,7 @@ import static android.view.accessibility.AccessibilityManager.STATE_FLAG_HIGH_TE import static android.view.accessibility.AccessibilityManager.STATE_FLAG_TOUCH_EXPLORATION_ENABLED; import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME; +import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.ALL; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.GESTURE; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.HARDWARE; import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.QUICK_SETTINGS; @@ -72,6 +73,7 @@ import com.android.internal.R; import com.android.internal.accessibility.AccessibilityShortcutController; import com.android.internal.accessibility.common.ShortcutConstants; import com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType; +import com.android.internal.accessibility.util.ShortcutUtils; import com.android.internal.util.test.FakeSettingsProvider; import org.junit.After; @@ -454,17 +456,7 @@ public class AccessibilityUserStateTest { mUserState.updateShortcutTargetsLocked(newTargets, QUICK_SETTINGS); - assertThat(mUserState.getA11yQsTargets()).isEqualTo(newTargets); - } - - @Test - public void getA11yQsTargets_returnsCopiedData() { - updateShortcutTargetsLocked_quickSettings_valueUpdated(); - - Set<String> targets = mUserState.getA11yQsTargets(); - targets.clear(); - - assertThat(mUserState.getA11yQsTargets()).isNotEmpty(); + assertThat(mUserState.getShortcutTargetsLocked(QUICK_SETTINGS)).isEqualTo(newTargets); } @Test @@ -539,6 +531,31 @@ public class AccessibilityUserStateTest { assertThat(mUserState.isShortcutMagnificationEnabledLocked()).isFalse(); } + @Test + public void getShortcutTargetsLocked_returnsCorrectTargets() { + for (int shortcutType : ShortcutConstants.USER_SHORTCUT_TYPES) { + if (((TRIPLETAP | TWOFINGER_DOUBLETAP) & shortcutType) == shortcutType) { + continue; + } + Set<String> expectedSet = Set.of(ShortcutUtils.convertToKey(shortcutType)); + mUserState.updateShortcutTargetsLocked(expectedSet, shortcutType); + + assertThat(mUserState.getShortcutTargetsLocked(shortcutType)) + .containsExactlyElementsIn(expectedSet); + } + } + + @Test + public void getShortcutTargetsLocked_returnsCopiedData() { + Set<String> set = Set.of("FOO", "BAR"); + mUserState.updateShortcutTargetsLocked(set, SOFTWARE); + + Set<String> targets = mUserState.getShortcutTargetsLocked(ALL); + targets.clear(); + + assertThat(mUserState.getShortcutTargetsLocked(ALL)).isNotEmpty(); + } + private int getSecureIntForUser(String key, int userId) { return Settings.Secure.getIntForUser(mMockResolver, key, -1, userId); } diff --git a/services/tests/servicestests/src/com/android/server/audio/FadeOutManagerTest.java b/services/tests/servicestests/src/com/android/server/audio/FadeOutManagerTest.java index fa94821d4ff2..3c27af4b0dfc 100644 --- a/services/tests/servicestests/src/com/android/server/audio/FadeOutManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/audio/FadeOutManagerTest.java @@ -34,6 +34,8 @@ import androidx.test.core.app.ApplicationProvider; import com.google.common.truth.Expect; +import com.android.server.utils.EventLogger; + import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -67,6 +69,8 @@ public final class FadeOutManagerTest { PlayerBase.PlayerIdCard mMockPlayerIdCard; @Mock AudioPlaybackConfiguration mMockPlaybackConfiguration; + @Mock + EventLogger mMockEventLogger; @Rule public final Expect expect = Expect.create(); @@ -193,7 +197,7 @@ public final class FadeOutManagerTest { String packageName, int uid, int flags) { MediaFocusControl mfc = new MediaFocusControl(mContext, null); return new FocusRequester(aa, AudioManager.AUDIOFOCUS_GAIN, flags, null, null, clientId, - null, packageName, uid, mfc, 1); + null, packageName, uid, mfc, 1, mMockEventLogger); } private PlayerBase.PlayerIdCard createPlayerIdCard(AudioAttributes aa, int playerType) { diff --git a/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java b/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java index 2f7b8d26bdd9..4ef37b9cc3cf 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/AuthSessionTest.java @@ -69,6 +69,7 @@ import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; import android.os.Binder; import android.os.IBinder; import android.os.RemoteException; +import android.os.UserManager; import android.platform.test.annotations.Presubmit; import android.security.KeyStoreAuthorization; import android.testing.TestableContext; @@ -119,6 +120,7 @@ public class AuthSessionTest { @Mock private AuthSession.ClientDeathReceiver mClientDeathReceiver; @Mock private BiometricFrameworkStatsLogger mBiometricFrameworkStatsLogger; @Mock private BiometricCameraManager mBiometricCameraManager; + @Mock private UserManager mUserManager; @Mock private BiometricManager mBiometricManager; private Random mRandom; @@ -846,7 +848,8 @@ public class AuthSessionTest { TEST_PACKAGE, checkDevicePolicyManager, mContext, - mBiometricCameraManager); + mBiometricCameraManager, + mUserManager); } private AuthSession createAuthSession(List<BiometricSensor> sensors, diff --git a/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java b/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java index 85e45f47900a..cf628add705a 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/PreAuthInfoTest.java @@ -39,6 +39,7 @@ import android.hardware.biometrics.Flags; import android.hardware.biometrics.IBiometricAuthenticator; import android.hardware.biometrics.PromptInfo; import android.os.RemoteException; +import android.os.UserManager; import android.platform.test.annotations.Presubmit; import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; @@ -64,6 +65,8 @@ public class PreAuthInfoTest { public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + private static final int USER_ID = 0; + private static final int OWNER_ID = 10; private static final int SENSOR_ID_FINGERPRINT = 0; private static final int SENSOR_ID_FACE = 1; private static final String TEST_PACKAGE_NAME = "PreAuthInfoTestPackage"; @@ -84,6 +87,8 @@ public class PreAuthInfoTest { BiometricService.SettingObserver mSettingObserver; @Mock BiometricCameraManager mBiometricCameraManager; + @Mock + UserManager mUserManager; @Before public void setup() throws RemoteException { @@ -118,9 +123,9 @@ public class PreAuthInfoTest { promptInfo.setAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG); promptInfo.setDisallowBiometricsIfPolicyExists(false /* checkDevicePolicy */); PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager, mDevicePolicyManager, - mSettingObserver, List.of(sensor), - 0 /* userId */, promptInfo, TEST_PACKAGE_NAME, - false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager); + mSettingObserver, List.of(sensor), USER_ID, promptInfo, TEST_PACKAGE_NAME, + false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager, + mUserManager); assertThat(preAuthInfo.eligibleSensors).isEmpty(); } @@ -134,9 +139,9 @@ public class PreAuthInfoTest { promptInfo.setAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG); promptInfo.setDisallowBiometricsIfPolicyExists(false /* checkDevicePolicy */); PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager, mDevicePolicyManager, - mSettingObserver, List.of(sensor), - 0 /* userId */, promptInfo, TEST_PACKAGE_NAME, - false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager); + mSettingObserver, List.of(sensor), USER_ID, promptInfo, TEST_PACKAGE_NAME, + false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager, + mUserManager); assertThat(preAuthInfo.eligibleSensors).hasSize(1); } @@ -151,9 +156,9 @@ public class PreAuthInfoTest { promptInfo.setAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG); promptInfo.setDisallowBiometricsIfPolicyExists(false /* checkDevicePolicy */); PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager, mDevicePolicyManager, - mSettingObserver, List.of(sensor), - 0 /* userId */, promptInfo, TEST_PACKAGE_NAME, - false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager); + mSettingObserver, List.of(sensor), USER_ID, promptInfo, TEST_PACKAGE_NAME, + false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager, + mUserManager); assertThat(preAuthInfo.eligibleSensors).hasSize(0); } @@ -171,9 +176,9 @@ public class PreAuthInfoTest { promptInfo.setAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG); promptInfo.setDisallowBiometricsIfPolicyExists(false /* checkDevicePolicy */); PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager, mDevicePolicyManager, - mSettingObserver, List.of(faceSensor, fingerprintSensor), - 0 /* userId */, promptInfo, TEST_PACKAGE_NAME, - false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager); + mSettingObserver, List.of(faceSensor, fingerprintSensor), USER_ID, + promptInfo, TEST_PACKAGE_NAME, false /* checkDevicePolicyManager */, mContext, + mBiometricCameraManager, mUserManager); assertThat(preAuthInfo.eligibleSensors).hasSize(0); assertThat(preAuthInfo.getCanAuthenticateResult()).isEqualTo( @@ -191,9 +196,9 @@ public class PreAuthInfoTest { promptInfo.setAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG); promptInfo.setDisallowBiometricsIfPolicyExists(false /* checkDevicePolicy */); PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager, mDevicePolicyManager, - mSettingObserver, List.of(faceSensor, fingerprintSensor), - 0 /* userId */, promptInfo, TEST_PACKAGE_NAME, - false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager); + mSettingObserver, List.of(faceSensor, fingerprintSensor), USER_ID, + promptInfo, TEST_PACKAGE_NAME, false /* checkDevicePolicyManager */, mContext, + mBiometricCameraManager, mUserManager); assertThat(preAuthInfo.eligibleSensors).hasSize(1); assertThat(preAuthInfo.eligibleSensors.get(0).modality).isEqualTo(TYPE_FINGERPRINT); @@ -209,8 +214,9 @@ public class PreAuthInfoTest { final PromptInfo promptInfo = new PromptInfo(); promptInfo.setAuthenticators(BiometricManager.Authenticators.IDENTITY_CHECK); final PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager, mDevicePolicyManager, - mSettingObserver, List.of(sensor), 0 /* userId */, promptInfo, TEST_PACKAGE_NAME, - false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager); + mSettingObserver, List.of(sensor), USER_ID, promptInfo, TEST_PACKAGE_NAME, + false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager, + mUserManager); assertThat(preAuthInfo.eligibleSensors).hasSize(1); } @@ -224,8 +230,9 @@ public class PreAuthInfoTest { final PromptInfo promptInfo = new PromptInfo(); promptInfo.setAuthenticators(BiometricManager.Authenticators.IDENTITY_CHECK); final PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager, mDevicePolicyManager, - mSettingObserver, List.of(), 0 /* userId */, promptInfo, TEST_PACKAGE_NAME, - false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager); + mSettingObserver, List.of(), USER_ID, promptInfo, TEST_PACKAGE_NAME, + false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager, + mUserManager); assertThat(preAuthInfo.eligibleSensors).hasSize(0); } @@ -241,8 +248,9 @@ public class PreAuthInfoTest { promptInfo.setAuthenticators(BiometricManager.Authenticators.IDENTITY_CHECK | BiometricManager.Authenticators.BIOMETRIC_STRONG); final PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager, mDevicePolicyManager, - mSettingObserver, List.of(sensor), 0 /* userId */, promptInfo, TEST_PACKAGE_NAME, - false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager); + mSettingObserver, List.of(sensor), USER_ID, promptInfo, TEST_PACKAGE_NAME, + false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager, + mUserManager); assertThat(preAuthInfo.eligibleSensors).hasSize(1); } @@ -257,8 +265,9 @@ public class PreAuthInfoTest { final PromptInfo promptInfo = new PromptInfo(); promptInfo.setAuthenticators(BiometricManager.Authenticators.IDENTITY_CHECK); final PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager, mDevicePolicyManager, - mSettingObserver, List.of(sensor), 0 /* userId */, promptInfo, TEST_PACKAGE_NAME, - false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager); + mSettingObserver, List.of(sensor), USER_ID, promptInfo, TEST_PACKAGE_NAME, + false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager, + mUserManager); assertThat(preAuthInfo.getCanAuthenticateResult()).isEqualTo( BiometricManager.BIOMETRIC_ERROR_IDENTITY_CHECK_NOT_ACTIVE); @@ -279,9 +288,9 @@ public class PreAuthInfoTest { promptInfo.setAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG); promptInfo.setDisallowBiometricsIfPolicyExists(false /* checkDevicePolicy */); PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager, mDevicePolicyManager, - mSettingObserver, List.of(faceSensor, fingerprintSensor), - 0 /* userId */, promptInfo, TEST_PACKAGE_NAME, - false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager); + mSettingObserver, List.of(faceSensor, fingerprintSensor), USER_ID, + promptInfo, TEST_PACKAGE_NAME, false /* checkDevicePolicyManager */, mContext, + mBiometricCameraManager, mUserManager); assertThat(preAuthInfo.eligibleSensors).hasSize(0); assertThat(preAuthInfo.getCanAuthenticateResult()).isEqualTo( @@ -299,11 +308,30 @@ public class PreAuthInfoTest { promptInfo.setAuthenticators(BiometricManager.Authenticators.IDENTITY_CHECK); promptInfo.setNegativeButtonText(TEST_PACKAGE_NAME); final PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager, mDevicePolicyManager, - mSettingObserver, List.of(sensor), 0 /* userId */, promptInfo, TEST_PACKAGE_NAME, - false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager); + mSettingObserver, List.of(sensor), USER_ID, promptInfo, TEST_PACKAGE_NAME, + false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager, + mUserManager); assertThat(promptInfo.getNegativeButtonText()).isEqualTo(TEST_PACKAGE_NAME); } + @Test + @RequiresFlagsEnabled(Flags.FLAG_EFFECTIVE_USER_BP) + public void testCredentialOwnerIdAsUserId() throws Exception { + when(mUserManager.getCredentialOwnerProfile(USER_ID)).thenReturn(OWNER_ID); + + final BiometricSensor sensor = getFaceSensor(); + final PromptInfo promptInfo = new PromptInfo(); + promptInfo.setAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG); + promptInfo.setNegativeButtonText(TEST_PACKAGE_NAME); + final PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager, mDevicePolicyManager, + mSettingObserver, List.of(sensor), USER_ID , promptInfo, TEST_PACKAGE_NAME, + false /* checkDevicePolicyManager */, mContext, mBiometricCameraManager, + mUserManager); + + assertThat(preAuthInfo.userId).isEqualTo(OWNER_ID); + assertThat(preAuthInfo.callingUserId).isEqualTo(USER_ID); + } + private BiometricSensor getFingerprintSensor() { BiometricSensor sensor = new BiometricSensor(mContext, SENSOR_ID_FINGERPRINT, TYPE_FINGERPRINT, BiometricManager.Authenticators.BIOMETRIC_STRONG, diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerOperationTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerOperationTest.java index 4f07380dfb5a..26e2614be001 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerOperationTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BiometricSchedulerOperationTest.java @@ -147,7 +147,7 @@ public class BiometricSchedulerOperationTest { when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal); assertThat(mInterruptableOperation.startWithCookie(mOnStartCallback, cookie)).isTrue(); - assertThat(mInterruptableOperation.startWithCookie(mOnStartCallback, cookie)).isFalse(); + assertThat(mInterruptableOperation.startWithCookie(mOnStartCallback, cookie)).isTrue(); } @Test @@ -201,7 +201,7 @@ public class BiometricSchedulerOperationTest { when(mInterruptableClientMonitor.getFreshDaemon()).thenReturn(mHal); assertThat(mInterruptableOperation.start(mOnStartCallback)).isTrue(); - assertThat(mInterruptableOperation.start(mOnStartCallback)).isFalse(); + assertThat(mInterruptableOperation.start(mOnStartCallback)).isTrue(); } @Test diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java index 3360e1d08bda..c741c6c041a2 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDevicePlaybackTest.java @@ -37,6 +37,7 @@ import android.hardware.hdmi.HdmiDeviceInfo; import android.hardware.hdmi.HdmiPortInfo; import android.hardware.hdmi.IHdmiControlCallback; import android.hardware.tv.cec.V1_0.SendMessageResult; +import android.os.IBinder; import android.os.Looper; import android.os.RemoteException; import android.os.test.TestLooper; @@ -2676,6 +2677,60 @@ public class HdmiCecLocalDevicePlaybackTest { } @Test + public void onActiveSourceLost_oneTouchPlay_noStandbyAfterTimeout() { + mHdmiCecLocalDevicePlayback.mService.getHdmiCecConfig().setStringValue( + HdmiControlManager.CEC_SETTING_NAME_POWER_STATE_CHANGE_ON_ACTIVE_SOURCE_LOST, + HdmiControlManager.POWER_STATE_CHANGE_ON_ACTIVE_SOURCE_LOST_STANDBY_NOW); + mHdmiCecLocalDevicePlayback.setActiveSource(mPlaybackLogicalAddress, + mPlaybackPhysicalAddress, "HdmiCecLocalDevicePlaybackTest"); + mPowerManager.setInteractive(true); + mNativeWrapper.clearResultMessages(); + mTestLooper.dispatchAll(); + + HdmiCecMessage activeSourceFromTv = + HdmiCecMessageBuilder.buildActiveSource(ADDR_TV, 0x0000); + HdmiCecMessage activeSourceFromPlayback = + HdmiCecMessageBuilder.buildActiveSource(mPlaybackLogicalAddress, + mPlaybackPhysicalAddress); + + assertThat(mHdmiCecLocalDevicePlayback.handleActiveSource(activeSourceFromTv)) + .isEqualTo(Constants.HANDLED); + assertThat(mHdmiCecLocalDevicePlayback.getActiveSource().logicalAddress).isEqualTo(ADDR_TV); + mTestLooper.dispatchAll(); + + // Pop-up is triggered. + mTestLooper.moveTimeForward(POPUP_AFTER_ACTIVE_SOURCE_LOST_DELAY_MS); + mTestLooper.dispatchAll(); + // RequestActiveSourceAction is triggered and TV confirms active source. + mNativeWrapper.onCecMessage(activeSourceFromTv); + mTestLooper.dispatchAll(); + + assertThat(mIsOnActiveSourceLostPopupActive).isTrue(); + mHdmiControlService.oneTouchPlay(new IHdmiControlCallback() { + @Override + public void onComplete(int result) throws RemoteException { + } + + @Override + public IBinder asBinder() { + return null; + } + }); + mTestLooper.dispatchAll(); + + assertThat(mIsOnActiveSourceLostPopupActive).isFalse(); + assertThat(mNativeWrapper.getResultMessages().contains(activeSourceFromPlayback)).isTrue(); + assertThat(mHdmiCecLocalDevicePlayback.getActiveSource().logicalAddress) + .isEqualTo(mPlaybackLogicalAddress); + assertThat(mHdmiCecLocalDevicePlayback.getActiveSource().physicalAddress) + .isEqualTo(mPlaybackPhysicalAddress); + mTestLooper.moveTimeForward(STANDBY_AFTER_ACTIVE_SOURCE_LOST_DELAY_MS); + mTestLooper.dispatchAll(); + + assertThat(mPowerManager.isInteractive()).isTrue(); + } + + @Test public void handleRoutingChange_addressNotAllocated_removeActiveSourceAction() { long allocationDelay = TimeUnit.SECONDS.toMillis(60); mHdmiCecLocalDevicePlayback.mPlaybackDeviceActionOnRoutingControl = diff --git a/services/tests/servicestests/src/com/android/server/security/advancedprotection/AdvancedProtectionServiceTest.java b/services/tests/servicestests/src/com/android/server/security/advancedprotection/AdvancedProtectionServiceTest.java index e97b48cb3489..24bf6ca507e6 100644 --- a/services/tests/servicestests/src/com/android/server/security/advancedprotection/AdvancedProtectionServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/security/advancedprotection/AdvancedProtectionServiceTest.java @@ -16,6 +16,8 @@ package com.android.server.security.advancedprotection; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.collection.IsIterableContainingInAnyOrder.containsInAnyOrder; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; @@ -28,15 +30,20 @@ import android.os.RemoteException; import android.os.test.FakePermissionEnforcer; import android.os.test.TestLooper; import android.provider.Settings; +import android.security.advancedprotection.AdvancedProtectionFeature; import android.security.advancedprotection.IAdvancedProtectionCallback; +import androidx.annotation.NonNull; + import com.android.server.security.advancedprotection.features.AdvancedProtectionHook; +import com.android.server.security.advancedprotection.features.AdvancedProtectionProvider; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; +import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; @SuppressLint("VisibleForTests") @@ -47,6 +54,7 @@ public class AdvancedProtectionServiceTest { private Context mContext; private AdvancedProtectionService.AdvancedProtectionStore mStore; private TestLooper mLooper; + AdvancedProtectionFeature mFeature = new AdvancedProtectionFeature("test-id"); @Before public void setup() throws Settings.SettingNotFoundException { @@ -70,8 +78,9 @@ public class AdvancedProtectionServiceTest { }; mLooper = new TestLooper(); + mService = new AdvancedProtectionService(mContext, mStore, mLooper.getLooper(), - mPermissionEnforcer, null); + mPermissionEnforcer, null, null); } @Test @@ -93,6 +102,12 @@ public class AdvancedProtectionServiceTest { AtomicBoolean callbackCaptor = new AtomicBoolean(false); AdvancedProtectionHook hook = new AdvancedProtectionHook(mContext, true) { + @NonNull + @Override + public AdvancedProtectionFeature getFeature() { + return mFeature; + } + @Override public boolean isAvailable() { return true; @@ -105,7 +120,7 @@ public class AdvancedProtectionServiceTest { }; mService = new AdvancedProtectionService(mContext, mStore, mLooper.getLooper(), - mPermissionEnforcer, hook); + mPermissionEnforcer, hook, null); mService.setAdvancedProtectionEnabled(true); mLooper.dispatchNext(); @@ -117,6 +132,12 @@ public class AdvancedProtectionServiceTest { AtomicBoolean callbackCalledCaptor = new AtomicBoolean(false); AdvancedProtectionHook hook = new AdvancedProtectionHook(mContext, true) { + @NonNull + @Override + public AdvancedProtectionFeature getFeature() { + return mFeature; + } + @Override public boolean isAvailable() { return false; @@ -129,7 +150,8 @@ public class AdvancedProtectionServiceTest { }; mService = new AdvancedProtectionService(mContext, mStore, mLooper.getLooper(), - mPermissionEnforcer, hook); + mPermissionEnforcer, hook, null); + mService.setAdvancedProtectionEnabled(true); mLooper.dispatchNext(); assertFalse(callbackCalledCaptor.get()); @@ -140,6 +162,12 @@ public class AdvancedProtectionServiceTest { AtomicBoolean callbackCalledCaptor = new AtomicBoolean(false); AdvancedProtectionHook hook = new AdvancedProtectionHook(mContext, true) { + @NonNull + @Override + public AdvancedProtectionFeature getFeature() { + return mFeature; + } + @Override public boolean isAvailable() { return true; @@ -152,7 +180,7 @@ public class AdvancedProtectionServiceTest { }; mService = new AdvancedProtectionService(mContext, mStore, mLooper.getLooper(), - mPermissionEnforcer, hook); + mPermissionEnforcer, hook, null); mService.setAdvancedProtectionEnabled(true); mLooper.dispatchNext(); assertTrue(callbackCalledCaptor.get()); @@ -208,6 +236,66 @@ public class AdvancedProtectionServiceTest { assertFalse(callbackCalledCaptor.get()); } + @Test + public void testGetFeatures() { + AdvancedProtectionFeature feature1 = new AdvancedProtectionFeature("id-1"); + AdvancedProtectionFeature feature2 = new AdvancedProtectionFeature("id-2"); + AdvancedProtectionHook hook = new AdvancedProtectionHook(mContext, true) { + @NonNull + @Override + public AdvancedProtectionFeature getFeature() { + return feature1; + } + + @Override + public boolean isAvailable() { + return true; + } + }; + + AdvancedProtectionProvider provider = new AdvancedProtectionProvider() { + @Override + public List<AdvancedProtectionFeature> getFeatures() { + return List.of(feature2); + } + }; + + mService = new AdvancedProtectionService(mContext, mStore, mLooper.getLooper(), + mPermissionEnforcer, hook, provider); + List<AdvancedProtectionFeature> features = mService.getAdvancedProtectionFeatures(); + assertThat(features, containsInAnyOrder(feature1, feature2)); + } + + @Test + public void testGetFeatures_featureNotAvailable() { + AdvancedProtectionFeature feature1 = new AdvancedProtectionFeature("id-1"); + AdvancedProtectionFeature feature2 = new AdvancedProtectionFeature("id-2"); + AdvancedProtectionHook hook = new AdvancedProtectionHook(mContext, true) { + @NonNull + @Override + public AdvancedProtectionFeature getFeature() { + return feature1; + } + + @Override + public boolean isAvailable() { + return false; + } + }; + + AdvancedProtectionProvider provider = new AdvancedProtectionProvider() { + @Override + public List<AdvancedProtectionFeature> getFeatures() { + return List.of(feature2); + } + }; + + mService = new AdvancedProtectionService(mContext, mStore, mLooper.getLooper(), + mPermissionEnforcer, hook, provider); + List<AdvancedProtectionFeature> features = mService.getAdvancedProtectionFeatures(); + assertThat(features, containsInAnyOrder(feature2)); + } + @Test public void testSetProtection_withoutPermission() { diff --git a/services/tests/servicestests/src/com/android/server/tv/tunerresourcemanager/TunerResourceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/tv/tunerresourcemanager/TunerResourceManagerServiceTest.java index a222ef04ac30..5852af780b8b 100644 --- a/services/tests/servicestests/src/com/android/server/tv/tunerresourcemanager/TunerResourceManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/tv/tunerresourcemanager/TunerResourceManagerServiceTest.java @@ -15,6 +15,8 @@ */ package com.android.server.tv.tunerresourcemanager; +import static android.media.tv.flags.Flags.FLAG_SET_RESOURCE_HOLDER_RETAIN; + import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.spy; @@ -39,7 +41,9 @@ import android.media.tv.tunerresourcemanager.TunerFrontendRequest; import android.media.tv.tunerresourcemanager.TunerLnbRequest; import android.media.tv.tunerresourcemanager.TunerResourceManager; import android.os.RemoteException; +import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; +import android.platform.test.flag.junit.SetFlagsRule; import androidx.test.InstrumentationRegistry; import androidx.test.filters.SmallTest; @@ -47,6 +51,7 @@ import androidx.test.filters.SmallTest; import com.google.common.truth.Correspondence; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.JUnit4; @@ -70,6 +75,8 @@ public class TunerResourceManagerServiceTest { private TunerResourceManagerService mTunerResourceManagerService; private boolean mIsForeground; + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + private final class TunerClient extends IResourcesReclaimListener.Stub { int[] mClientId; ClientProfile mProfile; @@ -125,19 +132,6 @@ public class TunerResourceManagerServiceTest { } } - private static final class TestResourcesReclaimListener extends IResourcesReclaimListener.Stub { - boolean mReclaimed; - - @Override - public void onReclaimResources() { - mReclaimed = true; - } - - public boolean isReclaimed() { - return mReclaimed; - } - } - // A correspondence to compare a FrontendResource and a TunerFrontendInfo. private static final Correspondence<FrontendResource, TunerFrontendInfo> FR_TFI_COMPARE = Correspondence.from((FrontendResource actual, TunerFrontendInfo expected) -> { @@ -485,6 +479,62 @@ public class TunerResourceManagerServiceTest { } @Test + @EnableFlags({FLAG_SET_RESOURCE_HOLDER_RETAIN}) + public void requestFrontendTest_NoFrontendAvailable_RequestWithEqualPriority() + throws RemoteException { + // Register clients + TunerClient client0 = new TunerClient(); + TunerClient client1 = new TunerClient(); + client0.register( + "0" /*sessionId*/, TvInputService.PRIORITY_HINT_USE_CASE_TYPE_PLAYBACK, 100); + client1.register( + "1" /*sessionId*/, TvInputService.PRIORITY_HINT_USE_CASE_TYPE_PLAYBACK, 100); + + // Init frontend resource. + TunerFrontendInfo[] infos = new TunerFrontendInfo[1]; + infos[0] = + tunerFrontendInfo(0 /*handle*/, FrontendSettings.TYPE_DVBT, 1 /*exclusiveGroupId*/); + mTunerResourceManagerService.setFrontendInfoListInternal(infos); + + // client0 requests for 1 frontend + TunerFrontendRequest request = + tunerFrontendRequest(client0.getId() /*clientId*/, FrontendSettings.TYPE_DVBT); + long[] frontendHandle = new long[1]; + assertThat(mTunerResourceManagerService.requestFrontendInternal(request, frontendHandle)) + .isTrue(); + assertThat(frontendHandle[0]).isEqualTo(infos[0].handle); + assertThat(client0.getProfile().getInUseFrontendHandles()) + .isEqualTo(new HashSet<Long>(Arrays.asList(infos[0].handle))); + + // setResourceHolderRetain sets mResourceHolderRetain to true to allow the Resource Holder + // (client0) to maintain ownership such as requester will not get the resources. + client1.getProfile().setResourceHolderRetain(true); + + request = tunerFrontendRequest(client1.getId() /*clientId*/, FrontendSettings.TYPE_DVBT); + assertThat(mTunerResourceManagerService.requestFrontendInternal(request, frontendHandle)) + .isFalse(); + assertThat(mTunerResourceManagerService.getFrontendResource(infos[0].handle).isInUse()) + .isTrue(); + assertThat(mTunerResourceManagerService.getFrontendResource(infos[0].handle) + .getOwnerClientId()) + .isEqualTo(client0.getId()); + assertThat(client0.isReclaimed()).isFalse(); + + // setResourceHolderRetain sets mResourceHolderRetain to false to allow the Resource + // Challenger (client1) to acquire the resource and Resource Holder loses ownership of the + // resources. + client1.getProfile().setResourceHolderRetain(false); + + assertThat(mTunerResourceManagerService.requestFrontendInternal(request, frontendHandle)) + .isTrue(); + assertThat(frontendHandle[0]).isEqualTo(infos[0].handle); + assertThat(mTunerResourceManagerService.getFrontendResource(infos[0].handle) + .getOwnerClientId()) + .isEqualTo(client1.getId()); + assertThat(client0.isReclaimed()).isTrue(); + } + + @Test public void releaseFrontendTest_UnderTheSameExclusiveGroup() throws RemoteException { // Register clients TunerClient client0 = new TunerClient(); @@ -565,6 +615,74 @@ public class TunerResourceManagerServiceTest { } @Test + @EnableFlags({FLAG_SET_RESOURCE_HOLDER_RETAIN}) + public void requestCasTest_NoCasAvailable_RequestWithEqualPriority() throws RemoteException { + // Register clients + TunerClient client0 = new TunerClient(); + TunerClient client1 = new TunerClient(); + client0.register( + "0" /*sessionId*/, TvInputService.PRIORITY_HINT_USE_CASE_TYPE_PLAYBACK, 100); + client1.register( + "1" /*sessionId*/, TvInputService.PRIORITY_HINT_USE_CASE_TYPE_PLAYBACK, 100); + + // Init cas resources. + mTunerResourceManagerService.updateCasInfoInternal(1 /*casSystemId*/, 2 /*maxSessionNum*/); + + CasSessionRequest request = casSessionRequest(client0.getId(), 1 /*casSystemId*/); + long[] casSessionHandle = new long[1]; + + // client0 requests for 2 cas sessions. + assertThat( + mTunerResourceManagerService.requestCasSessionInternal(request, casSessionHandle)) + .isTrue(); + assertThat( + mTunerResourceManagerService.requestCasSessionInternal(request, casSessionHandle)) + .isTrue(); + assertThat(mTunerResourceManagerService.getResourceIdFromHandle(casSessionHandle[0])) + .isEqualTo(1); + assertThat(client0.getProfile().getInUseCasSystemId()).isEqualTo(1); + assertThat(mTunerResourceManagerService.getCasResource(1).getOwnerClientIds()) + .isEqualTo(new HashSet<Integer>(Arrays.asList(client0.getId()))); + assertThat(mTunerResourceManagerService.getCasResource(1).isFullyUsed()).isTrue(); + + // setResourceHolderRetain sets mResourceHolderRetain to true to allow the Resource Holder + // to maintain ownership such as requester (client1) will not get the resources. + client1.getProfile().setResourceHolderRetain(true); + + request = casSessionRequest(client1.getId(), 1); + assertThat( + mTunerResourceManagerService.requestCasSessionInternal(request, casSessionHandle)) + .isFalse(); + assertThat(mTunerResourceManagerService.getResourceIdFromHandle(casSessionHandle[0])) + .isEqualTo(-1); + assertThat(client0.getProfile().getInUseCasSystemId()).isEqualTo(1); + assertThat(client1.getProfile().getInUseCasSystemId()) + .isEqualTo(ClientProfile.INVALID_RESOURCE_ID); + assertThat(mTunerResourceManagerService.getCasResource(1).getOwnerClientIds()) + .isEqualTo(new HashSet<Integer>(Arrays.asList(client0.getId()))); + assertThat(mTunerResourceManagerService.getCasResource(1).isFullyUsed()).isTrue(); + assertThat(client0.isReclaimed()).isFalse(); + + // setResourceHolderRetain sets mResourceHolderRetain to false to allow the Resource + // Challenger (client1) to acquire the resource and Resource Holder loses ownership of the + // resources. + client1.getProfile().setResourceHolderRetain(false); + + assertThat( + mTunerResourceManagerService.requestCasSessionInternal(request, casSessionHandle)) + .isTrue(); + assertThat(mTunerResourceManagerService.getResourceIdFromHandle(casSessionHandle[0])) + .isEqualTo(1); + assertThat(client0.getProfile().getInUseCasSystemId()) + .isEqualTo(ClientProfile.INVALID_RESOURCE_ID); + assertThat(client1.getProfile().getInUseCasSystemId()).isEqualTo(1); + assertThat(mTunerResourceManagerService.getCasResource(1).getOwnerClientIds()) + .isEqualTo(new HashSet<Integer>(Arrays.asList(client1.getId()))); + assertThat(mTunerResourceManagerService.getCasResource(1).isFullyUsed()).isFalse(); + assertThat(client0.isReclaimed()).isTrue(); + } + + @Test public void requestCiCamTest_NoCiCamAvailable_RequestWithHigherPriority() throws RemoteException { // Register clients @@ -612,6 +730,71 @@ public class TunerResourceManagerServiceTest { } @Test + @EnableFlags({FLAG_SET_RESOURCE_HOLDER_RETAIN}) + public void requestCiCamTest_NoCiCamAvailable_RequestWithEqualPriority() + throws RemoteException { + // Register clients + TunerClient client0 = new TunerClient(); + TunerClient client1 = new TunerClient(); + client0.register( + "0" /*sessionId*/, TvInputService.PRIORITY_HINT_USE_CASE_TYPE_PLAYBACK, 100); + client1.register( + "1" /*sessionId*/, TvInputService.PRIORITY_HINT_USE_CASE_TYPE_PLAYBACK, 100); + + // Init cicam/cas resources. + mTunerResourceManagerService.updateCasInfoInternal(1 /*casSystemId*/, 2 /*maxSessionNum*/); + + TunerCiCamRequest request = tunerCiCamRequest(client0.getId(), 1 /*ciCamId*/); + long[] ciCamHandle = new long[1]; + + // client0 request for 2 ciCam sessions. + assertThat(mTunerResourceManagerService.requestCiCamInternal(request, ciCamHandle)) + .isTrue(); + assertThat(mTunerResourceManagerService.requestCiCamInternal(request, ciCamHandle)) + .isTrue(); + assertThat(mTunerResourceManagerService.getResourceIdFromHandle(ciCamHandle[0])) + .isEqualTo(1); + assertThat(client0.getProfile().getInUseCiCamId()).isEqualTo(1); + assertThat(mTunerResourceManagerService.getCiCamResource(1).getOwnerClientIds()) + .isEqualTo(new HashSet<Integer>(Arrays.asList(client0.getId()))); + assertThat(mTunerResourceManagerService.getCiCamResource(1).isFullyUsed()).isTrue(); + + // setResourceHolderRetain sets mResourceHolderRetain to true to allow the Resource Holder + // (client0) to maintain ownership such as requester will not get the resources. + client1.getProfile().setResourceHolderRetain(true); + + request = tunerCiCamRequest(client1.getId(), 1); + assertThat(mTunerResourceManagerService.requestCiCamInternal(request, ciCamHandle)) + .isFalse(); + assertThat(mTunerResourceManagerService.getResourceIdFromHandle(ciCamHandle[0])) + .isEqualTo(-1); + assertThat(client0.getProfile().getInUseCiCamId()).isEqualTo(1); + assertThat(client1.getProfile().getInUseCiCamId()) + .isEqualTo(ClientProfile.INVALID_RESOURCE_ID); + assertThat(mTunerResourceManagerService.getCiCamResource(1).getOwnerClientIds()) + .isEqualTo(new HashSet<Integer>(Arrays.asList(client0.getId()))); + assertThat(mTunerResourceManagerService.getCiCamResource(1).isFullyUsed()).isTrue(); + assertThat(client0.isReclaimed()).isFalse(); + + // setResourceHolderRetain sets mResourceHolderRetain to false to allow the Resource + // Challenger (client1) to acquire the resource and Resource Holder loses ownership of the + // resources. + client1.getProfile().setResourceHolderRetain(false); + + assertThat(mTunerResourceManagerService.requestCiCamInternal(request, ciCamHandle)) + .isTrue(); + assertThat(mTunerResourceManagerService.getResourceIdFromHandle(ciCamHandle[0])) + .isEqualTo(1); + assertThat(client0.getProfile().getInUseCiCamId()) + .isEqualTo(ClientProfile.INVALID_RESOURCE_ID); + assertThat(client1.getProfile().getInUseCiCamId()).isEqualTo(1); + assertThat(mTunerResourceManagerService.getCiCamResource(1).getOwnerClientIds()) + .isEqualTo(new HashSet<Integer>(Arrays.asList(client1.getId()))); + assertThat(mTunerResourceManagerService.getCiCamResource(1).isFullyUsed()).isFalse(); + assertThat(client0.isReclaimed()).isTrue(); + } + + @Test public void releaseCasTest() throws RemoteException { // Register clients TunerClient client0 = new TunerClient(); @@ -721,6 +904,59 @@ public class TunerResourceManagerServiceTest { } @Test + @EnableFlags({FLAG_SET_RESOURCE_HOLDER_RETAIN}) + public void requestLnbTest_NoLnbAvailable_RequestWithEqualPriority() throws RemoteException { + // Register clients + TunerClient client0 = new TunerClient(); + TunerClient client1 = new TunerClient(); + client0.register( + "0" /*sessionId*/, TvInputService.PRIORITY_HINT_USE_CASE_TYPE_PLAYBACK, 100); + client1.register( + "1" /*sessionId*/, TvInputService.PRIORITY_HINT_USE_CASE_TYPE_PLAYBACK, 100); + + // Init lnb resources. + long[] lnbHandles = {1}; + mTunerResourceManagerService.setLnbInfoListInternal(lnbHandles); + + // client0 requests 1 lnb + TunerLnbRequest request = new TunerLnbRequest(); + request.clientId = client0.getId(); + long[] lnbHandle = new long[1]; + assertThat(mTunerResourceManagerService.requestLnbInternal(request, lnbHandle)).isTrue(); + assertThat(lnbHandle[0]).isEqualTo(lnbHandles[0]); + assertThat(client0.getProfile().getInUseLnbHandles()) + .isEqualTo(new HashSet<Long>(Arrays.asList(lnbHandles[0]))); + + // setResourceHolderRetain sets mResourceHolderRetain to true to allow the Resource Holder + // (client0) to maintain ownership such as requester will not get the resources. + client1.getProfile().setResourceHolderRetain(true); + + request = new TunerLnbRequest(); + request.clientId = client1.getId(); + + assertThat(mTunerResourceManagerService.requestLnbInternal(request, lnbHandle)).isFalse(); + assertThat(lnbHandle[0]).isNotEqualTo(lnbHandles[0]); + assertThat(mTunerResourceManagerService.getLnbResource(lnbHandles[0]).isInUse()).isTrue(); + assertThat(mTunerResourceManagerService.getLnbResource(lnbHandles[0]).getOwnerClientId()) + .isEqualTo(client0.getId()); + assertThat(client0.isReclaimed()).isFalse(); + assertThat(client1.getProfile().getInUseLnbHandles().size()).isEqualTo(0); + + // setResourceHolderRetain sets mResourceHolderRetain to false to allow the Resource + // Challenger (client1) to acquire the resource and Resource Holder loses ownership of the + // resources. + client1.getProfile().setResourceHolderRetain(false); + + assertThat(mTunerResourceManagerService.requestLnbInternal(request, lnbHandle)).isTrue(); + assertThat(lnbHandle[0]).isEqualTo(lnbHandles[0]); + assertThat(mTunerResourceManagerService.getLnbResource(lnbHandles[0]).isInUse()).isTrue(); + assertThat(mTunerResourceManagerService.getLnbResource(lnbHandles[0]).getOwnerClientId()) + .isEqualTo(client1.getId()); + assertThat(client0.isReclaimed()).isTrue(); + assertThat(client0.getProfile().getInUseLnbHandles().size()).isEqualTo(0); + } + + @Test public void releaseLnbTest() throws RemoteException { // Register clients TunerClient client0 = new TunerClient(); diff --git a/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java b/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java index def33551a820..aee9f0f3b880 100644 --- a/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java +++ b/services/tests/servicestests/src/com/android/server/webkit/TestSystemImpl.java @@ -36,18 +36,15 @@ public class TestSystemImpl implements SystemInterface { Map<String, Map<Integer, PackageInfo>> mPackages = new HashMap(); private final int mNumRelros; private final boolean mIsDebuggable; - private int mMultiProcessSetting; - private final boolean mMultiProcessDefault; public static final int PRIMARY_USER_ID = 0; - public TestSystemImpl(WebViewProviderInfo[] packageConfigs, int numRelros, boolean isDebuggable, - boolean multiProcessDefault) { + public TestSystemImpl(WebViewProviderInfo[] packageConfigs, int numRelros, + boolean isDebuggable) { mPackageConfigs = packageConfigs; mNumRelros = numRelros; mIsDebuggable = isDebuggable; mUsers.add(PRIMARY_USER_ID); - mMultiProcessDefault = multiProcessDefault; } public void addUser(int userId) { @@ -181,26 +178,8 @@ public class TestSystemImpl implements SystemInterface { } @Override - public int getMultiProcessSetting() { - return mMultiProcessSetting; - } - - @Override - public void setMultiProcessSetting(int value) { - mMultiProcessSetting = value; - } - - @Override - public void notifyZygote(boolean enableMultiProcess) {} - - @Override public void ensureZygoteStarted() {} @Override - public boolean isMultiProcessDefaultEnabled() { - return mMultiProcessDefault; - } - - @Override public void pinWebviewIfRequired(ApplicationInfo appInfo) {} } diff --git a/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java b/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java index 06479c84bfc7..42eb60958d53 100644 --- a/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/webkit/WebViewUpdateServiceTest.java @@ -16,8 +16,6 @@ package com.android.server.webkit; -import static android.webkit.Flags.updateServiceV2; - import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -27,8 +25,6 @@ import android.content.pm.PackageInfo; import android.content.pm.Signature; import android.os.Build; import android.os.Bundle; -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 android.util.Base64; @@ -62,7 +58,7 @@ import java.util.concurrent.CountDownLatch; public class WebViewUpdateServiceTest { private final static String TAG = WebViewUpdateServiceTest.class.getSimpleName(); - private WebViewUpdateServiceInterface mWebViewUpdateServiceImpl; + private WebViewUpdateServiceImpl2 mWebViewUpdateServiceImpl; private TestSystemImpl mTestSystemImpl; private static final String WEBVIEW_LIBRARY_FLAG = "com.android.webview.WebViewLibrary"; @@ -77,38 +73,23 @@ public class WebViewUpdateServiceTest { } private void setupWithPackages(WebViewProviderInfo[] packages) { - setupWithAllParameters(packages, 1 /* numRelros */, true /* isDebuggable */, - false /* multiProcessDefault */); + setupWithAllParameters(packages, 1 /* numRelros */, true /* isDebuggable */); } private void setupWithPackagesAndRelroCount(WebViewProviderInfo[] packages, int numRelros) { - setupWithAllParameters(packages, numRelros, true /* isDebuggable */, - false /* multiProcessDefault */); + setupWithAllParameters(packages, numRelros, true /* isDebuggable */); } private void setupWithPackagesNonDebuggable(WebViewProviderInfo[] packages) { - setupWithAllParameters(packages, 1 /* numRelros */, false /* isDebuggable */, - false /* multiProcessDefault */); - } - - private void setupWithPackagesAndMultiProcess(WebViewProviderInfo[] packages, - boolean multiProcessDefault) { - setupWithAllParameters(packages, 1 /* numRelros */, true /* isDebuggable */, - multiProcessDefault); + setupWithAllParameters(packages, 1 /* numRelros */, false /* isDebuggable */); } private void setupWithAllParameters(WebViewProviderInfo[] packages, int numRelros, - boolean isDebuggable, boolean multiProcessDefault) { - TestSystemImpl testing = new TestSystemImpl(packages, numRelros, isDebuggable, - multiProcessDefault); + boolean isDebuggable) { + TestSystemImpl testing = new TestSystemImpl(packages, numRelros, isDebuggable); mTestSystemImpl = Mockito.spy(testing); - if (updateServiceV2()) { - mWebViewUpdateServiceImpl = - new WebViewUpdateServiceImpl2(mTestSystemImpl); - } else { - mWebViewUpdateServiceImpl = - new WebViewUpdateServiceImpl(mTestSystemImpl); - } + mWebViewUpdateServiceImpl = + new WebViewUpdateServiceImpl2(mTestSystemImpl); } private void setEnabledAndValidPackageInfos(WebViewProviderInfo[] providers) { @@ -350,24 +331,6 @@ public class WebViewUpdateServiceTest { } @Test - @RequiresFlagsDisabled("android.webkit.update_service_v2") - // If the flag is set, will throw an exception because of no available by default provider. - public void testEmptyConfig() { - WebViewProviderInfo[] packages = new WebViewProviderInfo[0]; - setupWithPackages(packages); - setEnabledAndValidPackageInfos(packages); - - runWebViewBootPreparationOnMainSync(); - - Mockito.verify(mTestSystemImpl, Mockito.never()).onWebViewProviderChanged( - Matchers.anyObject()); - - WebViewProviderResponse response = mWebViewUpdateServiceImpl.waitForAndGetProvider(); - assertEquals(WebViewFactory.LIBLOAD_FAILED_LISTING_WEBVIEW_PACKAGES, response.status); - assertEquals(null, mWebViewUpdateServiceImpl.getCurrentWebViewPackage()); - } - - @Test public void testFailListingEmptyWebviewPackages() { String singlePackage = "singlePackage"; WebViewProviderInfo[] packages = new WebViewProviderInfo[]{ @@ -554,73 +517,6 @@ public class WebViewUpdateServiceTest { } } - /** - * Scenario for testing re-enabling a fallback package. - */ - @Test - @RequiresFlagsDisabled("android.webkit.update_service_v2") - public void testFallbackPackageEnabling() { - String testPackage = "testFallback"; - WebViewProviderInfo[] packages = new WebViewProviderInfo[] { - new WebViewProviderInfo( - testPackage, "", true /* default available */, true /* fallback */, null)}; - setupWithPackages(packages); - mTestSystemImpl.setPackageInfo( - createPackageInfo(testPackage, false /* enabled */ , true /* valid */, - true /* installed */)); - - // Check that the boot time logic re-enables the fallback package. - runWebViewBootPreparationOnMainSync(); - Mockito.verify(mTestSystemImpl).enablePackageForAllUsers( - Mockito.eq(testPackage), Mockito.eq(true)); - - // Fake the message about the enabling having changed the package state, - // and check we now use that package. - mWebViewUpdateServiceImpl.packageStateChanged( - testPackage, WebViewUpdateService.PACKAGE_CHANGED, TestSystemImpl.PRIMARY_USER_ID); - checkPreparationPhasesForPackage(testPackage, 1); - } - - /** - * Scenario for installing primary package when secondary in use. - * 1. Start with only secondary installed - * 2. Install primary - * 3. Primary should be used - */ - @Test - @RequiresFlagsDisabled("android.webkit.update_service_v2") - // If the flag is set, we don't automitally switch to secondary package unless it is - // chosen directly. - public void testInstallingPrimaryPackage() { - String primaryPackage = "primary"; - String secondaryPackage = "secondary"; - WebViewProviderInfo[] packages = new WebViewProviderInfo[] { - new WebViewProviderInfo( - primaryPackage, "", true /* default available */, false /* fallback */, null), - new WebViewProviderInfo( - secondaryPackage, "", true /* default available */, false /* fallback */, - null)}; - setupWithPackages(packages); - mTestSystemImpl.setPackageInfo( - createPackageInfo(secondaryPackage, true /* enabled */ , true /* valid */, - true /* installed */)); - - runWebViewBootPreparationOnMainSync(); - checkPreparationPhasesForPackage(secondaryPackage, - 1 /* first preparation for this package*/); - - // Install primary package - mTestSystemImpl.setPackageInfo( - createPackageInfo(primaryPackage, true /* enabled */ , true /* valid */, - true /* installed */)); - mWebViewUpdateServiceImpl.packageStateChanged(primaryPackage, - WebViewUpdateService.PACKAGE_ADDED_REPLACED, TestSystemImpl.PRIMARY_USER_ID); - - // Verify primary package used as provider, and secondary package killed - checkPreparationPhasesForPackage(primaryPackage, 1 /* first preparation for this package*/); - Mockito.verify(mTestSystemImpl).killPackageDependents(Mockito.eq(secondaryPackage)); - } - @Test public void testRemovingSecondarySelectsPrimarySingleUser() { for (PackageRemovalType removalType : REMOVAL_TYPES) { @@ -848,14 +744,6 @@ public class WebViewUpdateServiceTest { checkRecoverAfterFailListingWebviewPackages(true); } - @Test - @RequiresFlagsDisabled("android.webkit.update_service_v2") - // If the flag is set, we don't automitally switch to second package unless it is chosen - // directly. - public void testRecoverFailedListingWebViewPackagesAddedPackage() { - checkRecoverAfterFailListingWebviewPackages(false); - } - /** * Test that we can recover correctly from failing to list WebView packages. * settingsChange: whether to fail during changeProviderAndSetting or packageStateChanged @@ -1114,31 +1002,6 @@ public class WebViewUpdateServiceTest { } } - /** - * Ensure that the update service does not use an uninstalled package even if it is the only - * package available. - */ - @Test - @RequiresFlagsDisabled("android.webkit.update_service_v2") - // If the flag is set, we return the package even if it is not installed. - public void testWithSingleUninstalledPackage() { - String testPackageName = "test.package.name"; - WebViewProviderInfo[] webviewPackages = new WebViewProviderInfo[] { - new WebViewProviderInfo(testPackageName, "", - true /*default available*/, false /* fallback */, null)}; - setupWithPackages(webviewPackages); - mTestSystemImpl.setPackageInfo(createPackageInfo(testPackageName, true /* enabled */, - true /* valid */, false /* installed */)); - - runWebViewBootPreparationOnMainSync(); - - Mockito.verify(mTestSystemImpl, Mockito.never()).onWebViewProviderChanged( - Matchers.anyObject()); - WebViewProviderResponse response = mWebViewUpdateServiceImpl.waitForAndGetProvider(); - assertEquals(WebViewFactory.LIBLOAD_FAILED_LISTING_WEBVIEW_PACKAGES, response.status); - assertEquals(null, mWebViewUpdateServiceImpl.getCurrentWebViewPackage()); - } - @Test public void testNonhiddenPackageUserOverHidden() { checkVisiblePackageUserOverNonVisible(false /* multiUser*/, PackageRemovalType.HIDE); @@ -1374,95 +1237,6 @@ public class WebViewUpdateServiceTest { mWebViewUpdateServiceImpl.getCurrentWebViewPackage().versionName); } - @Test - @RequiresFlagsDisabled("android.webkit.update_service_v2") - public void testMultiProcessEnabledByDefault() { - testMultiProcessByDefault(true /* enabledByDefault */); - } - - @Test - @RequiresFlagsDisabled("android.webkit.update_service_v2") - public void testMultiProcessDisabledByDefault() { - testMultiProcessByDefault(false /* enabledByDefault */); - } - - private void testMultiProcessByDefault(boolean enabledByDefault) { - String primaryPackage = "primary"; - WebViewProviderInfo[] packages = new WebViewProviderInfo[] { - new WebViewProviderInfo( - primaryPackage, "", true /* default available */, false /* fallback */, null)}; - setupWithPackagesAndMultiProcess(packages, enabledByDefault); - mTestSystemImpl.setPackageInfo(createPackageInfo(primaryPackage, true /* enabled */, - true /* valid */, true /* installed */, null /* signatures */, - 10 /* lastUpdateTime*/, false /* not hidden */, 1000 /* versionCode */, - false /* isSystemApp */)); - - runWebViewBootPreparationOnMainSync(); - checkPreparationPhasesForPackage(primaryPackage, 1 /* first preparation phase */); - - // Check it's off by default - assertEquals(enabledByDefault, mWebViewUpdateServiceImpl.isMultiProcessEnabled()); - - // Test toggling it - mWebViewUpdateServiceImpl.enableMultiProcess(!enabledByDefault); - assertEquals(!enabledByDefault, mWebViewUpdateServiceImpl.isMultiProcessEnabled()); - mWebViewUpdateServiceImpl.enableMultiProcess(enabledByDefault); - assertEquals(enabledByDefault, mWebViewUpdateServiceImpl.isMultiProcessEnabled()); - } - - @Test - @RequiresFlagsDisabled("android.webkit.update_service_v2") - public void testMultiProcessEnabledByDefaultWithSettingsValue() { - testMultiProcessByDefaultWithSettingsValue( - true /* enabledByDefault */, Integer.MIN_VALUE, false /* expectEnabled */); - testMultiProcessByDefaultWithSettingsValue( - true /* enabledByDefault */, -999999, true /* expectEnabled */); - testMultiProcessByDefaultWithSettingsValue( - true /* enabledByDefault */, 0, true /* expectEnabled */); - testMultiProcessByDefaultWithSettingsValue( - true /* enabledByDefault */, 999999, true /* expectEnabled */); - } - - @Test - @RequiresFlagsDisabled("android.webkit.update_service_v2") - public void testMultiProcessDisabledByDefaultWithSettingsValue() { - testMultiProcessByDefaultWithSettingsValue( - false /* enabledByDefault */, Integer.MIN_VALUE, false /* expectEnabled */); - testMultiProcessByDefaultWithSettingsValue( - false /* enabledByDefault */, 0, false /* expectEnabled */); - testMultiProcessByDefaultWithSettingsValue( - false /* enabledByDefault */, 999999, false /* expectEnabled */); - testMultiProcessByDefaultWithSettingsValue( - false /* enabledByDefault */, Integer.MAX_VALUE, true /* expectEnabled */); - } - - /** - * Test the logic of the multiprocess setting depending on whether multiprocess is enabled by - * default, and what the setting is set to. - * @param enabledByDefault whether multiprocess is enabled by default. - * @param settingValue value of the multiprocess setting. - */ - private void testMultiProcessByDefaultWithSettingsValue( - boolean enabledByDefault, int settingValue, boolean expectEnabled) { - String primaryPackage = "primary"; - WebViewProviderInfo[] packages = new WebViewProviderInfo[] { - new WebViewProviderInfo( - primaryPackage, "", true /* default available */, false /* fallback */, null)}; - setupWithPackagesAndMultiProcess(packages, enabledByDefault); - mTestSystemImpl.setPackageInfo(createPackageInfo(primaryPackage, true /* enabled */, - true /* valid */, true /* installed */, null /* signatures */, - 10 /* lastUpdateTime*/, false /* not hidden */, 1000 /* versionCode */, - false /* isSystemApp */)); - - runWebViewBootPreparationOnMainSync(); - checkPreparationPhasesForPackage(primaryPackage, 1 /* first preparation phase */); - - mTestSystemImpl.setMultiProcessSetting(settingValue); - - assertEquals(expectEnabled, mWebViewUpdateServiceImpl.isMultiProcessEnabled()); - } - - /** * Ensure that packages with a targetSdkVersion targeting the current platform are valid, and * that packages targeting an older version are not valid. @@ -1507,7 +1281,6 @@ public class WebViewUpdateServiceTest { } @Test - @RequiresFlagsEnabled("android.webkit.update_service_v2") public void testDefaultWebViewPackageIsTheFirstAvailableByDefault() { String nonDefaultPackage = "nonDefaultPackage"; String defaultPackage1 = "defaultPackage1"; @@ -1524,7 +1297,6 @@ public class WebViewUpdateServiceTest { } @Test - @RequiresFlagsEnabled("android.webkit.update_service_v2") public void testDefaultWebViewPackageEnabling() { String testPackage = "testDefault"; WebViewProviderInfo[] packages = @@ -1548,7 +1320,6 @@ public class WebViewUpdateServiceTest { } @Test - @RequiresFlagsEnabled("android.webkit.update_service_v2") public void testDefaultWebViewPackageInstallingDuringStartUp() { String testPackage = "testDefault"; WebViewProviderInfo[] packages = @@ -1572,7 +1343,6 @@ public class WebViewUpdateServiceTest { } @Test - @RequiresFlagsEnabled("android.webkit.update_service_v2") public void testDefaultWebViewPackageInstallingAfterStartUp() { String testPackage = "testDefault"; WebViewProviderInfo[] packages = @@ -1603,7 +1373,6 @@ public class WebViewUpdateServiceTest { * the repair logic. */ @Test - @RequiresFlagsEnabled("android.webkit.update_service_v2") public void testAddingNewUserWithDefaultdPackageNotInstalled() { String testPackage = "testDefault"; WebViewProviderInfo[] packages = @@ -1651,7 +1420,6 @@ public class WebViewUpdateServiceTest { } @Test - @RequiresFlagsEnabled("android.webkit.update_service_v2") public void testDisabledDefaultPackageChosen() { PackageInfo disabledPackage = createPackageInfo( @@ -1664,7 +1432,6 @@ public class WebViewUpdateServiceTest { } @Test - @RequiresFlagsEnabled("android.webkit.update_service_v2") public void testUninstalledDefaultPackageChosen() { PackageInfo uninstalledPackage = createPackageInfo( 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 dec7f09f2da4..cbfdc5f61e3f 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -77,6 +77,8 @@ import static android.app.PendingIntent.FLAG_MUTABLE; import static android.app.PendingIntent.FLAG_ONE_SHOT; import static android.app.StatusBarManager.ACTION_KEYGUARD_PRIVATE_NOTIFICATIONS_CHANGED; import static android.app.StatusBarManager.EXTRA_KM_PRIVATE_NOTIFS_ALLOWED; +import static android.app.backup.NotificationLoggingConstants.DATA_TYPE_ZEN_CONFIG; +import static android.app.backup.NotificationLoggingConstants.DATA_TYPE_ZEN_RULES; import static android.content.pm.ActivityInfo.RESIZE_MODE_RESIZEABLE; import static android.content.pm.PackageManager.FEATURE_TELECOM; import static android.content.pm.PackageManager.FEATURE_WATCH; @@ -92,6 +94,7 @@ import static android.os.PowerWhitelistManager.REASON_NOTIFICATION_SERVICE; import static android.os.PowerWhitelistManager.TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_ALLOWED; import static android.os.UserHandle.USER_SYSTEM; import static android.os.UserManager.USER_TYPE_FULL_SECONDARY; +import static android.os.UserManager.USER_TYPE_FULL_SYSTEM; import static android.os.UserManager.USER_TYPE_PROFILE_CLONE; import static android.os.UserManager.USER_TYPE_PROFILE_MANAGED; import static android.os.UserManager.USER_TYPE_PROFILE_PRIVATE; @@ -109,6 +112,7 @@ import static android.service.notification.Condition.SOURCE_CONTEXT; import static android.service.notification.Condition.SOURCE_USER_ACTION; import static android.service.notification.Condition.STATE_TRUE; import static android.service.notification.Flags.FLAG_NOTIFICATION_CLASSIFICATION; +import static android.service.notification.Flags.FLAG_NOTIFICATION_CONVERSATION_CHANNEL_MANAGEMENT; import static android.service.notification.Flags.FLAG_NOTIFICATION_FORCE_GROUPING; import static android.service.notification.Flags.FLAG_REDACT_SENSITIVE_NOTIFICATIONS_FROM_UNTRUSTED_LISTENERS; import static android.service.notification.NotificationListenerService.FLAG_FILTER_TYPE_ALERTING; @@ -262,6 +266,7 @@ import android.os.WorkSource; import android.permission.PermissionManager; import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; +import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.FlagsParameterization; import android.platform.test.flag.junit.SetFlagsRule; import android.platform.test.rule.LimitDevicesRule; @@ -361,6 +366,7 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileOutputStream; +import java.io.OutputStream; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; @@ -481,6 +487,15 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { NotificationChannel mMinChannel = new NotificationChannel("min", "min", IMPORTANCE_MIN); + private final NotificationChannel mParentChannel = + new NotificationChannel(PARENT_CHANNEL_ID, "parentName", IMPORTANCE_DEFAULT); + private final NotificationChannel mConversationChannel = + new NotificationChannel(CONVERSATION_CHANNEL_ID, "conversationName", IMPORTANCE_DEFAULT); + + private static final String PARENT_CHANNEL_ID = "parentChannelId"; + private static final String CONVERSATION_CHANNEL_ID = "conversationChannelId"; + private static final String CONVERSATION_ID = "conversationId"; + private static final int NOTIFICATION_LOCATION_UNKNOWN = 0; private static final String VALID_CONVO_SHORTCUT_ID = "shortcut"; @@ -2880,6 +2895,44 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, + android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST}) + public void testRemoveScheduledForceGroup_onNotificationCanceled() throws Exception { + NotificationRecord r = generateNotificationRecord(mTestNotificationChannel, 0, "tag", null, + false); + when(mGroupHelper.onNotificationPosted(any(), anyBoolean())).thenReturn(false); + mService.addEnqueuedNotification(r); + NotificationManagerService.PostNotificationRunnable runnable = + mService.new PostNotificationRunnable(r.getKey(), r.getSbn().getPackageName(), + r.getUid(), mPostNotificationTrackerFactory.newTracker(null)); + runnable.run(); + waitForIdle(); + + // Post an update to the notification + NotificationRecord r_update = + generateNotificationRecord(mTestNotificationChannel, 0, "tag", null, false); + mService.addEnqueuedNotification(r_update); + runnable = mService.new PostNotificationRunnable(r_update.getKey(), + r_update.getSbn().getPackageName(), r_update.getUid(), + mPostNotificationTrackerFactory.newTracker(null)); + runnable.run(); + waitForIdle(); + + // Cancel the notification + mBinderService.cancelNotificationWithTag(r.getSbn().getPackageName(), + r.getSbn().getPackageName(), r.getSbn().getTag(), + r.getSbn().getId(), r.getSbn().getUserId()); + waitForIdle(); + + mTestableLooper.moveTimeForward(DELAY_FORCE_REGROUP_TIME); + waitForIdle(); + + // Check that onNotificationPostedWithDelay was canceled + verify(mGroupHelper, times(1)).onNotificationPosted(any(), anyBoolean()); + verify(mGroupHelper, never()).onNotificationPostedWithDelay(any(), any(), any()); + } + + @Test @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING) public void testEnqueueNotification_forceGrouped_clearsSummaryFlag() throws Exception { final String originalGroupName = "originalGroup"; @@ -4668,8 +4721,161 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { verify(mAmi).hasForegroundServiceNotification(anyString(), anyInt(), anyString()); } + private void setUpChannelsForConversationChannelTest() throws RemoteException { + when(mPreferencesHelper.getNotificationChannel( + eq(mPkg), eq(mUid), eq(PARENT_CHANNEL_ID), eq(false))) + .thenReturn(mParentChannel); + when(mPreferencesHelper.getConversationNotificationChannel( + eq(mPkg), eq(mUid), eq(PARENT_CHANNEL_ID), eq(CONVERSATION_ID), eq(false), eq(false))) + .thenReturn(mConversationChannel); + when(mPackageManager.getPackageUid(mPkg, 0, mUserId)).thenReturn(mUid); + } + + @Test + @RequiresFlagsEnabled(FLAG_NOTIFICATION_CONVERSATION_CHANNEL_MANAGEMENT) + public void createConversationChannelForPkgFromPrivilegedListener_cdm_success() throws Exception { + // Set up cdm + mService.setPreferencesHelper(mPreferencesHelper); + when(mCompanionMgr.getAssociations(mPkg, mUserId)) + .thenReturn(singletonList(mock(AssociationInfo.class))); + + // Set up parent channel + setUpChannelsForConversationChannelTest(); + final NotificationChannel parentChannelCopy = mParentChannel.copy(); + + NotificationChannel createdChannel = + mBinderService.createConversationNotificationChannelForPackageFromPrivilegedListener( + null, mPkg, mUser, PARENT_CHANNEL_ID, CONVERSATION_ID); + + // Verify that a channel is created and a copied channel is returned. + verify(mPreferencesHelper, times(1)).createNotificationChannel( + eq(mPkg), eq(mUid), any(), anyBoolean(), anyBoolean(), + eq(mUid), anyBoolean()); + assertThat(createdChannel).isNotSameInstanceAs(mConversationChannel); + assertThat(createdChannel).isEqualTo(mConversationChannel); + + // Verify that the channel creation is not directly use the parent channel. + verify(mPreferencesHelper, never()).createNotificationChannel( + anyString(), anyInt(), eq(mParentChannel), anyBoolean(), anyBoolean(), + anyInt(), anyBoolean()); + + // Verify that the content of parent channel is not changed. + assertThat(parentChannelCopy).isEqualTo(mParentChannel); + } + + @Test + @RequiresFlagsEnabled(FLAG_NOTIFICATION_CONVERSATION_CHANNEL_MANAGEMENT) + public void createConversationChannelForPkgFromPrivilegedListener_cdm_noAccess() throws Exception { + // Set up cdm without access + mService.setPreferencesHelper(mPreferencesHelper); + when(mCompanionMgr.getAssociations(mPkg, mUserId)) + .thenReturn(emptyList()); + + // Set up parent channel + setUpChannelsForConversationChannelTest(); + + try { + mBinderService.createConversationNotificationChannelForPackageFromPrivilegedListener( + null, mPkg, mUser, "parentId", "conversationId"); + fail("listeners that don't have a companion device shouldn't be able to call this"); + } catch (SecurityException e) { + // pass + } + + verify(mPreferencesHelper, never()).createNotificationChannel( + anyString(), anyInt(), any(), anyBoolean(), anyBoolean(), + anyInt(), anyBoolean()); + } + + @Test + @RequiresFlagsEnabled(FLAG_NOTIFICATION_CONVERSATION_CHANNEL_MANAGEMENT) + public void createConversationChannelForPkgFromPrivilegedListener_assistant_success() throws Exception { + // Set up assistant + mService.setPreferencesHelper(mPreferencesHelper); + when(mCompanionMgr.getAssociations(mPkg, mUserId)) + .thenReturn(emptyList()); + when(mAssistants.isServiceTokenValidLocked(any())).thenReturn(true); + + // Set up parent channel + setUpChannelsForConversationChannelTest(); + final NotificationChannel parentChannelCopy = mParentChannel.copy(); + + NotificationChannel createdChannel = + mBinderService.createConversationNotificationChannelForPackageFromPrivilegedListener( + null, mPkg, mUser, PARENT_CHANNEL_ID, CONVERSATION_ID); + + // Verify that a channel is created and a copied channel is returned. + verify(mPreferencesHelper, times(1)).createNotificationChannel( + eq(mPkg), eq(mUid), any(), anyBoolean(), anyBoolean(), + eq(mUid), anyBoolean()); + assertThat(createdChannel).isNotSameInstanceAs(mConversationChannel); + assertThat(createdChannel).isEqualTo(mConversationChannel); + + // Verify that the channel creation is not directly use the parent channel. + verify(mPreferencesHelper, never()).createNotificationChannel( + anyString(), anyInt(), eq(mParentChannel), anyBoolean(), anyBoolean(), + anyInt(), anyBoolean()); + + // Verify that the content of parent channel is not changed. + assertThat(parentChannelCopy).isEqualTo(mParentChannel); + } + + @Test + @RequiresFlagsEnabled(FLAG_NOTIFICATION_CONVERSATION_CHANNEL_MANAGEMENT) + public void createConversationChannelForPkgFromPrivilegedListener_assistant_noAccess() throws Exception { + // Set up assistant without access + mService.setPreferencesHelper(mPreferencesHelper); + when(mCompanionMgr.getAssociations(mPkg, mUserId)) + .thenReturn(emptyList()); + when(mAssistants.isServiceTokenValidLocked(any())).thenReturn(false); + + // Set up parent channel + setUpChannelsForConversationChannelTest(); + + try { + mBinderService.createConversationNotificationChannelForPackageFromPrivilegedListener( + null, mPkg, mUser, "parentId", "conversationId"); + fail("listeners that don't have a companion device shouldn't be able to call this"); + } catch (SecurityException e) { + // pass + } + + verify(mPreferencesHelper, never()).createNotificationChannel( + anyString(), anyInt(), any(), anyBoolean(), anyBoolean(), + anyInt(), anyBoolean()); + } + + @Test + @RequiresFlagsEnabled(FLAG_NOTIFICATION_CONVERSATION_CHANNEL_MANAGEMENT) + public void createConversationChannelForPkgFromPrivilegedListener_badUser() throws Exception { + // Set up bad user + mService.setPreferencesHelper(mPreferencesHelper); + when(mCompanionMgr.getAssociations(mPkg, mUserId)) + .thenReturn(singletonList(mock(AssociationInfo.class))); + mListener = mock(ManagedServices.ManagedServiceInfo.class); + mListener.component = new ComponentName(mPkg, mPkg); + when(mListener.enabledAndUserMatches(anyInt())).thenReturn(false); + when(mListeners.checkServiceTokenLocked(any())).thenReturn(mListener); + + // Set up parent channel + setUpChannelsForConversationChannelTest(); + + try { + mBinderService.createConversationNotificationChannelForPackageFromPrivilegedListener( + null, mPkg, mUser, "parentId", "conversationId"); + fail("listener getting channels from a user they cannot see"); + } catch (SecurityException e) { + // pass + } + + verify(mPreferencesHelper, never()).createNotificationChannel( + anyString(), anyInt(), any(), anyBoolean(), anyBoolean(), + anyInt(), anyBoolean()); + } + @Test - public void testUpdateNotificationChannelFromPrivilegedListener_success() throws Exception { + public void updateNotificationChannelFromPrivilegedListener_cdm_success() throws Exception { + mService.setPreferencesHelper(mPreferencesHelper); when(mCompanionMgr.getAssociations(mPkg, mUserId)) .thenReturn(singletonList(mock(AssociationInfo.class))); @@ -4689,10 +4895,54 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - public void testUpdateNotificationChannelFromPrivilegedListener_noAccess() throws Exception { + public void updateNotificationChannelFromPrivilegedListener_cdm_noAccess() throws Exception { + mService.setPreferencesHelper(mPreferencesHelper); + when(mCompanionMgr.getAssociations(mPkg, mUserId)) + .thenReturn(emptyList()); + + try { + mBinderService.updateNotificationChannelFromPrivilegedListener( + null, mPkg, Process.myUserHandle(), mTestNotificationChannel); + fail("listeners that don't have a companion device shouldn't be able to call this"); + } catch (SecurityException e) { + // pass + } + + verify(mPreferencesHelper, never()).updateNotificationChannel( + anyString(), anyInt(), any(), anyBoolean(), anyInt(), anyBoolean()); + + verify(mListeners, never()).notifyNotificationChannelChanged(eq(mPkg), + eq(Process.myUserHandle()), eq(mTestNotificationChannel), + eq(NotificationListenerService.NOTIFICATION_CHANNEL_OR_GROUP_UPDATED)); + } + + @Test + public void updateNotificationChannelFromPrivilegedListener_assistant_success() throws Exception { + mService.setPreferencesHelper(mPreferencesHelper); + when(mCompanionMgr.getAssociations(mPkg, mUserId)) + .thenReturn(emptyList()); + when(mAssistants.isServiceTokenValidLocked(any())).thenReturn(true); + when(mPreferencesHelper.getNotificationChannel(eq(mPkg), anyInt(), + eq(mTestNotificationChannel.getId()), anyBoolean())) + .thenReturn(mTestNotificationChannel); + + mBinderService.updateNotificationChannelFromPrivilegedListener( + null, mPkg, Process.myUserHandle(), mTestNotificationChannel); + + verify(mPreferencesHelper, times(1)).updateNotificationChannel( + anyString(), anyInt(), any(), anyBoolean(), anyInt(), anyBoolean()); + + verify(mListeners, never()).notifyNotificationChannelChanged(eq(mPkg), + eq(Process.myUserHandle()), eq(mTestNotificationChannel), + eq(NotificationListenerService.NOTIFICATION_CHANNEL_OR_GROUP_UPDATED)); + } + + @Test + public void updateNotificationChannelFromPrivilegedListener_assistant_noAccess() throws Exception { mService.setPreferencesHelper(mPreferencesHelper); when(mCompanionMgr.getAssociations(mPkg, mUserId)) .thenReturn(emptyList()); + when(mAssistants.isServiceTokenValidLocked(any())).thenReturn(false); try { mBinderService.updateNotificationChannelFromPrivilegedListener( @@ -4711,7 +4961,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - public void testUpdateNotificationChannelFromPrivilegedListener_badUser() throws Exception { + public void updateNotificationChannelFromPrivilegedListener_badUser() throws Exception { mService.setPreferencesHelper(mPreferencesHelper); when(mCompanionMgr.getAssociations(mPkg, mUserId)) .thenReturn(singletonList(mock(AssociationInfo.class))); @@ -4737,7 +4987,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - public void testUpdateNotificationChannelFromPrivilegedListener_noSoundUriPermission() + public void updateNotificationChannelFromPrivilegedListener_noSoundUriPermission() throws Exception { mService.setPreferencesHelper(mPreferencesHelper); when(mCompanionMgr.getAssociations(mPkg, mUserId)) @@ -4769,7 +5019,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test - public void testUpdateNotificationChannelFromPrivilegedListener_noSoundUriPermission_sameSound() + public void updateNotificationChannelFromPrivilegedListener_noSoundUriPermission_sameSound() throws Exception { mService.setPreferencesHelper(mPreferencesHelper); when(mCompanionMgr.getAssociations(mPkg, mUserId)) @@ -4801,7 +5051,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { @Test public void - testUpdateNotificationChannelFromPrivilegedListener_oldSoundNoUriPerm_newSoundHasUriPerm() + updateNotificationChannelFromPrivilegedListener_oldSoundNoUriPerm_newSoundHasUriPerm() throws Exception { mService.setPreferencesHelper(mPreferencesHelper); when(mCompanionMgr.getAssociations(mPkg, mUserId)) @@ -6499,6 +6749,35 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { } @Test + @EnableFlags(android.app.Flags.FLAG_BACKUP_RESTORE_LOGGING) + public void testReadPolicyXml_backupRestoreLogging() throws Exception { + BackupRestoreEventLogger logger = mock(BackupRestoreEventLogger.class); + + UserInfo ui = new UserInfo(ActivityManager.getCurrentUser(), "Clone", UserInfo.FLAG_FULL); + ui.userType = USER_TYPE_FULL_SYSTEM; + when(mUmInternal.getUserInfo(ActivityManager.getCurrentUser())).thenReturn(ui); + when(mPermissionHelper.getNotificationPermissionValues(0)).thenReturn(new ArrayMap<>()); + TypedXmlSerializer serializer = Xml.newFastSerializer(); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + serializer.setOutput(new BufferedOutputStream(baos), "utf-8"); + serializer.startDocument(null, true); + mService.writePolicyXml(baos, true, ActivityManager.getCurrentUser(), logger); + serializer.flush(); + + mService.readPolicyXml( + new BufferedInputStream(new ByteArrayInputStream(baos.toByteArray())), + true, ActivityManager.getCurrentUser(), logger); + + verify(logger).logItemsBackedUp(DATA_TYPE_ZEN_CONFIG, 1); + verify(logger, never()) + .logItemsBackupFailed(eq(DATA_TYPE_ZEN_CONFIG), anyInt(), anyString()); + + verify(logger).logItemsRestored(DATA_TYPE_ZEN_CONFIG, 1); + verify(logger, never()) + .logItemsRestoreFailed(eq(DATA_TYPE_ZEN_CONFIG), anyInt(), anyString()); + } + + @Test public void testLocaleChangedCallsUpdateDefaultZenModeRules() throws Exception { ZenModeHelper mZenModeHelper = mock(ZenModeHelper.class); mService.mZenModeHelper = mZenModeHelper; @@ -7662,7 +7941,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mService.mZenModeHelper = mZenModeHelper; NotificationManager.Policy userPolicy = new NotificationManager.Policy(0, 0, 0, SUPPRESSED_EFFECT_BADGE); - when(mZenModeHelper.getNotificationPolicy()).thenReturn(userPolicy); + when(mZenModeHelper.getNotificationPolicy(any())).thenReturn(userPolicy); NotificationManager.Policy appPolicy = new NotificationManager.Policy(0, 0, 0, SUPPRESSED_EFFECT_SCREEN_ON | SUPPRESSED_EFFECT_SCREEN_OFF); @@ -7682,7 +7961,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mService.mZenModeHelper = mZenModeHelper; NotificationManager.Policy userPolicy = new NotificationManager.Policy(0, 0, 0, SUPPRESSED_EFFECT_BADGE); - when(mZenModeHelper.getNotificationPolicy()).thenReturn(userPolicy); + when(mZenModeHelper.getNotificationPolicy(any())).thenReturn(userPolicy); NotificationManager.Policy appPolicy = new NotificationManager.Policy(0, 0, 0, SUPPRESSED_EFFECT_NOTIFICATION_LIST); @@ -7699,7 +7978,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mService.mZenModeHelper = mZenModeHelper; NotificationManager.Policy userPolicy = new NotificationManager.Policy(0, 0, 0, SUPPRESSED_EFFECT_BADGE); - when(mZenModeHelper.getNotificationPolicy()).thenReturn(userPolicy); + when(mZenModeHelper.getNotificationPolicy(any())).thenReturn(userPolicy); NotificationManager.Policy appPolicy = new NotificationManager.Policy(0, 0, 0, SUPPRESSED_EFFECT_SCREEN_ON | SUPPRESSED_EFFECT_STATUS_BAR); @@ -7717,7 +7996,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mService.mZenModeHelper = mZenModeHelper; NotificationManager.Policy userPolicy = new NotificationManager.Policy(0, 0, 0, SUPPRESSED_EFFECT_BADGE); - when(mZenModeHelper.getNotificationPolicy()).thenReturn(userPolicy); + when(mZenModeHelper.getNotificationPolicy(any())).thenReturn(userPolicy); NotificationManager.Policy appPolicy = new NotificationManager.Policy(0, 0, 0, SUPPRESSED_EFFECT_SCREEN_ON | SUPPRESSED_EFFECT_SCREEN_OFF); @@ -7736,7 +8015,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mService.mZenModeHelper = mZenModeHelper; NotificationManager.Policy userPolicy = new NotificationManager.Policy(0, 0, 0, SUPPRESSED_EFFECT_BADGE); - when(mZenModeHelper.getNotificationPolicy()).thenReturn(userPolicy); + when(mZenModeHelper.getNotificationPolicy(any())).thenReturn(userPolicy); NotificationManager.Policy appPolicy = new NotificationManager.Policy(0, 0, 0, SUPPRESSED_EFFECT_NOTIFICATION_LIST | SUPPRESSED_EFFECT_AMBIENT @@ -7756,7 +8035,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mService.mZenModeHelper = mZenModeHelper; NotificationManager.Policy userPolicy = new NotificationManager.Policy(0, 0, 0, SUPPRESSED_EFFECT_BADGE); - when(mZenModeHelper.getNotificationPolicy()).thenReturn(userPolicy); + when(mZenModeHelper.getNotificationPolicy(any())).thenReturn(userPolicy); NotificationManager.Policy appPolicy = new NotificationManager.Policy(0, 0, 0, SUPPRESSED_EFFECT_SCREEN_ON | SUPPRESSED_EFFECT_STATUS_BAR); @@ -10398,7 +10677,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mBinderService.addAutomaticZenRule(rule, "com.android.settings", false); // verify that zen mode helper gets passed in a package name of "android" - verify(mockZenModeHelper).addAutomaticZenRule(eq("android"), eq(rule), + verify(mockZenModeHelper).addAutomaticZenRule(any(), eq("android"), eq(rule), eq(ZenModeConfig.ORIGIN_SYSTEM), anyString(), anyInt()); } @@ -10420,7 +10699,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mBinderService.addAutomaticZenRule(rule, "com.android.settings", false); // verify that zen mode helper gets passed in a package name of "android" - verify(mockZenModeHelper).addAutomaticZenRule(eq("android"), eq(rule), + verify(mockZenModeHelper).addAutomaticZenRule(any(), eq("android"), eq(rule), eq(ZenModeConfig.ORIGIN_SYSTEM), anyString(), anyInt()); } @@ -10440,9 +10719,9 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mBinderService.addAutomaticZenRule(rule, "another.package", false); // verify that zen mode helper gets passed in the package name from the arg, not the owner - verify(mockZenModeHelper).addAutomaticZenRule( - eq("another.package"), eq(rule), eq(ZenModeConfig.ORIGIN_APP), - anyString(), anyInt()); // doesn't count as a system/systemui call + verify(mockZenModeHelper).addAutomaticZenRule(any(), eq("another.package"), eq(rule), + eq(ZenModeConfig.ORIGIN_APP), anyString(), + anyInt()); // doesn't count as a system/systemui call } @Test @@ -10459,7 +10738,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mBinderService.addAutomaticZenRule(rule, mPkg, /* fromUser= */ false); - verify(zenModeHelper).addAutomaticZenRule(eq(mPkg), eq(rule), anyInt(), any(), anyInt()); + verify(zenModeHelper).addAutomaticZenRule(any(), eq(mPkg), eq(rule), anyInt(), any(), + anyInt()); } @Test @@ -10494,7 +10774,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mBinderService.addAutomaticZenRule(rule, mPkg, /* fromUser= */ false); - verify(zenModeHelper).addAutomaticZenRule(eq(mPkg), eq(rule), anyInt(), any(), anyInt()); + verify(zenModeHelper).addAutomaticZenRule(any(), eq(mPkg), eq(rule), anyInt(), any(), + anyInt()); } @Test @@ -10527,7 +10808,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mBinderService.addAutomaticZenRule(rule, mPkg, /* fromUser= */ false); - verify(zenModeHelper).addAutomaticZenRule(eq(mPkg), eq(rule), anyInt(), any(), anyInt()); + verify(zenModeHelper).addAutomaticZenRule(any(), eq(mPkg), eq(rule), anyInt(), any(), + anyInt()); } private void addAutomaticZenRule_restrictedRuleTypeCannotBeUsedByRegularApps( @@ -10555,7 +10837,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mBinderService.addAutomaticZenRule(SOME_ZEN_RULE, "pkg", /* fromUser= */ true); - verify(zenModeHelper).addAutomaticZenRule(eq("pkg"), eq(SOME_ZEN_RULE), + verify(zenModeHelper).addAutomaticZenRule(any(), eq("pkg"), eq(SOME_ZEN_RULE), eq(ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI), anyString(), anyInt()); } @@ -10567,7 +10849,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mBinderService.addAutomaticZenRule(SOME_ZEN_RULE, "pkg", /* fromUser= */ false); - verify(zenModeHelper).addAutomaticZenRule(eq("pkg"), eq(SOME_ZEN_RULE), + verify(zenModeHelper).addAutomaticZenRule(any(), eq("pkg"), eq(SOME_ZEN_RULE), eq(ZenModeConfig.ORIGIN_SYSTEM), anyString(), anyInt()); } @@ -10579,7 +10861,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mBinderService.addAutomaticZenRule(SOME_ZEN_RULE, "pkg", /* fromUser= */ false); - verify(zenModeHelper).addAutomaticZenRule(eq("pkg"), eq(SOME_ZEN_RULE), + verify(zenModeHelper).addAutomaticZenRule(any(), eq("pkg"), eq(SOME_ZEN_RULE), eq(ZenModeConfig.ORIGIN_APP), anyString(), anyInt()); } @@ -10601,7 +10883,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mBinderService.updateAutomaticZenRule("id", SOME_ZEN_RULE, /* fromUser= */ true); - verify(zenModeHelper).updateAutomaticZenRule(eq("id"), eq(SOME_ZEN_RULE), + verify(zenModeHelper).updateAutomaticZenRule(any(), eq("id"), eq(SOME_ZEN_RULE), eq(ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI), anyString(), anyInt()); } @@ -10623,7 +10905,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mBinderService.removeAutomaticZenRule("id", /* fromUser= */ true); - verify(zenModeHelper).removeAutomaticZenRule(eq("id"), + verify(zenModeHelper).removeAutomaticZenRule(any(), eq("id"), eq(ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI), anyString(), anyInt()); } @@ -10648,7 +10930,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { SOURCE_USER_ACTION); mBinderService.setAutomaticZenRuleState("id", withSourceUser); - verify(zenModeHelper).setAutomaticZenRuleState(eq("id"), eq(withSourceUser), + verify(zenModeHelper).setAutomaticZenRuleState(any(), eq("id"), eq(withSourceUser), eq(ZenModeConfig.ORIGIN_USER_IN_APP), anyInt()); } @@ -10663,7 +10945,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { SOURCE_CONTEXT); mBinderService.setAutomaticZenRuleState("id", withSourceContext); - verify(zenModeHelper).setAutomaticZenRuleState(eq("id"), eq(withSourceContext), + verify(zenModeHelper).setAutomaticZenRuleState(any(), eq("id"), eq(withSourceContext), eq(ZenModeConfig.ORIGIN_APP), anyInt()); } @@ -10678,7 +10960,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { SOURCE_USER_ACTION); mBinderService.setAutomaticZenRuleState("id", withSourceContext); - verify(zenModeHelper).setAutomaticZenRuleState(eq("id"), eq(withSourceContext), + verify(zenModeHelper).setAutomaticZenRuleState(any(), eq("id"), eq(withSourceContext), eq(ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI), anyInt()); } @Test @@ -10692,10 +10974,35 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { SOURCE_CONTEXT); mBinderService.setAutomaticZenRuleState("id", withSourceContext); - verify(zenModeHelper).setAutomaticZenRuleState(eq("id"), eq(withSourceContext), + verify(zenModeHelper).setAutomaticZenRuleState(any(), eq("id"), eq(withSourceContext), eq(ZenModeConfig.ORIGIN_SYSTEM), anyInt()); } + + @Test + @EnableFlags(android.app.Flags.FLAG_MODES_MULTIUSER) + public void getAutomaticZenRules_fromSystem_readsWithCurrentUser() throws Exception { + ZenModeHelper zenModeHelper = setUpMockZenTest(); + mService.isSystemUid = true; + + // Representative used to verify getCallingZenUser(). + mBinderService.getAutomaticZenRules(); + + verify(zenModeHelper).getAutomaticZenRules(eq(UserHandle.CURRENT)); + } + + @Test + @EnableFlags(android.app.Flags.FLAG_MODES_MULTIUSER) + public void getAutomaticZenRules_fromNormalPackage_readsWithBinderUser() throws Exception { + ZenModeHelper zenModeHelper = setUpMockZenTest(); + mService.setCallerIsNormalPackage(); + + // Representative used to verify getCallingZenUser(). + mBinderService.getAutomaticZenRules(); + + verify(zenModeHelper).getAutomaticZenRules(eq(Binder.getCallingUserHandle())); + } + /** Prepares for a zen-related test that uses a mocked {@link ZenModeHelper}. */ private ZenModeHelper setUpMockZenTest() { ZenModeHelper zenModeHelper = mock(ZenModeHelper.class); @@ -15815,7 +16122,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { NotificationManager.Policy policy = new NotificationManager.Policy(0, 0, 0); mBinderService.setNotificationPolicy("package", policy, false); - verify(zenHelper).applyGlobalPolicyAsImplicitZenRule(eq("package"), anyInt(), eq(policy)); + verify(zenHelper).applyGlobalPolicyAsImplicitZenRule(any(), eq("package"), anyInt(), + eq(policy)); } @Test @@ -15831,7 +16139,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { NotificationManager.Policy policy = new NotificationManager.Policy(0, 0, 0); mBinderService.setNotificationPolicy("package", policy, false); - verify(zenModeHelper).setNotificationPolicy(eq(policy), anyInt(), anyInt()); + verify(zenModeHelper).setNotificationPolicy(any(), eq(policy), anyInt(), anyInt()); } @Test @@ -15878,9 +16186,9 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mBinderService.setNotificationPolicy("package", policy, false); if (canSetGlobalPolicy) { - verify(zenModeHelper).setNotificationPolicy(eq(policy), anyInt(), anyInt()); + verify(zenModeHelper).setNotificationPolicy(any(), eq(policy), anyInt(), anyInt()); } else { - verify(zenModeHelper).applyGlobalPolicyAsImplicitZenRule(anyString(), anyInt(), + verify(zenModeHelper).applyGlobalPolicyAsImplicitZenRule(any(), anyString(), anyInt(), eq(policy)); } } @@ -15898,7 +16206,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { NotificationManager.Policy policy = new NotificationManager.Policy(0, 0, 0); mBinderService.setNotificationPolicy("package", policy, false); - verify(zenModeHelper).setNotificationPolicy(eq(policy), anyInt(), anyInt()); + verify(zenModeHelper).setNotificationPolicy(any(), eq(policy), anyInt(), anyInt()); } @Test @@ -15913,7 +16221,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mBinderService.getNotificationPolicy("package"); - verify(zenHelper).getNotificationPolicyFromImplicitZenRule(eq("package")); + verify(zenHelper).getNotificationPolicyFromImplicitZenRule(any(), eq("package")); } @Test @@ -15928,7 +16236,7 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mBinderService.setInterruptionFilter("package", INTERRUPTION_FILTER_PRIORITY, false); - verify(zenHelper).applyGlobalZenModeAsImplicitZenRule(eq("package"), anyInt(), + verify(zenHelper).applyGlobalZenModeAsImplicitZenRule(any(), eq("package"), anyInt(), eq(ZEN_MODE_IMPORTANT_INTERRUPTIONS)); } @@ -15945,9 +16253,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mBinderService.setInterruptionFilter("package", INTERRUPTION_FILTER_PRIORITY, false); - verify(zenModeHelper).setManualZenMode(eq(ZEN_MODE_IMPORTANT_INTERRUPTIONS), eq(null), - eq(ZenModeConfig.ORIGIN_SYSTEM), anyString(), eq("package"), - anyInt()); + verify(zenModeHelper).setManualZenMode(any(), eq(ZEN_MODE_IMPORTANT_INTERRUPTIONS), + eq(null), eq(ZenModeConfig.ORIGIN_SYSTEM), anyString(), eq("package"), anyInt()); } @Test @@ -15991,10 +16298,10 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mBinderService.setInterruptionFilter("package", INTERRUPTION_FILTER_PRIORITY, false); if (canSetGlobalPolicy) { - verify(zenModeHelper).setManualZenMode(eq(ZEN_MODE_IMPORTANT_INTERRUPTIONS), eq(null), - eq(ZenModeConfig.ORIGIN_APP), anyString(), eq("package"), anyInt()); + verify(zenModeHelper).setManualZenMode(any(), eq(ZEN_MODE_IMPORTANT_INTERRUPTIONS), + eq(null), eq(ZenModeConfig.ORIGIN_APP), anyString(), eq("package"), anyInt()); } else { - verify(zenModeHelper).applyGlobalZenModeAsImplicitZenRule(anyString(), anyInt(), + verify(zenModeHelper).applyGlobalZenModeAsImplicitZenRule(any(), anyString(), anyInt(), eq(ZEN_MODE_IMPORTANT_INTERRUPTIONS)); } } @@ -16013,8 +16320,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mBinderService.requestInterruptionFilterFromListener(mock(INotificationListener.class), INTERRUPTION_FILTER_PRIORITY); - verify(mService.mZenModeHelper).applyGlobalZenModeAsImplicitZenRule(eq("pkg"), eq(mUid), - eq(ZEN_MODE_IMPORTANT_INTERRUPTIONS)); + verify(mService.mZenModeHelper).applyGlobalZenModeAsImplicitZenRule(any(), eq("pkg"), + eq(mUid), eq(ZEN_MODE_IMPORTANT_INTERRUPTIONS)); } @Test @@ -16031,9 +16338,9 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { mBinderService.requestInterruptionFilterFromListener(mock(INotificationListener.class), INTERRUPTION_FILTER_PRIORITY); - verify(mService.mZenModeHelper).setManualZenMode(eq(ZEN_MODE_IMPORTANT_INTERRUPTIONS), - eq(null), eq(ZenModeConfig.ORIGIN_SYSTEM), anyString(), - eq("pkg"), eq(mUid)); + verify(mService.mZenModeHelper).setManualZenMode(any(), + eq(ZEN_MODE_IMPORTANT_INTERRUPTIONS), eq(null), eq(ZenModeConfig.ORIGIN_SYSTEM), + anyString(), eq("pkg"), eq(mUid)); } @Test @@ -16111,8 +16418,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { throws Exception { setUpRealZenTest(); // Start with hasPriorityChannels=true, allowPriorityChannels=true ("default"). - mService.mZenModeHelper.setNotificationPolicy(new Policy(0, 0, 0, 0, - Policy.policyState(true, true), 0), + mService.mZenModeHelper.setNotificationPolicy(UserHandle.CURRENT, + new Policy(0, 0, 0, 0, Policy.policyState(true, true), 0), ZenModeConfig.ORIGIN_SYSTEM, Process.SYSTEM_UID); // The caller will supply states with "wrong" hasPriorityChannels. @@ -16142,8 +16449,8 @@ public class NotificationManagerServiceTest extends UiServiceTestCase { throws Exception { setUpRealZenTest(); // Start with hasPriorityChannels=true, allowPriorityChannels=true ("default"). - mService.mZenModeHelper.setNotificationPolicy(new Policy(0, 0, 0, 0, - Policy.policyState(true, true), 0), + mService.mZenModeHelper.setNotificationPolicy(UserHandle.CURRENT, + new Policy(0, 0, 0, 0, Policy.policyState(true, true), 0), ZenModeConfig.ORIGIN_SYSTEM, Process.SYSTEM_UID); mService.setCallerIsNormalPackage(); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java index 1a1da0f6d408..e1b478cd1a1b 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java @@ -359,7 +359,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { mTestNotificationPolicy = new NotificationManager.Policy(0, 0, 0, 0, NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND, 0); - when(mMockZenModeHelper.getNotificationPolicy()).thenReturn(mTestNotificationPolicy); + when(mMockZenModeHelper.getNotificationPolicy(any())).thenReturn(mTestNotificationPolicy); when(mAppOpsManager.noteOpNoThrow(anyInt(), anyInt(), anyString(), eq(null), anyString())).thenReturn(MODE_DEFAULT); @@ -493,7 +493,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { private void resetZenModeHelper() { reset(mMockZenModeHelper); - when(mMockZenModeHelper.getNotificationPolicy()).thenReturn(mTestNotificationPolicy); + when(mMockZenModeHelper.getNotificationPolicy(any())).thenReturn(mTestNotificationPolicy); } private void setUpPackageWithUid(String packageName, int uid) throws Exception { @@ -2632,9 +2632,10 @@ public class PreferencesHelperTest extends UiServiceTestCase { uid, false); assertFalse(mHelper.areChannelsBypassingDnd()); if (android.app.Flags.modesUi()) { - verify(mMockZenModeHelper, never()).updateHasPriorityChannels(anyBoolean()); + verify(mMockZenModeHelper, never()).updateHasPriorityChannels(any(), anyBoolean()); } else { - verify(mMockZenModeHelper, never()).setNotificationPolicy(any(), anyInt(), anyInt()); + verify(mMockZenModeHelper, never()).setNotificationPolicy(any(), any(), anyInt(), + anyInt()); } resetZenModeHelper(); @@ -2646,9 +2647,11 @@ public class PreferencesHelperTest extends UiServiceTestCase { uid, false); assertTrue(mHelper.areChannelsBypassingDnd()); if (android.app.Flags.modesUi()) { - verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(true)); + verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(UserHandle.CURRENT), + eq(true)); } else { - verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any(), anyInt(), anyInt()); + verify(mMockZenModeHelper, times(1)).setNotificationPolicy(eq(UserHandle.CURRENT), + any(), anyInt(), anyInt()); } resetZenModeHelper(); @@ -2656,18 +2659,21 @@ public class PreferencesHelperTest extends UiServiceTestCase { mHelper.deleteNotificationChannel(PKG_N_MR1, uid, channel.getId(), uid, false); assertTrue(mHelper.areChannelsBypassingDnd()); // channel2 can still bypass DND if (android.app.Flags.modesUi()) { - verify(mMockZenModeHelper, never()).updateHasPriorityChannels(anyBoolean()); + verify(mMockZenModeHelper, never()).updateHasPriorityChannels(any(), anyBoolean()); } else { - verify(mMockZenModeHelper, never()).setNotificationPolicy(any(), anyInt(), anyInt()); + verify(mMockZenModeHelper, never()).setNotificationPolicy(any(), any(), anyInt(), + anyInt()); } resetZenModeHelper(); mHelper.deleteNotificationChannel(PKG_N_MR1, uid, channel2.getId(), uid, false); assertFalse(mHelper.areChannelsBypassingDnd()); if (android.app.Flags.modesUi()) { - verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(false)); + verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(UserHandle.CURRENT), + eq(false)); } else { - verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any(), anyInt(), anyInt()); + verify(mMockZenModeHelper, times(1)).setNotificationPolicy(eq(UserHandle.CURRENT), + any(), anyInt(), anyInt()); } resetZenModeHelper(); } @@ -2685,9 +2691,10 @@ public class PreferencesHelperTest extends UiServiceTestCase { uid, false); assertFalse(mHelper.areChannelsBypassingDnd()); if (android.app.Flags.modesUi()) { - verify(mMockZenModeHelper, never()).updateHasPriorityChannels(anyBoolean()); + verify(mMockZenModeHelper, never()).updateHasPriorityChannels(any(), anyBoolean()); } else { - verify(mMockZenModeHelper, never()).setNotificationPolicy(any(), anyInt(), anyInt()); + verify(mMockZenModeHelper, never()).setNotificationPolicy(any(), any(), anyInt(), + anyInt()); } resetZenModeHelper(); @@ -2699,9 +2706,11 @@ public class PreferencesHelperTest extends UiServiceTestCase { assertTrue(mHelper.areChannelsBypassingDnd()); if (android.app.Flags.modesUi()) { - verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(true)); + verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(UserHandle.CURRENT), + eq(true)); } else { - verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any(), anyInt(), anyInt()); + verify(mMockZenModeHelper, times(1)).setNotificationPolicy(eq(UserHandle.CURRENT), + any(), anyInt(), anyInt()); } resetZenModeHelper(); } @@ -2719,9 +2728,10 @@ public class PreferencesHelperTest extends UiServiceTestCase { uid, false); assertFalse(mHelper.areChannelsBypassingDnd()); if (android.app.Flags.modesUi()) { - verify(mMockZenModeHelper, never()).updateHasPriorityChannels(anyBoolean()); + verify(mMockZenModeHelper, never()).updateHasPriorityChannels(any(), anyBoolean()); } else { - verify(mMockZenModeHelper, never()).setNotificationPolicy(any(), anyInt(), anyInt()); + verify(mMockZenModeHelper, never()).setNotificationPolicy(any(), any(), anyInt(), + anyInt()); } resetZenModeHelper(); @@ -2733,9 +2743,11 @@ public class PreferencesHelperTest extends UiServiceTestCase { uid, false); assertTrue(mHelper.areChannelsBypassingDnd()); if (android.app.Flags.modesUi()) { - verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(true)); + verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(UserHandle.CURRENT), + eq(true)); } else { - verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any(), anyInt(), anyInt()); + verify(mMockZenModeHelper, times(1)).setNotificationPolicy(eq(UserHandle.CURRENT), + any(), anyInt(), anyInt()); } resetZenModeHelper(); @@ -2743,18 +2755,21 @@ public class PreferencesHelperTest extends UiServiceTestCase { mHelper.deleteNotificationChannel(PKG_N_MR1, uid, channel.getId(), uid, false); assertTrue(mHelper.areChannelsBypassingDnd()); // channel2 can still bypass DND if (android.app.Flags.modesUi()) { - verify(mMockZenModeHelper, never()).updateHasPriorityChannels(anyBoolean()); + verify(mMockZenModeHelper, never()).updateHasPriorityChannels(any(), anyBoolean()); } else { - verify(mMockZenModeHelper, never()).setNotificationPolicy(any(), anyInt(), anyInt()); + verify(mMockZenModeHelper, never()).setNotificationPolicy(any(), any(), anyInt(), + anyInt()); } resetZenModeHelper(); mHelper.deleteNotificationChannel(PKG_N_MR1, uid, channel2.getId(), uid, false); assertFalse(mHelper.areChannelsBypassingDnd()); if (android.app.Flags.modesUi()) { - verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(false)); + verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(UserHandle.CURRENT), + eq(false)); } else { - verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any(), anyInt(), anyInt()); + verify(mMockZenModeHelper, times(1)).setNotificationPolicy(eq(UserHandle.CURRENT), + any(), anyInt(), anyInt()); } resetZenModeHelper(); } @@ -2767,7 +2782,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { // start in a 'allowed to bypass dnd state' mTestNotificationPolicy = new NotificationManager.Policy(0, 0, 0, 0, NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND, 0); - when(mMockZenModeHelper.getNotificationPolicy()).thenReturn(mTestNotificationPolicy); + when(mMockZenModeHelper.getNotificationPolicy(any())).thenReturn(mTestNotificationPolicy); mHelper.syncChannelsBypassingDnd(); // create notification channel that can bypass dnd, but app is blocked @@ -2783,9 +2798,11 @@ public class PreferencesHelperTest extends UiServiceTestCase { uid, false); assertFalse(mHelper.areChannelsBypassingDnd()); if (android.app.Flags.modesUi()) { - verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(false)); + verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(UserHandle.CURRENT), + eq(false)); } else { - verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any(), anyInt(), anyInt()); + verify(mMockZenModeHelper, times(1)).setNotificationPolicy(eq(UserHandle.CURRENT), + any(), anyInt(), anyInt()); } resetZenModeHelper(); } @@ -2798,7 +2815,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { // start in a 'allowed to bypass dnd state' mTestNotificationPolicy = new NotificationManager.Policy(0, 0, 0, 0, NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND, 0); - when(mMockZenModeHelper.getNotificationPolicy()).thenReturn(mTestNotificationPolicy); + when(mMockZenModeHelper.getNotificationPolicy(any())).thenReturn(mTestNotificationPolicy); mHelper.syncChannelsBypassingDnd(); // create notification channel that can bypass dnd, but app is blocked @@ -2809,9 +2826,11 @@ public class PreferencesHelperTest extends UiServiceTestCase { uid, false); assertFalse(mHelper.areChannelsBypassingDnd()); if (android.app.Flags.modesUi()) { - verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(false)); + verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(UserHandle.CURRENT), + eq(false)); } else { - verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any(), anyInt(), anyInt()); + verify(mMockZenModeHelper, times(1)).setNotificationPolicy(eq(UserHandle.CURRENT), + any(), anyInt(), anyInt()); } resetZenModeHelper(); } @@ -2824,7 +2843,7 @@ public class PreferencesHelperTest extends UiServiceTestCase { // start in a 'allowed to bypass dnd state' mTestNotificationPolicy = new NotificationManager.Policy(0, 0, 0, 0, NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND, 0); - when(mMockZenModeHelper.getNotificationPolicy()).thenReturn(mTestNotificationPolicy); + when(mMockZenModeHelper.getNotificationPolicy(any())).thenReturn(mTestNotificationPolicy); mHelper.syncChannelsBypassingDnd(); // create notification channel that can bypass dnd, but app is blocked @@ -2835,9 +2854,11 @@ public class PreferencesHelperTest extends UiServiceTestCase { uid, false); assertFalse(mHelper.areChannelsBypassingDnd()); if (android.app.Flags.modesUi()) { - verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(false)); + verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(UserHandle.CURRENT), + eq(false)); } else { - verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any(), anyInt(), anyInt()); + verify(mMockZenModeHelper, times(1)).setNotificationPolicy(eq(UserHandle.CURRENT), + any(), anyInt(), anyInt()); } resetZenModeHelper(); } @@ -2855,9 +2876,10 @@ public class PreferencesHelperTest extends UiServiceTestCase { uid, false); assertFalse(mHelper.areChannelsBypassingDnd()); if (android.app.Flags.modesUi()) { - verify(mMockZenModeHelper, never()).updateHasPriorityChannels(anyBoolean()); + verify(mMockZenModeHelper, never()).updateHasPriorityChannels(any(), anyBoolean()); } else { - verify(mMockZenModeHelper, never()).setNotificationPolicy(any(), anyInt(), anyInt()); + verify(mMockZenModeHelper, never()).setNotificationPolicy(any(), any(), anyInt(), + anyInt()); } resetZenModeHelper(); @@ -2867,9 +2889,11 @@ public class PreferencesHelperTest extends UiServiceTestCase { mHelper.updateNotificationChannel(PKG_N_MR1, uid, channel, true, SYSTEM_UID, true); assertTrue(mHelper.areChannelsBypassingDnd()); if (android.app.Flags.modesUi()) { - verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(true)); + verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(UserHandle.CURRENT), + eq(true)); } else { - verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any(), anyInt(), anyInt()); + verify(mMockZenModeHelper, times(1)).setNotificationPolicy(eq(UserHandle.CURRENT), + any(), anyInt(), anyInt()); } resetZenModeHelper(); @@ -2879,9 +2903,11 @@ public class PreferencesHelperTest extends UiServiceTestCase { mHelper.updateNotificationChannel(PKG_N_MR1, uid, channel, true, SYSTEM_UID, true); assertFalse(mHelper.areChannelsBypassingDnd()); if (android.app.Flags.modesUi()) { - verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(false)); + verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(UserHandle.CURRENT), + eq(false)); } else { - verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any(), anyInt(), anyInt()); + verify(mMockZenModeHelper, times(1)).setNotificationPolicy(eq(UserHandle.CURRENT), + any(), anyInt(), anyInt()); } resetZenModeHelper(); } @@ -2892,13 +2918,15 @@ public class PreferencesHelperTest extends UiServiceTestCase { // RankingHelper should change to false mTestNotificationPolicy = new NotificationManager.Policy(0, 0, 0, 0, NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND, 0); - when(mMockZenModeHelper.getNotificationPolicy()).thenReturn(mTestNotificationPolicy); + when(mMockZenModeHelper.getNotificationPolicy(any())).thenReturn(mTestNotificationPolicy); mHelper.syncChannelsBypassingDnd(); assertFalse(mHelper.areChannelsBypassingDnd()); if (android.app.Flags.modesUi()) { - verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(false)); + verify(mMockZenModeHelper, times(1)).updateHasPriorityChannels(eq(UserHandle.CURRENT), + eq(false)); } else { - verify(mMockZenModeHelper, times(1)).setNotificationPolicy(any(), anyInt(), anyInt()); + verify(mMockZenModeHelper, times(1)).setNotificationPolicy(eq(UserHandle.CURRENT), + any(), anyInt(), anyInt()); } resetZenModeHelper(); } @@ -2907,12 +2935,13 @@ public class PreferencesHelperTest extends UiServiceTestCase { public void testSetupNewZenModeHelper_cannotBypass() { // start notification policy off with mAreChannelsBypassingDnd = false mTestNotificationPolicy = new NotificationManager.Policy(0, 0, 0, 0, 0, 0); - when(mMockZenModeHelper.getNotificationPolicy()).thenReturn(mTestNotificationPolicy); + when(mMockZenModeHelper.getNotificationPolicy(any())).thenReturn(mTestNotificationPolicy); assertFalse(mHelper.areChannelsBypassingDnd()); if (android.app.Flags.modesUi()) { - verify(mMockZenModeHelper, never()).updateHasPriorityChannels(anyBoolean()); + verify(mMockZenModeHelper, never()).updateHasPriorityChannels(any(), anyBoolean()); } else { - verify(mMockZenModeHelper, never()).setNotificationPolicy(any(), anyInt(), anyInt()); + verify(mMockZenModeHelper, never()).setNotificationPolicy(any(), any(), anyInt(), + anyInt()); } resetZenModeHelper(); } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java index 5d4382a6331c..f90034614383 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java @@ -155,7 +155,7 @@ public class RankingHelperTest extends UiServiceTestCase { mTestNotificationPolicy = new NotificationManager.Policy(0, 0, 0, 0, NotificationManager.Policy.STATE_CHANNELS_BYPASSING_DND, 0); - when(mMockZenModeHelper.getNotificationPolicy()).thenReturn(mTestNotificationPolicy); + when(mMockZenModeHelper.getNotificationPolicy(any())).thenReturn(mTestNotificationPolicy); mHelper = new RankingHelper(getContext(), mHandler, mConfig, mMockZenModeHelper, mUsageStats, new String[] {ImportanceExtractor.class.getName()}, mock(IPlatformCompat.class), mGroupHelper); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java index 5709d884d427..3236f9501324 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java @@ -17,6 +17,8 @@ package com.android.server.notification; import static android.app.AutomaticZenRule.TYPE_BEDTIME; +import static android.app.Flags.FLAG_BACKUP_RESTORE_LOGGING; +import static android.app.Flags.FLAG_MODES_API; import static android.app.Flags.FLAG_MODES_UI; import static android.app.Flags.modesUi; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_AMBIENT; @@ -24,6 +26,8 @@ import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_FULL_SCRE import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_LIGHTS; import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK; import static android.app.NotificationManager.Policy.suppressedEffectsToString; +import static android.app.backup.NotificationLoggingConstants.DATA_TYPE_ZEN_CONFIG; +import static android.app.backup.NotificationLoggingConstants.DATA_TYPE_ZEN_RULES; import static android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS; import static android.provider.Settings.Global.ZEN_MODE_OFF; import static android.service.notification.Condition.SOURCE_UNKNOWN; @@ -52,17 +56,22 @@ import static junit.framework.TestCase.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.AutomaticZenRule; import android.app.Flags; import android.app.NotificationManager.Policy; +import android.app.backup.BackupRestoreEventLogger; import android.content.ComponentName; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Parcel; +import android.os.UserHandle; import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.platform.test.flag.junit.FlagsParameterization; @@ -135,7 +144,7 @@ public class ZenModeConfigTest extends UiServiceTestCase { @Parameters(name = "{0}") public static List<FlagsParameterization> getParams() { return FlagsParameterization.allCombinationsOf( - FLAG_MODES_UI); + FLAG_MODES_UI, FLAG_BACKUP_RESTORE_LOGGING); } public ZenModeConfigTest(FlagsParameterization flags) { @@ -144,7 +153,6 @@ public class ZenModeConfigTest extends UiServiceTestCase { @Before public final void setUp() { - mSetFlagsRule.enableFlags(Flags.FLAG_MODES_API); MockitoAnnotations.initMocks(this); mContext.setMockPackageManager(mPm); } @@ -515,6 +523,98 @@ public class ZenModeConfigTest extends UiServiceTestCase { } @Test + @EnableFlags({FLAG_MODES_UI, FLAG_BACKUP_RESTORE_LOGGING}) + public void testBackupRestore_fromPreModesUi() throws IOException, XmlPullParserException { + String xml = "<zen version=\"12\">\n" + + "<allow calls=\"true\" repeatCallers=\"true\" messages=\"true\"" + + " reminders=\"false\" events=\"false\" callsFrom=\"2\" messagesFrom=\"2\"" + + " alarms=\"true\" media=\"true\" system=\"false\" convos=\"true\"" + + " convosFrom=\"2\" priorityChannelsAllowed=\"true\" />\n" + + "<disallow visualEffects=\"157\" />\n" + + "<manual enabled=\"true\" zen=\"1\" creationTime=\"0\" modified=\"false\" />\n" + + "<state areChannelsBypassingDnd=\"true\" />\n" + + "</zen>"; + + BackupRestoreEventLogger logger = mock(BackupRestoreEventLogger.class); + readConfigXml(new ByteArrayInputStream(xml.getBytes()), logger); + + verify(logger).logItemsRestored(DATA_TYPE_ZEN_RULES, 1); + } + + @Test + public void testBackupRestore() throws IOException, XmlPullParserException { + ZenModeConfig config = new ZenModeConfig(); + ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule(); + rule.configurationActivity = CONFIG_ACTIVITY; + rule.component = OWNER; + rule.conditionId = CONDITION_ID; + rule.condition = CONDITION; + rule.enabled = ENABLED; + rule.creationTime = 123; + rule.id = "id"; + rule.zenMode = INTERRUPTION_FILTER; + rule.modified = true; + rule.name = NAME; + rule.setConditionOverride(OVERRIDE_DEACTIVATE); + rule.pkg = OWNER.getPackageName(); + rule.zenPolicy = POLICY; + + rule.allowManualInvocation = ALLOW_MANUAL; + rule.type = TYPE; + rule.userModifiedFields = 16; + rule.zenPolicyUserModifiedFields = 5; + rule.zenDeviceEffectsUserModifiedFields = 2; + rule.iconResName = ICON_RES_NAME; + rule.triggerDescription = TRIGGER_DESC; + rule.deletionInstant = Instant.ofEpochMilli(1701790147000L); + if (Flags.modesUi()) { + rule.disabledOrigin = ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI; + } + config.automaticRules.put(rule.id, rule); + + BackupRestoreEventLogger logger = null; + if (Flags.backupRestoreLogging()) { + logger = mock(BackupRestoreEventLogger.class); + } + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + writeConfigXml(config, XML_VERSION_MODES_API, true, baos, logger); + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + ZenModeConfig fromXml = readConfigXml(bais, logger); + + ZenModeConfig.ZenRule ruleActual = fromXml.automaticRules.get(rule.id); + assertEquals(rule.pkg, ruleActual.pkg); + assertEquals(OVERRIDE_NONE, ruleActual.getConditionOverride()); + assertEquals(rule.enabler, ruleActual.enabler); + assertEquals(rule.component, ruleActual.component); + assertEquals(rule.configurationActivity, ruleActual.configurationActivity); + assertEquals(rule.condition, ruleActual.condition); + assertEquals(rule.enabled, ruleActual.enabled); + assertEquals(rule.creationTime, ruleActual.creationTime); + assertEquals(rule.modified, ruleActual.modified); + assertEquals(rule.conditionId, ruleActual.conditionId); + assertEquals(rule.name, ruleActual.name); + assertEquals(rule.zenMode, ruleActual.zenMode); + + assertEquals(rule.allowManualInvocation, ruleActual.allowManualInvocation); + assertEquals(rule.iconResName, ruleActual.iconResName); + assertEquals(rule.type, ruleActual.type); + assertEquals(rule.userModifiedFields, ruleActual.userModifiedFields); + assertEquals(rule.zenPolicyUserModifiedFields, ruleActual.zenPolicyUserModifiedFields); + assertEquals(rule.zenDeviceEffectsUserModifiedFields, + ruleActual.zenDeviceEffectsUserModifiedFields); + assertEquals(rule.triggerDescription, ruleActual.triggerDescription); + assertEquals(rule.zenPolicy, ruleActual.zenPolicy); + assertEquals(rule.deletionInstant, ruleActual.deletionInstant); + if (Flags.modesUi()) { + assertEquals(rule.disabledOrigin, ruleActual.disabledOrigin); + } + if (Flags.backupRestoreLogging()) { + verify(logger).logItemsBackedUp(DATA_TYPE_ZEN_RULES, 2); + verify(logger).logItemsRestored(DATA_TYPE_ZEN_RULES, 2); + } + } + + @Test public void testWriteToParcel() { ZenModeConfig.ZenRule rule = new ZenModeConfig.ZenRule(); rule.configurationActivity = CONFIG_ACTIVITY; @@ -1023,9 +1123,9 @@ public class ZenModeConfigTest extends UiServiceTestCase { // write out entire config xml ByteArrayOutputStream baos = new ByteArrayOutputStream(); - writeConfigXml(config, XML_VERSION_MODES_API, /* forBackup= */ false, baos); + writeConfigXml(config, XML_VERSION_MODES_API, /* forBackup= */ false, baos, null); ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); - ZenModeConfig fromXml = readConfigXml(bais); + ZenModeConfig fromXml = readConfigXml(bais, null); // The result should be valid and contain a manual rule; the rule should have a non-null @@ -1055,9 +1155,9 @@ public class ZenModeConfigTest extends UiServiceTestCase { // write out entire config xml ByteArrayOutputStream baos = new ByteArrayOutputStream(); - writeConfigXml(config, XML_VERSION_MODES_API, /* forBackup= */ false, baos); + writeConfigXml(config, XML_VERSION_MODES_API, /* forBackup= */ false, baos, null); ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); - ZenModeConfig fromXml = readConfigXml(bais); + ZenModeConfig fromXml = readConfigXml(bais, null); // The result should have a manual rule; it should have a non-null ZenPolicy and a condition // whose state is true. The conditionId and enabler data should also be preserved. @@ -1084,9 +1184,9 @@ public class ZenModeConfigTest extends UiServiceTestCase { // write out entire config xml ByteArrayOutputStream baos = new ByteArrayOutputStream(); - writeConfigXml(config, XML_VERSION_MODES_API, /* forBackup= */ false, baos); + writeConfigXml(config, XML_VERSION_MODES_API, /* forBackup= */ false, baos, null); ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); - ZenModeConfig fromXml = readConfigXml(bais); + ZenModeConfig fromXml = readConfigXml(bais, null); // The result should have a manual rule; it should not be changed from the previous rule. assertThat(fromXml.manualRule).isEqualTo(config.manualRule); @@ -1213,9 +1313,9 @@ public class ZenModeConfigTest extends UiServiceTestCase { config.manualRule.enabled = false; ByteArrayOutputStream baos = new ByteArrayOutputStream(); - writeConfigXml(config, XML_VERSION_MODES_UI, /* forBackup= */ false, baos); + writeConfigXml(config, XML_VERSION_MODES_UI, /* forBackup= */ false, baos, null); ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); - ZenModeConfig fromXml = readConfigXml(bais); + ZenModeConfig fromXml = readConfigXml(bais, null); assertThat(fromXml.manualRule.enabled).isTrue(); } @@ -1359,23 +1459,23 @@ public class ZenModeConfigTest extends UiServiceTestCase { } private void writeConfigXml(ZenModeConfig config, Integer version, boolean forBackup, - ByteArrayOutputStream os) throws IOException { + ByteArrayOutputStream os, BackupRestoreEventLogger logger) throws IOException { String tag = ZEN_TAG; TypedXmlSerializer out = Xml.newFastSerializer(); out.setOutput(new BufferedOutputStream(os), "utf-8"); out.startDocument(null, true); out.startTag(null, tag); - config.writeXml(out, version, forBackup); + config.writeXml(out, version, forBackup, logger); out.endTag(null, tag); out.endDocument(); } - private ZenModeConfig readConfigXml(ByteArrayInputStream is) + private ZenModeConfig readConfigXml(ByteArrayInputStream is, BackupRestoreEventLogger logger) throws XmlPullParserException, IOException { TypedXmlPullParser parser = Xml.newFastPullParser(); parser.setInput(new BufferedInputStream(is), null); parser.nextTag(); - return ZenModeConfig.readXml(parser); + return ZenModeConfig.readXml(parser, logger); } } diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java index 8b3ac2baa7a0..0019b3e45e7b 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java @@ -20,7 +20,9 @@ import static android.app.AutomaticZenRule.TYPE_BEDTIME; import static android.app.AutomaticZenRule.TYPE_IMMERSIVE; import static android.app.AutomaticZenRule.TYPE_SCHEDULE_TIME; import static android.app.AutomaticZenRule.TYPE_UNKNOWN; +import static android.app.Flags.FLAG_BACKUP_RESTORE_LOGGING; import static android.app.Flags.FLAG_MODES_API; +import static android.app.Flags.FLAG_MODES_MULTIUSER; import static android.app.Flags.FLAG_MODES_UI; import static android.app.NotificationManager.AUTOMATIC_RULE_STATUS_ACTIVATED; import static android.app.NotificationManager.AUTOMATIC_RULE_STATUS_DEACTIVATED; @@ -61,6 +63,7 @@ import static android.service.notification.Condition.STATE_TRUE; import static android.service.notification.ZenModeConfig.ORIGIN_APP; import static android.service.notification.ZenModeConfig.ORIGIN_INIT; import static android.service.notification.ZenModeConfig.ORIGIN_INIT_USER; +import static android.service.notification.ZenModeConfig.ORIGIN_SYSTEM; import static android.service.notification.ZenModeConfig.ORIGIN_UNKNOWN; import static android.service.notification.ZenModeConfig.ORIGIN_USER_IN_APP; import static android.service.notification.ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI; @@ -81,6 +84,8 @@ import static com.android.os.dnd.DNDProtoEnums.PEOPLE_STARRED; import static com.android.os.dnd.DNDProtoEnums.ROOT_CONFIG; import static com.android.os.dnd.DNDProtoEnums.STATE_ALLOW; import static com.android.os.dnd.DNDProtoEnums.STATE_DISALLOW; +import static android.app.backup.NotificationLoggingConstants.DATA_TYPE_ZEN_CONFIG; +import static android.app.backup.NotificationLoggingConstants.DATA_TYPE_ZEN_RULES; import static com.android.server.notification.ZenModeEventLogger.ACTIVE_RULE_TYPE_MANUAL; import static com.android.server.notification.ZenModeHelper.RULE_LIMIT_PER_PACKAGE; @@ -102,6 +107,7 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.notNull; @@ -116,15 +122,17 @@ import static org.mockito.Mockito.withSettings; import android.Manifest; import android.annotation.Nullable; import android.annotation.SuppressLint; +import android.app.AlarmManager; import android.app.AppGlobals; import android.app.AppOpsManager; import android.app.AutomaticZenRule; import android.app.Flags; import android.app.NotificationManager; import android.app.NotificationManager.Policy; +import android.app.backup.BackupRestoreEventLogger; import android.app.compat.CompatChanges; import android.content.ComponentName; -import android.content.ContentResolver; +import android.content.Context; import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; @@ -271,7 +279,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { private TestableLooper mTestableLooper; private final TestClock mTestClock = new TestClock(); private ZenModeHelper mZenModeHelper; - private ContentResolver mContentResolver; @Mock DeviceEffectsApplier mDeviceEffectsApplier; @Mock @@ -282,8 +289,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Parameters(name = "{0}") public static List<FlagsParameterization> getParams() { - return FlagsParameterization.progressionOf(FLAG_MODES_API, - FLAG_MODES_UI); + return FlagsParameterization.allCombinationsOf(FLAG_MODES_UI, FLAG_BACKUP_RESTORE_LOGGING); } public ZenModeHelperTest(FlagsParameterization flags) { @@ -296,7 +302,6 @@ public class ZenModeHelperTest extends UiServiceTestCase { mTestableLooper = TestableLooper.get(this); mContext.ensureTestableResources(); - mContentResolver = mContext.getContentResolver(); mResources = mock(Resources.class, withSettings() .spiedInstance(mContext.getResources())); mPkg = mContext.getPackageName(); @@ -314,11 +319,16 @@ public class ZenModeHelperTest extends UiServiceTestCase { mContext.addMockSystemService(AppOpsManager.class, mAppOps); mContext.addMockSystemService(NotificationManager.class, mNotificationManager); + mContext.addMockSystemService(Context.ALARM_SERVICE, mock(AlarmManager.class)); mConditionProviders = new ConditionProviders(mContext, new UserProfiles(), AppGlobals.getPackageManager()); - mConditionProviders.addSystemProvider(new CountdownConditionProvider()); - mConditionProviders.addSystemProvider(new ScheduleConditionProvider()); + CountdownConditionProvider countdown = spy(new CountdownConditionProvider()); + ScheduleConditionProvider schedule = spy(new ScheduleConditionProvider()); + doNothing().when(countdown).notifyConditions(any()); + doNothing().when(schedule).notifyConditions(any()); + mConditionProviders.addSystemProvider(countdown); + mConditionProviders.addSystemProvider(schedule); mZenModeEventLogger = new ZenModeEventLoggerFake(mPackageManager); mZenModeHelper = new ZenModeHelper(mContext, mTestableLooper.getLooper(), mTestClock, mConditionProviders, mTestFlagResolver, mZenModeEventLogger); @@ -377,7 +387,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { ByteArrayOutputStream baos = new ByteArrayOutputStream(); serializer.setOutput(new BufferedOutputStream(baos), "utf-8"); serializer.startDocument(null, true); - mZenModeHelper.writeXml(serializer, false, version, UserHandle.USER_ALL); + mZenModeHelper.writeXml(serializer, false, version, UserHandle.USER_ALL, null); serializer.endDocument(); serializer.flush(); mZenModeHelper.setConfig(new ZenModeConfig(), null, ORIGIN_INIT, "writing xml", @@ -385,13 +395,14 @@ public class ZenModeHelperTest extends UiServiceTestCase { return baos; } - private ByteArrayOutputStream writeXmlAndPurgeForUser(Integer version, int userId) + private ByteArrayOutputStream writeXmlAndPurgeForUser(Integer version, int userId, + boolean forBackup, BackupRestoreEventLogger logger) throws Exception { TypedXmlSerializer serializer = Xml.newFastSerializer(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); serializer.setOutput(new BufferedOutputStream(baos), "utf-8"); serializer.startDocument(null, true); - mZenModeHelper.writeXml(serializer, true, version, userId); + mZenModeHelper.writeXml(serializer, forBackup, version, userId, logger); serializer.endDocument(); serializer.flush(); ZenModeConfig newConfig = new ZenModeConfig(); @@ -689,13 +700,12 @@ public class ZenModeHelperTest extends UiServiceTestCase { AutomaticZenRule azr = new AutomaticZenRule.Builder("OriginalName", CONDITION_ID) .setInterruptionFilter(INTERRUPTION_FILTER_NONE) .build(); - String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - azr, ZenModeConfig.ORIGIN_SYSTEM, "reason", SYSTEM_UID); + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), azr, ORIGIN_SYSTEM, "reason", SYSTEM_UID); // Enable rule - mZenModeHelper.setAutomaticZenRuleState(ruleId, - new Condition(azr.getConditionId(), "", STATE_TRUE), - ZenModeConfig.ORIGIN_SYSTEM, + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, + new Condition(azr.getConditionId(), "", STATE_TRUE), ORIGIN_SYSTEM, SYSTEM_UID); // Confirm that the consolidated policy doesn't allow anything @@ -723,13 +733,12 @@ public class ZenModeHelperTest extends UiServiceTestCase { AutomaticZenRule azr = new AutomaticZenRule.Builder("OriginalName", CONDITION_ID) .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS) .build(); - String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - azr, ZenModeConfig.ORIGIN_SYSTEM, "reason", SYSTEM_UID); + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), azr, ORIGIN_SYSTEM, "reason", SYSTEM_UID); // Enable rule - mZenModeHelper.setAutomaticZenRuleState(ruleId, - new Condition(azr.getConditionId(), "", STATE_TRUE), - ZenModeConfig.ORIGIN_SYSTEM, + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, + new Condition(azr.getConditionId(), "", STATE_TRUE), ORIGIN_SYSTEM, SYSTEM_UID); // Confirm that the consolidated policy allows only alarms and media and nothing else @@ -756,7 +765,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_NORMAL); // Set zen to priority-only with all notification sounds muted (so ringer will be muted) Policy totalSilence = new Policy(0, 0, 0); - mZenModeHelper.setNotificationPolicy(totalSilence, ORIGIN_APP, 1); + mZenModeHelper.setNotificationPolicy(UserHandle.CURRENT, totalSilence, ORIGIN_APP, 1); mZenModeHelper.mZenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS; // 2. verify ringer is unchanged @@ -793,8 +802,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { public void testRingerAffectedStreamsPriorityOnly() { // in priority only mode: // ringtone, notification and system streams are affected by ringer mode - mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, Uri.EMPTY, - ORIGIN_APP, "test", "caller", 1); + mZenModeHelper.setManualZenMode(UserHandle.CURRENT, ZEN_MODE_IMPORTANT_INTERRUPTIONS, + Uri.EMPTY, ORIGIN_APP, "test", "caller", 1); ZenModeHelper.RingerModeDelegate ringerModeDelegateRingerMuted = mZenModeHelper.new RingerModeDelegate(); @@ -810,9 +819,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { // even when ringer is muted (since all ringer sounds cannot bypass DND), // system stream is still affected by ringer mode - mZenModeHelper.setNotificationPolicy(new Policy(0, 0, 0), ORIGIN_APP, 1); - mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, Uri.EMPTY, - ORIGIN_APP, "test", "caller", 1); + mZenModeHelper.setNotificationPolicy(UserHandle.CURRENT, new Policy(0, 0, 0), ORIGIN_APP, + 1); + mZenModeHelper.setManualZenMode(UserHandle.CURRENT, ZEN_MODE_IMPORTANT_INTERRUPTIONS, + Uri.EMPTY, ORIGIN_APP, "test", "caller", 1); ZenModeHelper.RingerModeDelegate ringerModeDelegateRingerNotMuted = mZenModeHelper.new RingerModeDelegate(); @@ -919,7 +929,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // apply zen off multiple times - verify ringer is not set to normal when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_SILENT); for (int i = 0; i < 3; i++) { - mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, Uri.EMPTY, + mZenModeHelper.setManualZenMode(UserHandle.CURRENT, ZEN_MODE_OFF, Uri.EMPTY, ORIGIN_APP, "test", "caller", 1); } verify(mAudioManager, never()).setRingerModeInternal(AudioManager.RINGER_MODE_NORMAL, @@ -944,7 +954,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_SILENT); for (int i = 0; i < 3; i++) { // if zen doesn't change, zen should not reapply itself to the ringer - mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, Uri.EMPTY, + mZenModeHelper.setManualZenMode(UserHandle.CURRENT, ZEN_MODE_OFF, Uri.EMPTY, ORIGIN_APP, "test", "caller", 1); } verify(mAudioManager, never()).setRingerModeInternal(AudioManager.RINGER_MODE_NORMAL, @@ -969,7 +979,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { when(mAudioManager.getRingerModeInternal()).thenReturn(AudioManager.RINGER_MODE_VIBRATE); for (int i = 0; i < 3; i++) { // if zen doesn't change, zen should not reapply itself to the ringer - mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, Uri.EMPTY, + mZenModeHelper.setManualZenMode(UserHandle.CURRENT, ZEN_MODE_OFF, Uri.EMPTY, ORIGIN_APP, "test", "caller", 1); } verify(mAudioManager, never()).setRingerModeInternal(AudioManager.RINGER_MODE_NORMAL, @@ -985,9 +995,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { reset(mAudioManager); // Turn manual zen mode on - mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, - ORIGIN_APP, - null, "test", CUSTOM_PKG_UID); + mZenModeHelper.setManualZenMode(UserHandle.CURRENT, ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, + ORIGIN_APP, null, "test", CUSTOM_PKG_UID); // audio manager shouldn't do anything until the handler processes its messages verify(mAudioManager, never()).updateRingerModeAffectedStreamsInternal(); @@ -1012,6 +1021,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Turn manual zen mode on mZenModeHelper.setManualZenMode( + UserHandle.CURRENT, ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, ORIGIN_APP, @@ -1019,6 +1029,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { "test", CUSTOM_PKG_UID); mZenModeHelper.setManualZenMode( + UserHandle.CURRENT, ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, ORIGIN_APP, @@ -1042,19 +1053,22 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test public void testParcelConfig() { - mZenModeHelper.setNotificationPolicy(new Policy(PRIORITY_CATEGORY_EVENTS + mZenModeHelper.setNotificationPolicy(UserHandle.CURRENT, + new Policy(PRIORITY_CATEGORY_EVENTS | PRIORITY_CATEGORY_MESSAGES | PRIORITY_CATEGORY_REPEAT_CALLERS | PRIORITY_CATEGORY_CONVERSATIONS, PRIORITY_SENDERS_STARRED, PRIORITY_SENDERS_STARRED, 0, CONVERSATION_SENDERS_ANYONE), ORIGIN_UNKNOWN, 1); - mZenModeHelper.setManualZenRuleDeviceEffects(new ZenDeviceEffects.Builder() - .setShouldDimWallpaper(true) - .setShouldDisplayGrayscale(true) - .setShouldUseNightMode(true) - .build(), ORIGIN_UNKNOWN, "test", 1); - mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, Uri.EMPTY, - ORIGIN_UNKNOWN, "test", "me", 1); + mZenModeHelper.setManualZenRuleDeviceEffects(UserHandle.CURRENT, + new ZenDeviceEffects.Builder() + .setShouldDimWallpaper(true) + .setShouldDisplayGrayscale(true) + .setShouldUseNightMode(true) + .build(), + ORIGIN_UNKNOWN, "test", 1); + mZenModeHelper.setManualZenMode(UserHandle.CURRENT, ZEN_MODE_IMPORTANT_INTERRUPTIONS, + Uri.EMPTY, ORIGIN_UNKNOWN, "test", "me", 1); ZenModeConfig actual = mZenModeHelper.mConfig.copy(); @@ -1063,18 +1077,21 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test public void testWriteXml() throws Exception { - mZenModeHelper.setNotificationPolicy(new Policy(PRIORITY_CATEGORY_EVENTS + mZenModeHelper.setNotificationPolicy(UserHandle.CURRENT, + new Policy(PRIORITY_CATEGORY_EVENTS | PRIORITY_CATEGORY_MESSAGES | PRIORITY_CATEGORY_REPEAT_CALLERS | PRIORITY_CATEGORY_CONVERSATIONS, PRIORITY_SENDERS_STARRED, PRIORITY_SENDERS_STARRED, SUPPRESSED_EFFECT_BADGE, CONVERSATION_SENDERS_ANYONE), ORIGIN_UNKNOWN, 1); - mZenModeHelper.setManualZenRuleDeviceEffects(new ZenDeviceEffects.Builder() - .setShouldDimWallpaper(true) - .setShouldDisplayGrayscale(true) - .build(), ORIGIN_UNKNOWN, "test", 1); - mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, Uri.EMPTY, - ORIGIN_UNKNOWN, "test", "me", 1); + mZenModeHelper.setManualZenRuleDeviceEffects(UserHandle.CURRENT, + new ZenDeviceEffects.Builder() + .setShouldDimWallpaper(true) + .setShouldDisplayGrayscale(true) + .build(), + ORIGIN_UNKNOWN, "test", 1); + mZenModeHelper.setManualZenMode(UserHandle.CURRENT, ZEN_MODE_IMPORTANT_INTERRUPTIONS, + Uri.EMPTY, ORIGIN_UNKNOWN, "test", "me", 1); ZenModeConfig expected = mZenModeHelper.mConfig.copy(); if (Flags.modesUi()) { @@ -1085,7 +1102,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { ByteArrayOutputStream baos = writeXmlAndPurge(null); TypedXmlPullParser parser = getParserForByteStream(baos); - mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL); + mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL, null); assertEquals("Config mismatch: current vs expected: " + new ZenModeDiff.ConfigDiff(mZenModeHelper.mConfig, expected), expected, @@ -1094,9 +1111,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test public void testProto() throws InvalidProtocolBufferException { - mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, - Flags.modesApi() ? ORIGIN_USER_IN_SYSTEMUI : ZenModeConfig.ORIGIN_SYSTEM, - null, "test", CUSTOM_PKG_UID); + mZenModeHelper.setManualZenMode(UserHandle.CURRENT, ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, + Flags.modesApi() ? ORIGIN_USER_IN_SYSTEMUI : ORIGIN_SYSTEM, null, + "test", CUSTOM_PKG_UID); mZenModeHelper.mConfig.automaticRules = new ArrayMap<>(); // no automatic rules @@ -1161,7 +1178,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { parser.setInput(new BufferedInputStream( new ByteArrayInputStream(baos.toByteArray())), null); parser.nextTag(); - mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL); + mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL, null); List<StatsEvent> events = new LinkedList<>(); mZenModeHelper.pullRules(events); @@ -1319,8 +1336,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { List<StatsEvent> events = new LinkedList<>(); mZenModeHelper.pullRules(events); - mZenModeHelper.removeAutomaticZenRule(CUSTOM_RULE_ID, ORIGIN_APP, "test", - CUSTOM_PKG_UID); + mZenModeHelper.removeAutomaticZenRule(UserHandle.CURRENT, CUSTOM_RULE_ID, ORIGIN_APP, + "test", CUSTOM_PKG_UID); assertTrue(-1 == mZenModeHelper.mRulesUidCache.getOrDefault(CUSTOM_PKG_NAME + "|" + 0, -1)); } @@ -1348,9 +1365,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { public void testProtoWithManualRule() throws Exception { setupZenConfig(); mZenModeHelper.mConfig.automaticRules = getCustomAutomaticRules(); - mZenModeHelper.setManualZenMode(INTERRUPTION_FILTER_PRIORITY, Uri.EMPTY, - ORIGIN_APP, - "test", "me", 1); + mZenModeHelper.setManualZenMode(UserHandle.CURRENT, INTERRUPTION_FILTER_PRIORITY, Uri.EMPTY, + ORIGIN_APP, "test", "me", 1); List<StatsEvent> events = new LinkedList<>(); mZenModeHelper.pullRules(events); @@ -1370,6 +1386,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test public void testWriteXml_onlyBackupsTargetUser() throws Exception { + BackupRestoreEventLogger logger = null; + if (android.app.Flags.backupRestoreLogging()) { + logger = mock(BackupRestoreEventLogger.class); + } // Setup configs for user 10 and 11. setupZenConfig(); ZenModeConfig config10 = mZenModeHelper.mConfig.copy(); @@ -1386,15 +1406,16 @@ public class ZenModeHelperTest extends UiServiceTestCase { SYSTEM_UID); // Backup user 10 and reset values. - ByteArrayOutputStream baos = writeXmlAndPurgeForUser(null, 10); + ByteArrayOutputStream baos = writeXmlAndPurgeForUser(null, 10, true, logger); ZenModeConfig newConfig11 = new ZenModeConfig(); newConfig11.user = 11; mZenModeHelper.mConfigs.put(11, newConfig11); // Parse backup data. TypedXmlPullParser parser = getParserForByteStream(baos); - mZenModeHelper.readXml(parser, true, 10); - mZenModeHelper.readXml(parser, true, 11); + mZenModeHelper.readXml(parser, true, 10, logger); + parser = getParserForByteStream(baos); + mZenModeHelper.readXml(parser, true, 11, logger); ZenModeConfig actual = mZenModeHelper.mConfigs.get(10); if (Flags.modesUi()) { @@ -1408,39 +1429,72 @@ public class ZenModeHelperTest extends UiServiceTestCase { "Config mismatch: current vs expected: " + new ZenModeDiff.ConfigDiff(actual, config10), config10, actual); assertNotEquals("Expected config mismatch", config11, mZenModeHelper.mConfigs.get(11)); + + if (android.app.Flags.backupRestoreLogging()) { + verify(logger).logItemsBackedUp(DATA_TYPE_ZEN_CONFIG, 1); + // If this is modes_ui, this is manual + single default rule + // If not modes_ui, it's two default automatic rules + manual policy + verify(logger).logItemsBackedUp(DATA_TYPE_ZEN_RULES, Flags.modesUi() ? 2 : 3); + verify(logger, never()) + .logItemsBackupFailed(anyString(), anyInt(), anyString()); + + verify(logger, times(2)).logItemsRestored(DATA_TYPE_ZEN_RULES, Flags.modesUi() ? 2 : 3); + verify(logger, never()) + .logItemsRestoreFailed(anyString(), anyInt(), anyString()); + } } @Test public void testReadXmlRestore_forSystemUser() throws Exception { + BackupRestoreEventLogger logger = null; + if (android.app.Flags.backupRestoreLogging()) { + logger = mock(BackupRestoreEventLogger.class); + } setupZenConfig(); // one enabled automatic rule mZenModeHelper.mConfig.automaticRules = getCustomAutomaticRules(); ZenModeConfig original = mZenModeHelper.mConfig.copy(); - ByteArrayOutputStream baos = writeXmlAndPurgeForUser(null, UserHandle.USER_SYSTEM); + ByteArrayOutputStream baos = writeXmlAndPurgeForUser( + null, UserHandle.USER_SYSTEM, true, logger); TypedXmlPullParser parser = getParserForByteStream(baos); - mZenModeHelper.readXml(parser, true, UserHandle.USER_SYSTEM); + mZenModeHelper.readXml(parser, true, UserHandle.USER_SYSTEM, logger); assertEquals("Config mismatch: current vs original: " + new ZenModeDiff.ConfigDiff(mZenModeHelper.mConfig, original), original, mZenModeHelper.mConfig); assertEquals(original.hashCode(), mZenModeHelper.mConfig.hashCode()); + + if (android.app.Flags.backupRestoreLogging()) { + verify(logger).logItemsBackedUp(DATA_TYPE_ZEN_CONFIG, 1); + verify(logger).logItemsBackedUp(DATA_TYPE_ZEN_RULES, 2); + verify(logger, never()) + .logItemsBackupFailed(anyString(), anyInt(), anyString()); + verify(logger).logItemsRestored(DATA_TYPE_ZEN_RULES, 2); + verify(logger, never()) + .logItemsRestoreFailed(anyString(), anyInt(), anyString()); + } } /** Restore should ignore the data's user id and restore for the target user. */ @Test public void testReadXmlRestore_forNonSystemUser() throws Exception { + BackupRestoreEventLogger logger = null; + if (android.app.Flags.backupRestoreLogging()) { + logger = mock(BackupRestoreEventLogger.class); + } // Setup config. setupZenConfig(); mZenModeHelper.mConfig.automaticRules = getCustomAutomaticRules(); ZenModeConfig expected = mZenModeHelper.mConfig.copy(); // Backup data for user 0. - ByteArrayOutputStream baos = writeXmlAndPurgeForUser(null, UserHandle.USER_SYSTEM); + ByteArrayOutputStream baos = writeXmlAndPurgeForUser( + null, UserHandle.USER_SYSTEM, true, logger); // Restore data for user 10. TypedXmlPullParser parser = getParserForByteStream(baos); - mZenModeHelper.readXml(parser, true, 10); + mZenModeHelper.readXml(parser, true, 10, logger); ZenModeConfig actual = mZenModeHelper.mConfigs.get(10); expected.user = 10; @@ -1454,17 +1508,21 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test public void testReadXmlRestore_doesNotEnableManualRule() throws Exception { + BackupRestoreEventLogger logger = null; + if (android.app.Flags.backupRestoreLogging()) { + logger = mock(BackupRestoreEventLogger.class); + } setupZenConfig(); // Turn on manual zen mode - mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, + mZenModeHelper.setManualZenMode(UserHandle.CURRENT, ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, ORIGIN_USER_IN_SYSTEMUI, "", "someCaller", SYSTEM_UID); ZenModeConfig original = mZenModeHelper.mConfig.copy(); assertThat(original.isManualActive()).isTrue(); ByteArrayOutputStream baos = writeXmlAndPurge(null); TypedXmlPullParser parser = getParserForByteStream(baos); - mZenModeHelper.readXml(parser, true, UserHandle.USER_ALL); + mZenModeHelper.readXml(parser, true, UserHandle.USER_ALL, logger); ZenModeConfig result = mZenModeHelper.getConfig(); assertThat(result.isManualActive()).isFalse(); @@ -1518,7 +1576,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { parser.setInput(new BufferedInputStream( new ByteArrayInputStream(baos.toByteArray())), null); parser.nextTag(); - mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL); + mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL, null); ZenModeConfig.ZenRule original = expected.automaticRules.get(ruleId); ZenModeConfig.ZenRule current = mZenModeHelper.mConfig.automaticRules.get(ruleId); @@ -1529,6 +1587,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test public void testReadXmlRestoreWithZenPolicy_forSystemUser() throws Exception { + BackupRestoreEventLogger logger = null; + if (android.app.Flags.backupRestoreLogging()) { + logger = mock(BackupRestoreEventLogger.class); + } final String ruleId = "customRule"; setupZenConfig(); @@ -1560,9 +1622,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { SystemZenRules.maybeUpgradeRules(mContext, expected); } - ByteArrayOutputStream baos = writeXmlAndPurgeForUser(null, UserHandle.USER_SYSTEM); + ByteArrayOutputStream baos = writeXmlAndPurgeForUser( + null, UserHandle.USER_SYSTEM, true, logger); TypedXmlPullParser parser = getParserForByteStream(baos); - mZenModeHelper.readXml(parser, true, UserHandle.USER_SYSTEM); + mZenModeHelper.readXml(parser, true, UserHandle.USER_SYSTEM, logger); ZenModeConfig.ZenRule original = expected.automaticRules.get(ruleId); ZenModeConfig.ZenRule current = mZenModeHelper.mConfig.automaticRules.get(ruleId); @@ -1574,7 +1637,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test public void testReadXmlRulesNotOverridden() throws Exception { setupZenConfig(); - Policy originalPolicy = mZenModeHelper.getNotificationPolicy(); + Policy originalPolicy = mZenModeHelper.getNotificationPolicy(UserHandle.CURRENT); // automatic zen rule is enabled on upgrade so rules should not be overriden to default ArrayMap<String, ZenModeConfig.ZenRule> enabledAutoRule = new ArrayMap<>(); @@ -1594,10 +1657,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { parser.setInput(new BufferedInputStream( new ByteArrayInputStream(baos.toByteArray())), null); parser.nextTag(); - mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL); + mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL, null); assertTrue(mZenModeHelper.mConfig.automaticRules.containsKey("customRule")); - assertEquals(originalPolicy, mZenModeHelper.getNotificationPolicy()); + assertEquals(originalPolicy, mZenModeHelper.getNotificationPolicy(UserHandle.CURRENT)); } @Test @@ -1614,7 +1677,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { parser.setInput(new BufferedInputStream( new ByteArrayInputStream(xml.getBytes())), null); parser.nextTag(); - mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL); + mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL, null); assertTrue(mZenModeHelper.mConfig.getZenPolicy().shouldShowAllVisualEffects()); @@ -1630,7 +1693,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { parser.setInput(new BufferedInputStream( new ByteArrayInputStream(xml.getBytes())), null); parser.nextTag(); - mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL); + mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL, null); assertTrue(mZenModeHelper.mConfig.getZenPolicy().shouldShowAllVisualEffects()); } @@ -1649,7 +1712,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { parser.setInput(new BufferedInputStream( new ByteArrayInputStream(xml.getBytes())), null); parser.nextTag(); - mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL); + mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL, null); assertTrue(mZenModeHelper.mConfig.getZenPolicy().shouldShowAllVisualEffects()); } @@ -1668,7 +1731,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { parser.setInput(new BufferedInputStream( new ByteArrayInputStream(xml.getBytes())), null); parser.nextTag(); - mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL); + mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL, null); assertThat(mZenModeHelper.mConfig.getZenPolicy() .isVisualEffectAllowed(VISUAL_EFFECT_FULL_SCREEN_INTENT, true)).isFalse(); @@ -1693,7 +1756,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { parser.setInput(new BufferedInputStream( new ByteArrayInputStream(xml.getBytes())), null); parser.nextTag(); - mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL); + mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL, null); assertThat(mZenModeHelper.mConfig.getZenPolicy() .isVisualEffectAllowed(VISUAL_EFFECT_PEEK, true)).isFalse(); @@ -1712,7 +1775,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { parser.setInput(new BufferedInputStream( new ByteArrayInputStream(xml.getBytes())), null); parser.nextTag(); - mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL); + mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL, null); assertThat(mZenModeHelper.mConfig.getZenPolicy() .isVisualEffectAllowed(VISUAL_EFFECT_FULL_SCREEN_INTENT, true)).isFalse(); @@ -1727,7 +1790,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test public void testReadXmlResetDefaultRules() throws Exception { setupZenConfig(); - Policy originalPolicy = mZenModeHelper.getNotificationPolicy(); + Policy originalPolicy = mZenModeHelper.getNotificationPolicy(UserHandle.CURRENT); // no enabled automatic zen rules and no default rules // so rules should be overridden by default rules @@ -1739,7 +1802,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { parser.setInput(new BufferedInputStream( new ByteArrayInputStream(baos.toByteArray())), null); parser.nextTag(); - mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL); + mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL, null); // check default rules ArrayMap<String, ZenModeConfig.ZenRule> rules = mZenModeHelper.mConfig.automaticRules; @@ -1748,13 +1811,13 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertTrue(rules.containsKey(defaultId)); } - assertEquals(originalPolicy, mZenModeHelper.getNotificationPolicy()); + assertEquals(originalPolicy, mZenModeHelper.getNotificationPolicy(UserHandle.CURRENT)); } @Test public void testReadXmlAllDisabledRulesResetDefaultRules() throws Exception { setupZenConfig(); - Policy originalPolicy = mZenModeHelper.getNotificationPolicy(); + Policy originalPolicy = mZenModeHelper.getNotificationPolicy(UserHandle.CURRENT); // all automatic zen rules are disabled on upgrade (and default rules don't already exist) // so rules should be overriden by default rules @@ -1775,7 +1838,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { parser.setInput(new BufferedInputStream( new ByteArrayInputStream(baos.toByteArray())), null); parser.nextTag(); - mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL); + mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL, null); // check default rules ArrayMap<String, ZenModeConfig.ZenRule> rules = mZenModeHelper.mConfig.automaticRules; @@ -1785,14 +1848,14 @@ public class ZenModeHelperTest extends UiServiceTestCase { } assertFalse(rules.containsKey("customRule")); - assertEquals(originalPolicy, mZenModeHelper.getNotificationPolicy()); + assertEquals(originalPolicy, mZenModeHelper.getNotificationPolicy(UserHandle.CURRENT)); } @Test @DisableFlags(FLAG_MODES_UI) // modes_ui has only 1 default rule public void testReadXmlOnlyOneDefaultRuleExists() throws Exception { setupZenConfig(); - Policy originalPolicy = mZenModeHelper.getNotificationPolicy(); + Policy originalPolicy = mZenModeHelper.getNotificationPolicy(UserHandle.CURRENT); // all automatic zen rules are disabled on upgrade and only one default rule exists // so rules should be overriden to the default rules @@ -1829,7 +1892,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { parser.setInput(new BufferedInputStream( new ByteArrayInputStream(baos.toByteArray())), null); parser.nextTag(); - mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL); + mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL, null); // check default rules ArrayMap<String, ZenModeConfig.ZenRule> rules = mZenModeHelper.mConfig.automaticRules; @@ -1839,13 +1902,13 @@ public class ZenModeHelperTest extends UiServiceTestCase { } assertThat(rules).doesNotContainKey("customRule"); - assertEquals(originalPolicy, mZenModeHelper.getNotificationPolicy()); + assertEquals(originalPolicy, mZenModeHelper.getNotificationPolicy(UserHandle.CURRENT)); } @Test public void testReadXmlDefaultRulesExist() throws Exception { setupZenConfig(); - Policy originalPolicy = mZenModeHelper.getNotificationPolicy(); + Policy originalPolicy = mZenModeHelper.getNotificationPolicy(UserHandle.CURRENT); // Default rules exist so rules should not be overridden by defaults ArrayMap<String, ZenModeConfig.ZenRule> automaticRules = new ArrayMap<>(); @@ -1899,7 +1962,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { parser.setInput(new BufferedInputStream( new ByteArrayInputStream(baos.toByteArray())), null); parser.nextTag(); - mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL); + mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL, null); // check default rules int expectedNumAutoRules = 1 + ZenModeConfig.getDefaultRuleIds().size(); // custom + default @@ -1910,7 +1973,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { } assertThat(rules).containsKey("customRule"); - assertEquals(originalPolicy, mZenModeHelper.getNotificationPolicy()); + assertEquals(originalPolicy, mZenModeHelper.getNotificationPolicy(UserHandle.CURRENT)); List<StatsEvent> events = new LinkedList<>(); mZenModeHelper.pullRules(events); @@ -1923,7 +1986,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // When reading XML for something that is already on the modes API system, make sure no // rules' policies get changed. setupZenConfig(); - Policy originalPolicy = mZenModeHelper.getNotificationPolicy(); + Policy originalPolicy = mZenModeHelper.getNotificationPolicy(UserHandle.CURRENT); // Shared for rules ArrayMap<String, ZenModeConfig.ZenRule> enabledAutoRules = new ArrayMap<>(); @@ -1952,10 +2015,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { parser.setInput(new BufferedInputStream( new ByteArrayInputStream(baos.toByteArray())), null); parser.nextTag(); - mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL); + mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL, null); // basic check: global config maintained - assertEquals(originalPolicy, mZenModeHelper.getNotificationPolicy()); + assertEquals(originalPolicy, mZenModeHelper.getNotificationPolicy(UserHandle.CURRENT)); // Find our automatic rules. ArrayMap<String, ZenModeConfig.ZenRule> rules = mZenModeHelper.mConfig.automaticRules; @@ -1972,7 +2035,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // a custom policy matching the global config for any automatic rule with no specified // policy. setupZenConfig(); - Policy originalPolicy = mZenModeHelper.getNotificationPolicy(); + Policy originalPolicy = mZenModeHelper.getNotificationPolicy(UserHandle.CURRENT); ArrayMap<String, ZenModeConfig.ZenRule> enabledAutoRule = new ArrayMap<>(); ZenModeConfig.ZenRule customRule = new ZenModeConfig.ZenRule(); @@ -1991,10 +2054,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { parser.setInput(new BufferedInputStream( new ByteArrayInputStream(baos.toByteArray())), null); parser.nextTag(); - mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL); + mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL, null); // basic check: global config maintained - assertEquals(originalPolicy, mZenModeHelper.getNotificationPolicy()); + assertEquals(originalPolicy, mZenModeHelper.getNotificationPolicy(UserHandle.CURRENT)); // Find our automatic rule and check that it has a policy set now ArrayMap<String, ZenModeConfig.ZenRule> rules = mZenModeHelper.mConfig.automaticRules; @@ -2024,7 +2087,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { // underspecified ZenPolicy, we fill in all of the gaps with things from the global config // in order to maintain consistency of behavior. setupZenConfig(); - Policy originalPolicy = mZenModeHelper.getNotificationPolicy(); + Policy originalPolicy = mZenModeHelper.getNotificationPolicy(UserHandle.CURRENT); ArrayMap<String, ZenModeConfig.ZenRule> enabledAutoRule = new ArrayMap<>(); ZenModeConfig.ZenRule customRule = new ZenModeConfig.ZenRule(); @@ -2048,10 +2111,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { parser.setInput(new BufferedInputStream( new ByteArrayInputStream(baos.toByteArray())), null); parser.nextTag(); - mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL); + mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL, null); // basic check: global config maintained - assertEquals(originalPolicy, mZenModeHelper.getNotificationPolicy()); + assertEquals(originalPolicy, mZenModeHelper.getNotificationPolicy(UserHandle.CURRENT)); // Find our automatic rule and check that it has a policy set now ArrayMap<String, ZenModeConfig.ZenRule> rules = mZenModeHelper.mConfig.automaticRules; @@ -2114,7 +2177,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { parser.setInput(new BufferedInputStream( new ByteArrayInputStream(baos.toByteArray())), null); parser.nextTag(); - mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL); + mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL, null); // check default rules ArrayMap<String, ZenModeConfig.ZenRule> rules = mZenModeHelper.mConfig.automaticRules; @@ -2166,7 +2229,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { parser.setInput(new BufferedInputStream( new ByteArrayInputStream(baos.toByteArray())), null); parser.nextTag(); - mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL); + mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL, null); // Implicit rule was updated. assertThat(mZenModeHelper.mConfig.automaticRules.get(implicitRuleBeforeModesUi.id)) @@ -2204,7 +2267,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { parser.setInput(new BufferedInputStream( new ByteArrayInputStream(baos.toByteArray())), null); parser.nextTag(); - mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL); + mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL, null); // Both rules were untouched assertThat(mZenModeHelper.mConfig.automaticRules.get(implicitRuleWithModesUi.id)) @@ -2345,8 +2408,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); // We need the package name to be something that's not "android" so there aren't any // existing rules under that package. - String id = mZenModeHelper.addAutomaticZenRule("pkgname", zenRule, ORIGIN_APP, - "test", CUSTOM_PKG_UID); + String id = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, "pkgname", zenRule, + ORIGIN_APP, "test", CUSTOM_PKG_UID); assertNotNull(id); } try { @@ -2356,8 +2419,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), new ZenPolicy.Builder().build(), NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id = mZenModeHelper.addAutomaticZenRule("pkgname", zenRule, ORIGIN_APP, - "test", CUSTOM_PKG_UID); + String id = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, "pkgname", zenRule, + ORIGIN_APP, "test", CUSTOM_PKG_UID); fail("allowed too many rules to be created"); } catch (IllegalArgumentException e) { // yay @@ -2377,8 +2440,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig.toScheduleConditionId(si), new ZenPolicy.Builder().build(), NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id = mZenModeHelper.addAutomaticZenRule("pkgname", zenRule, ORIGIN_APP, - "test", CUSTOM_PKG_UID); + String id = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, "pkgname", zenRule, + ORIGIN_APP, "test", CUSTOM_PKG_UID); assertNotNull(id); } try { @@ -2388,8 +2451,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), new ZenPolicy.Builder().build(), NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id = mZenModeHelper.addAutomaticZenRule("pkgname", zenRule, ORIGIN_APP, - "test", CUSTOM_PKG_UID); + String id = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, "pkgname", zenRule, + ORIGIN_APP, "test", CUSTOM_PKG_UID); fail("allowed too many rules to be created"); } catch (IllegalArgumentException e) { // yay @@ -2409,8 +2472,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig.toScheduleConditionId(si), new ZenPolicy.Builder().build(), NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id = mZenModeHelper.addAutomaticZenRule("pkgname", zenRule, ORIGIN_APP, - "test", CUSTOM_PKG_UID); + String id = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, "pkgname", zenRule, + ORIGIN_APP, "test", CUSTOM_PKG_UID); assertNotNull(id); } try { @@ -2420,8 +2483,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), new ZenPolicy.Builder().build(), NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id = mZenModeHelper.addAutomaticZenRule("pkgname", zenRule, ORIGIN_APP, - "test", CUSTOM_PKG_UID); + String id = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, "pkgname", zenRule, + ORIGIN_APP, "test", CUSTOM_PKG_UID); fail("allowed too many rules to be created"); } catch (IllegalArgumentException e) { // yay @@ -2436,8 +2499,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), new ZenPolicy.Builder().build(), NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id = mZenModeHelper.addAutomaticZenRule("android", zenRule, - ZenModeConfig.ORIGIN_SYSTEM, "test", SYSTEM_UID); + String id = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, "android", zenRule, + ORIGIN_SYSTEM, "test", SYSTEM_UID); assertTrue(id != null); ZenModeConfig.ZenRule ruleInConfig = mZenModeHelper.mConfig.automaticRules.get(id); @@ -2457,8 +2520,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { new ComponentName("android", "ScheduleConditionProvider"), ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id = mZenModeHelper.addAutomaticZenRule("android", zenRule, - ZenModeConfig.ORIGIN_SYSTEM, "test", SYSTEM_UID); + String id = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, "android", zenRule, + ORIGIN_SYSTEM, "test", SYSTEM_UID); assertTrue(id != null); ZenModeConfig.ZenRule ruleInConfig = mZenModeHelper.mConfig.automaticRules.get(id); @@ -2483,8 +2546,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { new ComponentName("android", "ScheduleConditionProvider"), ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id1 = mZenModeHelper.addAutomaticZenRule("android", zenRule1, - ZenModeConfig.ORIGIN_SYSTEM, "test", SYSTEM_UID); + String id1 = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, "android", zenRule1, + ORIGIN_SYSTEM, "test", SYSTEM_UID); // Zen rule with partially-filled policy: should get all of the filled fields set, and the // rest filled with default state @@ -2498,8 +2561,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { .showFullScreenIntent(true) .build(), NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id2 = mZenModeHelper.addAutomaticZenRule("android", zenRule2, - ZenModeConfig.ORIGIN_SYSTEM, "test", SYSTEM_UID); + String id2 = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, "android", zenRule2, + ORIGIN_SYSTEM, "test", SYSTEM_UID); // rule 1 should exist assertThat(id1).isNotNull(); @@ -2544,9 +2607,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { new ZenPolicy.Builder().build(), NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id = mZenModeHelper.addAutomaticZenRule(null, zenRule, ORIGIN_APP, "test", - CUSTOM_PKG_UID); - mZenModeHelper.setAutomaticZenRuleState(zenRule.getConditionId(), + String id = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, null, zenRule, + ORIGIN_APP, "test", CUSTOM_PKG_UID); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, zenRule.getConditionId(), new Condition(zenRule.getConditionId(), "", STATE_TRUE), ORIGIN_APP, CUSTOM_PKG_UID); @@ -2564,8 +2627,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { new ZenPolicy.Builder().build(), NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id = mZenModeHelper.addAutomaticZenRule(null, zenRule, ORIGIN_APP, "test", - CUSTOM_PKG_UID); + String id = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, null, zenRule, + ORIGIN_APP, "test", CUSTOM_PKG_UID); AutomaticZenRule zenRule2 = new AutomaticZenRule("NEW", null, @@ -2574,7 +2637,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { new ZenPolicy.Builder().build(), NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - mZenModeHelper.updateAutomaticZenRule(id, zenRule2, ORIGIN_APP, "", CUSTOM_PKG_UID); + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, id, zenRule2, ORIGIN_APP, "", + CUSTOM_PKG_UID); ZenModeConfig.ZenRule ruleInConfig = mZenModeHelper.mConfig.automaticRules.get(id); assertEquals("NEW", ruleInConfig.name); @@ -2589,15 +2653,16 @@ public class ZenModeHelperTest extends UiServiceTestCase { new ZenPolicy.Builder().build(), NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id = mZenModeHelper.addAutomaticZenRule(null, zenRule, ORIGIN_APP, "test", - CUSTOM_PKG_UID); + String id = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, null, zenRule, + ORIGIN_APP, "test", CUSTOM_PKG_UID); assertTrue(id != null); ZenModeConfig.ZenRule ruleInConfig = mZenModeHelper.mConfig.automaticRules.get(id); assertTrue(ruleInConfig != null); assertEquals(zenRule.getName(), ruleInConfig.name); - mZenModeHelper.removeAutomaticZenRule(id, ORIGIN_APP, "test", CUSTOM_PKG_UID); + mZenModeHelper.removeAutomaticZenRule(UserHandle.CURRENT, id, ORIGIN_APP, "test", + CUSTOM_PKG_UID); assertNull(mZenModeHelper.mConfig.automaticRules.get(id)); } @@ -2609,16 +2674,16 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), new ZenPolicy.Builder().build(), NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id = mZenModeHelper.addAutomaticZenRule(null, zenRule, ORIGIN_APP, "test", - CUSTOM_PKG_UID); + String id = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, null, zenRule, + ORIGIN_APP, "test", CUSTOM_PKG_UID); assertTrue(id != null); ZenModeConfig.ZenRule ruleInConfig = mZenModeHelper.mConfig.automaticRules.get(id); assertTrue(ruleInConfig != null); assertEquals(zenRule.getName(), ruleInConfig.name); - mZenModeHelper.removeAutomaticZenRules(mContext.getPackageName(), ORIGIN_APP, "test", - CUSTOM_PKG_UID); + mZenModeHelper.removeAutomaticZenRules(UserHandle.CURRENT, mContext.getPackageName(), + ORIGIN_APP, "test", CUSTOM_PKG_UID); assertNull(mZenModeHelper.mConfig.automaticRules.get(id)); } @@ -2633,18 +2698,18 @@ public class ZenModeHelperTest extends UiServiceTestCase { new ComponentName(mPkg, "ScheduleConditionProvider"), sharedUri, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id = mZenModeHelper.addAutomaticZenRule(mPkg, zenRule, - ZenModeConfig.ORIGIN_SYSTEM, "test", SYSTEM_UID); + String id = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, zenRule, + ORIGIN_SYSTEM, "test", SYSTEM_UID); AutomaticZenRule zenRule2 = new AutomaticZenRule("name2", new ComponentName(mPkg, "ScheduleConditionProvider"), sharedUri, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id2 = mZenModeHelper.addAutomaticZenRule(mPkg, zenRule2, - ZenModeConfig.ORIGIN_SYSTEM, "test", SYSTEM_UID); + String id2 = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, zenRule2, + ORIGIN_SYSTEM, "test", SYSTEM_UID); Condition condition = new Condition(sharedUri, "", STATE_TRUE); - mZenModeHelper.setAutomaticZenRuleState(sharedUri, condition, - ZenModeConfig.ORIGIN_SYSTEM, SYSTEM_UID); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, sharedUri, condition, + ORIGIN_SYSTEM, SYSTEM_UID); for (ZenModeConfig.ZenRule rule : mZenModeHelper.mConfig.automaticRules.values()) { if (rule.id.equals(id)) { @@ -2658,8 +2723,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { } condition = new Condition(sharedUri, "", STATE_FALSE); - mZenModeHelper.setAutomaticZenRuleState(sharedUri, condition, - ZenModeConfig.ORIGIN_SYSTEM, SYSTEM_UID); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, sharedUri, condition, + ORIGIN_SYSTEM, SYSTEM_UID); for (ZenModeConfig.ZenRule rule : mZenModeHelper.mConfig.automaticRules.values()) { if (rule.id.equals(id)) { @@ -2689,14 +2754,15 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setShouldMaximizeDoze(true) .build(); - String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), new AutomaticZenRule.Builder("Rule", CONDITION_ID) .setOwner(OWNER) .setDeviceEffects(zde) .build(), ORIGIN_APP, "reasons", 0); - AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(ruleId); + AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); assertThat(savedRule.getDeviceEffects()).isEqualTo( new ZenDeviceEffects.Builder() .setShouldDisplayGrayscale(true) @@ -2722,14 +2788,15 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setShouldMaximizeDoze(true) .build(); - String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), new AutomaticZenRule.Builder("Rule", CONDITION_ID) .setOwner(OWNER) .setDeviceEffects(zde) .build(), - ZenModeConfig.ORIGIN_SYSTEM, "reasons", 0); + ORIGIN_SYSTEM, "reasons", 0); - AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(ruleId); + AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); assertThat(savedRule.getDeviceEffects()).isEqualTo(zde); } @@ -2749,7 +2816,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setShouldMaximizeDoze(true) .build(); - String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), new AutomaticZenRule.Builder("Rule", CONDITION_ID) .setOwner(OWNER) .setDeviceEffects(zde) @@ -2757,7 +2825,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { ORIGIN_USER_IN_SYSTEMUI, "reasons", 0); - AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(ruleId); + AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); assertThat(savedRule.getDeviceEffects()).isEqualTo(zde); } @@ -2769,26 +2837,27 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setShouldDisableTapToWake(true) .addExtraEffect("extra") .build(); - String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), new AutomaticZenRule.Builder("Rule", CONDITION_ID) .setOwner(OWNER) .setDeviceEffects(original) .build(), - ZenModeConfig.ORIGIN_SYSTEM, "reasons", 0); + ORIGIN_SYSTEM, "reasons", 0); ZenDeviceEffects updateFromApp = new ZenDeviceEffects.Builder() .setShouldUseNightMode(true) // Good .setShouldMaximizeDoze(true) // Bad .addExtraEffect("should be rejected") // Bad .build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, new AutomaticZenRule.Builder("Rule", CONDITION_ID) .setOwner(OWNER) .setDeviceEffects(updateFromApp) .build(), ORIGIN_APP, "reasons", 0); - AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(ruleId); + AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); assertThat(savedRule.getDeviceEffects()).isEqualTo( new ZenDeviceEffects.Builder() .setShouldUseNightMode(true) // From update. @@ -2803,24 +2872,25 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenDeviceEffects original = new ZenDeviceEffects.Builder() .setShouldDisableTapToWake(true) .build(); - String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), new AutomaticZenRule.Builder("Rule", CONDITION_ID) .setOwner(OWNER) .setDeviceEffects(original) .build(), - ZenModeConfig.ORIGIN_SYSTEM, "reasons", 0); + ORIGIN_SYSTEM, "reasons", 0); ZenDeviceEffects updateFromSystem = new ZenDeviceEffects.Builder() .setShouldUseNightMode(true) // Good .setShouldMaximizeDoze(true) // Also good .build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, new AutomaticZenRule.Builder("Rule", CONDITION_ID) .setDeviceEffects(updateFromSystem) .build(), - ZenModeConfig.ORIGIN_SYSTEM, "reasons", 0); + ORIGIN_SYSTEM, "reasons", 0); - AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(ruleId); + AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); assertThat(savedRule.getDeviceEffects()).isEqualTo(updateFromSystem); } @@ -2830,12 +2900,13 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenDeviceEffects original = new ZenDeviceEffects.Builder() .setShouldDisableTapToWake(true) .build(); - String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), new AutomaticZenRule.Builder("Rule", CONDITION_ID) .setOwner(OWNER) .setDeviceEffects(original) .build(), - ZenModeConfig.ORIGIN_SYSTEM, "reasons", 0); + ORIGIN_SYSTEM, "reasons", 0); ZenDeviceEffects updateFromUser = new ZenDeviceEffects.Builder() .setShouldUseNightMode(true) @@ -2844,13 +2915,13 @@ public class ZenModeHelperTest extends UiServiceTestCase { // even with this line removed, tap to wake would be set to false. .setShouldDisableTapToWake(false) .build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, new AutomaticZenRule.Builder("Rule", CONDITION_ID) .setDeviceEffects(updateFromUser) .build(), ORIGIN_USER_IN_SYSTEMUI, "reasons", 0); - AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(ruleId); + AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); assertThat(savedRule.getDeviceEffects()).isEqualTo(updateFromUser); } @@ -2860,7 +2931,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { public void updateAutomaticZenRule_nullPolicy_doesNothing() { // Test that when updateAutomaticZenRule is called with a null policy, nothing changes // about the existing policy. - String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), new AutomaticZenRule.Builder("Rule", CONDITION_ID) .setOwner(OWNER) .setZenPolicy(new ZenPolicy.Builder() @@ -2869,13 +2941,13 @@ public class ZenModeHelperTest extends UiServiceTestCase { .build(), ORIGIN_APP, "reasons", 0); - mZenModeHelper.updateAutomaticZenRule(ruleId, + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, new AutomaticZenRule.Builder("Rule", CONDITION_ID) // no zen policy .build(), ORIGIN_APP, "reasons", 0); - AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(ruleId); + AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); assertThat(savedRule.getZenPolicy().getPriorityCategoryCalls()) .isEqualTo(STATE_DISALLOW); } @@ -2886,7 +2958,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Test that when updating an automatic zen rule with an existing policy, the newly set // fields overwrite those from the previous policy, but unset fields in the new policy // keep values from the previous one. - String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), new AutomaticZenRule.Builder("Rule", CONDITION_ID) .setOwner(OWNER) .setZenPolicy(new ZenPolicy.Builder() @@ -2895,9 +2968,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { .allowReminders(true) .build()) .build(), - ZenModeConfig.ORIGIN_SYSTEM, "reasons", 0); + ORIGIN_SYSTEM, "reasons", 0); - mZenModeHelper.updateAutomaticZenRule(ruleId, + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, new AutomaticZenRule.Builder("Rule", CONDITION_ID) .setZenPolicy(new ZenPolicy.Builder() .allowCalls(ZenPolicy.PEOPLE_TYPE_CONTACTS) @@ -2905,7 +2978,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { .build(), ORIGIN_APP, "reasons", 0); - AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(ruleId); + AutomaticZenRule savedRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); assertThat(savedRule.getZenPolicy().getPriorityCategoryCalls()) .isEqualTo(STATE_ALLOW); // from update assertThat(savedRule.getZenPolicy().getPriorityCallSenders()) @@ -2931,9 +3004,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { AutomaticZenRule bedtime = new AutomaticZenRule.Builder("Bedtime Mode (TM)", CONDITION_ID) .setType(TYPE_BEDTIME) .build(); - String bedtimeRuleId = mZenModeHelper.addAutomaticZenRule("pkg", bedtime, - ORIGIN_APP, - "reason", CUSTOM_PKG_UID); + String bedtimeRuleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, "pkg", + bedtime, ORIGIN_APP, "reason", CUSTOM_PKG_UID); assertThat(mZenModeHelper.mConfig.automaticRules.keySet()).containsExactly(bedtimeRuleId); } @@ -2952,9 +3024,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { AutomaticZenRule bedtime = new AutomaticZenRule.Builder("Bedtime Mode (TM)", CONDITION_ID) .setType(TYPE_BEDTIME) .build(); - String bedtimeRuleId = mZenModeHelper.addAutomaticZenRule("pkg", bedtime, - ORIGIN_APP, - "reason", CUSTOM_PKG_UID); + String bedtimeRuleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, "pkg", + bedtime, ORIGIN_APP, "reason", CUSTOM_PKG_UID); assertThat(mZenModeHelper.mConfig.automaticRules.keySet()).containsExactly( ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID, bedtimeRuleId); @@ -2974,9 +3045,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { AutomaticZenRule bedtime = new AutomaticZenRule.Builder("Bedtime Mode (TM)", CONDITION_ID) .setType(TYPE_BEDTIME) .build(); - String bedtimeRuleId = mZenModeHelper.addAutomaticZenRule("pkg", bedtime, - ORIGIN_APP, - "reason", CUSTOM_PKG_UID); + String bedtimeRuleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, "pkg", + bedtime, ORIGIN_APP, "reason", CUSTOM_PKG_UID); assertThat(mZenModeHelper.mConfig.automaticRules.keySet()).containsExactly( ZenModeConfig.EVERY_NIGHT_DEFAULT_RULE_ID, bedtimeRuleId); @@ -2995,16 +3065,16 @@ public class ZenModeHelperTest extends UiServiceTestCase { AutomaticZenRule futureBedtime = new AutomaticZenRule.Builder("Bedtime (?)", CONDITION_ID) .build(); - String bedtimeRuleId = mZenModeHelper.addAutomaticZenRule(mPkg, futureBedtime, - ORIGIN_APP, "reason", CUSTOM_PKG_UID); + String bedtimeRuleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, + futureBedtime, ORIGIN_APP, "reason", CUSTOM_PKG_UID); assertThat(mZenModeHelper.mConfig.automaticRules.keySet()) .containsExactly(sleepingRule.id, bedtimeRuleId); AutomaticZenRule bedtime = new AutomaticZenRule.Builder("Bedtime (!)", CONDITION_ID) .setType(TYPE_BEDTIME) .build(); - mZenModeHelper.updateAutomaticZenRule(bedtimeRuleId, bedtime, ORIGIN_APP, "reason", - CUSTOM_PKG_UID); + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, bedtimeRuleId, bedtime, + ORIGIN_APP, "reason", CUSTOM_PKG_UID); assertThat(mZenModeHelper.mConfig.automaticRules.keySet()).containsExactly(bedtimeRuleId); } @@ -3015,16 +3085,16 @@ public class ZenModeHelperTest extends UiServiceTestCase { setupZenConfig(); // note that caller=null because that's how it comes in from NMS.setZenMode - mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, - ZenModeConfig.ORIGIN_SYSTEM, "", null, SYSTEM_UID); + mZenModeHelper.setManualZenMode(UserHandle.CURRENT, ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, + ORIGIN_SYSTEM, "", null, SYSTEM_UID); // confirm that setting zen mode via setManualZenMode changed the zen mode correctly assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, mZenModeHelper.mZenMode); assertEquals(true, mZenModeHelper.mConfig.manualRule.allowManualInvocation); // and also that it works to turn it back off again - mZenModeHelper.setManualZenMode(Global.ZEN_MODE_OFF, null, ZenModeConfig.ORIGIN_SYSTEM, - "", null, SYSTEM_UID); + mZenModeHelper.setManualZenMode(UserHandle.CURRENT, Global.ZEN_MODE_OFF, null, + ORIGIN_SYSTEM, "", null, SYSTEM_UID); assertEquals(Global.ZEN_MODE_OFF, mZenModeHelper.mZenMode); } @@ -3039,22 +3109,21 @@ public class ZenModeHelperTest extends UiServiceTestCase { AutomaticZenRule activeRule = new AutomaticZenRule.Builder("Test", CONDITION_ID) .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) .build(); - String activeRuleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - activeRule, ORIGIN_APP, "add it", CUSTOM_PKG_UID); - mZenModeHelper.setAutomaticZenRuleState(activeRuleId, CONDITION_TRUE, - ORIGIN_APP, - CUSTOM_PKG_UID); + String activeRuleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), activeRule, ORIGIN_APP, "add it", CUSTOM_PKG_UID); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, activeRuleId, + CONDITION_TRUE, ORIGIN_APP, CUSTOM_PKG_UID); AutomaticZenRule inactiveRule = new AutomaticZenRule.Builder("Test", CONDITION_ID) .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) .build(); - String inactiveRuleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - inactiveRule, ORIGIN_APP, "add it", CUSTOM_PKG_UID); + String inactiveRuleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), inactiveRule, ORIGIN_APP, "add it", CUSTOM_PKG_UID); assertWithMessage("Failure for origin " + origin.name()) .that(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_IMPORTANT_INTERRUPTIONS); // User turns DND off. - mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null, origin.value(), + mZenModeHelper.setManualZenMode(UserHandle.CURRENT, ZEN_MODE_OFF, null, origin.value(), "snoozing", "systemui", SYSTEM_UID); assertWithMessage("Failure for origin " + origin.name()) .that(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF); @@ -3078,21 +3147,20 @@ public class ZenModeHelperTest extends UiServiceTestCase { AutomaticZenRule activeRule = new AutomaticZenRule.Builder("Test", CONDITION_ID) .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) .build(); - String activeRuleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - activeRule, ORIGIN_APP, "add it", CUSTOM_PKG_UID); - mZenModeHelper.setAutomaticZenRuleState(activeRuleId, CONDITION_TRUE, - ORIGIN_APP, - CUSTOM_PKG_UID); + String activeRuleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), activeRule, ORIGIN_APP, "add it", CUSTOM_PKG_UID); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, activeRuleId, + CONDITION_TRUE, ORIGIN_APP, CUSTOM_PKG_UID); AutomaticZenRule inactiveRule = new AutomaticZenRule.Builder("Test", CONDITION_ID) .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) .build(); - String inactiveRuleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - inactiveRule, ORIGIN_APP, "add it", CUSTOM_PKG_UID); + String inactiveRuleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), inactiveRule, ORIGIN_APP, "add it", CUSTOM_PKG_UID); assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_IMPORTANT_INTERRUPTIONS); // User turns DND off. - mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null, origin.value(), + mZenModeHelper.setManualZenMode(UserHandle.CURRENT, ZEN_MODE_OFF, null, origin.value(), "snoozing", "systemui", SYSTEM_UID); ZenModeConfig config = mZenModeHelper.mConfig; if (origin == ZenChangeOrigin.ORIGIN_USER_IN_SYSTEMUI) { @@ -3123,15 +3191,15 @@ public class ZenModeHelperTest extends UiServiceTestCase { setupZenConfig(); // note that caller=null because that's how it comes in from NMS.setZenMode - mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, - ZenModeConfig.ORIGIN_SYSTEM, "", null, SYSTEM_UID); + mZenModeHelper.setManualZenMode(UserHandle.CURRENT, ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, + ORIGIN_SYSTEM, "", null, SYSTEM_UID); // confirm that setting zen mode via setManualZenMode changed the zen mode correctly assertEquals(ZEN_MODE_IMPORTANT_INTERRUPTIONS, mZenModeHelper.mZenMode); // and also that it works to turn it back off again - mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null, ZenModeConfig.ORIGIN_SYSTEM, "", - null, SYSTEM_UID); + mZenModeHelper.setManualZenMode(UserHandle.CURRENT, ZEN_MODE_OFF, null, + ORIGIN_SYSTEM, "", null, SYSTEM_UID); assertEquals(ZEN_MODE_OFF, mZenModeHelper.mZenMode); } @@ -3144,14 +3212,13 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Turn zen mode on (to important_interruptions) // Need to additionally call the looper in order to finish the post-apply-config process - mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, - Flags.modesApi() ? ORIGIN_USER_IN_SYSTEMUI : ZenModeConfig.ORIGIN_SYSTEM, "", - null, SYSTEM_UID); + mZenModeHelper.setManualZenMode(UserHandle.CURRENT, ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, + Flags.modesApi() ? ORIGIN_USER_IN_SYSTEMUI : ORIGIN_SYSTEM, "", null, SYSTEM_UID); // Now turn zen mode off, but via a different package UID -- this should get registered as // "not an action by the user" because some other app is changing zen mode - mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null, ORIGIN_APP, "", null, - CUSTOM_PKG_UID); + mZenModeHelper.setManualZenMode(UserHandle.CURRENT, ZEN_MODE_OFF, null, ORIGIN_APP, "", + null, CUSTOM_PKG_UID); // In total, this should be 2 loggable changes assertEquals(2, mZenModeEventLogger.numLoggedChanges()); @@ -3220,22 +3287,21 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), null, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule, - ORIGIN_APP, "test", CUSTOM_PKG_UID); + String id = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), zenRule, ORIGIN_APP, "test", CUSTOM_PKG_UID); // Event 1: Mimic the rule coming on automatically by setting the Condition to STATE_TRUE // Note that pre-modes_ui, this event serves as a test that automatic changes to an app's // that look like they're coming from the system are attributed to the app, but when // modes_ui is true, we opt to trust the provided change origin. - mZenModeHelper.setAutomaticZenRuleState(id, + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, id, new Condition(zenRule.getConditionId(), "", STATE_TRUE), - Flags.modesUi() ? ORIGIN_APP : ZenModeConfig.ORIGIN_SYSTEM, - CUSTOM_PKG_UID); + Flags.modesUi() ? ORIGIN_APP : ORIGIN_SYSTEM, CUSTOM_PKG_UID); // Event 2: "User" turns off the automatic rule (sets it to not enabled) zenRule.setEnabled(false); - mZenModeHelper.updateAutomaticZenRule(id, zenRule, - Flags.modesApi() ? ORIGIN_USER_IN_SYSTEMUI : ZenModeConfig.ORIGIN_SYSTEM, "", + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, id, zenRule, + Flags.modesApi() ? ORIGIN_USER_IN_SYSTEMUI : ORIGIN_SYSTEM, "", SYSTEM_UID); AutomaticZenRule systemRule = new AutomaticZenRule("systemRule", @@ -3244,18 +3310,19 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), null, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String systemId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), systemRule, - Flags.modesApi() ? ORIGIN_USER_IN_SYSTEMUI : ZenModeConfig.ORIGIN_SYSTEM, "test", + String systemId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), systemRule, + Flags.modesApi() ? ORIGIN_USER_IN_SYSTEMUI : ORIGIN_SYSTEM, "test", SYSTEM_UID); // Event 3: turn on the system rule - mZenModeHelper.setAutomaticZenRuleState(systemId, + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, systemId, new Condition(zenRule.getConditionId(), "", STATE_TRUE), - ZenModeConfig.ORIGIN_SYSTEM, SYSTEM_UID); + ORIGIN_SYSTEM, SYSTEM_UID); // Event 4: "User" deletes the rule - mZenModeHelper.removeAutomaticZenRule(systemId, - Flags.modesApi() ? ORIGIN_USER_IN_SYSTEMUI : ZenModeConfig.ORIGIN_SYSTEM, "", + mZenModeHelper.removeAutomaticZenRule(UserHandle.CURRENT, systemId, + Flags.modesApi() ? ORIGIN_USER_IN_SYSTEMUI : ORIGIN_SYSTEM, "", SYSTEM_UID); // In total, this represents 4 events assertEquals(4, mZenModeEventLogger.numLoggedChanges()); @@ -3311,7 +3378,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertFalse(mZenModeEventLogger.getIsUserAction(2)); assertEquals(SYSTEM_UID, mZenModeEventLogger.getPackageUid(2)); assertThat(mZenModeEventLogger.getChangeOrigin(2)).isEqualTo( - Flags.modesUi() ? ZenModeConfig.ORIGIN_SYSTEM : 0); + Flags.modesUi() ? ORIGIN_SYSTEM : 0); // When the system rule is deleted, we consider this a user action that turns DND off // (again) @@ -3339,27 +3406,27 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), null, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule, - ORIGIN_APP, "test", CUSTOM_PKG_UID); + String id = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), zenRule, ORIGIN_APP, "test", CUSTOM_PKG_UID); // Event 1: Mimic the rule coming on manually when the user turns it on in the app // ("Turn on bedtime now" because user goes to bed earlier). - mZenModeHelper.setAutomaticZenRuleState(id, + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, id, new Condition(zenRule.getConditionId(), "", STATE_TRUE, SOURCE_USER_ACTION), ORIGIN_USER_IN_APP, CUSTOM_PKG_UID); // Event 2: App deactivates the rule automatically (it's 8 AM, bedtime schedule ends) - mZenModeHelper.setAutomaticZenRuleState(id, + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, id, new Condition(zenRule.getConditionId(), "", STATE_FALSE, SOURCE_SCHEDULE), ORIGIN_APP, CUSTOM_PKG_UID); // Event 3: App activates the rule automatically (it's now 11 PM, bedtime schedule starts) - mZenModeHelper.setAutomaticZenRuleState(id, + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, id, new Condition(zenRule.getConditionId(), "", STATE_TRUE, SOURCE_SCHEDULE), ORIGIN_APP, CUSTOM_PKG_UID); // Event 4: User deactivates the rule manually (they get up before 8 AM on the next day) - mZenModeHelper.setAutomaticZenRuleState(id, + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, id, new Condition(zenRule.getConditionId(), "", STATE_FALSE, SOURCE_USER_ACTION), ORIGIN_USER_IN_APP, CUSTOM_PKG_UID); @@ -3427,21 +3494,23 @@ public class ZenModeHelperTest extends UiServiceTestCase { setupZenConfig(); // First just turn zen mode on - mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, + mZenModeHelper.setManualZenMode(UserHandle.CURRENT, ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, ORIGIN_USER_IN_SYSTEMUI, "", null, SYSTEM_UID); // Now change the policy slightly; want to confirm that this'll be reflected in the logs ZenModeConfig newConfig = mZenModeHelper.mConfig.copy(); - mZenModeHelper.setNotificationPolicy(new Policy(PRIORITY_CATEGORY_ALARMS, 0, 0), + mZenModeHelper.setNotificationPolicy(UserHandle.CURRENT, + new Policy(PRIORITY_CATEGORY_ALARMS, 0, 0), ORIGIN_USER_IN_SYSTEMUI, SYSTEM_UID); // Turn zen mode off; we want to make sure policy changes do not get logged when zen mode // is off. - mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null, ZenModeConfig.ORIGIN_SYSTEM, "", - null, SYSTEM_UID); + mZenModeHelper.setManualZenMode(UserHandle.CURRENT, ZEN_MODE_OFF, null, + ORIGIN_SYSTEM, "", null, SYSTEM_UID); // Change the policy again - mZenModeHelper.setNotificationPolicy(new Policy(PRIORITY_CATEGORY_REPEAT_CALLERS, 0, 0), + mZenModeHelper.setNotificationPolicy(UserHandle.CURRENT, + new Policy(PRIORITY_CATEGORY_REPEAT_CALLERS, 0, 0), ORIGIN_USER_IN_SYSTEMUI, SYSTEM_UID); // Total events: we only expect ones for turning on, changing policy, and turning off @@ -3485,8 +3554,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), null, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule, - ZenModeConfig.ORIGIN_SYSTEM, "test", SYSTEM_UID); + String id = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), zenRule, ORIGIN_SYSTEM, "test", SYSTEM_UID); // Rule 2, same as rule 1 AutomaticZenRule zenRule2 = new AutomaticZenRule("name2", @@ -3495,8 +3564,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), null, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id2 = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule2, - ZenModeConfig.ORIGIN_SYSTEM, "test", SYSTEM_UID); + String id2 = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), zenRule2, ORIGIN_SYSTEM, "test", SYSTEM_UID); // Rule 3, has stricter settings than the default settings ZenModeConfig ruleConfig = mZenModeHelper.mConfig.copy(); @@ -3507,28 +3576,27 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), ruleConfig.getZenPolicy(), NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id3 = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule3, - ZenModeConfig.ORIGIN_SYSTEM, "test", SYSTEM_UID); + String id3 = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), zenRule3, ORIGIN_SYSTEM, "test", SYSTEM_UID); // First: turn on rule 1 - mZenModeHelper.setAutomaticZenRuleState(id, - new Condition(zenRule.getConditionId(), "", STATE_TRUE), - ZenModeConfig.ORIGIN_SYSTEM, SYSTEM_UID); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, id, + new Condition(zenRule.getConditionId(), "", STATE_TRUE), ORIGIN_SYSTEM, SYSTEM_UID); // Second: turn on rule 2 - mZenModeHelper.setAutomaticZenRuleState(id2, - new Condition(zenRule2.getConditionId(), "", STATE_TRUE), - ZenModeConfig.ORIGIN_SYSTEM, SYSTEM_UID); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, id2, + new Condition(zenRule2.getConditionId(), "", STATE_TRUE), ORIGIN_SYSTEM, + SYSTEM_UID); // Third: turn on rule 3 - mZenModeHelper.setAutomaticZenRuleState(id3, - new Condition(zenRule3.getConditionId(), "", STATE_TRUE), - ZenModeConfig.ORIGIN_SYSTEM, SYSTEM_UID); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, id3, + new Condition(zenRule3.getConditionId(), "", STATE_TRUE), ORIGIN_SYSTEM, + SYSTEM_UID); // Fourth: Turn *off* rule 2 - mZenModeHelper.setAutomaticZenRuleState(id2, - new Condition(zenRule2.getConditionId(), "", STATE_FALSE), - ZenModeConfig.ORIGIN_SYSTEM, SYSTEM_UID); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, id2, + new Condition(zenRule2.getConditionId(), "", STATE_FALSE), ORIGIN_SYSTEM, + SYSTEM_UID); // This should result in a total of four events assertEquals(4, mZenModeEventLogger.numLoggedChanges()); @@ -3612,8 +3680,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), manualRulePolicy, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule, - ORIGIN_APP, "test", SYSTEM_UID); + String id = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), zenRule, ORIGIN_APP, "test", SYSTEM_UID); // Rule 2, same as rule 1 but owned by the system AutomaticZenRule zenRule2 = new AutomaticZenRule("name2", @@ -3622,41 +3690,38 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), manualRulePolicy, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id2 = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule2, - ORIGIN_USER_IN_SYSTEMUI, "test", SYSTEM_UID); + String id2 = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), zenRule2, ORIGIN_USER_IN_SYSTEMUI, "test", SYSTEM_UID); // Turn on rule 1; call looks like it's from the system. Because setting a condition is // typically an automatic (non-user-initiated) action, expect the calling UID to be // re-evaluated to the one associated with CUSTOM_PKG_NAME. // When modes_ui is true: we expect the change origin to be the source of truth. - mZenModeHelper.setAutomaticZenRuleState(id, + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, id, new Condition(zenRule.getConditionId(), "", STATE_TRUE), - Flags.modesUi() ? ORIGIN_APP : ZenModeConfig.ORIGIN_SYSTEM, - SYSTEM_UID); + Flags.modesUi() ? ORIGIN_APP : ORIGIN_SYSTEM, SYSTEM_UID); // Second: turn on rule 2. This is a system-owned rule and the UID should not be modified // (nor even looked up; the mock PackageManager won't handle "android" as input). - mZenModeHelper.setAutomaticZenRuleState(id2, - new Condition(zenRule2.getConditionId(), "", STATE_TRUE), - ZenModeConfig.ORIGIN_SYSTEM, SYSTEM_UID); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, id2, + new Condition(zenRule2.getConditionId(), "", STATE_TRUE), ORIGIN_SYSTEM, + SYSTEM_UID); // Disable rule 1. Because this looks like a user action, the UID should not be modified // from the system-provided one unless modes_ui is true. zenRule.setEnabled(false); - mZenModeHelper.updateAutomaticZenRule(id, zenRule, + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, id, zenRule, ORIGIN_USER_IN_SYSTEMUI, "", SYSTEM_UID); // Add a manual rule. Any manual rule changes should not get calling uids reassigned. - mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, - ORIGIN_APP, - "", null, CUSTOM_PKG_UID); + mZenModeHelper.setManualZenMode(UserHandle.CURRENT, ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, + ORIGIN_APP, "", null, CUSTOM_PKG_UID); // Change rule 2's condition, but from some other UID. Since it doesn't look like it's from // the system, we keep the UID info. // Note that this probably shouldn't be able to occur in real scenarios. - mZenModeHelper.setAutomaticZenRuleState(id2, - new Condition(zenRule2.getConditionId(), "", STATE_FALSE), - ORIGIN_APP, 12345); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, id2, + new Condition(zenRule2.getConditionId(), "", STATE_FALSE), ORIGIN_APP, 12345); // That was 5 events total assertEquals(5, mZenModeEventLogger.numLoggedChanges()); @@ -3712,32 +3777,31 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Turn on zen mode with a manual rule with an enabler set. This should *not* count // as a user action, and *should* get its UID reassigned. - mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, - ZenModeConfig.ORIGIN_SYSTEM, "", CUSTOM_PKG_NAME, SYSTEM_UID); + mZenModeHelper.setManualZenMode(UserHandle.CURRENT, ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, + ORIGIN_SYSTEM, "", CUSTOM_PKG_NAME, SYSTEM_UID); assertEquals(1, mZenModeEventLogger.numLoggedChanges()); // Now change apps bypassing to true ZenModeConfig newConfig = mZenModeHelper.mConfig.copy(); newConfig.areChannelsBypassingDnd = true; - mZenModeHelper.setNotificationPolicy(newConfig.toNotificationPolicy(), - ZenModeConfig.ORIGIN_SYSTEM, SYSTEM_UID); + mZenModeHelper.setNotificationPolicy(UserHandle.CURRENT, newConfig.toNotificationPolicy(), + ORIGIN_SYSTEM, SYSTEM_UID); assertEquals(2, mZenModeEventLogger.numLoggedChanges()); // and then back to false, all without changing anything else newConfig.areChannelsBypassingDnd = false; - mZenModeHelper.setNotificationPolicy(newConfig.toNotificationPolicy(), - ZenModeConfig.ORIGIN_SYSTEM, SYSTEM_UID); + mZenModeHelper.setNotificationPolicy(UserHandle.CURRENT, newConfig.toNotificationPolicy(), + ORIGIN_SYSTEM, SYSTEM_UID); assertEquals(3, mZenModeEventLogger.numLoggedChanges()); // Turn off manual mode, call from a package: don't reset UID even though enabler is set - mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null, ORIGIN_APP, "", + mZenModeHelper.setManualZenMode(UserHandle.CURRENT, ZEN_MODE_OFF, null, ORIGIN_APP, "", CUSTOM_PKG_NAME, 12345); assertEquals(4, mZenModeEventLogger.numLoggedChanges()); // And likewise when turning it back on again - mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, - ORIGIN_APP, - "", CUSTOM_PKG_NAME, 12345); + mZenModeHelper.setManualZenMode(UserHandle.CURRENT, ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, + ORIGIN_APP, "", CUSTOM_PKG_NAME, 12345); // These are 5 events in total. assertEquals(5, mZenModeEventLogger.numLoggedChanges()); @@ -3782,8 +3846,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { setupZenConfig(); // First just turn zen mode on - mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, - ZenModeConfig.ORIGIN_SYSTEM, "", null, SYSTEM_UID); + mZenModeHelper.setManualZenMode(UserHandle.CURRENT, ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, + ORIGIN_SYSTEM, "", null, SYSTEM_UID); // Now change only the channels part of the policy; want to confirm that this'll be // reflected in the logs @@ -3793,8 +3857,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { oldPolicy.priorityMessageSenders, oldPolicy.suppressedVisualEffects, STATE_PRIORITY_CHANNELS_BLOCKED, oldPolicy.priorityConversationSenders); - mZenModeHelper.setNotificationPolicy(newPolicy, - ZenModeConfig.ORIGIN_SYSTEM, SYSTEM_UID); + mZenModeHelper.setNotificationPolicy(UserHandle.CURRENT, newPolicy, ORIGIN_SYSTEM, + SYSTEM_UID); // Total events: one for turning on, one for changing policy assertThat(mZenModeEventLogger.numLoggedChanges()).isEqualTo(2); @@ -3834,16 +3898,16 @@ public class ZenModeHelperTest extends UiServiceTestCase { Uri.parse("condition"), null, NotificationManager.INTERRUPTION_FILTER_ALL, true); - String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule, - ORIGIN_APP, "test", CUSTOM_PKG_UID); + String id = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), zenRule, ORIGIN_APP, "test", CUSTOM_PKG_UID); // Event 1: App activates the rule automatically. - mZenModeHelper.setAutomaticZenRuleState(id, + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, id, new Condition(zenRule.getConditionId(), "", STATE_TRUE, SOURCE_SCHEDULE), ORIGIN_APP, CUSTOM_PKG_UID); // Event 2: App deactivates the rule automatically. - mZenModeHelper.setAutomaticZenRuleState(id, + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, id, new Condition(zenRule.getConditionId(), "", STATE_FALSE, SOURCE_SCHEDULE), ORIGIN_APP, CUSTOM_PKG_UID); @@ -3876,35 +3940,33 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setInterruptionFilter(INTERRUPTION_FILTER_ALL) .setType(TYPE_BEDTIME) .build(); - String bedtimeRuleId = mZenModeHelper.addAutomaticZenRule(mPkg, bedtime, - ORIGIN_APP, - "reason", CUSTOM_PKG_UID); + String bedtimeRuleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, bedtime, + ORIGIN_APP, "reason", CUSTOM_PKG_UID); // Create immersive rule AutomaticZenRule immersive = new AutomaticZenRule.Builder("Immersed", CONDITION_ID) .setType(TYPE_IMMERSIVE) .setZenPolicy(mZenModeHelper.mConfig.getZenPolicy()) // same as the manual rule .build(); - String immersiveId = mZenModeHelper.addAutomaticZenRule(mPkg, immersive, - ORIGIN_APP, - "reason", CUSTOM_PKG_UID); + String immersiveId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, immersive, + ORIGIN_APP, "reason", CUSTOM_PKG_UID); // Event 1: Activate bedtime rule. This doesn't turn on notification filtering - mZenModeHelper.setAutomaticZenRuleState(bedtimeRuleId, + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, bedtimeRuleId, new Condition(bedtime.getConditionId(), "", STATE_TRUE, SOURCE_SCHEDULE), ORIGIN_APP, CUSTOM_PKG_UID); // Event 2: turn on manual zen mode. Manual rule will have ACTIVE_RULE_TYPE_MANUAL - mZenModeHelper.setManualZenMode(ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, - ZenModeConfig.ORIGIN_SYSTEM, "", null, SYSTEM_UID); + mZenModeHelper.setManualZenMode(UserHandle.CURRENT, ZEN_MODE_IMPORTANT_INTERRUPTIONS, null, + ORIGIN_SYSTEM, "", null, SYSTEM_UID); // Event 3: Turn immersive on - mZenModeHelper.setAutomaticZenRuleState(immersiveId, + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, immersiveId, new Condition(immersive.getConditionId(), "", STATE_TRUE, SOURCE_SCHEDULE), ORIGIN_APP, CUSTOM_PKG_UID); // Event 4: Turn off bedtime mode, leaving just manual + immersive - mZenModeHelper.setAutomaticZenRuleState(bedtimeRuleId, + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, bedtimeRuleId, new Condition(bedtime.getConditionId(), "", STATE_FALSE, SOURCE_SCHEDULE), ORIGIN_APP, CUSTOM_PKG_UID); @@ -3966,15 +4028,15 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), null, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule, - ZenModeConfig.ORIGIN_SYSTEM, "test", SYSTEM_UID); + String id = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), zenRule, ORIGIN_SYSTEM, "test", SYSTEM_UID); // enable the rule - mZenModeHelper.setAutomaticZenRuleState(id, + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, id, new Condition(zenRule.getConditionId(), "", STATE_TRUE), - ZenModeConfig.ORIGIN_SYSTEM, SYSTEM_UID); + ORIGIN_SYSTEM, SYSTEM_UID); - assertEquals(mZenModeHelper.getNotificationPolicy(), + assertEquals(mZenModeHelper.getNotificationPolicy(UserHandle.CURRENT), mZenModeHelper.getConsolidatedNotificationPolicy()); // inspect the consolidated policy. Based on setupZenConfig() values. @@ -4002,13 +4064,13 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), null, // null policy NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule, - ZenModeConfig.ORIGIN_SYSTEM, "test", SYSTEM_UID); + String id = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), zenRule, ORIGIN_SYSTEM, "test", SYSTEM_UID); // enable the rule - mZenModeHelper.setAutomaticZenRuleState(id, + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, id, new Condition(zenRule.getConditionId(), "", STATE_TRUE), - ZenModeConfig.ORIGIN_SYSTEM, SYSTEM_UID); + ORIGIN_SYSTEM, SYSTEM_UID); // inspect the consolidated policy, which should match the device default settings. assertThat(ZenAdapters.notificationPolicyToZenPolicy(mZenModeHelper.mConsolidatedPolicy)) @@ -4040,13 +4102,12 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), customPolicy, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule, - ZenModeConfig.ORIGIN_SYSTEM, "test", SYSTEM_UID); + String id = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), zenRule, ORIGIN_SYSTEM, "test", SYSTEM_UID); // enable the rule; this will update the consolidated policy - mZenModeHelper.setAutomaticZenRuleState(id, - new Condition(zenRule.getConditionId(), "", STATE_TRUE), - ZenModeConfig.ORIGIN_SYSTEM, SYSTEM_UID); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, id, + new Condition(zenRule.getConditionId(), "", STATE_TRUE), ORIGIN_SYSTEM, SYSTEM_UID); // since this is the only active rule, the consolidated policy should match the custom // policy for every field specified, and take default values (from device default or @@ -4085,13 +4146,12 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), customPolicy, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule, - ZenModeConfig.ORIGIN_SYSTEM, "test", SYSTEM_UID); + String id = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), zenRule, ORIGIN_SYSTEM, "test", SYSTEM_UID); // enable the rule; this will update the consolidated policy - mZenModeHelper.setAutomaticZenRuleState(id, - new Condition(zenRule.getConditionId(), "", STATE_TRUE), - ZenModeConfig.ORIGIN_SYSTEM, SYSTEM_UID); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, id, + new Condition(zenRule.getConditionId(), "", STATE_TRUE), ORIGIN_SYSTEM, SYSTEM_UID); // since this is the only active rule, the consolidated policy should match the custom // policy for every field specified, and take default values (from either device default @@ -4125,13 +4185,12 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), null, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule, - ZenModeConfig.ORIGIN_SYSTEM, "test", SYSTEM_UID); + String id = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), zenRule, ORIGIN_SYSTEM, "test", SYSTEM_UID); // enable rule 1 - mZenModeHelper.setAutomaticZenRuleState(id, - new Condition(zenRule.getConditionId(), "", STATE_TRUE), - ZenModeConfig.ORIGIN_SYSTEM, SYSTEM_UID); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, id, + new Condition(zenRule.getConditionId(), "", STATE_TRUE), ORIGIN_SYSTEM, SYSTEM_UID); // custom policy for rule 2 ZenPolicy customPolicy = new ZenPolicy.Builder() @@ -4149,13 +4208,13 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), customPolicy, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id2 = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule2, - ZenModeConfig.ORIGIN_SYSTEM, "test", SYSTEM_UID); + String id2 = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), zenRule2, ORIGIN_SYSTEM, "test", SYSTEM_UID); // enable rule 2; this will update the consolidated policy - mZenModeHelper.setAutomaticZenRuleState(id2, + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, id2, new Condition(zenRule2.getConditionId(), "", STATE_TRUE), - ZenModeConfig.ORIGIN_SYSTEM, SYSTEM_UID); + ORIGIN_SYSTEM, SYSTEM_UID); // now both rules should be on, and the consolidated policy should reflect the most // restrictive option of each of the two @@ -4186,13 +4245,12 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), null, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule, - ZenModeConfig.ORIGIN_SYSTEM, "test", SYSTEM_UID); + String id = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), zenRule, ORIGIN_SYSTEM, "test", SYSTEM_UID); // enable rule 1 - mZenModeHelper.setAutomaticZenRuleState(id, - new Condition(zenRule.getConditionId(), "", STATE_TRUE), - ZenModeConfig.ORIGIN_SYSTEM, SYSTEM_UID); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, id, + new Condition(zenRule.getConditionId(), "", STATE_TRUE), ORIGIN_SYSTEM, SYSTEM_UID); // custom policy for rule 2 ZenPolicy customPolicy = new ZenPolicy.Builder() @@ -4210,13 +4268,13 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), customPolicy, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id2 = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule2, - ZenModeConfig.ORIGIN_SYSTEM, "test", SYSTEM_UID); + String id2 = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), zenRule2, ORIGIN_SYSTEM, "test", SYSTEM_UID); // enable rule 2; this will update the consolidated policy - mZenModeHelper.setAutomaticZenRuleState(id2, - new Condition(zenRule2.getConditionId(), "", STATE_TRUE), - ZenModeConfig.ORIGIN_SYSTEM, SYSTEM_UID); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, id2, + new Condition(zenRule2.getConditionId(), "", STATE_TRUE), ORIGIN_SYSTEM, + SYSTEM_UID); // now both rules should be on, and the consolidated policy should reflect the most // restrictive option of each of the two @@ -4252,13 +4310,12 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), customPolicy, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule, - ZenModeConfig.ORIGIN_SYSTEM, "test", SYSTEM_UID); + String id = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), zenRule, ORIGIN_SYSTEM, "test", SYSTEM_UID); // enable the rule; this will update the consolidated policy - mZenModeHelper.setAutomaticZenRuleState(id, - new Condition(zenRule.getConditionId(), "", STATE_TRUE), - ZenModeConfig.ORIGIN_SYSTEM, SYSTEM_UID); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, id, + new Condition(zenRule.getConditionId(), "", STATE_TRUE), ORIGIN_SYSTEM, SYSTEM_UID); // confirm that channels make it through assertTrue(mZenModeHelper.mConsolidatedPolicy.allowPriorityChannels()); @@ -4274,13 +4331,13 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), strictPolicy, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String id2 = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), zenRule2, - ZenModeConfig.ORIGIN_SYSTEM, "test", SYSTEM_UID); + String id2 = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), zenRule2, ORIGIN_SYSTEM, "test", SYSTEM_UID); // enable rule 2; this will update the consolidated policy - mZenModeHelper.setAutomaticZenRuleState(id2, - new Condition(zenRule2.getConditionId(), "", STATE_TRUE), - ZenModeConfig.ORIGIN_SYSTEM, SYSTEM_UID); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, id2, + new Condition(zenRule2.getConditionId(), "", STATE_TRUE), ORIGIN_SYSTEM, + SYSTEM_UID); // rule 2 should override rule 1 assertFalse(mZenModeHelper.mConsolidatedPolicy.allowPriorityChannels()); @@ -4305,9 +4362,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { .allowSystem(true) .build(), NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - String rule1Id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - zenRuleWithPriority, ORIGIN_APP, "test", CUSTOM_PKG_UID); - mZenModeHelper.setAutomaticZenRuleState(rule1Id, + String rule1Id = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), zenRuleWithPriority, ORIGIN_APP, "test", CUSTOM_PKG_UID); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, rule1Id, new Condition(zenRuleWithPriority.getConditionId(), "", STATE_TRUE), ORIGIN_APP, CUSTOM_PKG_UID); @@ -4318,9 +4375,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { Uri.parse("priority"), new ZenPolicy.Builder().disallowAllSounds().build(), NotificationManager.INTERRUPTION_FILTER_ALL, true); - String rule2Id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - zenRuleWithAll, ORIGIN_APP, "test", CUSTOM_PKG_UID); - mZenModeHelper.setAutomaticZenRuleState(rule2Id, + String rule2Id = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), zenRuleWithAll, ORIGIN_APP, "test", CUSTOM_PKG_UID); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, rule2Id, new Condition(zenRuleWithPriority.getConditionId(), "", STATE_TRUE), ORIGIN_APP, CUSTOM_PKG_UID); @@ -4364,7 +4421,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { rule.triggerDescription = TRIGGER_DESC; mZenModeHelper.mConfig.automaticRules.put(rule.id, rule); - AutomaticZenRule actual = mZenModeHelper.getAutomaticZenRule(rule.id); + AutomaticZenRule actual = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, rule.id); assertEquals(NAME, actual.getName()); assertEquals(OWNER, actual.getOwner()); @@ -4400,8 +4457,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setManualInvocationAllowed(ALLOW_MANUAL) .build(); - String ruleId = mZenModeHelper.addAutomaticZenRule(OWNER.getPackageName(), azr, - ORIGIN_APP, "add", CUSTOM_PKG_UID); + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + OWNER.getPackageName(), azr, ORIGIN_APP, "add", CUSTOM_PKG_UID); ZenModeConfig.ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); @@ -4430,17 +4487,17 @@ public class ZenModeHelperTest extends UiServiceTestCase { AutomaticZenRule azrBase = new AutomaticZenRule.Builder("OriginalName", CONDITION_ID) .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) .build(); - String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - azrBase, ORIGIN_APP, "reason", SYSTEM_UID); - AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId); + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), azrBase, ORIGIN_APP, "reason", SYSTEM_UID); + AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); // Checks the name can be changed by the app because the user has not modified it. AutomaticZenRule azrUpdate = new AutomaticZenRule.Builder(rule) .setName("NewName") .build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, ORIGIN_APP, "reason", - SYSTEM_UID); - rule = mZenModeHelper.getAutomaticZenRule(ruleId); + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, azrUpdate, ORIGIN_APP, + "reason", SYSTEM_UID); + rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); assertThat(rule.getName()).isEqualTo("NewName"); // The user modifies some other field in the rule, which makes the rule as a whole not @@ -4448,35 +4505,35 @@ public class ZenModeHelperTest extends UiServiceTestCase { azrUpdate = new AutomaticZenRule.Builder(rule) .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS) .build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, ORIGIN_USER_IN_SYSTEMUI, "reason", - SYSTEM_UID); + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, azrUpdate, + ORIGIN_USER_IN_SYSTEMUI, "reason", SYSTEM_UID); // ...but the app can still modify the name, because the name itself hasn't been modified // by the user. azrUpdate = new AutomaticZenRule.Builder(rule) .setName("NewAppName") .build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, ORIGIN_APP, "reason", - SYSTEM_UID); - rule = mZenModeHelper.getAutomaticZenRule(ruleId); + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, azrUpdate, ORIGIN_APP, + "reason", SYSTEM_UID); + rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); assertThat(rule.getName()).isEqualTo("NewAppName"); // The user modifies the name. azrUpdate = new AutomaticZenRule.Builder(rule) .setName("UserProvidedName") .build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, ORIGIN_USER_IN_SYSTEMUI, "reason", - SYSTEM_UID); - rule = mZenModeHelper.getAutomaticZenRule(ruleId); + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, azrUpdate, + ORIGIN_USER_IN_SYSTEMUI, "reason", SYSTEM_UID); + rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); assertThat(rule.getName()).isEqualTo("UserProvidedName"); // The app is no longer able to modify the name. azrUpdate = new AutomaticZenRule.Builder(rule) .setName("NewAppName") .build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, ORIGIN_APP, "reason", - SYSTEM_UID); - rule = mZenModeHelper.getAutomaticZenRule(ruleId); + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, azrUpdate, ORIGIN_APP, + "reason", SYSTEM_UID); + rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); assertThat(rule.getName()).isEqualTo("UserProvidedName"); } @@ -4490,9 +4547,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setDeviceEffects(new ZenDeviceEffects.Builder().build()) .build(); // Adds the rule using the app, to avoid having any user modified bits set. - String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - azrBase, ORIGIN_APP, "reason", SYSTEM_UID); - AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId); + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), azrBase, ORIGIN_APP, "reason", SYSTEM_UID); + AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); // Modifies the filter, icon, zen policy, and device effects ZenPolicy policy = new ZenPolicy.Builder(rule.getZenPolicy()) @@ -4510,9 +4567,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { .build(); // Update the rule with the AZR from origin user. - mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, ORIGIN_USER_IN_SYSTEMUI, "reason", - SYSTEM_UID); - rule = mZenModeHelper.getAutomaticZenRule(ruleId); + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, azrUpdate, + ORIGIN_USER_IN_SYSTEMUI, "reason", SYSTEM_UID); + rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); // UPDATE_ORIGIN_USER should change the bitmask and change the values. assertThat(rule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_PRIORITY); @@ -4547,9 +4604,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { .build()) .build(); // Adds the rule using the app, to avoid having any user modified bits set. - String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - azrBase, ORIGIN_APP, "reason", SYSTEM_UID); - AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId); + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), azrBase, ORIGIN_APP, "reason", SYSTEM_UID); + AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); // Modifies the icon, zen policy and device effects ZenPolicy policy = new ZenPolicy.Builder(rule.getZenPolicy()) @@ -4567,9 +4624,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { .build(); // Update the rule with the AZR from origin systemUI. - mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, ZenModeConfig.ORIGIN_SYSTEM, + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, azrUpdate, ORIGIN_SYSTEM, "reason", SYSTEM_UID); - rule = mZenModeHelper.getAutomaticZenRule(ruleId); + rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); // UPDATE_ORIGIN_SYSTEM_OR_SYSTEMUI should change the value but NOT update the bitmask. assertThat(rule.getIconResId()).isEqualTo(ICON_RES_ID); @@ -4597,9 +4654,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { .build()) .build(); // Adds the rule using the app, to avoid having any user modified bits set. - String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - azrBase, ORIGIN_APP, "reason", SYSTEM_UID); - AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId); + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), azrBase, ORIGIN_APP, "reason", SYSTEM_UID); + AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); ZenPolicy policy = new ZenPolicy.Builder() .allowReminders(true) @@ -4615,8 +4672,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Since the rule is not already user modified, UPDATE_ORIGIN_APP can modify the rule. // The bitmask is not modified. - mZenModeHelper.updateAutomaticZenRule(ruleId, azrUpdate, ORIGIN_APP, "reason", - SYSTEM_UID); + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, azrUpdate, ORIGIN_APP, + "reason", SYSTEM_UID); ZenRule storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); assertThat(storedRule.userModifiedFields).isEqualTo(0); @@ -4630,8 +4687,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertThat(storedRule.zenDeviceEffectsUserModifiedFields).isEqualTo(0); // Creates another rule, this time from user. This will have user modified bits set. - String ruleIdUser = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - azrBase, ORIGIN_USER_IN_SYSTEMUI, "reason", SYSTEM_UID); + String ruleIdUser = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), azrBase, ORIGIN_USER_IN_SYSTEMUI, "reason", SYSTEM_UID); storedRule = mZenModeHelper.mConfig.automaticRules.get(ruleIdUser); int ruleModifiedFields = storedRule.userModifiedFields; int rulePolicyModifiedFields = storedRule.zenPolicyUserModifiedFields; @@ -4639,9 +4696,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Zen rule update coming from the app again. This cannot fully update the rule, because // the rule is already considered user modified. - mZenModeHelper.updateAutomaticZenRule(ruleIdUser, azrUpdate, ORIGIN_APP, + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleIdUser, azrUpdate, ORIGIN_APP, "reason", SYSTEM_UID); - AutomaticZenRule ruleUser = mZenModeHelper.getAutomaticZenRule(ruleIdUser); + AutomaticZenRule ruleUser = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, + ruleIdUser); // The app can only change the value if the rule is not already user modified, // so the rule is not changed, and neither is the bitmask. @@ -4670,9 +4728,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setShouldDisplayGrayscale(true) .build()) .build(); - String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - azrBase, ORIGIN_APP, "reason", SYSTEM_UID); - AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId); + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), azrBase, ORIGIN_APP, "reason", SYSTEM_UID); + AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); // The values are modified but the bitmask is not. assertThat(rule.getZenPolicy().getPriorityCategoryReminders()) @@ -4692,8 +4750,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setDeviceEffects(zde) .build(); // Adds the rule using the app, to avoid having any user modified bits set. - String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - azrBase, ORIGIN_APP, "reason", SYSTEM_UID); + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), azrBase, ORIGIN_APP, "reason", SYSTEM_UID); AutomaticZenRule azr = new AutomaticZenRule.Builder(azrBase) // Sets Device Effects to null @@ -4702,9 +4760,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Zen rule update coming from app, but since the rule isn't already // user modified, it can be updated. - mZenModeHelper.updateAutomaticZenRule(ruleId, azr, ORIGIN_APP, "reason", + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, azr, ORIGIN_APP, "reason", SYSTEM_UID); - AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId); + AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); // When AZR's ZenDeviceEffects is null, the updated rule's device effects are kept. assertThat(rule.getDeviceEffects()).isEqualTo(zde); @@ -4718,9 +4776,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setZenPolicy(POLICY) .build(); // Adds the rule using the app, to avoid having any user modified bits set. - String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - azrBase, ORIGIN_APP, "reason", SYSTEM_UID); - AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId); + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), azrBase, ORIGIN_APP, "reason", SYSTEM_UID); + AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); AutomaticZenRule azr = new AutomaticZenRule.Builder(azrBase) // Set zen policy to null @@ -4729,9 +4787,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Zen rule update coming from app, but since the rule isn't already // user modified, it can be updated. - mZenModeHelper.updateAutomaticZenRule(ruleId, azr, ORIGIN_APP, "reason", + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, azr, ORIGIN_APP, "reason", SYSTEM_UID); - rule = mZenModeHelper.getAutomaticZenRule(ruleId); + rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); // When AZR's ZenPolicy is null, we expect the updated rule's policy to be unchanged // (equivalent to the provided policy, with additional fields filled in with defaults). @@ -4750,9 +4808,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { // .setDeviceEffects(new ZenDeviceEffects.Builder().build()) .build(); // Adds the rule using the app, to avoid having any user modified bits set. - String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - azrBase, ORIGIN_APP, "reason", SYSTEM_UID); - AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId); + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), azrBase, ORIGIN_APP, "reason", SYSTEM_UID); + AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); // Create a fully populated ZenPolicy. ZenPolicy policy = new ZenPolicy.Builder() @@ -4780,9 +4838,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Applies the update to the rule. // Default config defined in getDefaultConfigParser() is used as the original rule. - mZenModeHelper.updateAutomaticZenRule(ruleId, azr, ORIGIN_USER_IN_SYSTEMUI, "reason", - SYSTEM_UID); - rule = mZenModeHelper.getAutomaticZenRule(ruleId); + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, azr, + ORIGIN_USER_IN_SYSTEMUI, "reason", SYSTEM_UID); + rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); // New ZenPolicy differs from the default config assertThat(rule.getZenPolicy()).isNotNull(); @@ -4811,9 +4869,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setDeviceEffects(null) .build(); // Adds the rule using the app, to avoid having any user modified bits set. - String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - azrBase, ORIGIN_APP, "reason", SYSTEM_UID); - AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId); + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), azrBase, ORIGIN_APP, "reason", SYSTEM_UID); + AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); ZenDeviceEffects deviceEffects = new ZenDeviceEffects.Builder() .setShouldDisplayGrayscale(true) @@ -4823,9 +4881,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { .build(); // Applies the update to the rule. - mZenModeHelper.updateAutomaticZenRule(ruleId, azr, ORIGIN_USER_IN_SYSTEMUI, "reason", - SYSTEM_UID); - rule = mZenModeHelper.getAutomaticZenRule(ruleId); + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, azr, + ORIGIN_USER_IN_SYSTEMUI, "reason", SYSTEM_UID); + rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); // New ZenDeviceEffects is used; all fields considered set, since previously were null. assertThat(rule.getDeviceEffects()).isNotNull(); @@ -4848,8 +4906,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), null, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - final String createdId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - zenRule, ZenModeConfig.ORIGIN_SYSTEM, "test", SYSTEM_UID); + final String createdId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), zenRule, ORIGIN_SYSTEM, "test", SYSTEM_UID); CountDownLatch latch = new CountDownLatch(1); final int[] actualStatus = new int[1]; @@ -4865,7 +4923,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.addCallback(callback); zenRule.setEnabled(false); - mZenModeHelper.updateAutomaticZenRule(createdId, zenRule, ZenModeConfig.ORIGIN_SYSTEM, + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, createdId, zenRule, ORIGIN_SYSTEM, "", SYSTEM_UID); assertTrue(latch.await(500, TimeUnit.MILLISECONDS)); @@ -4883,8 +4941,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), null, NotificationManager.INTERRUPTION_FILTER_PRIORITY, false); - final String createdId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - zenRule, ZenModeConfig.ORIGIN_SYSTEM, "test", SYSTEM_UID); + final String createdId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), zenRule, ORIGIN_SYSTEM, "test", SYSTEM_UID); CountDownLatch latch = new CountDownLatch(1); final int[] actualStatus = new int[1]; @@ -4900,7 +4958,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.addCallback(callback); zenRule.setEnabled(true); - mZenModeHelper.updateAutomaticZenRule(createdId, zenRule, ZenModeConfig.ORIGIN_SYSTEM, + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, createdId, zenRule, ORIGIN_SYSTEM, "", SYSTEM_UID); assertTrue(latch.await(500, TimeUnit.MILLISECONDS)); @@ -4919,8 +4977,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), null, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - final String createdId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - zenRule, ZenModeConfig.ORIGIN_SYSTEM, "test", SYSTEM_UID); + final String createdId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), zenRule, ORIGIN_SYSTEM, "test", SYSTEM_UID); CountDownLatch latch = new CountDownLatch(1); final int[] actualStatus = new int[1]; @@ -4935,9 +4993,9 @@ public class ZenModeHelperTest extends UiServiceTestCase { }; mZenModeHelper.addCallback(callback); - mZenModeHelper.setAutomaticZenRuleState(createdId, + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, createdId, new Condition(zenRule.getConditionId(), "", STATE_TRUE), - ZenModeConfig.ORIGIN_SYSTEM, SYSTEM_UID); + ORIGIN_SYSTEM, SYSTEM_UID); assertTrue(latch.await(500, TimeUnit.MILLISECONDS)); if (CompatChanges.isChangeEnabled(ZenModeHelper.SEND_ACTIVATION_AZR_STATUSES)) { @@ -4959,8 +5017,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), null, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - final String createdId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - zenRule, ZenModeConfig.ORIGIN_SYSTEM, "test", SYSTEM_UID); + final String createdId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), zenRule, ORIGIN_SYSTEM, "test", SYSTEM_UID); CountDownLatch latch = new CountDownLatch(1); final int[] actualStatus = new int[2]; @@ -4977,12 +5035,11 @@ public class ZenModeHelperTest extends UiServiceTestCase { }; mZenModeHelper.addCallback(callback); - mZenModeHelper.setAutomaticZenRuleState(createdId, - new Condition(zenRule.getConditionId(), "", STATE_TRUE), - ZenModeConfig.ORIGIN_SYSTEM, SYSTEM_UID); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, createdId, + new Condition(zenRule.getConditionId(), "", STATE_TRUE), ORIGIN_SYSTEM, SYSTEM_UID); - mZenModeHelper.setManualZenMode(Global.ZEN_MODE_OFF, null, ZenModeConfig.ORIGIN_SYSTEM, - null, "", SYSTEM_UID); + mZenModeHelper.setManualZenMode(UserHandle.CURRENT, Global.ZEN_MODE_OFF, null, + ORIGIN_SYSTEM, null, "", SYSTEM_UID); assertTrue(latch.await(500, TimeUnit.MILLISECONDS)); if (CompatChanges.isChangeEnabled(ZenModeHelper.SEND_ACTIVATION_AZR_STATUSES)) { @@ -5004,8 +5061,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), null, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - final String createdId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - zenRule, ZenModeConfig.ORIGIN_SYSTEM, "test", SYSTEM_UID); + final String createdId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), zenRule, ORIGIN_SYSTEM, "test", SYSTEM_UID); CountDownLatch latch = new CountDownLatch(1); final int[] actualStatus = new int[2]; @@ -5022,13 +5079,12 @@ public class ZenModeHelperTest extends UiServiceTestCase { }; mZenModeHelper.addCallback(callback); - mZenModeHelper.setAutomaticZenRuleState(createdId, - new Condition(zenRule.getConditionId(), "", STATE_TRUE), - ZenModeConfig.ORIGIN_SYSTEM, SYSTEM_UID); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, createdId, + new Condition(zenRule.getConditionId(), "", STATE_TRUE), ORIGIN_SYSTEM, SYSTEM_UID); - mZenModeHelper.setAutomaticZenRuleState(createdId, - new Condition(zenRule.getConditionId(), "", STATE_FALSE), - ORIGIN_APP, CUSTOM_PKG_UID); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, createdId, + new Condition(zenRule.getConditionId(), "", STATE_FALSE), ORIGIN_APP, + CUSTOM_PKG_UID); assertTrue(latch.await(500, TimeUnit.MILLISECONDS)); if (CompatChanges.isChangeEnabled(ZenModeHelper.SEND_ACTIVATION_AZR_STATUSES)) { @@ -5049,21 +5105,20 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenModeConfig.toScheduleConditionId(new ScheduleInfo()), null, NotificationManager.INTERRUPTION_FILTER_PRIORITY, true); - final String createdId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - zenRule, ZenModeConfig.ORIGIN_SYSTEM, "test", SYSTEM_UID); + final String createdId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), zenRule, ORIGIN_SYSTEM, "test", SYSTEM_UID); // Event 1: Mimic the rule coming on automatically by setting the Condition to STATE_TRUE - mZenModeHelper.setAutomaticZenRuleState(createdId, - new Condition(zenRule.getConditionId(), "", STATE_TRUE), - ZenModeConfig.ORIGIN_SYSTEM, SYSTEM_UID); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, createdId, + new Condition(zenRule.getConditionId(), "", STATE_TRUE), ORIGIN_SYSTEM, SYSTEM_UID); // Event 2: Snooze rule by turning off DND - mZenModeHelper.setManualZenMode(Global.ZEN_MODE_OFF, null, ZenModeConfig.ORIGIN_SYSTEM, - "", null, SYSTEM_UID); + mZenModeHelper.setManualZenMode(UserHandle.CURRENT, Global.ZEN_MODE_OFF, null, + ORIGIN_SYSTEM, "", null, SYSTEM_UID); // Event 3: "User" turns off the automatic rule (sets it to not enabled) zenRule.setEnabled(false); - mZenModeHelper.updateAutomaticZenRule(createdId, zenRule, ZenModeConfig.ORIGIN_SYSTEM, + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, createdId, zenRule, ORIGIN_SYSTEM, "", SYSTEM_UID); assertEquals(OVERRIDE_NONE, @@ -5078,17 +5133,17 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setConfigurationActivity(new ComponentName(mPkg, "cls")) .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) .build(); - String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, rule, ORIGIN_APP, "reason", - CUSTOM_PKG_UID); - mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_TRUE, ORIGIN_APP, - CUSTOM_PKG_UID); + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, rule, + ORIGIN_APP, "reason", CUSTOM_PKG_UID); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, CONDITION_TRUE, + ORIGIN_APP, CUSTOM_PKG_UID); assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_IMPORTANT_INTERRUPTIONS); AutomaticZenRule updateWithDiff = new AutomaticZenRule.Builder(rule) .setTriggerDescription("Whenever") .build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, updateWithDiff, ORIGIN_APP, "reason", - CUSTOM_PKG_UID); + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, updateWithDiff, + ORIGIN_APP, "reason", CUSTOM_PKG_UID); assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF); assertThat(mZenModeHelper.mConfig.automaticRules.get(ruleId).condition).isNull(); @@ -5102,15 +5157,15 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setConfigurationActivity(new ComponentName(mPkg, "cls")) .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) .build(); - String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, rule, ORIGIN_APP, "reason", - CUSTOM_PKG_UID); - mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_TRUE, ORIGIN_APP, - CUSTOM_PKG_UID); + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, rule, + ORIGIN_APP, "reason", CUSTOM_PKG_UID); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, CONDITION_TRUE, + ORIGIN_APP, CUSTOM_PKG_UID); assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_IMPORTANT_INTERRUPTIONS); AutomaticZenRule updateUnchanged = new AutomaticZenRule.Builder(rule).build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, updateUnchanged, ORIGIN_APP, "reason", - CUSTOM_PKG_UID); + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, updateUnchanged, + ORIGIN_APP, "reason", CUSTOM_PKG_UID); assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_IMPORTANT_INTERRUPTIONS); assertThat(mZenModeHelper.mConfig.automaticRules.get(ruleId).condition).isEqualTo( @@ -5125,17 +5180,17 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setConfigurationActivity(new ComponentName(mPkg, "cls")) .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) .build(); - String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, rule, ORIGIN_APP, "reason", - CUSTOM_PKG_UID); - mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_TRUE, ORIGIN_APP, - CUSTOM_PKG_UID); + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, rule, + ORIGIN_APP, "reason", CUSTOM_PKG_UID); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, CONDITION_TRUE, + ORIGIN_APP, CUSTOM_PKG_UID); assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_IMPORTANT_INTERRUPTIONS); AutomaticZenRule updateWithDiff = new AutomaticZenRule.Builder(rule) .setTriggerDescription("Whenever") .build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, updateWithDiff, ORIGIN_USER_IN_SYSTEMUI, - "reason", SYSTEM_UID); + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, updateWithDiff, + ORIGIN_USER_IN_SYSTEMUI, "reason", SYSTEM_UID); assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_IMPORTANT_INTERRUPTIONS); assertThat(mZenModeHelper.mConfig.automaticRules.get(ruleId).condition).isEqualTo( @@ -5153,17 +5208,16 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setConfigurationActivity(new ComponentName(mPkg, "cls")) .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) .build(); - String ruleId = - mZenModeHelper.addAutomaticZenRule( - mPkg, rule, ORIGIN_APP, "reason", CUSTOM_PKG_UID); - mZenModeHelper.setAutomaticZenRuleState( - ruleId, CONDITION_TRUE, ORIGIN_APP, CUSTOM_PKG_UID); + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, rule, + ORIGIN_APP, "reason", CUSTOM_PKG_UID); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, CONDITION_TRUE, + ORIGIN_APP, CUSTOM_PKG_UID); assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_IMPORTANT_INTERRUPTIONS); AutomaticZenRule updateWithDiff = new AutomaticZenRule.Builder(rule).setTriggerDescription("Whenever").build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, updateWithDiff, ORIGIN_USER_IN_SYSTEMUI, - "reason", SYSTEM_UID); + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, updateWithDiff, + ORIGIN_USER_IN_SYSTEMUI, "reason", SYSTEM_UID); assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_IMPORTANT_INTERRUPTIONS); assertThat(mZenModeHelper.mConfig.automaticRules.get(ruleId).condition).isEqualTo( @@ -5178,16 +5232,16 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setConfigurationActivity(new ComponentName(mPkg, "cls")) .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) .build(); - String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, rule, ORIGIN_APP, "reason", - CUSTOM_PKG_UID); - mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_TRUE, ORIGIN_APP, - CUSTOM_PKG_UID); + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, rule, + ORIGIN_APP, "reason", CUSTOM_PKG_UID); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, CONDITION_TRUE, + ORIGIN_APP, CUSTOM_PKG_UID); assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_IMPORTANT_INTERRUPTIONS); - mZenModeHelper.updateAutomaticZenRule(ruleId, + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, new AutomaticZenRule.Builder(rule).setEnabled(false).build(), ORIGIN_USER_IN_SYSTEMUI, "disable", SYSTEM_UID); - mZenModeHelper.updateAutomaticZenRule(ruleId, + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, new AutomaticZenRule.Builder(rule).setEnabled(true).build(), ORIGIN_USER_IN_SYSTEMUI, "enable", SYSTEM_UID); @@ -5203,16 +5257,16 @@ public class ZenModeHelperTest extends UiServiceTestCase { AutomaticZenRule original = new AutomaticZenRule.Builder("Rule", Uri.EMPTY) .setOwner(new ComponentName("android", "some.old.cps")) .build(); - String ruleId = mZenModeHelper.addAutomaticZenRule("android", original, - ZenModeConfig.ORIGIN_SYSTEM, "reason", SYSTEM_UID); + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, "android", original, + ORIGIN_SYSTEM, "reason", SYSTEM_UID); AutomaticZenRule update = new AutomaticZenRule.Builder("Rule", Uri.EMPTY) .setOwner(new ComponentName("android", "brand.new.cps")) .build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, update, ORIGIN_USER_IN_SYSTEMUI, "reason", - SYSTEM_UID); + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, update, + ORIGIN_USER_IN_SYSTEMUI, "reason", SYSTEM_UID); - AutomaticZenRule result = mZenModeHelper.getAutomaticZenRule(ruleId); + AutomaticZenRule result = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); assertThat(result).isNotNull(); assertThat(result.getOwner().getClassName()).isEqualTo("brand.new.cps"); } @@ -5223,16 +5277,16 @@ public class ZenModeHelperTest extends UiServiceTestCase { AutomaticZenRule original = new AutomaticZenRule.Builder("Rule", Uri.EMPTY) .setOwner(new ComponentName(mContext.getPackageName(), "old.third.party.cps")) .build(); - String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), original, - ORIGIN_APP, "reason", CUSTOM_PKG_UID); + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), original, ORIGIN_APP, "reason", CUSTOM_PKG_UID); AutomaticZenRule update = new AutomaticZenRule.Builder("Rule", Uri.EMPTY) .setOwner(new ComponentName(mContext.getPackageName(), "new.third.party.cps")) .build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, update, ORIGIN_USER_IN_SYSTEMUI, "reason", - SYSTEM_UID); + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, update, + ORIGIN_USER_IN_SYSTEMUI, "reason", SYSTEM_UID); - AutomaticZenRule result = mZenModeHelper.getAutomaticZenRule(ruleId); + AutomaticZenRule result = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); assertThat(result).isNotNull(); assertThat(result.getOwner().getClassName()).isEqualTo("old.third.party.cps"); } @@ -5247,14 +5301,14 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setShouldSuppressAmbientDisplay(true) .setShouldDimWallpaper(true) .build()); - mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_TRUE, ORIGIN_APP, - CUSTOM_PKG_UID); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, CONDITION_TRUE, + ORIGIN_APP, CUSTOM_PKG_UID); mTestableLooper.processAllMessages(); verify(mDeviceEffectsApplier).apply(any(), eq(ORIGIN_APP)); // Now delete the (currently active!) rule. For example, assume this is done from settings. - mZenModeHelper.removeAutomaticZenRule(ruleId, ORIGIN_USER_IN_SYSTEMUI, "remove", - SYSTEM_UID); + mZenModeHelper.removeAutomaticZenRule(UserHandle.CURRENT, ruleId, ORIGIN_USER_IN_SYSTEMUI, + "remove", SYSTEM_UID); mTestableLooper.processAllMessages(); verify(mDeviceEffectsApplier).apply(eq(NO_EFFECTS), eq(ORIGIN_USER_IN_SYSTEMUI)); @@ -5273,8 +5327,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { String ruleId = addRuleWithEffects(effects); verifyNoMoreInteractions(mDeviceEffectsApplier); - mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_TRUE, ORIGIN_APP, - CUSTOM_PKG_UID); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, CONDITION_TRUE, + ORIGIN_APP, CUSTOM_PKG_UID); mTestableLooper.processAllMessages(); verify(mDeviceEffectsApplier).apply(eq(effects), eq(ORIGIN_APP)); @@ -5287,13 +5341,13 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenDeviceEffects zde = new ZenDeviceEffects.Builder().setShouldUseNightMode(true).build(); String ruleId = addRuleWithEffects(zde); - mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_TRUE, ORIGIN_APP, - CUSTOM_PKG_UID); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, CONDITION_TRUE, + ORIGIN_APP, CUSTOM_PKG_UID); mTestableLooper.processAllMessages(); verify(mDeviceEffectsApplier).apply(eq(zde), eq(ORIGIN_APP)); - mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_FALSE, ORIGIN_APP, - CUSTOM_PKG_UID); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, CONDITION_FALSE, + ORIGIN_APP, CUSTOM_PKG_UID); mTestableLooper.processAllMessages(); verify(mDeviceEffectsApplier).apply(eq(NO_EFFECTS), eq(ORIGIN_APP)); @@ -5310,8 +5364,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setShouldDisplayGrayscale(true) .addExtraEffect("ONE") .build()); - mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_TRUE, ORIGIN_APP, - CUSTOM_PKG_UID); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, CONDITION_TRUE, + ORIGIN_APP, CUSTOM_PKG_UID); mTestableLooper.processAllMessages(); verify(mDeviceEffectsApplier).apply( eq(new ZenDeviceEffects.Builder() @@ -5326,8 +5380,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setShouldDimWallpaper(true) .addExtraEffect("TWO") .build()); - mZenModeHelper.setAutomaticZenRuleState(secondRuleId, CONDITION_TRUE, ORIGIN_APP, - CUSTOM_PKG_UID); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, secondRuleId, CONDITION_TRUE, + ORIGIN_APP, CUSTOM_PKG_UID); mTestableLooper.processAllMessages(); verify(mDeviceEffectsApplier).apply( @@ -5350,15 +5404,15 @@ public class ZenModeHelperTest extends UiServiceTestCase { .addExtraEffect("extra_effect") .build(); String ruleId = addRuleWithEffects(zde); - mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_TRUE, ORIGIN_APP, - CUSTOM_PKG_UID); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, CONDITION_TRUE, + ORIGIN_APP, CUSTOM_PKG_UID); mTestableLooper.processAllMessages(); verify(mDeviceEffectsApplier).apply(eq(zde), eq(ORIGIN_APP)); // Now create and activate a second rule that doesn't add any more effects. String secondRuleId = addRuleWithEffects(zde); - mZenModeHelper.setAutomaticZenRuleState(secondRuleId, CONDITION_TRUE, ORIGIN_APP, - CUSTOM_PKG_UID); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, secondRuleId, CONDITION_TRUE, + ORIGIN_APP, CUSTOM_PKG_UID); mTestableLooper.processAllMessages(); verifyNoMoreInteractions(mDeviceEffectsApplier); @@ -5371,8 +5425,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { String ruleId = addRuleWithEffects(zde); verify(mDeviceEffectsApplier, never()).apply(any(), anyInt()); - mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_TRUE, ORIGIN_APP, - CUSTOM_PKG_UID); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, CONDITION_TRUE, + ORIGIN_APP, CUSTOM_PKG_UID); mTestableLooper.processAllMessages(); verify(mDeviceEffectsApplier, never()).apply(any(), anyInt()); @@ -5412,8 +5466,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { AutomaticZenRule rule = new AutomaticZenRule.Builder("Test", CONDITION_ID) .setDeviceEffects(effects) .build(); - return mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule, - ZenModeConfig.ORIGIN_SYSTEM, "reasons", SYSTEM_UID); + return mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mContext.getPackageName(), + rule, ORIGIN_SYSTEM, "reasons", SYSTEM_UID); } @Test @@ -5426,35 +5480,37 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) .setZenPolicy(new ZenPolicy.Builder().allowRepeatCallers(false).build()) .build(); - String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule, - ORIGIN_APP, "add it", CUSTOM_PKG_UID); - assertThat(mZenModeHelper.getAutomaticZenRule(ruleId).getCreationTime()).isEqualTo(1000); + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), rule, ORIGIN_APP, "add it", CUSTOM_PKG_UID); + assertThat(mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId).getCreationTime()) + .isEqualTo(1000); // User customizes it. AutomaticZenRule userUpdate = new AutomaticZenRule.Builder(rule) .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS) .setZenPolicy(new ZenPolicy.Builder().allowRepeatCallers(true).build()) .build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdate, ORIGIN_USER_IN_SYSTEMUI, - "userUpdate", SYSTEM_UID); + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, userUpdate, + ORIGIN_USER_IN_SYSTEMUI, "userUpdate", SYSTEM_UID); // App deletes it. mTestClock.advanceByMillis(1000); - mZenModeHelper.removeAutomaticZenRule(ruleId, ORIGIN_APP, "delete it", + mZenModeHelper.removeAutomaticZenRule(UserHandle.CURRENT, ruleId, ORIGIN_APP, "delete it", CUSTOM_PKG_UID); assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(0); assertThat(mZenModeHelper.mConfig.deletedRules).hasSize(1); // App adds it again. mTestClock.advanceByMillis(1000); - String newRuleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule, - ORIGIN_APP, "add it again", CUSTOM_PKG_UID); + String newRuleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), rule, ORIGIN_APP, "add it again", CUSTOM_PKG_UID); // Verify that the rule was restored: // - id and creation time is the same as the original one. // - ZenPolicy is the one that the user had set. // - rule still has the user-modified fields. - AutomaticZenRule finalRule = mZenModeHelper.getAutomaticZenRule(newRuleId); + AutomaticZenRule finalRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, + newRuleId); assertThat(finalRule.getCreationTime()).isEqualTo(1000); // And not 3000. assertThat(newRuleId).isEqualTo(ruleId); assertThat(finalRule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_ALARMS); @@ -5481,24 +5537,26 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) .setZenPolicy(new ZenPolicy.Builder().allowRepeatCallers(false).build()) .build(); - String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule, - ORIGIN_APP, "add it", CUSTOM_PKG_UID); - assertThat(mZenModeHelper.getAutomaticZenRule(ruleId).getCreationTime()).isEqualTo(1000); + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), rule, ORIGIN_APP, "add it", CUSTOM_PKG_UID); + assertThat(mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId).getCreationTime()) + .isEqualTo(1000); // App deletes it. mTestClock.advanceByMillis(1000); - mZenModeHelper.removeAutomaticZenRule(ruleId, ORIGIN_APP, "delete it", + mZenModeHelper.removeAutomaticZenRule(UserHandle.CURRENT, ruleId, ORIGIN_APP, "delete it", CUSTOM_PKG_UID); assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(0); assertThat(mZenModeHelper.mConfig.deletedRules).hasSize(0); // App adds it again. mTestClock.advanceByMillis(1000); - String newRuleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule, - ORIGIN_APP, "add it again", CUSTOM_PKG_UID); + String newRuleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), rule, ORIGIN_APP, "add it again", CUSTOM_PKG_UID); // Verify that the rule was recreated. This means id and creation time are new. - AutomaticZenRule finalRule = mZenModeHelper.getAutomaticZenRule(newRuleId); + AutomaticZenRule finalRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, + newRuleId); assertThat(finalRule.getCreationTime()).isEqualTo(3000); assertThat(newRuleId).isNotEqualTo(ruleId); } @@ -5513,9 +5571,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) .setZenPolicy(new ZenPolicy.Builder().allowRepeatCallers(false).build()) .build(); - String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule, - ORIGIN_APP, "add it", CUSTOM_PKG_UID); - assertThat(mZenModeHelper.getAutomaticZenRule(ruleId).getCreationTime()).isEqualTo(1000); + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), rule, ORIGIN_APP, "add it", CUSTOM_PKG_UID); + assertThat(mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId).getCreationTime()) + .isEqualTo(1000); // User customizes it. mTestClock.advanceByMillis(1000); @@ -5523,24 +5582,26 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS) .setZenPolicy(new ZenPolicy.Builder().allowRepeatCallers(true).build()) .build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdate, ORIGIN_USER_IN_SYSTEMUI, - "userUpdate", SYSTEM_UID); + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, userUpdate, + ORIGIN_USER_IN_SYSTEMUI, "userUpdate", SYSTEM_UID); // App deletes it. mTestClock.advanceByMillis(1000); - mZenModeHelper.removeAutomaticZenRule(ruleId, ORIGIN_APP, "delete it", + mZenModeHelper.removeAutomaticZenRule(UserHandle.CURRENT, ruleId, ORIGIN_APP, "delete it", CUSTOM_PKG_UID); assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(0); assertThat(mZenModeHelper.mConfig.deletedRules).hasSize(1); // User creates it again (unusual case, but ok). mTestClock.advanceByMillis(1000); - String newRuleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule, - ORIGIN_USER_IN_SYSTEMUI, "add it anew", SYSTEM_UID); + String newRuleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), rule, ORIGIN_USER_IN_SYSTEMUI, "add it anew", + SYSTEM_UID); // Verify that the rule was recreated. This means id and creation time are new, and the rule // matches the latest data supplied to addAZR. - AutomaticZenRule finalRule = mZenModeHelper.getAutomaticZenRule(newRuleId); + AutomaticZenRule finalRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, + newRuleId); assertThat(finalRule.getCreationTime()).isEqualTo(4000); assertThat(newRuleId).isNotEqualTo(ruleId); assertThat(finalRule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_PRIORITY); @@ -5561,9 +5622,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) .setZenPolicy(new ZenPolicy.Builder().allowRepeatCallers(false).build()) .build(); - String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule, - ORIGIN_APP, "add it", CUSTOM_PKG_UID); - assertThat(mZenModeHelper.getAutomaticZenRule(ruleId).getCreationTime()).isEqualTo(1000); + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), rule, ORIGIN_APP, "add it", CUSTOM_PKG_UID); + assertThat(mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId).getCreationTime()) + .isEqualTo(1000); // User customizes it. mTestClock.advanceByMillis(1000); @@ -5571,23 +5633,24 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS) .setZenPolicy(new ZenPolicy.Builder().allowRepeatCallers(true).build()) .build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdate, ORIGIN_USER_IN_SYSTEMUI, - "userUpdate", SYSTEM_UID); + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, userUpdate, + ORIGIN_USER_IN_SYSTEMUI, "userUpdate", SYSTEM_UID); // User deletes it. mTestClock.advanceByMillis(1000); - mZenModeHelper.removeAutomaticZenRule(ruleId, ORIGIN_USER_IN_SYSTEMUI, "delete it", - SYSTEM_UID); + mZenModeHelper.removeAutomaticZenRule(UserHandle.CURRENT, ruleId, ORIGIN_USER_IN_SYSTEMUI, + "delete it", SYSTEM_UID); assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(0); assertThat(mZenModeHelper.mConfig.deletedRules).hasSize(0); // App creates it again. mTestClock.advanceByMillis(1000); - String newRuleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule, - ORIGIN_APP, "add it again", CUSTOM_PKG_UID); + String newRuleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), rule, ORIGIN_APP, "add it again", CUSTOM_PKG_UID); // Verify that the rule was recreated. This means id and creation time are new. - AutomaticZenRule finalRule = mZenModeHelper.getAutomaticZenRule(newRuleId); + AutomaticZenRule finalRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, + newRuleId); assertThat(finalRule.getCreationTime()).isEqualTo(4000); assertThat(newRuleId).isNotEqualTo(ruleId); } @@ -5601,18 +5664,18 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setOwner(new ComponentName("first", "owner")) .setInterruptionFilter(INTERRUPTION_FILTER_ALL) .build(); - String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule, - ORIGIN_APP, "add it", CUSTOM_PKG_UID); + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), rule, ORIGIN_APP, "add it", CUSTOM_PKG_UID); // User customizes it. AutomaticZenRule userUpdate = new AutomaticZenRule.Builder(rule) .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) .build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdate, ORIGIN_USER_IN_SYSTEMUI, - "userUpdate", SYSTEM_UID); + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, userUpdate, + ORIGIN_USER_IN_SYSTEMUI, "userUpdate", SYSTEM_UID); // App deletes it. It's preserved for a possible restoration. - mZenModeHelper.removeAutomaticZenRule(ruleId, ORIGIN_APP, "delete it", + mZenModeHelper.removeAutomaticZenRule(UserHandle.CURRENT, ruleId, ORIGIN_APP, "delete it", CUSTOM_PKG_UID); assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(0); assertThat(mZenModeHelper.mConfig.deletedRules).hasSize(1); @@ -5622,12 +5685,14 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setOwner(new ComponentName("second", "owner")) .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS) .build(); - String newRuleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), - readdingWithDifferentOwner, ORIGIN_APP, "add it again", CUSTOM_PKG_UID); + String newRuleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), readdingWithDifferentOwner, ORIGIN_APP, "add it again", + CUSTOM_PKG_UID); // Verify that the rule was NOT restored: assertThat(newRuleId).isNotEqualTo(ruleId); - AutomaticZenRule finalRule = mZenModeHelper.getAutomaticZenRule(newRuleId); + AutomaticZenRule finalRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, + newRuleId); assertThat(finalRule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_ALARMS); assertThat(finalRule.getOwner()).isEqualTo(new ComponentName("second", "owner")); @@ -5643,23 +5708,23 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.mConfig.automaticRules.clear(); // Start with a bunch of customized rules where conditionUris are not unique. - String id1 = mZenModeHelper.addAutomaticZenRule("pkg1", + String id1 = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, "pkg1", new AutomaticZenRule.Builder("Test1", Uri.parse("uri1")).build(), ORIGIN_APP, "add it", CUSTOM_PKG_UID); - String id2 = mZenModeHelper.addAutomaticZenRule("pkg1", + String id2 = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, "pkg1", new AutomaticZenRule.Builder("Test2", Uri.parse("uri2")).build(), ORIGIN_APP, "add it", CUSTOM_PKG_UID); - String id3 = mZenModeHelper.addAutomaticZenRule("pkg1", + String id3 = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, "pkg1", new AutomaticZenRule.Builder("Test3", Uri.parse("uri2")).build(), ORIGIN_APP, "add it", CUSTOM_PKG_UID); - String id4 = mZenModeHelper.addAutomaticZenRule("pkg2", + String id4 = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, "pkg2", new AutomaticZenRule.Builder("Test4", Uri.parse("uri1")).build(), ORIGIN_APP, "add it", CUSTOM_PKG_UID); - String id5 = mZenModeHelper.addAutomaticZenRule("pkg2", + String id5 = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, "pkg2", new AutomaticZenRule.Builder("Test5", Uri.parse("uri1")).build(), ORIGIN_APP, "add it", CUSTOM_PKG_UID); @@ -5667,11 +5732,16 @@ public class ZenModeHelperTest extends UiServiceTestCase { zenRule.userModifiedFields = AutomaticZenRule.FIELD_INTERRUPTION_FILTER; } - mZenModeHelper.removeAutomaticZenRule(id1, ORIGIN_APP, "begone", CUSTOM_PKG_UID); - mZenModeHelper.removeAutomaticZenRule(id2, ORIGIN_APP, "begone", CUSTOM_PKG_UID); - mZenModeHelper.removeAutomaticZenRule(id3, ORIGIN_APP, "begone", CUSTOM_PKG_UID); - mZenModeHelper.removeAutomaticZenRule(id4, ORIGIN_APP, "begone", CUSTOM_PKG_UID); - mZenModeHelper.removeAutomaticZenRule(id5, ORIGIN_APP, "begone", CUSTOM_PKG_UID); + mZenModeHelper.removeAutomaticZenRule(UserHandle.CURRENT, id1, ORIGIN_APP, "begone", + CUSTOM_PKG_UID); + mZenModeHelper.removeAutomaticZenRule(UserHandle.CURRENT, id2, ORIGIN_APP, "begone", + CUSTOM_PKG_UID); + mZenModeHelper.removeAutomaticZenRule(UserHandle.CURRENT, id3, ORIGIN_APP, "begone", + CUSTOM_PKG_UID); + mZenModeHelper.removeAutomaticZenRule(UserHandle.CURRENT, id4, ORIGIN_APP, "begone", + CUSTOM_PKG_UID); + mZenModeHelper.removeAutomaticZenRule(UserHandle.CURRENT, id5, ORIGIN_APP, "begone", + CUSTOM_PKG_UID); assertThat(mZenModeHelper.mConfig.deletedRules.keySet()) .containsExactly("pkg1|uri1", "pkg1|uri2", "pkg2|uri1"); @@ -5685,11 +5755,11 @@ public class ZenModeHelperTest extends UiServiceTestCase { public void removeAllZenRules_preservedForRestoring() { mZenModeHelper.mConfig.automaticRules.clear(); - mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), + mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mContext.getPackageName(), new AutomaticZenRule.Builder("Test1", Uri.parse("uri1")).build(), ORIGIN_APP, "add it", CUSTOM_PKG_UID); - mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), + mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mContext.getPackageName(), new AutomaticZenRule.Builder("Test2", Uri.parse("uri2")).build(), ORIGIN_APP, "add it", CUSTOM_PKG_UID); @@ -5698,8 +5768,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { zenRule.userModifiedFields = AutomaticZenRule.FIELD_INTERRUPTION_FILTER; } - mZenModeHelper.removeAutomaticZenRules(mContext.getPackageName(), ORIGIN_APP, - "begone", CUSTOM_PKG_UID); + mZenModeHelper.removeAutomaticZenRules(UserHandle.CURRENT, mContext.getPackageName(), + ORIGIN_APP, "begone", CUSTOM_PKG_UID); assertThat(mZenModeHelper.mConfig.deletedRules).hasSize(2); } @@ -5716,8 +5786,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.mConfig.deletedRules.put(ZenModeConfig.deletedRuleKey(pkg1Rule), pkg1Rule); mZenModeHelper.mConfig.deletedRules.put(ZenModeConfig.deletedRuleKey(pkg2Rule), pkg2Rule); - mZenModeHelper.removeAutomaticZenRules("pkg1", - ZenModeConfig.ORIGIN_SYSTEM, "goodbye pkg1", SYSTEM_UID); + mZenModeHelper.removeAutomaticZenRules(UserHandle.CURRENT, "pkg1", + ORIGIN_SYSTEM, "goodbye pkg1", SYSTEM_UID); // Preserved rules from pkg1 are gone; those from pkg2 are still there. assertThat(mZenModeHelper.mConfig.deletedRules.values().stream().map(r -> r.pkg) @@ -5733,36 +5803,37 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setConditionId(CONDITION_ID) .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) .build(); - String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule, - ORIGIN_APP, "add it", CUSTOM_PKG_UID); + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), rule, ORIGIN_APP, "add it", CUSTOM_PKG_UID); // User customizes it. AutomaticZenRule userUpdate = new AutomaticZenRule.Builder(rule) .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS) .build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdate, ORIGIN_USER_IN_SYSTEMUI, - "userUpdate", SYSTEM_UID); + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, userUpdate, + ORIGIN_USER_IN_SYSTEMUI, "userUpdate", SYSTEM_UID); assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF); // App activates it. - mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_TRUE, ORIGIN_APP, - CUSTOM_PKG_UID); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, CONDITION_TRUE, + ORIGIN_APP, CUSTOM_PKG_UID); assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_ALARMS); // App deletes it. - mZenModeHelper.removeAutomaticZenRule(ruleId, ORIGIN_APP, "delete it", + mZenModeHelper.removeAutomaticZenRule(UserHandle.CURRENT, ruleId, ORIGIN_APP, "delete it", CUSTOM_PKG_UID); assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(0); assertThat(mZenModeHelper.mConfig.deletedRules).hasSize(1); assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF); // App adds it again. - String newRuleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule, - ORIGIN_APP, "add it again", CUSTOM_PKG_UID); + String newRuleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), rule, ORIGIN_APP, "add it again", CUSTOM_PKG_UID); // The rule is restored... assertThat(newRuleId).isEqualTo(ruleId); - AutomaticZenRule finalRule = mZenModeHelper.getAutomaticZenRule(newRuleId); + AutomaticZenRule finalRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, + newRuleId); assertThat(finalRule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_ALARMS); // ... but it is NOT active @@ -5782,40 +5853,41 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setConditionId(CONDITION_ID) .setInterruptionFilter(INTERRUPTION_FILTER_PRIORITY) .build(); - String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule, - ORIGIN_APP, "add it", CUSTOM_PKG_UID); + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), rule, ORIGIN_APP, "add it", CUSTOM_PKG_UID); // User customizes it. AutomaticZenRule userUpdate = new AutomaticZenRule.Builder(rule) .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS) .build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdate, ORIGIN_USER_IN_SYSTEMUI, - "userUpdate", SYSTEM_UID); + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, userUpdate, + ORIGIN_USER_IN_SYSTEMUI, "userUpdate", SYSTEM_UID); assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF); // App activates it. - mZenModeHelper.setAutomaticZenRuleState(ruleId, CONDITION_TRUE, ORIGIN_APP, - CUSTOM_PKG_UID); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, CONDITION_TRUE, + ORIGIN_APP, CUSTOM_PKG_UID); assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_ALARMS); // User snoozes it. - mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null, ZenModeConfig.ORIGIN_SYSTEM, + mZenModeHelper.setManualZenMode(UserHandle.CURRENT, ZEN_MODE_OFF, null, ORIGIN_SYSTEM, "snoozing", "systemui", SYSTEM_UID); assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF); // App deletes it. - mZenModeHelper.removeAutomaticZenRule(ruleId, ORIGIN_APP, "delete it", + mZenModeHelper.removeAutomaticZenRule(UserHandle.CURRENT, ruleId, ORIGIN_APP, "delete it", CUSTOM_PKG_UID); assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(0); assertThat(mZenModeHelper.mConfig.deletedRules).hasSize(1); // App adds it again. - String newRuleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), rule, - ORIGIN_APP, "add it again", CUSTOM_PKG_UID); + String newRuleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), rule, ORIGIN_APP, "add it again", CUSTOM_PKG_UID); // The rule is restored... assertThat(newRuleId).isEqualTo(ruleId); - AutomaticZenRule finalRule = mZenModeHelper.getAutomaticZenRule(newRuleId); + AutomaticZenRule finalRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, + newRuleId); assertThat(finalRule.getInterruptionFilter()).isEqualTo(INTERRUPTION_FILTER_ALARMS); // ... but it is NEITHER active NOR snoozed. @@ -5888,7 +5960,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test @EnableFlags(FLAG_MODES_API) public void getAutomaticZenRuleState_ownedRule_returnsRuleState() { - String id = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), + String id = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), new AutomaticZenRule.Builder("Rule", CONDITION_ID) .setConfigurationActivity( new ComponentName(mContext.getPackageName(), "Blah")) @@ -5896,18 +5969,23 @@ public class ZenModeHelperTest extends UiServiceTestCase { ORIGIN_APP, "reasons", CUSTOM_PKG_UID); // Null condition -> STATE_FALSE - assertThat(mZenModeHelper.getAutomaticZenRuleState(id)).isEqualTo(Condition.STATE_FALSE); + assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, id)) + .isEqualTo(Condition.STATE_FALSE); - mZenModeHelper.setAutomaticZenRuleState(id, CONDITION_TRUE, ORIGIN_APP, + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, id, CONDITION_TRUE, ORIGIN_APP, CUSTOM_PKG_UID); - assertThat(mZenModeHelper.getAutomaticZenRuleState(id)).isEqualTo(Condition.STATE_TRUE); + assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, id)) + .isEqualTo(Condition.STATE_TRUE); - mZenModeHelper.setAutomaticZenRuleState(id, CONDITION_FALSE, ORIGIN_APP, + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, id, CONDITION_FALSE, ORIGIN_APP, CUSTOM_PKG_UID); - assertThat(mZenModeHelper.getAutomaticZenRuleState(id)).isEqualTo(Condition.STATE_FALSE); + assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, id)) + .isEqualTo(Condition.STATE_FALSE); - mZenModeHelper.removeAutomaticZenRule(id, ORIGIN_APP, "", CUSTOM_PKG_UID); - assertThat(mZenModeHelper.getAutomaticZenRuleState(id)).isEqualTo(Condition.STATE_UNKNOWN); + mZenModeHelper.removeAutomaticZenRule(UserHandle.CURRENT, id, ORIGIN_APP, "", + CUSTOM_PKG_UID); + assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, id)) + .isEqualTo(Condition.STATE_UNKNOWN); } @Test @@ -5922,8 +6000,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.setConfig(config, null, ORIGIN_INIT, "", SYSTEM_UID); assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_ALARMS); - assertThat(mZenModeHelper.getAutomaticZenRuleState("systemRule")).isEqualTo( - Condition.STATE_UNKNOWN); + assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, "systemRule")) + .isEqualTo(Condition.STATE_UNKNOWN); } @Test @@ -5940,7 +6018,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_ALARMS); // Should be ignored. - mZenModeHelper.setAutomaticZenRuleState("otherRule", + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, "otherRule", new Condition(otherRule.conditionId, "off", Condition.STATE_FALSE), ORIGIN_APP, CUSTOM_PKG_UID); @@ -5961,7 +6039,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_ALARMS); // Should be ignored. - mZenModeHelper.setAutomaticZenRuleState(otherRule.conditionId, + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, otherRule.conditionId, new Condition(otherRule.conditionId, "off", Condition.STATE_FALSE), ORIGIN_APP, CUSTOM_PKG_UID); @@ -5972,7 +6050,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { @EnableFlags(FLAG_MODES_API) public void testCallbacks_policy() throws Exception { setupZenConfig(); - assertThat(mZenModeHelper.getNotificationPolicy().allowReminders()).isTrue(); + assertThat(mZenModeHelper.getNotificationPolicy(UserHandle.CURRENT).allowReminders()) + .isTrue(); SettableFuture<Policy> futurePolicy = SettableFuture.create(); mZenModeHelper.addCallback(new ZenModeHelper.Callback() { @Override @@ -5982,7 +6061,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { }); Policy totalSilencePolicy = new Policy(0, 0, 0); - mZenModeHelper.setNotificationPolicy(totalSilencePolicy, ORIGIN_APP, CUSTOM_PKG_UID); + mZenModeHelper.setNotificationPolicy(UserHandle.CURRENT, totalSilencePolicy, ORIGIN_APP, + CUSTOM_PKG_UID); Policy callbackPolicy = futurePolicy.get(1, TimeUnit.SECONDS); assertThat(callbackPolicy.allowReminders()).isFalse(); @@ -6000,13 +6080,14 @@ public class ZenModeHelperTest extends UiServiceTestCase { } }); - String totalSilenceRuleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), + String totalSilenceRuleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), new AutomaticZenRule.Builder("Rule", CONDITION_ID) .setOwner(OWNER) .setInterruptionFilter(INTERRUPTION_FILTER_NONE) .build(), ORIGIN_APP, "reasons", 0); - mZenModeHelper.setAutomaticZenRuleState(totalSilenceRuleId, + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, totalSilenceRuleId, new Condition(CONDITION_ID, "", STATE_TRUE), ORIGIN_APP, CUSTOM_PKG_UID); Policy callbackPolicy = futureConsolidatedPolicy.get(1, TimeUnit.SECONDS); @@ -6018,8 +6099,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { public void applyGlobalZenModeAsImplicitZenRule_createsImplicitRuleAndActivatesIt() { mZenModeHelper.mConfig.automaticRules.clear(); - mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, - ZEN_MODE_IMPORTANT_INTERRUPTIONS); + mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(UserHandle.CURRENT, CUSTOM_PKG_NAME, + CUSTOM_PKG_UID, ZEN_MODE_IMPORTANT_INTERRUPTIONS); assertThat(mZenModeHelper.mConfig.automaticRules.values()) .comparingElementsUsing(IGNORE_METADATA) @@ -6034,13 +6115,14 @@ public class ZenModeHelperTest extends UiServiceTestCase { public void applyGlobalZenModeAsImplicitZenRule_updatesImplicitRuleAndActivatesIt() { mZenModeHelper.mConfig.automaticRules.clear(); - mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, - ZEN_MODE_IMPORTANT_INTERRUPTIONS); - mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null, ORIGIN_APP, "test", "test", 0); + mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(UserHandle.CURRENT, CUSTOM_PKG_NAME, + CUSTOM_PKG_UID, ZEN_MODE_IMPORTANT_INTERRUPTIONS); + mZenModeHelper.setManualZenMode(UserHandle.CURRENT, ZEN_MODE_OFF, null, ORIGIN_APP, "test", + "test", 0); assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(1); - mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, - ZEN_MODE_ALARMS); + mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(UserHandle.CURRENT, CUSTOM_PKG_NAME, + CUSTOM_PKG_UID, ZEN_MODE_ALARMS); assertThat(mZenModeHelper.mConfig.automaticRules.values()) .comparingElementsUsing(IGNORE_METADATA) @@ -6057,22 +6139,22 @@ public class ZenModeHelperTest extends UiServiceTestCase { String pkg = mContext.getPackageName(); // From app, call "setInterruptionFilter" and create and implicit rule. - mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(pkg, CUSTOM_PKG_UID, + mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(UserHandle.CURRENT, pkg, CUSTOM_PKG_UID, ZEN_MODE_IMPORTANT_INTERRUPTIONS); String ruleId = getOnlyElement(mZenModeHelper.mConfig.automaticRules.keySet()); assertThat(getOnlyElement(mZenModeHelper.mConfig.automaticRules.values()).zenMode) .isEqualTo(ZEN_MODE_IMPORTANT_INTERRUPTIONS); // From user, update that rule's interruption filter. - AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId); + AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); AutomaticZenRule userUpdateRule = new AutomaticZenRule.Builder(rule) .setInterruptionFilter(INTERRUPTION_FILTER_ALARMS) .build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdateRule, ORIGIN_USER_IN_SYSTEMUI, - "reason", SYSTEM_UID); + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, userUpdateRule, + ORIGIN_USER_IN_SYSTEMUI, "reason", SYSTEM_UID); // From app, call "setInterruptionFilter" again. - mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(pkg, CUSTOM_PKG_UID, + mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(UserHandle.CURRENT, pkg, CUSTOM_PKG_UID, ZEN_MODE_NO_INTERRUPTIONS); // The app's update was ignored, and the user's update is still current, and the current @@ -6089,22 +6171,22 @@ public class ZenModeHelperTest extends UiServiceTestCase { String pkg = mContext.getPackageName(); // From app, call "setInterruptionFilter" and create and implicit rule. - mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(pkg, CUSTOM_PKG_UID, + mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(UserHandle.CURRENT, pkg, CUSTOM_PKG_UID, ZEN_MODE_IMPORTANT_INTERRUPTIONS); String ruleId = getOnlyElement(mZenModeHelper.mConfig.automaticRules.keySet()); assertThat(getOnlyElement(mZenModeHelper.mConfig.automaticRules.values()).zenMode) .isEqualTo(ZEN_MODE_IMPORTANT_INTERRUPTIONS); // From user, update something in that rule, but not the interruption filter. - AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId); + AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); AutomaticZenRule userUpdateRule = new AutomaticZenRule.Builder(rule) .setName("Renamed") .build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdateRule, ORIGIN_USER_IN_SYSTEMUI, - "reason", SYSTEM_UID); + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, userUpdateRule, + ORIGIN_USER_IN_SYSTEMUI, "reason", SYSTEM_UID); // From app, call "setInterruptionFilter" again. - mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(pkg, CUSTOM_PKG_UID, + mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(UserHandle.CURRENT, pkg, CUSTOM_PKG_UID, ZEN_MODE_NO_INTERRUPTIONS); // The app's update was accepted, and the current mode is the one that they wanted. @@ -6117,13 +6199,13 @@ public class ZenModeHelperTest extends UiServiceTestCase { @EnableFlags(FLAG_MODES_API) public void applyGlobalZenModeAsImplicitZenRule_modeOff_deactivatesImplicitRule() { mZenModeHelper.mConfig.automaticRules.clear(); - mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(mPkg, CUSTOM_PKG_UID, + mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(UserHandle.CURRENT, mPkg, CUSTOM_PKG_UID, ZEN_MODE_IMPORTANT_INTERRUPTIONS); assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(1); assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).condition.state) .isEqualTo(STATE_TRUE); - mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(mPkg, CUSTOM_PKG_UID, + mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(UserHandle.CURRENT, mPkg, CUSTOM_PKG_UID, ZEN_MODE_OFF); assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).condition.state) @@ -6135,8 +6217,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { public void applyGlobalZenModeAsImplicitZenRule_modeOffButNoPreviousRule_ignored() { mZenModeHelper.mConfig.automaticRules.clear(); - mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, - ZEN_MODE_OFF); + mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(UserHandle.CURRENT, CUSTOM_PKG_NAME, + CUSTOM_PKG_UID, ZEN_MODE_OFF); assertThat(mZenModeHelper.mConfig.automaticRules).isEmpty(); } @@ -6146,18 +6228,19 @@ public class ZenModeHelperTest extends UiServiceTestCase { public void applyGlobalZenModeAsImplicitZenRule_update_unsnoozesRule() { mZenModeHelper.mConfig.automaticRules.clear(); - mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, - ZEN_MODE_IMPORTANT_INTERRUPTIONS); + mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(UserHandle.CURRENT, CUSTOM_PKG_NAME, + CUSTOM_PKG_UID, ZEN_MODE_IMPORTANT_INTERRUPTIONS); assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(1); assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).getConditionOverride()) .isEqualTo(OVERRIDE_NONE); - mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null, ORIGIN_APP, "test", "test", 0); + mZenModeHelper.setManualZenMode(UserHandle.CURRENT, ZEN_MODE_OFF, null, ORIGIN_APP, "test", + "test", 0); assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).getConditionOverride()) .isEqualTo(OVERRIDE_DEACTIVATE); - mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, - ZEN_MODE_ALARMS); + mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(UserHandle.CURRENT, CUSTOM_PKG_NAME, + CUSTOM_PKG_UID, ZEN_MODE_ALARMS); assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).getConditionOverride()) .isEqualTo(OVERRIDE_NONE); @@ -6166,14 +6249,57 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test + @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + public void applyGlobalZenModeAsImplicitZenRule_again_refreshesRuleName() { + mZenModeHelper.mConfig.automaticRules.clear(); + mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(UserHandle.CURRENT, + mContext.getPackageName(), CUSTOM_PKG_UID, ZEN_MODE_IMPORTANT_INTERRUPTIONS); + assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(1); + assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).name) + .isEqualTo("Do Not Disturb (" + CUSTOM_APP_LABEL + ")"); + // "Break" the rule name to check that applying again restores it. + mZenModeHelper.mConfig.automaticRules.valueAt(0).name = "BOOM!"; + + mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(UserHandle.CURRENT, + mContext.getPackageName(), CUSTOM_PKG_UID, ZEN_MODE_ALARMS); + + assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).name) + .isEqualTo("Do Not Disturb (" + CUSTOM_APP_LABEL + ")"); + } + + @Test + @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + public void applyGlobalZenModeAsImplicitZenRule_again_doesNotChangeCustomizedRuleName() { + mZenModeHelper.mConfig.automaticRules.clear(); + mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(UserHandle.CURRENT, + mContext.getPackageName(), CUSTOM_PKG_UID, ZEN_MODE_IMPORTANT_INTERRUPTIONS); + assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(1); + assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).name) + .isEqualTo("Do Not Disturb (" + CUSTOM_APP_LABEL + ")"); + String ruleId = ZenModeConfig.implicitRuleId(mContext.getPackageName()); + + // User chooses a new name. + AutomaticZenRule azr = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, + new AutomaticZenRule.Builder(azr).setName("User chose this").build(), + ORIGIN_USER_IN_SYSTEMUI, "reason", SYSTEM_UID); + + // App triggers the rule again. + mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(UserHandle.CURRENT, + mContext.getPackageName(), CUSTOM_PKG_UID, ZEN_MODE_ALARMS); + + assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).name) + .isEqualTo("User chose this"); + } + + @Test @DisableFlags(FLAG_MODES_API) public void applyGlobalZenModeAsImplicitZenRule_flagOff_ignored() { mZenModeHelper.mConfig.automaticRules.clear(); withoutWtfCrash( - () -> mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, - CUSTOM_PKG_UID, - ZEN_MODE_IMPORTANT_INTERRUPTIONS)); + () -> mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(UserHandle.CURRENT, + CUSTOM_PKG_NAME, CUSTOM_PKG_UID, ZEN_MODE_IMPORTANT_INTERRUPTIONS)); assertThat(mZenModeHelper.mConfig.automaticRules).isEmpty(); } @@ -6186,7 +6312,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { Policy policy = new Policy(PRIORITY_CATEGORY_CALLS | PRIORITY_CATEGORY_CONVERSATIONS, PRIORITY_SENDERS_CONTACTS, PRIORITY_SENDERS_STARRED, Policy.getAllSuppressedVisualEffects(), CONVERSATION_SENDERS_IMPORTANT); - mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, policy); + mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(UserHandle.CURRENT, CUSTOM_PKG_NAME, + CUSTOM_PKG_UID, policy); ZenPolicy expectedZenPolicy = new ZenPolicy.Builder() .disallowAllSounds() @@ -6210,14 +6337,15 @@ public class ZenModeHelperTest extends UiServiceTestCase { Policy original = new Policy(PRIORITY_CATEGORY_CALLS | PRIORITY_CATEGORY_CONVERSATIONS, PRIORITY_SENDERS_CONTACTS, PRIORITY_SENDERS_STARRED, Policy.getAllSuppressedVisualEffects(), CONVERSATION_SENDERS_IMPORTANT); - mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, - original); + mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(UserHandle.CURRENT, CUSTOM_PKG_NAME, + CUSTOM_PKG_UID, original); // Change priorityCallSenders: contacts -> starred. Policy updated = new Policy(PRIORITY_CATEGORY_CALLS | PRIORITY_CATEGORY_CONVERSATIONS, PRIORITY_SENDERS_STARRED, PRIORITY_SENDERS_STARRED, Policy.getAllSuppressedVisualEffects(), CONVERSATION_SENDERS_IMPORTANT); - mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, updated); + mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(UserHandle.CURRENT, CUSTOM_PKG_NAME, + CUSTOM_PKG_UID, updated); ZenPolicy expectedZenPolicy = new ZenPolicy.Builder() .disallowAllSounds() @@ -6241,7 +6369,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { // From app, call "setNotificationPolicy" and create and implicit rule. Policy originalPolicy = new Policy(PRIORITY_CATEGORY_MEDIA, 0, 0); - mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(pkg, CUSTOM_PKG_UID, originalPolicy); + mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(UserHandle.CURRENT, pkg, CUSTOM_PKG_UID, + originalPolicy); String ruleId = getOnlyElement(mZenModeHelper.mConfig.automaticRules.keySet()); // Store this for checking later. @@ -6249,18 +6378,19 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.mConfig.getZenPolicy()).allowMedia(true).build(); // From user, update that rule's policy. - AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId); + AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); ZenPolicy userUpdateZenPolicy = new ZenPolicy.Builder().disallowAllSounds() .allowAlarms(true).build(); AutomaticZenRule userUpdateRule = new AutomaticZenRule.Builder(rule) .setZenPolicy(userUpdateZenPolicy) .build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdateRule, ORIGIN_USER_IN_SYSTEMUI, - "reason", SYSTEM_UID); + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, userUpdateRule, + ORIGIN_USER_IN_SYSTEMUI, "reason", SYSTEM_UID); // From app, call "setNotificationPolicy" again. Policy appUpdatePolicy = new Policy(PRIORITY_CATEGORY_SYSTEM, 0, 0); - mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(pkg, CUSTOM_PKG_UID, appUpdatePolicy); + mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(UserHandle.CURRENT, pkg, CUSTOM_PKG_UID, + appUpdatePolicy); // The app's update was ignored, and the user's update is still current. assertThat(mZenModeHelper.mConfig.automaticRules.values()) @@ -6281,7 +6411,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { // From app, call "setNotificationPolicy" and create and implicit rule. Policy originalPolicy = new Policy(PRIORITY_CATEGORY_MEDIA, 0, 0); - mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(pkg, CUSTOM_PKG_UID, originalPolicy); + mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(UserHandle.CURRENT, pkg, CUSTOM_PKG_UID, + originalPolicy); String ruleId = getOnlyElement(mZenModeHelper.mConfig.automaticRules.keySet()); // Store this for checking later. @@ -6289,16 +6420,17 @@ public class ZenModeHelperTest extends UiServiceTestCase { mZenModeHelper.mConfig.getZenPolicy()).allowMedia(true).build(); // From user, update something in that rule, but not the ZenPolicy. - AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(ruleId); + AutomaticZenRule rule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); AutomaticZenRule userUpdateRule = new AutomaticZenRule.Builder(rule) .setName("Rule renamed, not touching policy") .build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, userUpdateRule, ORIGIN_USER_IN_SYSTEMUI, - "reason", SYSTEM_UID); + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, userUpdateRule, + ORIGIN_USER_IN_SYSTEMUI, "reason", SYSTEM_UID); // From app, call "setNotificationPolicy" again. Policy appUpdatePolicy = new Policy(PRIORITY_CATEGORY_SYSTEM, 0, 0); - mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(pkg, CUSTOM_PKG_UID, appUpdatePolicy); + mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(UserHandle.CURRENT, pkg, CUSTOM_PKG_UID, + appUpdatePolicy); // The app's update was applied. ZenPolicy appsSecondZenPolicy = new ZenPolicy.Builder() @@ -6311,13 +6443,57 @@ public class ZenModeHelperTest extends UiServiceTestCase { } @Test + @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + public void applyGlobalPolicyAsImplicitZenRule_again_refreshesRuleName() { + mZenModeHelper.mConfig.automaticRules.clear(); + mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(UserHandle.CURRENT, + mContext.getPackageName(), CUSTOM_PKG_UID, new Policy(0, 0, 0)); + assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(1); + assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).name) + .isEqualTo("Do Not Disturb (" + CUSTOM_APP_LABEL + ")"); + // "Break" the rule name to check that updating it again restores it. + mZenModeHelper.mConfig.automaticRules.valueAt(0).name = "BOOM!"; + + mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(UserHandle.CURRENT, + mContext.getPackageName(), CUSTOM_PKG_UID, new Policy(0, 0, 0)); + + assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).name) + .isEqualTo("Do Not Disturb (" + CUSTOM_APP_LABEL + ")"); + } + + @Test + @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) + public void applyGlobalPolicyAsImplicitZenRule_again_doesNotChangeCustomizedRuleName() { + mZenModeHelper.mConfig.automaticRules.clear(); + mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(UserHandle.CURRENT, + mContext.getPackageName(), CUSTOM_PKG_UID, new Policy(0, 0, 0)); + assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(1); + assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).name) + .isEqualTo("Do Not Disturb (" + CUSTOM_APP_LABEL + ")"); + String ruleId = ZenModeConfig.implicitRuleId(mContext.getPackageName()); + + // User chooses a new name. + AutomaticZenRule azr = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, ruleId); + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, + new AutomaticZenRule.Builder(azr).setName("User chose this").build(), + ORIGIN_USER_IN_SYSTEMUI, "reason", SYSTEM_UID); + + // App updates the implicit rule again. + mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(UserHandle.CURRENT, + mContext.getPackageName(), CUSTOM_PKG_UID, new Policy(0, 0, 0)); + + assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).name) + .isEqualTo("User chose this"); + } + + @Test @DisableFlags(FLAG_MODES_API) public void applyGlobalPolicyAsImplicitZenRule_flagOff_ignored() { mZenModeHelper.mConfig.automaticRules.clear(); withoutWtfCrash( - () -> mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, - CUSTOM_PKG_UID, new Policy(0, 0, 0))); + () -> mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(UserHandle.CURRENT, + CUSTOM_PKG_NAME, CUSTOM_PKG_UID, new Policy(0, 0, 0))); assertThat(mZenModeHelper.mConfig.automaticRules).isEmpty(); } @@ -6329,11 +6505,11 @@ public class ZenModeHelperTest extends UiServiceTestCase { PRIORITY_SENDERS_CONTACTS, PRIORITY_SENDERS_STARRED, Policy.getAllSuppressedVisualEffects(), STATE_FALSE, CONVERSATION_SENDERS_IMPORTANT); - mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, - writtenPolicy); + mZenModeHelper.applyGlobalPolicyAsImplicitZenRule(UserHandle.CURRENT, CUSTOM_PKG_NAME, + CUSTOM_PKG_UID, writtenPolicy); Policy readPolicy = mZenModeHelper.getNotificationPolicyFromImplicitZenRule( - CUSTOM_PKG_NAME); + UserHandle.CURRENT, CUSTOM_PKG_NAME); assertThat(readPolicy).isEqualTo(writtenPolicy); } @@ -6343,15 +6519,15 @@ public class ZenModeHelperTest extends UiServiceTestCase { @DisableFlags(FLAG_MODES_UI) public void getNotificationPolicyFromImplicitZenRule_ruleWithoutPolicy_copiesGlobalPolicy() { // Implicit rule will get the global policy at the time of rule creation. - mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID, - ZEN_MODE_IMPORTANT_INTERRUPTIONS); + mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(UserHandle.CURRENT, CUSTOM_PKG_NAME, + CUSTOM_PKG_UID, ZEN_MODE_IMPORTANT_INTERRUPTIONS); // If the policy then changes afterwards, it should inherit updates because user cannot // edit the policy in the UI. - mZenModeHelper.setNotificationPolicy(new Policy(PRIORITY_CATEGORY_ALARMS, 0, 0), - ORIGIN_APP, 1); + mZenModeHelper.setNotificationPolicy(UserHandle.CURRENT, + new Policy(PRIORITY_CATEGORY_ALARMS, 0, 0), ORIGIN_APP, 1); Policy readPolicy = mZenModeHelper.getNotificationPolicyFromImplicitZenRule( - CUSTOM_PKG_NAME); + UserHandle.CURRENT, CUSTOM_PKG_NAME); assertThat(readPolicy).isNotNull(); assertThat(readPolicy.allowCalls()).isFalse(); @@ -6362,10 +6538,11 @@ public class ZenModeHelperTest extends UiServiceTestCase { @EnableFlags(FLAG_MODES_API) public void getNotificationPolicyFromImplicitZenRule_noImplicitRule_returnsGlobalPolicy() { Policy policy = new Policy(PRIORITY_CATEGORY_CALLS, PRIORITY_SENDERS_STARRED, 0); - mZenModeHelper.setNotificationPolicy(policy, ORIGIN_APP, CUSTOM_PKG_UID); + mZenModeHelper.setNotificationPolicy(UserHandle.CURRENT, policy, ORIGIN_APP, + CUSTOM_PKG_UID); Policy readPolicy = mZenModeHelper.getNotificationPolicyFromImplicitZenRule( - CUSTOM_PKG_NAME); + UserHandle.CURRENT, CUSTOM_PKG_NAME); assertThat(readPolicy).isNotNull(); assertThat(readPolicy.allowCalls()).isTrue(); @@ -6398,7 +6575,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZEN_MODE_IMPORTANT_INTERRUPTIONS, previousManualZenPolicy); Policy newManualPolicy = new Policy(PRIORITY_CATEGORY_EVENTS, 0, 0); - mZenModeHelper.setNotificationPolicy(newManualPolicy, ORIGIN_APP, CUSTOM_PKG_UID); + mZenModeHelper.setNotificationPolicy(UserHandle.CURRENT, newManualPolicy, ORIGIN_APP, + CUSTOM_PKG_UID); ZenPolicy newManualZenPolicy = ZenAdapters.notificationPolicyToZenPolicy(newManualPolicy); // Only app rules with default or same-as-manual policies were updated. @@ -6426,10 +6604,12 @@ public class ZenModeHelperTest extends UiServiceTestCase { when(mResources.getResourceName(resourceId)).thenReturn(veryLongResourceName); when(mResources.getIdentifier(veryLongResourceName, null, null)).thenReturn(resourceId); - String ruleId = mZenModeHelper.addAutomaticZenRule(mContext.getPackageName(), + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, + mContext.getPackageName(), new AutomaticZenRule.Builder("Rule", CONDITION_ID).setIconResId(resourceId).build(), ORIGIN_APP, "reason", CUSTOM_PKG_UID); - AutomaticZenRule storedRule = mZenModeHelper.getAutomaticZenRule(ruleId); + AutomaticZenRule storedRule = mZenModeHelper.getAutomaticZenRule(UserHandle.CURRENT, + ruleId); assertThat(storedRule.getIconResId()).isEqualTo(0); } @@ -6440,8 +6620,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { ZenDeviceEffects effects = new ZenDeviceEffects.Builder() .setShouldDimWallpaper(true) .build(); - mZenModeHelper.setManualZenRuleDeviceEffects(effects, ORIGIN_USER_IN_SYSTEMUI, "settings", - SYSTEM_UID); + mZenModeHelper.setManualZenRuleDeviceEffects(UserHandle.CURRENT, effects, + ORIGIN_USER_IN_SYSTEMUI, "settings", SYSTEM_UID); assertThat(mZenModeHelper.getConfig().manualRule).isNotNull(); assertThat(mZenModeHelper.getConfig().isManualActive()).isFalse(); @@ -6451,14 +6631,14 @@ public class ZenModeHelperTest extends UiServiceTestCase { @Test @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI}) public void setManualZenRuleDeviceEffects_preexistingMode() { - mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, Uri.EMPTY, ORIGIN_USER_IN_SYSTEMUI, - "create manual rule", "settings", SYSTEM_UID); + mZenModeHelper.setManualZenMode(UserHandle.CURRENT, ZEN_MODE_OFF, Uri.EMPTY, + ORIGIN_USER_IN_SYSTEMUI, "create manual rule", "settings", SYSTEM_UID); ZenDeviceEffects effects = new ZenDeviceEffects.Builder() .setShouldDimWallpaper(true) .build(); - mZenModeHelper.setManualZenRuleDeviceEffects(effects, ORIGIN_USER_IN_SYSTEMUI, "settings", - SYSTEM_UID); + mZenModeHelper.setManualZenRuleDeviceEffects(UserHandle.CURRENT, effects, + ORIGIN_USER_IN_SYSTEMUI, "settings", SYSTEM_UID); assertThat(mZenModeHelper.getConfig().manualRule).isNotNull(); assertThat(mZenModeHelper.getConfig().isManualActive()).isFalse(); @@ -6473,7 +6653,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setEnabled(false) .build(); - String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, startsDisabled, + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, startsDisabled, ORIGIN_APP, "new", CUSTOM_PKG_UID); @@ -6488,7 +6668,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setOwner(new ComponentName(mPkg, "SomeProvider")) .setEnabled(true) .build(); - String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, startsEnabled, + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, startsEnabled, ORIGIN_APP, "new", CUSTOM_PKG_UID); assertThat(mZenModeHelper.mConfig.automaticRules.get(ruleId).disabledOrigin).isEqualTo( @@ -6497,8 +6677,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { AutomaticZenRule nowDisabled = new AutomaticZenRule.Builder(startsEnabled) .setEnabled(false) .build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, nowDisabled, ORIGIN_USER_IN_SYSTEMUI, "off", - SYSTEM_UID); + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, nowDisabled, + ORIGIN_USER_IN_SYSTEMUI, "off", SYSTEM_UID); assertThat(mZenModeHelper.mConfig.automaticRules.get(ruleId).disabledOrigin).isEqualTo( ORIGIN_USER_IN_SYSTEMUI); @@ -6511,14 +6691,14 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setOwner(new ComponentName(mPkg, "SomeProvider")) .setEnabled(true) .build(); - String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, startsEnabled, + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, startsEnabled, ORIGIN_APP, "new", CUSTOM_PKG_UID); AutomaticZenRule nowDisabled = new AutomaticZenRule.Builder(startsEnabled) .setEnabled(false) .build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, nowDisabled, ORIGIN_USER_IN_SYSTEMUI, "off", - SYSTEM_UID); + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, nowDisabled, + ORIGIN_USER_IN_SYSTEMUI, "off", SYSTEM_UID); assertThat(mZenModeHelper.mConfig.automaticRules.get(ruleId).disabledOrigin).isEqualTo( ORIGIN_USER_IN_SYSTEMUI); @@ -6526,8 +6706,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { AutomaticZenRule nowRenamed = new AutomaticZenRule.Builder(nowDisabled) .setName("Fancy pants rule") .build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, nowRenamed, ORIGIN_APP, "update", - CUSTOM_PKG_UID); + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, nowRenamed, ORIGIN_APP, + "update", CUSTOM_PKG_UID); // Identity of the disabler is preserved. assertThat(mZenModeHelper.mConfig.automaticRules.get(ruleId).disabledOrigin).isEqualTo( @@ -6541,14 +6721,14 @@ public class ZenModeHelperTest extends UiServiceTestCase { .setOwner(new ComponentName(mPkg, "SomeProvider")) .setEnabled(true) .build(); - String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, startsEnabled, + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, startsEnabled, ORIGIN_APP, "new", CUSTOM_PKG_UID); AutomaticZenRule nowDisabled = new AutomaticZenRule.Builder(startsEnabled) .setEnabled(false) .build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, nowDisabled, ORIGIN_USER_IN_SYSTEMUI, "off", - SYSTEM_UID); + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, nowDisabled, + ORIGIN_USER_IN_SYSTEMUI, "off", SYSTEM_UID); assertThat(mZenModeHelper.mConfig.automaticRules.get(ruleId).disabledOrigin).isEqualTo( ORIGIN_USER_IN_SYSTEMUI); @@ -6556,8 +6736,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { AutomaticZenRule nowEnabled = new AutomaticZenRule.Builder(nowDisabled) .setEnabled(true) .build(); - mZenModeHelper.updateAutomaticZenRule(ruleId, nowEnabled, ORIGIN_APP, "on", - CUSTOM_PKG_UID); + mZenModeHelper.updateAutomaticZenRule(UserHandle.CURRENT, ruleId, nowEnabled, ORIGIN_APP, + "on", CUSTOM_PKG_UID); // Identity of the disabler was cleared. assertThat(mZenModeHelper.mConfig.automaticRules.get(ruleId).disabledOrigin).isEqualTo( @@ -6570,10 +6750,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond")) .setPackage(mPkg) .build(); - String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, rule, ORIGIN_APP, "adding", - CUSTOM_PKG_UID); + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, rule, + ORIGIN_APP, "adding", CUSTOM_PKG_UID); - mZenModeHelper.setAutomaticZenRuleState(ruleId, + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, new Condition(rule.getConditionId(), "manual-on", STATE_TRUE, SOURCE_USER_ACTION), ORIGIN_USER_IN_SYSTEMUI, SYSTEM_UID); @@ -6588,13 +6768,13 @@ public class ZenModeHelperTest extends UiServiceTestCase { AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond")) .setPackage(mPkg) .build(); - String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, rule, ORIGIN_APP, "adding", - CUSTOM_PKG_UID); + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, rule, + ORIGIN_APP, "adding", CUSTOM_PKG_UID); Condition autoOn = new Condition(rule.getConditionId(), "auto-on", STATE_TRUE, SOURCE_CONTEXT); ZenRule zenRule; - mZenModeHelper.setAutomaticZenRuleState(ruleId, + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, new Condition(rule.getConditionId(), "manual-on", STATE_TRUE, SOURCE_USER_ACTION), ORIGIN_USER_IN_SYSTEMUI, SYSTEM_UID); zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); @@ -6602,7 +6782,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_ACTIVATE); assertThat(zenRule.condition).isNull(); - mZenModeHelper.setAutomaticZenRuleState(ruleId, + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, new Condition(rule.getConditionId(), "manual-off", STATE_FALSE, SOURCE_USER_ACTION), ORIGIN_USER_IN_SYSTEMUI, SYSTEM_UID); zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); @@ -6611,7 +6791,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertThat(zenRule.condition).isNull(); // Bonus check: app has resumed control over the rule and can now turn it on. - mZenModeHelper.setAutomaticZenRuleState(ruleId, autoOn, ORIGIN_APP, CUSTOM_PKG_UID); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, autoOn, ORIGIN_APP, + CUSTOM_PKG_UID); zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); assertThat(zenRule.isActive()).isTrue(); assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_NONE); @@ -6624,21 +6805,22 @@ public class ZenModeHelperTest extends UiServiceTestCase { AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond")) .setPackage(mPkg) .build(); - String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, rule, ORIGIN_APP, "adding", - CUSTOM_PKG_UID); + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, rule, + ORIGIN_APP, "adding", CUSTOM_PKG_UID); Condition autoOn = new Condition(rule.getConditionId(), "auto-on", STATE_TRUE, SOURCE_CONTEXT); Condition autoOff = new Condition(rule.getConditionId(), "auto-off", STATE_FALSE, SOURCE_CONTEXT); ZenRule zenRule; - mZenModeHelper.setAutomaticZenRuleState(ruleId, autoOn, ORIGIN_APP, CUSTOM_PKG_UID); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, autoOn, ORIGIN_APP, + CUSTOM_PKG_UID); zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); assertThat(zenRule.isActive()).isTrue(); assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_NONE); assertThat(zenRule.condition).isEqualTo(autoOn); - mZenModeHelper.setAutomaticZenRuleState(ruleId, + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, new Condition(rule.getConditionId(), "manual-off", STATE_FALSE, SOURCE_USER_ACTION), ORIGIN_USER_IN_SYSTEMUI, SYSTEM_UID); zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); @@ -6646,7 +6828,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_DEACTIVATE); assertThat(zenRule.condition).isEqualTo(autoOn); - mZenModeHelper.setAutomaticZenRuleState(ruleId, + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, new Condition(rule.getConditionId(), "manual-on", STATE_TRUE, SOURCE_USER_ACTION), ORIGIN_USER_IN_SYSTEMUI, SYSTEM_UID); zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); @@ -6655,7 +6837,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertThat(zenRule.condition).isEqualTo(autoOn); // Bonus check: app has resumed control over the rule and can now turn it off. - mZenModeHelper.setAutomaticZenRuleState(ruleId, autoOff, ORIGIN_APP, CUSTOM_PKG_UID); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, autoOff, ORIGIN_APP, + CUSTOM_PKG_UID); zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); assertThat(zenRule.isActive()).isFalse(); assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_NONE); @@ -6668,10 +6851,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond")) .setPackage(mPkg) .build(); - String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, rule, ORIGIN_APP, "adding", - CUSTOM_PKG_UID); + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, rule, + ORIGIN_APP, "adding", CUSTOM_PKG_UID); - mZenModeHelper.setAutomaticZenRuleState(ruleId, + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, new Condition(rule.getConditionId(), "auto-on", STATE_TRUE, SOURCE_CONTEXT), ORIGIN_APP, CUSTOM_PKG_UID); ZenRule zenRuleOn = mZenModeHelper.mConfig.automaticRules.get(ruleId); @@ -6679,7 +6862,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { assertThat(zenRuleOn.getConditionOverride()).isEqualTo(OVERRIDE_NONE); assertThat(zenRuleOn.condition).isNotNull(); - mZenModeHelper.setAutomaticZenRuleState(ruleId, + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, new Condition(rule.getConditionId(), "manual-off", STATE_FALSE, SOURCE_USER_ACTION), ORIGIN_USER_IN_SYSTEMUI, SYSTEM_UID); ZenRule zenRuleOff = mZenModeHelper.mConfig.automaticRules.get(ruleId); @@ -6694,31 +6877,31 @@ public class ZenModeHelperTest extends UiServiceTestCase { AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond")) .setPackage(mPkg) .build(); - String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, rule, ORIGIN_APP, "adding", - CUSTOM_PKG_UID); + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, rule, + ORIGIN_APP, "adding", CUSTOM_PKG_UID); ZenRule zenRule; - mZenModeHelper.setAutomaticZenRuleState(ruleId, + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, new Condition(rule.getConditionId(), "manual-on", STATE_TRUE, SOURCE_USER_ACTION), ORIGIN_USER_IN_SYSTEMUI, SYSTEM_UID); zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); assertThat(zenRule.isActive()).isTrue(); assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_ACTIVATE); - mZenModeHelper.setAutomaticZenRuleState(ruleId, + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, new Condition(rule.getConditionId(), "auto-off", STATE_FALSE, SOURCE_CONTEXT), ORIGIN_APP, CUSTOM_PKG_UID); zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); assertThat(zenRule.isActive()).isTrue(); assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_ACTIVATE); - mZenModeHelper.setAutomaticZenRuleState(ruleId, + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, new Condition(rule.getConditionId(), "auto-on", STATE_TRUE, SOURCE_CONTEXT), ORIGIN_APP, CUSTOM_PKG_UID); zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); assertThat(zenRule.isActive()).isTrue(); assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_NONE); - mZenModeHelper.setAutomaticZenRuleState(ruleId, + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, new Condition(rule.getConditionId(), "auto-off", STATE_FALSE, SOURCE_CONTEXT), ORIGIN_APP, CUSTOM_PKG_UID); zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); @@ -6731,39 +6914,39 @@ public class ZenModeHelperTest extends UiServiceTestCase { AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond")) .setPackage(mPkg) .build(); - String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, rule, ORIGIN_APP, "adding", - CUSTOM_PKG_UID); + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, rule, + ORIGIN_APP, "adding", CUSTOM_PKG_UID); ZenRule zenRule; - mZenModeHelper.setAutomaticZenRuleState(ruleId, + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, new Condition(rule.getConditionId(), "auto-on", STATE_TRUE, SOURCE_CONTEXT), ORIGIN_APP, CUSTOM_PKG_UID); zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); assertThat(zenRule.isActive()).isTrue(); assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_NONE); - mZenModeHelper.setAutomaticZenRuleState(ruleId, + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, new Condition(rule.getConditionId(), "manual-off", STATE_FALSE, SOURCE_USER_ACTION), ORIGIN_USER_IN_SYSTEMUI, SYSTEM_UID); zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); assertThat(zenRule.isActive()).isFalse(); assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_DEACTIVATE); - mZenModeHelper.setAutomaticZenRuleState(ruleId, + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, new Condition(rule.getConditionId(), "auto-on", STATE_TRUE, SOURCE_CONTEXT), ORIGIN_APP, CUSTOM_PKG_UID); zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); assertThat(zenRule.isActive()).isFalse(); assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_DEACTIVATE); - mZenModeHelper.setAutomaticZenRuleState(ruleId, + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, new Condition(rule.getConditionId(), "auto-off", STATE_FALSE, SOURCE_CONTEXT), ORIGIN_APP, CUSTOM_PKG_UID); zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); assertThat(zenRule.isActive()).isFalse(); assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_NONE); - mZenModeHelper.setAutomaticZenRuleState(ruleId, + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, new Condition(rule.getConditionId(), "auto-on", STATE_TRUE, SOURCE_CONTEXT), ORIGIN_APP, CUSTOM_PKG_UID); zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); @@ -6777,18 +6960,18 @@ public class ZenModeHelperTest extends UiServiceTestCase { AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond")) .setPackage(mPkg) .build(); - String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, rule, ORIGIN_APP, "adding", - CUSTOM_PKG_UID); + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, rule, + ORIGIN_APP, "adding", CUSTOM_PKG_UID); // User manually turns on rule from SysUI / Settings... - mZenModeHelper.setAutomaticZenRuleState(ruleId, + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, new Condition(rule.getConditionId(), "manual-on-from-sysui", STATE_TRUE, SOURCE_USER_ACTION), ORIGIN_USER_IN_SYSTEMUI, SYSTEM_UID); assertThat(getZenRule(ruleId).isActive()).isTrue(); assertThat(getZenRule(ruleId).getConditionOverride()).isEqualTo(OVERRIDE_ACTIVATE); // ... and they can turn it off manually from inside the app. - mZenModeHelper.setAutomaticZenRuleState(ruleId, + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, new Condition(rule.getConditionId(), "manual-off-from-app", STATE_FALSE, SOURCE_USER_ACTION), ORIGIN_USER_IN_APP, CUSTOM_PKG_UID); assertThat(getZenRule(ruleId).isActive()).isFalse(); @@ -6801,25 +6984,25 @@ public class ZenModeHelperTest extends UiServiceTestCase { AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond")) .setPackage(mPkg) .build(); - String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, rule, ORIGIN_APP, "adding", - CUSTOM_PKG_UID); + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, rule, + ORIGIN_APP, "adding", CUSTOM_PKG_UID); // Rule is activated due to its schedule. - mZenModeHelper.setAutomaticZenRuleState(ruleId, + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, new Condition(rule.getConditionId(), "auto-on-from-app", STATE_TRUE, SOURCE_SCHEDULE), ORIGIN_APP, CUSTOM_PKG_UID); assertThat(getZenRule(ruleId).isActive()).isTrue(); assertThat(getZenRule(ruleId).getConditionOverride()).isEqualTo(OVERRIDE_NONE); // User manually turns off rule from SysUI / Settings... - mZenModeHelper.setAutomaticZenRuleState(ruleId, + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, new Condition(rule.getConditionId(), "manual-off-from-sysui", STATE_FALSE, SOURCE_USER_ACTION), ORIGIN_USER_IN_SYSTEMUI, SYSTEM_UID); assertThat(getZenRule(ruleId).isActive()).isFalse(); assertThat(getZenRule(ruleId).getConditionOverride()).isEqualTo(OVERRIDE_DEACTIVATE); // ... and they can turn it on manually from inside the app. - mZenModeHelper.setAutomaticZenRuleState(ruleId, + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, new Condition(rule.getConditionId(), "manual-on-from-app", STATE_TRUE, SOURCE_USER_ACTION), ORIGIN_USER_IN_APP, CUSTOM_PKG_UID); assertThat(getZenRule(ruleId).isActive()).isTrue(); @@ -6832,19 +7015,19 @@ public class ZenModeHelperTest extends UiServiceTestCase { AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond")) .setPackage(mPkg) .build(); - String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, rule, ORIGIN_APP, "adding", - CUSTOM_PKG_UID); + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, rule, + ORIGIN_APP, "adding", CUSTOM_PKG_UID); // Rule is manually activated by the user in the app. // This turns the rule on, but is NOT an override... - mZenModeHelper.setAutomaticZenRuleState(ruleId, + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, new Condition(rule.getConditionId(), "manual-on-from-app", STATE_TRUE, SOURCE_USER_ACTION), ORIGIN_USER_IN_APP, CUSTOM_PKG_UID); assertThat(getZenRule(ruleId).isActive()).isTrue(); assertThat(getZenRule(ruleId).getConditionOverride()).isEqualTo(OVERRIDE_NONE); // ... so the app can turn it off when its schedule is over. - mZenModeHelper.setAutomaticZenRuleState(ruleId, + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, new Condition(rule.getConditionId(), "auto-off-from-app", STATE_FALSE, SOURCE_SCHEDULE), ORIGIN_APP, CUSTOM_PKG_UID); assertThat(getZenRule(ruleId).isActive()).isFalse(); @@ -6909,12 +7092,13 @@ public class ZenModeHelperTest extends UiServiceTestCase { AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond")) .setPackage(mPkg) .build(); - String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, rule, ORIGIN_APP, "adding", - CUSTOM_PKG_UID); - mZenModeHelper.setAutomaticZenRuleState(ruleId, + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, rule, + ORIGIN_APP, "adding", CUSTOM_PKG_UID); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, new Condition(rule.getConditionId(), "manual-on", STATE_TRUE, SOURCE_USER_ACTION), ORIGIN_USER_IN_SYSTEMUI, SYSTEM_UID); - assertThat(mZenModeHelper.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_TRUE); + assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, ruleId)) + .isEqualTo(STATE_TRUE); ZenRule zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_ACTIVATE); assertThat(zenRule.condition).isNull(); @@ -6925,15 +7109,17 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Now simulate a reboot -> reload the configuration after purging. TypedXmlPullParser parser = getParserForByteStream(xmlBytes); - mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL); + mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL, null); if (Flags.modesUi()) { - assertThat(mZenModeHelper.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_TRUE); + assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, ruleId)) + .isEqualTo(STATE_TRUE); zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_ACTIVATE); assertThat(zenRule.condition).isNull(); } else { - assertThat(mZenModeHelper.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_FALSE); + assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, ruleId)) + .isEqualTo(STATE_FALSE); } } @@ -6944,15 +7130,16 @@ public class ZenModeHelperTest extends UiServiceTestCase { AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond")) .setPackage(mPkg) .build(); - String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, rule, ORIGIN_APP, "adding", - CUSTOM_PKG_UID); - mZenModeHelper.setAutomaticZenRuleState(ruleId, + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, rule, + ORIGIN_APP, "adding", CUSTOM_PKG_UID); + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, new Condition(rule.getConditionId(), "auto-on", STATE_TRUE, SOURCE_CONTEXT), ORIGIN_APP, CUSTOM_PKG_UID); - mZenModeHelper.setAutomaticZenRuleState(ruleId, + mZenModeHelper.setAutomaticZenRuleState(UserHandle.CURRENT, ruleId, new Condition(rule.getConditionId(), "snooze", STATE_FALSE, SOURCE_USER_ACTION), ORIGIN_USER_IN_SYSTEMUI, SYSTEM_UID); - assertThat(mZenModeHelper.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_FALSE); + assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, ruleId)) + .isEqualTo(STATE_FALSE); ZenRule zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_DEACTIVATE); assertThat(zenRule.condition).isNotNull(); @@ -6963,9 +7150,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { // Now simulate a reboot -> reload the configuration after purging. TypedXmlPullParser parser = getParserForByteStream(xmlBytes); - mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL); + mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL, null); - assertThat(mZenModeHelper.getAutomaticZenRuleState(ruleId)).isEqualTo(STATE_TRUE); + assertThat(mZenModeHelper.getAutomaticZenRuleState(UserHandle.CURRENT, ruleId)) + .isEqualTo(STATE_TRUE); zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId); assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_NONE); assertThat(zenRule.condition).isNotNull(); @@ -6978,8 +7166,8 @@ public class ZenModeHelperTest extends UiServiceTestCase { AutomaticZenRule azr = new AutomaticZenRule.Builder("Rule", Uri.parse("cond")) .setPackage(mPkg) .build(); - String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, azr, ORIGIN_APP, "adding", - CUSTOM_PKG_UID); + String ruleId = mZenModeHelper.addAutomaticZenRule(UserHandle.CURRENT, mPkg, azr, + ORIGIN_APP, "adding", CUSTOM_PKG_UID); ZenRule zenRule = checkNotNull(mZenModeHelper.mConfig.automaticRules.get(ruleId)); @@ -7001,7 +7189,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { ByteArrayOutputStream xmlBytes = writeXmlAndPurge(ZenModeConfig.XML_VERSION_MODES_UI); TypedXmlPullParser parser = getParserForByteStream(xmlBytes); - mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL); + mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL, null); assertThat(mZenModeHelper.mConfig.automaticRules).doesNotContainKey( ZenModeConfig.EVENTS_OBSOLETE_RULE_ID); @@ -7021,7 +7209,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { ByteArrayOutputStream xmlBytes = writeXmlAndPurge(ZenModeConfig.XML_VERSION_MODES_UI); TypedXmlPullParser parser = getParserForByteStream(xmlBytes); - mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL); + mZenModeHelper.readXml(parser, false, UserHandle.USER_ALL, null); assertThat(mZenModeHelper.mConfig.automaticRules).containsKey( ZenModeConfig.EVENTS_OBSOLETE_RULE_ID); @@ -7039,15 +7227,62 @@ public class ZenModeHelperTest extends UiServiceTestCase { .allowPriorityChannels(false) .build(); - mZenModeHelper.updateHasPriorityChannels(true); - assertThat(mZenModeHelper.getNotificationPolicy().hasPriorityChannels()).isTrue(); + mZenModeHelper.updateHasPriorityChannels(UserHandle.CURRENT, true); + assertThat(mZenModeHelper.getNotificationPolicy(UserHandle.CURRENT).hasPriorityChannels()) + .isTrue(); // getNotificationPolicy() gets its policy from the manual rule; channels not permitted - assertThat(mZenModeHelper.getNotificationPolicy().allowPriorityChannels()).isFalse(); + assertThat(mZenModeHelper.getNotificationPolicy(UserHandle.CURRENT).allowPriorityChannels()) + .isFalse(); + + mZenModeHelper.updateHasPriorityChannels(UserHandle.CURRENT, false); + assertThat(mZenModeHelper.getNotificationPolicy(UserHandle.CURRENT).hasPriorityChannels()) + .isFalse(); + assertThat(mZenModeHelper.getNotificationPolicy(UserHandle.CURRENT).allowPriorityChannels()) + .isFalse(); + } + + @Test + @EnableFlags(FLAG_MODES_MULTIUSER) + public void setManualZenMode_fromCurrentUser_updatesCurrentConfig() { + // Initialize default configurations (default rules) for both users. + mZenModeHelper.onUserSwitched(1); + mZenModeHelper.onUserSwitched(2); + UserHandle currentUser = UserHandle.of(2); + ZenModeHelper.Callback callback = mock(ZenModeHelper.Callback.class); + mZenModeHelper.addCallback(callback); + + mZenModeHelper.setManualZenMode(currentUser, ZEN_MODE_ALARMS, null, ORIGIN_APP, "reason", + mPkg, CUSTOM_PKG_UID); + + assertThat(mZenModeHelper.mConfig.isManualActive()).isTrue(); + assertThat(mZenModeHelper.mConfigs.get(1).isManualActive()).isFalse(); - mZenModeHelper.updateHasPriorityChannels(false); - assertThat(mZenModeHelper.getNotificationPolicy().hasPriorityChannels()).isFalse(); - assertThat(mZenModeHelper.getNotificationPolicy().allowPriorityChannels()).isFalse(); + // And we sent the broadcast announcing the change. + mTestableLooper.processAllMessages(); + verify(callback).onZenModeChanged(); + } + + @Test + @EnableFlags(FLAG_MODES_MULTIUSER) + public void setInterruptionFilter_fromNonCurrentUser_updatesNonCurrentConfig() { + // Initialize default configurations (default rules) for both users. + // Afterwards, 2 is current, and 1 is background. + mZenModeHelper.onUserSwitched(1); + mZenModeHelper.onUserSwitched(2); + UserHandle backgroundUser = UserHandle.of(1); + ZenModeHelper.Callback callback = mock(ZenModeHelper.Callback.class); + mZenModeHelper.addCallback(callback); + + mZenModeHelper.setManualZenMode(backgroundUser, ZEN_MODE_ALARMS, null, ORIGIN_APP, "reason", + mPkg, CUSTOM_PKG_UID); + + assertThat(mZenModeHelper.mConfig.isManualActive()).isFalse(); + assertThat(mZenModeHelper.mConfigs.get(1).isManualActive()).isTrue(); + + // And no broadcasts is sent for "background" changes (they were not evaluated). + mTestableLooper.processAllMessages(); + verify(callback, never()).onZenModeChanged(); } private static void addZenRule(ZenModeConfig config, String id, String ownerPkg, int zenMode, @@ -7100,9 +7335,10 @@ public class ZenModeHelperTest extends UiServiceTestCase { rule.zenMode = zenMode; rule.zenPolicy = policy; rule.pkg = ownerPkg; - rule.name = CUSTOM_APP_LABEL; - if (!Flags.modesUi()) { - rule.iconResName = ICON_RES_NAME; + if (Flags.modesUi()) { + rule.name = mContext.getString(R.string.zen_mode_implicit_name, CUSTOM_APP_LABEL); + } else { + rule.name = CUSTOM_APP_LABEL; } rule.triggerDescription = mContext.getString(R.string.zen_mode_implicit_trigger_description, CUSTOM_APP_LABEL); @@ -7122,7 +7358,7 @@ public class ZenModeHelperTest extends UiServiceTestCase { SUPPRESSED_EFFECT_BADGE, 0, CONVERSATION_SENDERS_IMPORTANT); - mZenModeHelper.setNotificationPolicy(customPolicy, ORIGIN_UNKNOWN, 1); + mZenModeHelper.setNotificationPolicy(UserHandle.CURRENT, customPolicy, ORIGIN_UNKNOWN, 1); if (!Flags.modesUi()) { mZenModeHelper.mConfig.manualRule = null; } diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java index 8aa8a84206e2..093359042a3e 100644 --- a/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java +++ b/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java @@ -59,7 +59,7 @@ import android.os.test.TestLooper; import android.os.vibrator.Flags; import android.os.vibrator.PrebakedSegment; import android.os.vibrator.PrimitiveSegment; -import android.os.vibrator.PwleSegment; +import android.os.vibrator.PwlePoint; import android.os.vibrator.RampSegment; import android.os.vibrator.StepSegment; import android.os.vibrator.VibrationConfig; @@ -888,7 +888,7 @@ public class VibrationThreadTest { fakeVibrator.setMinEnvelopeEffectControlPointDurationMillis(20); VibrationEffect effect = VibrationEffect.startWaveformEnvelope() - .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 60f, /*timeMillis=*/ 20) + .addControlPoint(/*amplitude=*/ 0.1f, /*frequencyHz=*/ 60f, /*timeMillis=*/ 20) .addControlPoint(/*amplitude=*/ 0.3f, /*frequencyHz=*/ 100f, /*timeMillis=*/ 30) .addControlPoint(/*amplitude=*/ 0.4f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 20) .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 30) @@ -902,20 +902,47 @@ public class VibrationThreadTest { verifyCallbacksTriggered(vibration, Status.FINISHED); assertFalse(mControllers.get(VIBRATOR_ID).isVibrating()); assertEquals(Arrays.asList( - expectedPwle(/* startAmplitude= */ 0.0f, /* endAmplitude= */ 0.0f, - /* startFrequencyHz= */ 60f, /* endFrequencyHz= */ 60f, - /* duration= */ 20), - expectedPwle(/* startAmplitude= */ 0.0f, /* endAmplitude= */ 0.3f, - /* startFrequencyHz= */ 60f, /* endFrequencyHz= */ 100f, - /* duration= */ 30), - expectedPwle(/* startAmplitude= */ 0.3f, /* endAmplitude= */ 0.4f, - /* startFrequencyHz= */ 100f, /* endFrequencyHz= */ 120f, - /* duration= */ 20), - expectedPwle(/* startAmplitude= */ 0.4f, /* endAmplitude= */ 0.0f, - /* startFrequencyHz= */ 120f, /* endFrequencyHz= */ 120f, - /* duration= */ 30) - ), - fakeVibrator.getEffectSegments(vibration.id)); + expectedPwle(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 60f, /*timeMillis=*/ 0), + expectedPwle(/*amplitude=*/ 0.1f, /*frequencyHz=*/ 60f, /*timeMillis=*/ 20), + expectedPwle(/*amplitude=*/ 0.3f, /*frequencyHz=*/ 100f, /*timeMillis=*/ 30), + expectedPwle(/*amplitude=*/ 0.4f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 20), + expectedPwle(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 30) + ), fakeVibrator.getEffectPwlePoints(vibration.id)); + + } + + @Test + @EnableFlags(android.os.vibrator.Flags.FLAG_NORMALIZED_PWLE_EFFECTS) + public void vibrate_singleVibratorPwle_withInitialFrequency_runsComposePwleV2() { + FakeVibratorControllerProvider fakeVibrator = mVibratorProviders.get(VIBRATOR_ID); + fakeVibrator.setCapabilities(IVibrator.CAP_COMPOSE_PWLE_EFFECTS_V2); + fakeVibrator.setResonantFrequency(150); + fakeVibrator.setFrequenciesHz(new float[]{30f, 50f, 100f, 120f, 150f}); + fakeVibrator.setOutputAccelerationsGs(new float[]{0.3f, 0.5f, 1.0f, 0.8f, 0.6f}); + fakeVibrator.setMaxEnvelopeEffectSize(10); + fakeVibrator.setMinEnvelopeEffectControlPointDurationMillis(20); + + VibrationEffect effect = VibrationEffect.startWaveformEnvelope(/*initialFrequency=*/ 30) + .addControlPoint(/*amplitude=*/ 0.1f, /*frequencyHz=*/ 60f, /*timeMillis=*/ 20) + .addControlPoint(/*amplitude=*/ 0.3f, /*frequencyHz=*/ 100f, /*timeMillis=*/ 30) + .addControlPoint(/*amplitude=*/ 0.4f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 20) + .addControlPoint(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 30) + .build(); + HalVibration vibration = startThreadAndDispatcher(effect); + waitForCompletion(); + + verify(mManagerHooks).noteVibratorOn(eq(UID), eq(100L)); + verify(mManagerHooks).noteVibratorOff(eq(UID)); + verify(mControllerCallbacks).onComplete(eq(VIBRATOR_ID), eq(vibration.id)); + verifyCallbacksTriggered(vibration, Status.FINISHED); + assertFalse(mControllers.get(VIBRATOR_ID).isVibrating()); + assertEquals(Arrays.asList( + expectedPwle(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 30f, /*timeMillis=*/ 0), + expectedPwle(/*amplitude=*/ 0.1f, /*frequencyHz=*/ 60f, /*timeMillis=*/ 20), + expectedPwle(/*amplitude=*/ 0.3f, /*frequencyHz=*/ 100f, /*timeMillis=*/ 30), + expectedPwle(/*amplitude=*/ 0.4f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 20), + expectedPwle(/*amplitude=*/ 0.0f, /*frequencyHz=*/ 120f, /*timeMillis=*/ 30) + ), fakeVibrator.getEffectPwlePoints(vibration.id)); } @@ -2013,10 +2040,8 @@ public class VibrationThreadTest { duration); } - private VibrationEffectSegment expectedPwle(float startAmplitude, float endAmplitude, - float startFrequencyHz, float endFrequencyHz, int duration) { - return new PwleSegment(startAmplitude, endAmplitude, startFrequencyHz, endFrequencyHz, - duration); + private PwlePoint expectedPwle(float amplitude, float frequencyHz, int timeMillis) { + return new PwlePoint(amplitude, frequencyHz, timeMillis); } private List<Float> expectedAmplitudes(int... amplitudes) { diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibratorControllerTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibratorControllerTest.java index bc8db3be5f27..0978f48491cc 100644 --- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorControllerTest.java +++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorControllerTest.java @@ -44,7 +44,7 @@ import android.os.VibratorInfo; import android.os.test.TestLooper; import android.os.vibrator.PrebakedSegment; import android.os.vibrator.PrimitiveSegment; -import android.os.vibrator.PwleSegment; +import android.os.vibrator.PwlePoint; import android.os.vibrator.RampSegment; import androidx.test.InstrumentationRegistry; @@ -274,9 +274,9 @@ public class VibratorControllerTest { when(mNativeWrapperMock.composePwleV2(any(), anyLong())).thenReturn(15L); VibratorController controller = createController(); - PwleSegment[] primitives = new PwleSegment[]{ - new PwleSegment(/* startAmplitude= */ 0, /* endAmplitude= */ 1, - /* startFrequencyHz= */ 100, /* endFrequencyHz= */ 200, /* duration= */ 10) + PwlePoint[] primitives = new PwlePoint[]{ + new PwlePoint(/*amplitude=*/ 0, /*frequencyHz=*/ 100, /*timeMillis=*/ 0), + new PwlePoint(/*amplitude=*/ 1, /*frequencyHz=*/ 200, /*timeMillis=*/ 10) }; assertEquals(15L, controller.on(primitives, 12)); assertTrue(controller.isVibrating()); diff --git a/services/tests/vibrator/utils/com/android/server/vibrator/FakeVibratorControllerProvider.java b/services/tests/vibrator/utils/com/android/server/vibrator/FakeVibratorControllerProvider.java index 2c3e9b29d11f..4dc59c20c431 100644 --- a/services/tests/vibrator/utils/com/android/server/vibrator/FakeVibratorControllerProvider.java +++ b/services/tests/vibrator/utils/com/android/server/vibrator/FakeVibratorControllerProvider.java @@ -26,7 +26,7 @@ import android.os.VibrationEffect; import android.os.VibratorInfo; import android.os.vibrator.PrebakedSegment; import android.os.vibrator.PrimitiveSegment; -import android.os.vibrator.PwleSegment; +import android.os.vibrator.PwlePoint; import android.os.vibrator.RampSegment; import android.os.vibrator.StepSegment; import android.os.vibrator.VibrationEffectSegment; @@ -50,6 +50,7 @@ public final class FakeVibratorControllerProvider { private final Map<Long, PrebakedSegment> mEnabledAlwaysOnEffects = new HashMap<>(); private final Map<Long, List<VibrationEffectSegment>> mEffectSegments = new TreeMap<>(); private final Map<Long, List<VibrationEffect.VendorEffect>> mVendorEffects = new TreeMap<>(); + private final Map<Long, List<PwlePoint>> mEffectPwlePoints = new TreeMap<>(); private final Map<Long, List<Integer>> mBraking = new HashMap<>(); private final List<Float> mAmplitudes = new ArrayList<>(); private final List<Boolean> mExternalControlStates = new ArrayList<>(); @@ -91,6 +92,10 @@ public final class FakeVibratorControllerProvider { mVendorEffects.computeIfAbsent(vibrationId, k -> new ArrayList<>()).add(vendorEffect); } + void recordEffectPwlePoint(long vibrationId, PwlePoint pwlePoint) { + mEffectPwlePoints.computeIfAbsent(vibrationId, k -> new ArrayList<>()).add(pwlePoint); + } + void recordBraking(long vibrationId, int braking) { mBraking.computeIfAbsent(vibrationId, k -> new ArrayList<>()).add(braking); } @@ -195,11 +200,11 @@ public final class FakeVibratorControllerProvider { } @Override - public long composePwleV2(PwleSegment[] primitives, long vibrationId) { + public long composePwleV2(PwlePoint[] pwlePoints, long vibrationId) { long duration = 0; - for (PwleSegment primitive: primitives) { - duration += primitive.getDuration(); - recordEffectSegment(vibrationId, primitive); + for (PwlePoint pwlePoint: pwlePoints) { + duration += pwlePoint.getTimeMillis(); + recordEffectPwlePoint(vibrationId, pwlePoint); } applyLatency(mOnLatency); scheduleListener(duration, vibrationId); @@ -482,6 +487,28 @@ public final class FakeVibratorControllerProvider { return result; } + /** Return list of {@link PwlePoint} played by this controller, in order. */ + public List<PwlePoint> getEffectPwlePoints(long vibrationId) { + if (mEffectPwlePoints.containsKey(vibrationId)) { + return new ArrayList<>(mEffectPwlePoints.get(vibrationId)); + } else { + return new ArrayList<>(); + } + } + + /** + * Returns a list of all vibrations' {@link PwlePoint}s, for external-use where vibration + * IDs aren't exposed. + */ + public List<PwlePoint> getAllEffectPwlePoints() { + // Returns segments in order of vibrationId, which increases over time. TreeMap gives order. + ArrayList<PwlePoint> result = new ArrayList<>(); + for (List<PwlePoint> subList : mEffectPwlePoints.values()) { + result.addAll(subList); + } + return result; + } + /** Return list of states set for external control to the fake vibrator hardware. */ public List<Boolean> getExternalControlStates() { return mExternalControlStates; diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java index 3742249bdffe..00c9691835db 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatActivityRobot.java @@ -143,6 +143,10 @@ class AppCompatActivityRobot { doReturn(naturalOrientation).when(mDisplayContent).getNaturalOrientation(); } + void setDisplayIgnoreActivitySizeRestrictions(boolean enabled) { + doReturn(enabled).when(mDisplayContent).isDisplayIgnoreActivitySizeRestrictions(); + } + void configureTaskBounds(@NonNull Rect taskBounds) { doReturn(taskBounds).when(mTaskStack.top()).getBounds(); } @@ -255,6 +259,10 @@ class AppCompatActivityRobot { doReturn(embedded).when(mActivityStack.top()).isEmbedded(); } + void setTopActivityHasLetterboxedBounds(boolean letterboxed) { + doReturn(letterboxed).when(mActivityStack.top()).areBoundsLetterboxed(); + } + void setTopActivityVisible(boolean isVisible) { doReturn(isVisible).when(mActivityStack.top()).isVisible(); } diff --git a/services/tests/wmtests/src/com/android/server/wm/AppCompatAspectRatioOverridesTest.java b/services/tests/wmtests/src/com/android/server/wm/AppCompatAspectRatioOverridesTest.java index b83911337c5c..14ef913e28db 100644 --- a/services/tests/wmtests/src/com/android/server/wm/AppCompatAspectRatioOverridesTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/AppCompatAspectRatioOverridesTest.java @@ -17,9 +17,12 @@ package com.android.server.wm; import static android.content.pm.ActivityInfo.OVERRIDE_MIN_ASPECT_RATIO; +import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_3_2; import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_FULLSCREEN; +import static android.view.Surface.ROTATION_90; import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_MIN_ASPECT_RATIO_OVERRIDE; +import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE; import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE; import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE; @@ -29,10 +32,13 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import android.compat.testing.PlatformCompatChangeRule; +import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import androidx.annotation.NonNull; +import com.android.window.flags.Flags; + import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges; import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges; @@ -288,6 +294,93 @@ public class AppCompatAspectRatioOverridesTest extends WindowTestsBase { }); } + @Test + @EnableFlags(Flags.FLAG_VDM_FORCE_APP_UNIVERSAL_RESIZABLE_API) + public void testHasFullscreenOverride_displayIgnoreActivitySizeRestrictionsTrue() { + runTestScenario((robot) -> { + robot.applyOnActivity((a) -> { + a.setDisplayIgnoreActivitySizeRestrictions(true); + a.createActivityWithComponent(); + }); + + robot.checkHasFullscreenOverride(true); + }); + } + + @Test + @EnableFlags(Flags.FLAG_VDM_FORCE_APP_UNIVERSAL_RESIZABLE_API) + public void testHasFullscreenOverride_displayIgnoreActivitySizeRestrictionsFalse() { + runTestScenario((robot) -> { + robot.applyOnActivity((a) -> { + a.setDisplayIgnoreActivitySizeRestrictions(false); + a.createActivityWithComponent(); + }); + + robot.checkHasFullscreenOverride(false); + }); + } + + @Test + @EnableFlags(Flags.FLAG_VDM_FORCE_APP_UNIVERSAL_RESIZABLE_API) + public void testPropFalse_displayIgnoreActivitySizeRestrictionsTrue_notOverridden() { + runTestScenario((robot) -> { + robot.prop().disable(PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE); + robot.applyOnActivity((a) -> { + a.setDisplayIgnoreActivitySizeRestrictions(true); + a.createActivityWithComponent(); + }); + + robot.checkHasFullscreenOverride(false); + }); + } + + @Test + @EnableFlags(Flags.FLAG_VDM_FORCE_APP_UNIVERSAL_RESIZABLE_API) + public void testPropTrue_displayIgnoreActivitySizeRestrictionsFalse_notOverridden() { + runTestScenario((robot) -> { + robot.prop().enable(PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE); + robot.applyOnActivity((a) -> { + a.setDisplayIgnoreActivitySizeRestrictions(false); + a.createActivityWithComponent(); + }); + + robot.checkHasFullscreenOverride(false); + }); + } + + @Test + @EnableFlags(Flags.FLAG_VDM_FORCE_APP_UNIVERSAL_RESIZABLE_API) + public void testNotInSizeCompatMode_displayIgnoreActivitySizeRestrictionsTrue() { + runTestScenario((robot) -> { + robot.applyOnActivity((a) -> { + a.createActivityWithComponent(); + a.setDisplayIgnoreActivitySizeRestrictions(true); + a.configureTopActivity(/* minAspect */ -1f, /* maxAspect */-1f, + SCREEN_ORIENTATION_LANDSCAPE, true); + a.rotateDisplayForTopActivity(ROTATION_90); + + a.checkTopActivityInSizeCompatMode(false); + }); + }); + } + + @Test + @EnableFlags(Flags.FLAG_VDM_FORCE_APP_UNIVERSAL_RESIZABLE_API) + public void testInSizeCompatMode_displayIgnoreActivitySizeRestrictionsFalse() { + runTestScenario((robot) -> { + robot.applyOnActivity((a) -> { + a.createActivityWithComponent(); + a.setIgnoreOrientationRequest(true); + a.setDisplayIgnoreActivitySizeRestrictions(false); + a.configureTopActivity(/* minAspect */ -1f, /* maxAspect */-1f, + SCREEN_ORIENTATION_LANDSCAPE, true); + a.rotateDisplayForTopActivity(ROTATION_90); + + a.checkTopActivityInSizeCompatMode(true); + }); + }); + } + /** * Runs a test scenario providing a Robot. */ @@ -366,6 +459,11 @@ public class AppCompatAspectRatioOverridesTest extends WindowTestsBase { } } + void checkHasFullscreenOverride(boolean expected) { + assertEquals(expected, + getTopActivityAppCompatAspectRatioOverrides().hasFullscreenOverride()); + } + private AppCompatAspectRatioOverrides getTopActivityAppCompatAspectRatioOverrides() { return activity().top().mAppCompatController.getAppCompatAspectRatioOverrides(); } diff --git a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java index 20dcdde63cc4..d6be9159694b 100644 --- a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java @@ -24,7 +24,9 @@ import static android.view.WindowManager.LayoutParams.FIRST_APPLICATION_WINDOW; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY; import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION; +import static android.view.WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION; import static android.window.BackNavigationInfo.typeToString; +import static android.window.SystemOverrideOnBackInvokedCallback.OVERRIDE_UNDEFINED; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; @@ -103,6 +105,13 @@ public class BackNavigationControllerTests extends WindowTestsBase { @Before public void setUp() throws Exception { + final TransitionController transitionController = mAtm.getTransitionController(); + final Transition fakeTransition = new Transition(TRANSIT_PREPARE_BACK_NAVIGATION, + 0 /* flag */, transitionController, transitionController.mSyncEngine); + spyOn(transitionController); + doReturn(fakeTransition).when(transitionController) + .createTransition(anyInt(), anyInt()); + final BackNavigationController original = new BackNavigationController(); original.setWindowManager(mWm); mBackNavigationController = Mockito.spy(original); @@ -111,6 +120,7 @@ public class BackNavigationControllerTests extends WindowTestsBase { LocalServices.addService(WindowManagerInternal.class, mWindowManagerInternal); mBackAnimationAdapter = mock(BackAnimationAdapter.class); doReturn(true).when(mBackAnimationAdapter).isAnimatable(anyInt()); + Mockito.doNothing().when(mBackNavigationController).startAnimation(); mNavigationMonitor = mock(BackNavigationController.NavigationMonitor.class); mRootHomeTask = initHomeActivity(); } @@ -446,7 +456,7 @@ public class BackNavigationControllerTests extends WindowTestsBase { new OnBackInvokedCallbackInfo( callback, OnBackInvokedDispatcher.PRIORITY_DEFAULT, - /* isAnimationCallback = */ false)); + /* isAnimationCallback = */ false, OVERRIDE_UNDEFINED)); BackNavigationInfo backNavigationInfo = startBackNavigation(); assertWithMessage("BackNavigationInfo").that(backNavigationInfo).isNotNull(); @@ -467,7 +477,7 @@ public class BackNavigationControllerTests extends WindowTestsBase { new OnBackInvokedCallbackInfo( callback, OnBackInvokedDispatcher.PRIORITY_DEFAULT, - /* isAnimationCallback = */ true)); + /* isAnimationCallback = */ true, OVERRIDE_UNDEFINED)); BackNavigationInfo backNavigationInfo = startBackNavigation(); assertWithMessage("BackNavigationInfo").that(backNavigationInfo).isNotNull(); @@ -608,7 +618,7 @@ public class BackNavigationControllerTests extends WindowTestsBase { new OnBackInvokedCallbackInfo( callback, OnBackInvokedDispatcher.PRIORITY_DEFAULT, - /* isAnimationCallback = */ false)); + /* isAnimationCallback = */ false, OVERRIDE_UNDEFINED)); BackNavigationInfo backNavigationInfo = startBackNavigation(); assertThat(backNavigationInfo).isNull(); @@ -722,7 +732,7 @@ public class BackNavigationControllerTests extends WindowTestsBase { new OnBackInvokedCallbackInfo( callback, OnBackInvokedDispatcher.PRIORITY_SYSTEM, - /* isAnimationCallback = */ false)); + /* isAnimationCallback = */ false, OVERRIDE_UNDEFINED)); return callback; } @@ -732,7 +742,7 @@ public class BackNavigationControllerTests extends WindowTestsBase { new OnBackInvokedCallbackInfo( callback, OnBackInvokedDispatcher.PRIORITY_DEFAULT, - /* isAnimationCallback = */ false)); + /* isAnimationCallback = */ false, OVERRIDE_UNDEFINED)); return callback; } diff --git a/services/tests/wmtests/src/com/android/server/wm/DeferredDisplayUpdaterDiffTest.java b/services/tests/wmtests/src/com/android/server/wm/DeferredDisplayUpdaterDiffTest.java index 44b69f18eb04..c9c31dfe5307 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DeferredDisplayUpdaterDiffTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/DeferredDisplayUpdaterDiffTest.java @@ -39,6 +39,7 @@ import android.view.DisplayAddress; import android.view.DisplayCutout; import android.view.DisplayInfo; import android.view.DisplayShape; +import android.view.FrameRateCategoryRate; import android.view.RoundedCorner; import android.view.RoundedCorners; import android.view.SurfaceControl.RefreshRateRange; @@ -235,6 +236,9 @@ public class DeferredDisplayUpdaterDiffTest { } else if (type.isArray() && type.getComponentType().equals(Display.Mode.class)) { field.set(first, new Display.Mode[]{new Display.Mode(100, 200, 300)}); field.set(second, new Display.Mode[]{new Display.Mode(10, 20, 30)}); + } else if (type.equals(FrameRateCategoryRate.class)) { + field.set(first, new FrameRateCategoryRate(16666667, 11111111)); + field.set(second, new FrameRateCategoryRate(11111111, 8333333)); } else { throw new IllegalArgumentException("Field " + field + " is not supported by this test, please add implementation of setting " diff --git a/services/tests/wmtests/src/com/android/server/wm/TransparentPolicyTest.java b/services/tests/wmtests/src/com/android/server/wm/TransparentPolicyTest.java index 5187f87cd6db..42752c326615 100644 --- a/services/tests/wmtests/src/com/android/server/wm/TransparentPolicyTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/TransparentPolicyTest.java @@ -217,6 +217,19 @@ public class TransparentPolicyTest extends WindowTestsBase { }); } + @Test + public void testNotApplyStrategyToTranslucentActivitiesOverNotLetterboxedActivities() { + runTestScenario((robot) -> { + robot.transparentActivity((ta) -> { + ta.activity().setTopActivityHasLetterboxedBounds(false); + ta.launchTransparentActivityInTask(); + + ta.checkTopActivityTransparentPolicyStartNotInvoked(); + ta.checkTopActivityTransparentPolicyStateIsRunning(/* running */ false); + }); + }); + } + @EnableFlags(com.android.window.flags.Flags.FLAG_RESPECT_NON_TOP_VISIBLE_FIXED_ORIENTATION) @Test public void testNotRunStrategyToTranslucentActivitiesIfRespectOrientation() { @@ -388,6 +401,7 @@ public class TransparentPolicyTest extends WindowTestsBase { mTransparentActivityRobot = new AppCompatTransparentActivityRobot(activity()); // We always create at least an opaque activity in a Task activity().createNewTaskWithBaseActivity(); + activity().setTopActivityHasLetterboxedBounds(true); } @Override diff --git a/telephony/common/com/android/internal/telephony/TelephonyPermissions.java b/telephony/common/com/android/internal/telephony/TelephonyPermissions.java index 4ccbc32c4b54..66d75f780fbe 100644 --- a/telephony/common/com/android/internal/telephony/TelephonyPermissions.java +++ b/telephony/common/com/android/internal/telephony/TelephonyPermissions.java @@ -907,29 +907,6 @@ public final class TelephonyPermissions { } /** - * Ensure the caller (or self, if not processing an IPC) has - * {@link android.Manifest.permission#READ_PRIVILEGED_PHONE_STATE} or - * {@link android.Manifest.permission#READ_PHONE_NUMBERS}. - * - * @throws SecurityException if the caller does not have the required permission/privileges - */ - @RequiresPermission(anyOf = { - android.Manifest.permission.READ_PHONE_NUMBERS, - android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE - }) - public static boolean checkCallingOrSelfReadPrivilegedPhoneStatePermissionOrReadPhoneNumber( - Context context, int subId, String callingPackage, @Nullable String callingFeatureId, - String message) { - if (!SubscriptionManager.isValidSubscriptionId(subId)) { - return false; - } - return (context.checkCallingOrSelfPermission( - Manifest.permission.READ_PRIVILEGED_PHONE_STATE) == PERMISSION_GRANTED - || checkCallingOrSelfReadPhoneNumber(context, subId, callingPackage, - callingFeatureId, message)); - } - - /** * @return true if the specified {@code uid} is for a system or phone process, no matter if runs * as system user or not. */ diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java index 6535b9b8e429..0808cc3f1a75 100644 --- a/telephony/java/android/telephony/CarrierConfigManager.java +++ b/telephony/java/android/telephony/CarrierConfigManager.java @@ -11415,6 +11415,7 @@ public class CarrierConfigManager { sDefaults.putIntArray(KEY_CELLULAR_SERVICE_CAPABILITIES_INT_ARRAY, new int[]{1, 2, 3}); sDefaults.putInt(KEY_WEAR_CONNECTIVITY_BT_TO_CELL_DELAY_MS_INT, -1); sDefaults.putInt(KEY_WEAR_CONNECTIVITY_EXTEND_BT_TO_CELL_DELAY_ON_WIFI_MS_INT, -1); + sDefaults.putInt(KEY_SATELLITE_SOS_MAX_DATAGRAM_SIZE, 255); } /** diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java index fad59f8bb37b..6f2c8623fd71 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -133,6 +133,7 @@ import com.android.internal.telephony.OperatorInfo; import com.android.internal.telephony.PhoneConstants; import com.android.internal.telephony.RILConstants; import com.android.internal.telephony.flags.Flags; +import com.android.internal.telephony.uicc.IccUtils; import com.android.internal.telephony.util.TelephonyUtils; import com.android.telephony.Rlog; @@ -6282,14 +6283,17 @@ public class TelephonyManager { * The contents of the file is a <b>Ip Multimedia Service Private User Identity</b> of the user * as defined in the section 4.2.2 of 3GPP TS 131 103. * - * @return IMPI (IMS private user identity) of type string. + * @return IMPI (IMS private user identity) of type string or null if the IMPI isn't present + * on the ISIM. * @throws IllegalStateException in case the ISIM has’t been loaded * @throws SecurityException if the caller does not have the required permission/privileges * @hide */ - @NonNull + @FlaggedApi(Flags.FLAG_SUPPORT_ISIM_RECORD) + @SystemApi @RequiresPermission(android.Manifest.permission.USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER) @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) + @Nullable public String getImsPrivateUserIdentity() { try { IPhoneSubInfo info = getSubscriberInfoService(); @@ -6370,6 +6374,9 @@ public class TelephonyManager { * The contents of the file are <b>Ip Multimedia Service Public User Identities</b> of the user * as defined in the section 4.2.4 of 3GPP TS 131 103. It contains one or more records. * + * Requires that the calling app has READ_PRIVILEGED_PHONE_STATE permission or carrier + * privileges. + * * @return List of public user identities of type android.net.Uri or empty list if * EF_IMPU is not available. * @throws IllegalStateException in case the ISIM hasn’t been loaded @@ -6378,18 +6385,18 @@ public class TelephonyManager { * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide */ - @NonNull - @RequiresPermission(anyOf = {android.Manifest.permission.READ_PHONE_NUMBERS, - android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE}) + @FlaggedApi(Flags.FLAG_SUPPORT_ISIM_RECORD) + @SystemApi + @RequiresPermission(value = Manifest.permission.READ_PRIVILEGED_PHONE_STATE, conditional = true) @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) + @NonNull public List<Uri> getImsPublicUserIdentities() { try { IPhoneSubInfo info = getSubscriberInfoService(); if (info == null) { throw new RuntimeException("IMPU error: Subscriber Info is null"); } - return info.getImsPublicUserIdentities(getSubId(), getOpPackageName(), - getAttributionTag()); + return info.getImsPublicUserIdentities(getSubId(), getOpPackageName()); } catch (IllegalArgumentException | NullPointerException ex) { Rlog.e(TAG, "getImsPublicUserIdentities Exception = " + ex); } catch (RemoteException ex) { @@ -8684,7 +8691,10 @@ public class TelephonyManager { * @return an array of PCSCF strings with one PCSCF per string, or null if * not present or not loaded * @hide + * @deprecated use {@link #getImsPcscfAddresses()} instead. */ + @Deprecated + @FlaggedApi(Flags.FLAG_SUPPORT_ISIM_RECORD) @UnsupportedAppUsage public String[] getIsimPcscf() { try { @@ -8701,6 +8711,40 @@ public class TelephonyManager { } } + /** + * Returns the IMS Proxy Call Session Control Function(P-CSCF) that were loaded from the ISIM. + * + * Requires that the calling app has READ_PRIVILEGED_PHONE_STATE permission or carrier + * privileges. + * + * @return List of P-CSCF address strings or empty list if not available. + * @throws IllegalStateException in case the ISIM hasn’t been loaded + * @throws SecurityException if the caller does not have the required permission/privilege + * @throws UnsupportedOperationException If the device does not have + * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. + * @hide + */ + @FlaggedApi(Flags.FLAG_SUPPORT_ISIM_RECORD) + @SystemApi + @RequiresPermission(value = Manifest.permission.READ_PRIVILEGED_PHONE_STATE, conditional = true) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) + @NonNull + public List<String> getImsPcscfAddresses() { + try { + IPhoneSubInfo info = getSubscriberInfoService(); + if (info == null) { + throw new RuntimeException("P-CSCF error: Subscriber Info is null"); + } + return info.getImsPcscfAddresses(getSubId(), getOpPackageName()); + } catch (IllegalArgumentException | NullPointerException ex) { + Rlog.e(TAG, "getImsPcscfAddresses Exception = " + ex); + } catch (RemoteException ex) { + Rlog.e(TAG, "getImsPcscfAddresses Exception = " + ex); + ex.rethrowAsRuntimeException(); + } + return Collections.EMPTY_LIST; + } + /** UICC application type is unknown or not specified */ public static final int APPTYPE_UNKNOWN = PhoneConstants.APPTYPE_UNKNOWN; /** UICC application type is SIM */ @@ -8934,8 +8978,10 @@ public class TelephonyManager { * @throws UnsupportedOperationException If the device does not have * {@link PackageManager#FEATURE_TELEPHONY_SUBSCRIPTION}. * @hide + * @deprecated Use {@link #getSimServiceTable(int, Executor, OutcomeReceiver)} instead. */ - + @Deprecated + @FlaggedApi(Flags.FLAG_SUPPORT_ISIM_RECORD) @Nullable @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) @@ -8963,6 +9009,55 @@ public class TelephonyManager { } /** + * Fetches the sim service table from the EFUST/EFIST based on the application type + * {@link #APPTYPE_USIM} or {@link #APPTYPE_ISIM}. + * The USIM service table EF is described in as per Section 4.2.8 of 3GPP TS 31.102. + * The ISIM service table EF is described in as per Section 4.2.7 of 3GPP TS 31.103. + * + * @param appType of type int of either {@link #APPTYPE_USIM} or {@link #APPTYPE_ISIM}. + * @param executor executor to run the callback on. + * @param callback callback object to which the result will be delivered. + * @hide + */ + @FlaggedApi(Flags.FLAG_SUPPORT_ISIM_RECORD) + @SystemApi + @RequiresPermission(android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE) + @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION) + public void getSimServiceTable(@UiccAppType int appType, + @NonNull @CallbackExecutor Executor executor, + @NonNull OutcomeReceiver<byte[], Exception> callback) { + Objects.requireNonNull(executor); + Objects.requireNonNull(callback); + + IPhoneSubInfo info = getSubscriberInfoService(); + if (info == null) { + executor.execute(() -> callback.onError( + new RuntimeException("getSimServiceTable: Subscriber Info is null"))); + return; + } + + try { + String serviceTable; + if (appType == APPTYPE_ISIM) { + serviceTable = info.getIsimIst(getSubId()); + } else if ((appType == APPTYPE_USIM)) { + serviceTable = info.getSimServiceTable(getSubId(), APPTYPE_USIM); + } else { + serviceTable = null; + } + + if (serviceTable == null) { + executor.execute(() -> callback.onResult(new byte[0])); + } else { + byte[] simServiceTable = IccUtils.hexStringToBytes(serviceTable); + executor.execute(() -> callback.onResult(simServiceTable)); + } + } catch (Exception ex) { + executor.execute(() -> callback.onError(ex)); + } + } + + /** * Resets the {@link android.telephony.ims.ImsService} associated with the specified sim slot. * Used by diagnostic apps to force the IMS stack to be disabled and re-enabled in an effort to * recover from scenarios where the {@link android.telephony.ims.ImsService} gets in to a bad diff --git a/telephony/java/android/telephony/ims/ImsCallSessionListener.java b/telephony/java/android/telephony/ims/ImsCallSessionListener.java index 5946535ce3af..00d4bd09b7c4 100644 --- a/telephony/java/android/telephony/ims/ImsCallSessionListener.java +++ b/telephony/java/android/telephony/ims/ImsCallSessionListener.java @@ -16,6 +16,7 @@ package android.telephony.ims; +import android.annotation.FlaggedApi; import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; @@ -31,6 +32,7 @@ import android.telephony.ims.stub.ImsCallSessionImplBase.MediaStreamType; import android.util.Log; import com.android.ims.internal.IImsCallSession; +import com.android.internal.telephony.flags.Flags; import java.util.ArrayList; import java.util.Objects; @@ -802,8 +804,8 @@ public class ImsCallSessionListener { /** * Notifies the result of transfer request. - * @hide */ + @FlaggedApi(Flags.FLAG_SUPPORT_IMS_MMTEL_INTERFACE) public void callSessionTransferred() { try { mListener.callSessionTransferred(); @@ -813,13 +815,13 @@ public class ImsCallSessionListener { } /** - * Notifies the result of transfer request. + * Notifies the result of the transfer request failure. * * @param reasonInfo {@link ImsReasonInfo} containing a reason for the * session transfer failure - * @hide */ - public void callSessionTransferFailed(ImsReasonInfo reasonInfo) { + @FlaggedApi(Flags.FLAG_SUPPORT_IMS_MMTEL_INTERFACE) + public void callSessionTransferFailed(@NonNull ImsReasonInfo reasonInfo) { try { mListener.callSessionTransferFailed(reasonInfo); } catch (RemoteException e) { @@ -839,8 +841,8 @@ public class ImsCallSessionListener { * @param bitsPerSecond This value is the bitrate requested by the other party UE through * RTP CMR, RTCPAPP or TMMBR, and ImsStack converts this value to the MAC bitrate * (defined in TS36.321, range: 0 ~ 8000 kbit/s). - * @hide */ + @FlaggedApi(Flags.FLAG_SUPPORT_IMS_MMTEL_INTERFACE) public final void callSessionSendAnbrQuery(@MediaStreamType int mediaType, @MediaStreamDirection int direction, @IntRange(from = 0) int bitsPerSecond) { Log.d(TAG, "callSessionSendAnbrQuery in imscallsessonListener"); diff --git a/telephony/java/android/telephony/ims/feature/ConnectionFailureInfo.java b/telephony/java/android/telephony/ims/feature/ConnectionFailureInfo.java index 88d9aae3e19d..81cddb79533c 100644 --- a/telephony/java/android/telephony/ims/feature/ConnectionFailureInfo.java +++ b/telephony/java/android/telephony/ims/feature/ConnectionFailureInfo.java @@ -16,12 +16,16 @@ package android.telephony.ims.feature; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; +import android.annotation.SystemApi; import android.os.Parcel; import android.os.Parcelable; import android.util.SparseArray; +import com.android.internal.telephony.flags.Flags; + import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -30,6 +34,8 @@ import java.lang.annotation.RetentionPolicy; * * @hide */ +@FlaggedApi(Flags.FLAG_SUPPORT_IMS_MMTEL_INTERFACE) +@SystemApi public final class ConnectionFailureInfo implements Parcelable { /** @hide */ @@ -67,7 +73,7 @@ public final class ConnectionFailureInfo implements Parcelable { public static final int REASON_RRC_TIMEOUT = 6; /** Device currently not in service */ public static final int REASON_NO_SERVICE = 7; - /** The PDN is no more active */ + /** The PDN is no longer active */ public static final int REASON_PDN_NOT_AVAILABLE = 8; /** Radio resource is busy with another subscription */ public static final int REASON_RF_BUSY = 9; @@ -135,6 +141,8 @@ public final class ConnectionFailureInfo implements Parcelable { /** * @return the cause code from the network or modem specific to the failure. + * See 3GPP TS 24.401 Annex A (Cause values for EPS mobility management) and + * 3GPP TS 24.501 Annex A (Cause values for 5GS mobility management). */ public int getCauseCode() { return mCauseCode; diff --git a/telephony/java/android/telephony/ims/feature/ImsTrafficSessionCallback.java b/telephony/java/android/telephony/ims/feature/ImsTrafficSessionCallback.java index 245ee1511d98..0029d492b811 100644 --- a/telephony/java/android/telephony/ims/feature/ImsTrafficSessionCallback.java +++ b/telephony/java/android/telephony/ims/feature/ImsTrafficSessionCallback.java @@ -16,20 +16,26 @@ package android.telephony.ims.feature; +import android.annotation.FlaggedApi; import android.annotation.NonNull; +import android.annotation.SystemApi; + +import com.android.internal.telephony.flags.Flags; /** * A callback class used to receive the result of {@link MmTelFeature#startImsTrafficSession}. * @hide */ +@FlaggedApi(Flags.FLAG_SUPPORT_IMS_MMTEL_INTERFACE) +@SystemApi public interface ImsTrafficSessionCallback { /** The modem is ready to process the IMS traffic. */ void onReady(); /** - * Notifies that any IMS traffic is not sent to network due to any failure - * on cellular networks. IMS service shall call {@link MmTelFeature#stopImsTrafficSession()} + * Notifies that any IMS traffic can not be sent to the network due to the provided cellular + * network failure. IMS service shall call {@link MmTelFeature#stopImsTrafficSession()} * when receiving this callback. * * @param info The information of the failure. diff --git a/telephony/java/android/telephony/ims/feature/MmTelFeature.java b/telephony/java/android/telephony/ims/feature/MmTelFeature.java index 3f02ae9e13df..c6b11d7b9c97 100644 --- a/telephony/java/android/telephony/ims/feature/MmTelFeature.java +++ b/telephony/java/android/telephony/ims/feature/MmTelFeature.java @@ -16,6 +16,8 @@ package android.telephony.ims.feature; +import static com.android.internal.telephony.flags.Flags.FLAG_SUPPORT_IMS_MMTEL_INTERFACE; + import android.annotation.CallbackExecutor; import android.annotation.FlaggedApi; import android.annotation.IntDef; @@ -964,6 +966,8 @@ public class MmTelFeature extends ImsFeature { * * @hide */ + @FlaggedApi(FLAG_SUPPORT_IMS_MMTEL_INTERFACE) + @SystemApi public static final int EPS_FALLBACK_REASON_NO_NETWORK_TRIGGER = 1; /** @@ -976,6 +980,8 @@ public class MmTelFeature extends ImsFeature { * * @hide */ + @FlaggedApi(FLAG_SUPPORT_IMS_MMTEL_INTERFACE) + @SystemApi public static final int EPS_FALLBACK_REASON_NO_NETWORK_RESPONSE = 2; /** @hide */ @@ -1003,36 +1009,50 @@ public class MmTelFeature extends ImsFeature { * Emergency call * @hide */ + @FlaggedApi(FLAG_SUPPORT_IMS_MMTEL_INTERFACE) + @SystemApi public static final int IMS_TRAFFIC_TYPE_EMERGENCY = 0; /** * Emergency SMS * @hide */ + @FlaggedApi(FLAG_SUPPORT_IMS_MMTEL_INTERFACE) + @SystemApi public static final int IMS_TRAFFIC_TYPE_EMERGENCY_SMS = 1; /** * Voice call * @hide */ + @FlaggedApi(FLAG_SUPPORT_IMS_MMTEL_INTERFACE) + @SystemApi public static final int IMS_TRAFFIC_TYPE_VOICE = 2; /** * Video call * @hide */ + @FlaggedApi(FLAG_SUPPORT_IMS_MMTEL_INTERFACE) + @SystemApi public static final int IMS_TRAFFIC_TYPE_VIDEO = 3; /** * SMS over IMS * @hide */ + @FlaggedApi(FLAG_SUPPORT_IMS_MMTEL_INTERFACE) + @SystemApi public static final int IMS_TRAFFIC_TYPE_SMS = 4; /** * IMS registration and subscription for reg event package (signaling) * @hide */ + @FlaggedApi(FLAG_SUPPORT_IMS_MMTEL_INTERFACE) + @SystemApi public static final int IMS_TRAFFIC_TYPE_REGISTRATION = 5; /** * Ut/XCAP (XML Configuration Access Protocol) * @hide */ + @FlaggedApi(FLAG_SUPPORT_IMS_MMTEL_INTERFACE) + @SystemApi public static final int IMS_TRAFFIC_TYPE_UT_XCAP = 6; /** @hide */ @@ -1046,11 +1066,15 @@ public class MmTelFeature extends ImsFeature { * Indicates that the traffic is an incoming traffic. * @hide */ + @FlaggedApi(FLAG_SUPPORT_IMS_MMTEL_INTERFACE) + @SystemApi public static final int IMS_TRAFFIC_DIRECTION_INCOMING = 0; /** * Indicates that the traffic is an outgoing traffic. * @hide */ + @FlaggedApi(FLAG_SUPPORT_IMS_MMTEL_INTERFACE) + @SystemApi public static final int IMS_TRAFFIC_DIRECTION_OUTGOING = 1; private IImsMmTelListener mListener; @@ -1291,9 +1315,11 @@ public class MmTelFeature extends ImsFeature { /** * Triggers the EPS fallback procedure. * - * @param reason specifies the reason that causes EPS fallback. + * @param reason specifies the reason that EPS fallback was triggered. * @hide */ + @FlaggedApi(FLAG_SUPPORT_IMS_MMTEL_INTERFACE) + @SystemApi public final void triggerEpsFallback(@EpsFallbackReason int reason) { IImsMmTelListener listener = getListener(); if (listener == null) { @@ -1344,6 +1370,8 @@ public class MmTelFeature extends ImsFeature { * * @hide */ + @FlaggedApi(FLAG_SUPPORT_IMS_MMTEL_INTERFACE) + @SystemApi public final void startImsTrafficSession(@ImsTrafficType int trafficType, @AccessNetworkConstants.RadioAccessNetworkType int accessNetworkType, @ImsTrafficDirection int trafficDirection, @@ -1387,6 +1415,8 @@ public class MmTelFeature extends ImsFeature { * * @hide */ + @FlaggedApi(FLAG_SUPPORT_IMS_MMTEL_INTERFACE) + @SystemApi public final void modifyImsTrafficSession( @AccessNetworkConstants.RadioAccessNetworkType int accessNetworkType, @NonNull ImsTrafficSessionCallback callback) { @@ -1417,6 +1447,8 @@ public class MmTelFeature extends ImsFeature { * * @hide */ + @FlaggedApi(FLAG_SUPPORT_IMS_MMTEL_INTERFACE) + @SystemApi public final void stopImsTrafficSession(@NonNull ImsTrafficSessionCallback callback) { IImsMmTelListener listener = getListener(); if (listener == null) { diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java index c78dbe788660..be02232abe1b 100644 --- a/telephony/java/android/telephony/satellite/SatelliteManager.java +++ b/telephony/java/android/telephony/satellite/SatelliteManager.java @@ -26,6 +26,7 @@ import android.annotation.Nullable; import android.annotation.RequiresFeature; import android.annotation.RequiresPermission; import android.annotation.SystemApi; +import android.annotation.SystemService; import android.content.Context; import android.content.pm.PackageManager; import android.os.Binder; @@ -39,6 +40,7 @@ import android.telephony.SubscriptionManager; import android.telephony.TelephonyCallback; import android.telephony.TelephonyFrameworkInitializer; import android.telephony.TelephonyManager; +import android.telephony.TelephonyRegistryManager; import com.android.internal.telephony.IIntegerConsumer; import com.android.internal.telephony.ITelephony; @@ -61,14 +63,19 @@ import java.util.function.Consumer; import java.util.stream.Collectors; /** - * Manages satellite operations such as provisioning, pointing, messaging, location sharing, etc. - * To get the object, call {@link Context#getSystemService(String)}. + * Manages satellite states such as monitoring enabled state and operations such as provisioning, + * pointing, messaging, location sharing, etc. * - * @hide + * <p>To get the object, call {@link Context#getSystemService(String)} with + * {@link Context#SATELLITE_SERVICE}. + * + * <p>SatelliteManager is intended for use on devices with feature + * {@link PackageManager#FEATURE_TELEPHONY_SATELLITE}. On devices without the feature, the behavior + * is not reliable. */ +@SystemService(Context.SATELLITE_SERVICE) +@FlaggedApi(Flags.FLAG_SATELLITE_STATE_CHANGE_LISTENER) @RequiresFeature(PackageManager.FEATURE_TELEPHONY_SATELLITE) -@SystemApi -@FlaggedApi(Flags.FLAG_OEM_ENABLED_SATELLITE_FLAG) public final class SatelliteManager { private static final String TAG = "SatelliteManager"; @@ -104,6 +111,8 @@ public final class SatelliteManager { */ @Nullable private final Context mContext; + private TelephonyRegistryManager mTelephonyRegistryMgr; + /** * Create an instance of the SatelliteManager. * @@ -737,6 +746,65 @@ public final class SatelliteManager { "android.telephony.METADATA_SATELLITE_MANUAL_CONNECT_P2P_SUPPORT"; /** + * Registers a {@link SatelliteStateChangeListener} to receive callbacks when the satellite + * state may have changed. + * + * <p>The callback method is immediately triggered with latest state on invoking this method if + * the state change has been notified before. + * + * @param executor The {@link Executor} where the {@code listener} will be invoked + * @param listener The listener to monitor the satellite state change + * + * @see SatelliteStateChangeListener + * @see TelephonyManager#hasCarrierPrivileges() + */ + @FlaggedApi(Flags.FLAG_SATELLITE_STATE_CHANGE_LISTENER) + @RequiresPermission(anyOf = {android.Manifest.permission.READ_BASIC_PHONE_STATE, + android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE, + android.Manifest.permission.READ_PHONE_STATE, + "carrier privileges"}) + public void registerStateChangeListener(@NonNull @CallbackExecutor Executor executor, + @NonNull SatelliteStateChangeListener listener) { + if (mContext == null) { + throw new IllegalStateException("Telephony service is null"); + } + + mTelephonyRegistryMgr = mContext.getSystemService(TelephonyRegistryManager.class); + if (mTelephonyRegistryMgr == null) { + throw new IllegalStateException("Telephony registry service is null"); + } + mTelephonyRegistryMgr.addSatelliteStateChangeListener(executor, listener); + } + + /** + * Unregisters the {@link SatelliteStateChangeListener} previously registered with + * {@link #registerStateChangeListener(Executor, SatelliteStateChangeListener)}. + * + * <p>It will be a no-op if the {@code listener} is not currently registered. + * + * @param listener The listener to unregister + * + * @see SatelliteStateChangeListener + * @see TelephonyManager#hasCarrierPrivileges() + */ + @FlaggedApi(Flags.FLAG_SATELLITE_STATE_CHANGE_LISTENER) + @RequiresPermission(anyOf = {android.Manifest.permission.READ_BASIC_PHONE_STATE, + android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE, + android.Manifest.permission.READ_PHONE_STATE, + "carrier privileges"}) + public void unregisterStateChangeListener(@NonNull SatelliteStateChangeListener listener) { + if (mContext == null) { + throw new IllegalStateException("Telephony service is null"); + } + + mTelephonyRegistryMgr = mContext.getSystemService(TelephonyRegistryManager.class); + if (mTelephonyRegistryMgr == null) { + throw new IllegalStateException("Telephony registry service is null"); + } + mTelephonyRegistryMgr.removeSatelliteStateChangeListener(listener); + } + + /** * Request to enable or disable the satellite modem and demo mode. * If satellite modem and cellular modem cannot work concurrently, * then this will disable the cellular modem if satellite modem is enabled, diff --git a/telephony/java/android/telephony/satellite/SatelliteStateChangeListener.java b/telephony/java/android/telephony/satellite/SatelliteStateChangeListener.java new file mode 100644 index 000000000000..3aa910dfcc80 --- /dev/null +++ b/telephony/java/android/telephony/satellite/SatelliteStateChangeListener.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.telephony.satellite; + +import android.annotation.FlaggedApi; + +import com.android.internal.telephony.flags.Flags; + +import java.util.concurrent.Executor; + +/** + * A listener interface to monitor satellite state change events. + * + * <p>Call + * {@link SatelliteManager#registerStateChangeListener(Executor, SatelliteStateChangeListener)} + * to monitor. Call + * {@link SatelliteManager#unregisterStateChangeListener(SatelliteStateChangeListener)} to cancel. + * + * @see SatelliteManager#registerStateChangeListener(Executor, SatelliteStateChangeListener) + * @see SatelliteManager#unregisterStateChangeListener(SatelliteStateChangeListener) + */ +@FlaggedApi(Flags.FLAG_SATELLITE_STATE_CHANGE_LISTENER) +public interface SatelliteStateChangeListener { + /** + * Called when satellite modem enabled state may have changed. + * + * <p>Note:there is no guarantee that this callback will only be invoked upon a change of state. + * In other word, in some cases, the callback may report with the same enabled states. It is the + * caller's responsibility to filter uninterested states. + * + * <p>Note:satellite enabled state is a device state that is NOT associated with subscription or + * SIM slot. + * + * @param isEnabled {@code true} means satellite modem is enabled. + */ + void onEnabledStateChanged(boolean isEnabled); +} diff --git a/telephony/java/com/android/internal/telephony/IPhoneSubInfo.aidl b/telephony/java/com/android/internal/telephony/IPhoneSubInfo.aidl index b4d93fddd7aa..974cc14ae444 100644 --- a/telephony/java/com/android/internal/telephony/IPhoneSubInfo.aidl +++ b/telephony/java/com/android/internal/telephony/IPhoneSubInfo.aidl @@ -208,10 +208,9 @@ interface IPhoneSubInfo { /** * Fetches the ISIM public user identities (EF_IMPU) from UICC based on subId */ - @JavaPassthrough(annotation="@android.annotation.RequiresPermission(" + - "anyOf={android.Manifest.permission.READ_PHONE_NUMBERS, android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE})") - List<Uri> getImsPublicUserIdentities(int subId, String callingPackage, - String callingFeatureId); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(" + + "android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE)") + List<Uri> getImsPublicUserIdentities(int subId, String callingPackage); /** * Returns the IMS Service Table (IST) that was loaded from the ISIM. @@ -227,6 +226,20 @@ interface IPhoneSubInfo { String[] getIsimPcscf(int subId); /** + * Fetches IMS Proxy Call Session Control Function(P-CSCF) based on the subscription. + * + * @param subId subscriptionId + * @param callingPackage package name of the caller + * @return List of IMS Proxy Call Session Control Function strings. + * @throws IllegalArgumentException if the subscriptionId is not valid + * @throws IllegalStateException in case the ISIM hasn’t been loaded. + * @throws SecurityException if the caller does not have the required permission + */ + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(" + + "android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE)") + List<String> getImsPcscfAddresses(int subId, String callingPackage); + + /** * Returns the response of the SIM application on the UICC to authentication * challenge/response algorithm. The data string and challenge response are * Base64 encoded Strings. diff --git a/test-mock/src/android/test/mock/MockContext.java b/test-mock/src/android/test/mock/MockContext.java index 6bf7ff501217..3247d1fe5222 100644 --- a/test-mock/src/android/test/mock/MockContext.java +++ b/test-mock/src/android/test/mock/MockContext.java @@ -53,6 +53,7 @@ import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.util.List; import java.util.concurrent.Executor; /** @@ -607,6 +608,12 @@ public class MockContext extends Context { throw new UnsupportedOperationException(); } + /** @hide */ + @Override + public List<IntentFilter> getRegisteredIntentFilters(BroadcastReceiver receiver) { + throw new UnsupportedOperationException(); + } + @Override public ComponentName startService(Intent service) { throw new UnsupportedOperationException(); diff --git a/tests/AppJankTest/src/android/app/jank/tests/JankDataProcessorTest.java b/tests/AppJankTest/src/android/app/jank/tests/JankDataProcessorTest.java new file mode 100644 index 000000000000..2cd625eec032 --- /dev/null +++ b/tests/AppJankTest/src/android/app/jank/tests/JankDataProcessorTest.java @@ -0,0 +1,279 @@ +/* + * 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.app.jank.tests; + +import static org.junit.Assert.assertEquals; + +import android.app.jank.Flags; +import android.app.jank.JankDataProcessor; +import android.app.jank.StateTracker; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.view.Choreographer; +import android.view.SurfaceControl; + +import androidx.test.annotation.UiThreadTest; +import androidx.test.core.app.ActivityScenario; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.List; + +@RunWith(AndroidJUnit4.class) +public class JankDataProcessorTest { + + private Choreographer mChoreographer; + private StateTracker mStateTracker; + private JankDataProcessor mJankDataProcessor; + private static final int NANOS_PER_MS = 1_000_000; + private static String sActivityName; + private static ActivityScenario<EmptyActivity> sEmptyActivityActivityScenario; + private static final int APP_ID = 25; + + @BeforeClass + public static void classSetup() { + sEmptyActivityActivityScenario = ActivityScenario.launch(EmptyActivity.class); + sActivityName = sEmptyActivityActivityScenario.toString(); + } + + @AfterClass + public static void classTearDown() { + sEmptyActivityActivityScenario.close(); + } + + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + + @Before + @UiThreadTest + public void setup() { + mChoreographer = Choreographer.getInstance(); + mStateTracker = new StateTracker(mChoreographer); + mJankDataProcessor = new JankDataProcessor(mStateTracker); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) + public void processJankData_multipleFramesAndStates_attributesTotalFramesCorrectly() { + List<SurfaceControl.JankData> jankData = getMockJankData_vsyncId_inRange(); + mStateTracker.addPendingStateData(getMockStateData_vsyncId_inRange()); + + mJankDataProcessor.processJankData(jankData, sActivityName, APP_ID); + + long totalFramesAttributed = getTotalFramesCounted(); + + // Each state is active for each frame that is passed in, there are two states being tested + // which is why jankData.size is multiplied by 2. + assertEquals(jankData.size() * 2, totalFramesAttributed); + } + + /** + * Each JankData frame has an associated vsyncid, only frames that have vsyncids between the + * StatData start and end vsyncids should be counted. This test confirms that if JankData + * does not share any frames with the states then no jank stats are added. + */ + @Test + @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) + public void processJankData_outOfRangeVsyncId_skipOutOfRangeVsyncIds() { + List<SurfaceControl.JankData> jankData = getMockJankData_vsyncId_inRange(); + mStateTracker.addPendingStateData(getMockStateData_vsyncId_outOfRange()); + + mJankDataProcessor.processJankData(jankData, sActivityName, APP_ID); + + assertEquals(0, mJankDataProcessor.getPendingJankStats().size()); + } + + /** + * It's expected to see many duplicate widget states, if a user is scrolling then + * pauses and resumes scrolling again, we may get three widget states two of which are the same. + * State 1: {Scroll,WidgetId,Scrolling} State 2: {Scroll,WidgetId,None} + * State 3: {Scroll,WidgetId,Scrolling} + * These duplicate states should coalesce into only one Jank stat. This test confirms that + * behavior. + */ + @Test + @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) + public void processJankData_duplicateStates_confirmDuplicatesCoalesce() { + // getMockStateData will return 10 states 5 of which are set to none and 5 of which are + // scrolling. + mStateTracker.addPendingStateData(getMockStateData_vsyncId_inRange()); + + mJankDataProcessor.processJankData(getMockJankData_vsyncId_inRange(), sActivityName, + APP_ID); + + // Confirm the duplicate states are coalesced down to 2 stats 1 for the scrolling state + // another for the none state. + assertEquals(2, mJankDataProcessor.getPendingJankStats().size()); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) + public void processJankData_inRangeVsyncIds_confirmOnlyInRangeFramesCounted() { + List<SurfaceControl.JankData> jankData = getMockJankData_vsyncId_inRange(); + int inRangeFrameCount = jankData.size(); + + mStateTracker.addPendingStateData(getMockStateData_vsyncId_inRange()); + mJankDataProcessor.processJankData(jankData, sActivityName, APP_ID); + + // Two states are active for each frame which is why inRangeFrameCount is multiplied by 2. + assertEquals(inRangeFrameCount * 2, getTotalFramesCounted()); + } + + @Test + @RequiresFlagsEnabled(Flags.FLAG_DETAILED_APP_JANK_METRICS_API) + public void processJankData_inRangeVsyncIds_confirmHistogramCountMatchesFrameCount() { + List<SurfaceControl.JankData> jankData = getMockJankData_vsyncId_inRange(); + mStateTracker.addPendingStateData(getMockStateData_vsyncId_inRange()); + mJankDataProcessor.processJankData(jankData, sActivityName, APP_ID); + + long totalFrames = getTotalFramesCounted(); + long histogramFrames = getHistogramFrameCount(); + + assertEquals(totalFrames, histogramFrames); + } + + // TODO b/375005277 add tests that cover logging and releasing resources back to pool. + + private long getTotalFramesCounted() { + return mJankDataProcessor.getPendingJankStats().values() + .stream().mapToLong(stat -> stat.getTotalFrames()).sum(); + } + + private long getHistogramFrameCount() { + long totalHistogramFrames = 0; + + for (JankDataProcessor.PendingJankStat stats : + mJankDataProcessor.getPendingJankStats().values()) { + int[] overrunHistogram = stats.getFrameOverrunBuckets(); + + for (int i = 0; i < overrunHistogram.length; i++) { + totalHistogramFrames += overrunHistogram[i]; + } + } + + return totalHistogramFrames; + } + + /** + * Out of range data will have a mVsyncIdStart and mVsyncIdEnd values set to below 25. + */ + private List<StateTracker.StateData> getMockStateData_vsyncId_outOfRange() { + ArrayList<StateTracker.StateData> stateData = new ArrayList<StateTracker.StateData>(); + StateTracker.StateData newStateData = new StateTracker.StateData(); + newStateData.mVsyncIdEnd = 20; + newStateData.mStateDataKey = "Test1_OutBand"; + newStateData.mVsyncIdStart = 1; + newStateData.mWidgetState = "scrolling"; + newStateData.mWidgetId = "widgetId"; + newStateData.mWidgetCategory = "Scroll"; + stateData.add(newStateData); + + newStateData = new StateTracker.StateData(); + newStateData.mVsyncIdEnd = 24; + newStateData.mStateDataKey = "Test1_InBand"; + newStateData.mVsyncIdStart = 20; + newStateData.mWidgetState = "Idle"; + newStateData.mWidgetId = "widgetId"; + newStateData.mWidgetCategory = "Scroll"; + stateData.add(newStateData); + + newStateData = new StateTracker.StateData(); + newStateData.mVsyncIdEnd = 20; + newStateData.mStateDataKey = "Test1_OutBand"; + newStateData.mVsyncIdStart = 12; + newStateData.mWidgetState = "Idle"; + newStateData.mWidgetId = "widgetId"; + newStateData.mWidgetCategory = "Scroll"; + stateData.add(newStateData); + + return stateData; + } + + /** + * This method returns two unique states, one state is set to scrolling the other is set + * to none. Both states will have the same startvsyncid to ensure each state is counted the same + * number of times. This keeps logic in asserts easier to reason about. Both states will have + * a startVsyncId between 25 and 35. + */ + private List<StateTracker.StateData> getMockStateData_vsyncId_inRange() { + ArrayList<StateTracker.StateData> stateData = new ArrayList<StateTracker.StateData>(); + + for (int i = 0; i < 10; i++) { + StateTracker.StateData newStateData = new StateTracker.StateData(); + newStateData.mVsyncIdEnd = Long.MAX_VALUE; + newStateData.mStateDataKey = "Test1_" + (i % 2 == 0 ? "scrolling" : "none"); + // Divide i by two to ensure both the scrolling and none states get the same vsyncid + // This makes asserts in tests easier to reason about as each state should be counted + // the same number of times. + newStateData.mVsyncIdStart = 25 + (i / 2); + newStateData.mWidgetState = i % 2 == 0 ? "scrolling" : "none"; + newStateData.mWidgetId = "widgetId"; + newStateData.mWidgetCategory = "Scroll"; + + stateData.add(newStateData); + } + + return stateData; + } + + /** + * In range data will have a frameVsyncId value between 25 and 35. + */ + private List<SurfaceControl.JankData> getMockJankData_vsyncId_inRange() { + ArrayList<SurfaceControl.JankData> mockData = new ArrayList<>(); + + for (int i = 0; i < 10; i++) { + mockData.add(new SurfaceControl.JankData( + /*frameVsyncId*/25 + i, + SurfaceControl.JankData.JANK_NONE, + NANOS_PER_MS * ((long) i), + NANOS_PER_MS * ((long) i), + NANOS_PER_MS * ((long) i))); + + } + + return mockData; + } + + /** + * Out of range data will have frameVsyncId values below 25. + */ + private List<SurfaceControl.JankData> getMockJankData_vsyncId_outOfRange() { + ArrayList<SurfaceControl.JankData> mockData = new ArrayList<>(); + + for (int i = 0; i < 10; i++) { + mockData.add(new SurfaceControl.JankData( + /*frameVsyncId*/i, + SurfaceControl.JankData.JANK_NONE, + NANOS_PER_MS * ((long) i), + NANOS_PER_MS * ((long) i), + NANOS_PER_MS * ((long) i))); + + } + + return mockData; + } + +} diff --git a/tests/Input/src/com/android/server/input/InputGestureManagerTests.kt b/tests/Input/src/com/android/server/input/InputGestureManagerTests.kt new file mode 100644 index 000000000000..01c56b7148cd --- /dev/null +++ b/tests/Input/src/com/android/server/input/InputGestureManagerTests.kt @@ -0,0 +1,155 @@ +/* + * Copyright 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.input + +import android.hardware.input.InputGestureData +import android.hardware.input.InputManager +import android.hardware.input.KeyGestureEvent +import android.platform.test.annotations.Presubmit +import android.view.KeyEvent +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test + +/** + * Tests for custom keyboard glyph map configuration. + * + * Build/Install/Run: + * atest InputTests:CustomInputGestureManagerTests + */ +@Presubmit +class InputGestureManagerTests { + + companion object { + const val USER_ID = 1 + } + + private lateinit var inputGestureManager: InputGestureManager + + @Before + fun setup() { + inputGestureManager = InputGestureManager() + } + + @Test + fun addRemoveCustomGesture() { + val customGesture = InputGestureData.Builder() + .setTrigger( + InputGestureData.createKeyTrigger( + KeyEvent.KEYCODE_H, + KeyEvent.META_META_ON + ) + ) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_HOME) + .build() + val result = inputGestureManager.addCustomInputGesture(USER_ID, customGesture) + assertEquals(InputManager.CUSTOM_INPUT_GESTURE_RESULT_SUCCESS, result) + assertEquals( + listOf(customGesture), + inputGestureManager.getCustomInputGestures(USER_ID) + ) + + inputGestureManager.removeCustomInputGesture(USER_ID, customGesture) + assertEquals( + listOf<InputGestureData>(), + inputGestureManager.getCustomInputGestures(USER_ID) + ) + } + + @Test + fun removeNonExistentGesture() { + val customGesture = InputGestureData.Builder() + .setTrigger( + InputGestureData.createKeyTrigger( + KeyEvent.KEYCODE_H, + KeyEvent.META_META_ON + ) + ) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_HOME) + .build() + val result = inputGestureManager.removeCustomInputGesture(USER_ID, customGesture) + assertEquals(InputManager.CUSTOM_INPUT_GESTURE_RESULT_ERROR_DOES_NOT_EXIST, result) + assertEquals( + listOf<InputGestureData>(), + inputGestureManager.getCustomInputGestures(USER_ID) + ) + } + + @Test + fun addAlreadyExistentGesture() { + val customGesture = InputGestureData.Builder() + .setTrigger( + InputGestureData.createKeyTrigger( + KeyEvent.KEYCODE_H, + KeyEvent.META_META_ON + ) + ) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_HOME) + .build() + inputGestureManager.addCustomInputGesture(USER_ID, customGesture) + val customGesture2 = InputGestureData.Builder() + .setTrigger( + InputGestureData.createKeyTrigger( + KeyEvent.KEYCODE_H, + KeyEvent.META_META_ON + ) + ) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_BACK) + .build() + val result = inputGestureManager.addCustomInputGesture(USER_ID, customGesture2) + assertEquals(InputManager.CUSTOM_INPUT_GESTURE_RESULT_ERROR_ALREADY_EXISTS, result) + assertEquals( + listOf(customGesture), + inputGestureManager.getCustomInputGestures(USER_ID) + ) + } + + @Test + fun addRemoveAllExistentGestures() { + val customGesture = InputGestureData.Builder() + .setTrigger( + InputGestureData.createKeyTrigger( + KeyEvent.KEYCODE_H, + KeyEvent.META_META_ON + ) + ) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_HOME) + .build() + inputGestureManager.addCustomInputGesture(USER_ID, customGesture) + val customGesture2 = InputGestureData.Builder() + .setTrigger( + InputGestureData.createKeyTrigger( + KeyEvent.KEYCODE_DEL, + KeyEvent.META_META_ON + ) + ) + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_BACK) + .build() + inputGestureManager.addCustomInputGesture(USER_ID, customGesture2) + + assertEquals( + listOf(customGesture, customGesture2), + inputGestureManager.getCustomInputGestures(USER_ID) + ) + + inputGestureManager.removeAllCustomInputGestures(USER_ID) + assertEquals( + listOf<InputGestureData>(), + inputGestureManager.getCustomInputGestures(USER_ID) + ) + } +}
\ No newline at end of file diff --git a/tests/Input/src/com/android/server/input/InputShellCommandTest.java b/tests/Input/src/com/android/server/input/InputShellCommandTest.java index 11f46335f017..a236244546cb 100644 --- a/tests/Input/src/com/android/server/input/InputShellCommandTest.java +++ b/tests/Input/src/com/android/server/input/InputShellCommandTest.java @@ -133,6 +133,21 @@ public class InputShellCommandTest { assertThat(mInputEventInjector.mInjectedEvents).isEmpty(); } + @Test + public void testSwipeCommandEventFrequency() { + int[] durations = {100, 300, 500}; + for (int durationMillis: durations) { + mInputEventInjector.mInjectedEvents.clear(); + runCommand(String.format("swipe 200 800 200 200 %d", durationMillis)); + + // Add 2 events for ACTION_DOWN and ACTION_UP. + final int maxEventNum = + (int) Math.ceil(InputShellCommand.SWIPE_EVENT_HZ_DEFAULT + * (float) durationMillis / 1000) + 2; + assertThat(mInputEventInjector.mInjectedEvents.size()).isAtMost(maxEventNum); + } + } + private InputEvent getSingleInjectedInputEvent() { assertThat(mInputEventInjector.mInjectedEvents).hasSize(1); return mInputEventInjector.mInjectedEvents.get(0); diff --git a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt index 19dea0c5e74a..0b147d63ed08 100644 --- a/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt +++ b/tests/Input/src/com/android/server/input/KeyGestureControllerTests.kt @@ -22,8 +22,10 @@ import android.content.pm.PackageManager import android.content.res.Resources import android.hardware.input.IInputManager import android.hardware.input.AidlKeyGestureEvent +import android.hardware.input.AppLaunchData import android.hardware.input.IKeyGestureEventListener import android.hardware.input.IKeyGestureHandler +import android.hardware.input.InputGestureData import android.hardware.input.InputManager import android.hardware.input.InputManagerGlobal import android.hardware.input.KeyGestureEvent @@ -232,7 +234,7 @@ class KeyGestureControllerTests { keyGestureController.handleKeyGesture(/* deviceId = */ 0, intArrayOf(KeyEvent.KEYCODE_HOME), /* modifierState = */ 0, KeyGestureEvent.KEY_GESTURE_TYPE_HOME, KeyGestureEvent.ACTION_GESTURE_COMPLETE, /* displayId */ 0, - /* focusedToken = */ null, /* flags = */ 0 + /* focusedToken = */ null, /* flags = */ 0, /* appLaunchData = */null ) assertEquals( @@ -259,6 +261,7 @@ class KeyGestureControllerTests { val expectedKeys: IntArray, val expectedModifierState: Int, val expectedActions: IntArray, + val expectedAppLaunchData: AppLaunchData? = null, ) { override fun toString(): String = name } @@ -884,6 +887,86 @@ class KeyGestureControllerTests { } @Test + @EnableFlags(com.android.window.flags.Flags.FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS) + fun testSnapLeftFreeformTask() { + val keyGestureController = KeyGestureController(context, testLooper.looper) + testKeyGestureInternal( + keyGestureController, + TestData( + "ALT + [ -> Resizes a task to fit the left half of the screen", + intArrayOf( + KeyEvent.KEYCODE_ALT_LEFT, + KeyEvent.KEYCODE_LEFT_BRACKET + ), + KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_LEFT_FREEFORM_WINDOW, + intArrayOf(KeyEvent.KEYCODE_LEFT_BRACKET), + KeyEvent.META_ALT_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ) + ) + } + + @Test + @EnableFlags(com.android.window.flags.Flags.FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS) + fun testSnapRightFreeformTask() { + val keyGestureController = KeyGestureController(context, testLooper.looper) + testKeyGestureInternal( + keyGestureController, + TestData( + "ALT + ] -> Resizes a task to fit the right half of the screen", + intArrayOf( + KeyEvent.KEYCODE_ALT_LEFT, + KeyEvent.KEYCODE_RIGHT_BRACKET + ), + KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW, + intArrayOf(KeyEvent.KEYCODE_RIGHT_BRACKET), + KeyEvent.META_ALT_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ) + ) + } + + @Test + @EnableFlags(com.android.window.flags.Flags.FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS) + fun testMaximizeFreeformTask() { + val keyGestureController = KeyGestureController(context, testLooper.looper) + testKeyGestureInternal( + keyGestureController, + TestData( + "ALT + '=' -> Maximizes a task to fit the screen", + intArrayOf( + KeyEvent.KEYCODE_ALT_LEFT, + KeyEvent.KEYCODE_EQUALS + ), + KeyGestureEvent.KEY_GESTURE_TYPE_MAXIMIZE_FREEFORM_WINDOW, + intArrayOf(KeyEvent.KEYCODE_EQUALS), + KeyEvent.META_ALT_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ) + ) + } + + @Test + @EnableFlags(com.android.window.flags.Flags.FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS) + fun testRestoreFreeformTask() { + val keyGestureController = KeyGestureController(context, testLooper.looper) + testKeyGestureInternal( + keyGestureController, + TestData( + "ALT + '-' -> Restores a task size to its previous bounds", + intArrayOf( + KeyEvent.KEYCODE_ALT_LEFT, + KeyEvent.KEYCODE_MINUS + ), + KeyGestureEvent.KEY_GESTURE_TYPE_RESTORE_FREEFORM_WINDOW_SIZE, + intArrayOf(KeyEvent.KEYCODE_MINUS), + KeyEvent.META_ALT_ON, + intArrayOf(KeyGestureEvent.ACTION_GESTURE_COMPLETE) + ) + ) + } + + @Test fun testCapsLockPressNotified() { val keyGestureController = KeyGestureController(context, testLooper.looper) val listener = KeyGestureEventListener() @@ -975,6 +1058,62 @@ class KeyGestureControllerTests { testKeyGestureInternal(keyGestureController, test) } + @Keep + private fun customInputGesturesTestArguments(): Array<TestData> { + return arrayOf( + TestData( + "META + ALT + Q -> Go Home", + intArrayOf( + KeyEvent.KEYCODE_META_LEFT, + KeyEvent.KEYCODE_ALT_LEFT, + KeyEvent.KEYCODE_Q + ), + KeyGestureEvent.KEY_GESTURE_TYPE_HOME, + intArrayOf(KeyEvent.KEYCODE_Q), + KeyEvent.META_META_ON or KeyEvent.META_ALT_ON, + intArrayOf( + KeyGestureEvent.ACTION_GESTURE_COMPLETE + ) + ), + TestData( + "META + ALT + Q -> Launch app", + intArrayOf( + KeyEvent.KEYCODE_CTRL_LEFT, + KeyEvent.KEYCODE_SHIFT_LEFT, + KeyEvent.KEYCODE_Q + ), + KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION, + intArrayOf(KeyEvent.KEYCODE_Q), + KeyEvent.META_CTRL_ON or KeyEvent.META_SHIFT_ON, + intArrayOf( + KeyGestureEvent.ACTION_GESTURE_COMPLETE + ), + AppLaunchData.createLaunchDataForComponent("com.test", "com.test.BookmarkTest") + ), + ) + } + + @Test + @Parameters(method = "customInputGesturesTestArguments") + fun testCustomKeyGestures(test: TestData) { + val keyGestureController = KeyGestureController(context, testLooper.looper) + val builder = InputGestureData.Builder() + .setKeyGestureType(test.expectedKeyGestureType) + .setTrigger( + InputGestureData.createKeyTrigger( + test.expectedKeys[0], + test.expectedModifierState + ) + ); + if (test.expectedAppLaunchData != null) { + builder.setAppLaunchData(test.expectedAppLaunchData) + } + val inputGestureData = builder.build(); + + keyGestureController.addCustomInputGesture(0, inputGestureData.aidlData) + testKeyGestureInternal(keyGestureController, test) + } + private fun testKeyGestureInternal(keyGestureController: KeyGestureController, test: TestData) { var handleEvents = mutableListOf<KeyGestureEvent>() val handler = KeyGestureHandler { event, _ -> @@ -1013,6 +1152,11 @@ class KeyGestureControllerTests { test.expectedActions[i], event.action ) + assertEquals( + "Test: $test doesn't produce correct app launch data", + test.expectedAppLaunchData, + event.appLaunchData + ) } keyGestureController.unregisterKeyGestureHandler(handler, 0) diff --git a/tests/graphics/HwAccelerationTest/AndroidManifest.xml b/tests/graphics/HwAccelerationTest/AndroidManifest.xml index db3a992b9c7b..05b2f4c53b15 100644 --- a/tests/graphics/HwAccelerationTest/AndroidManifest.xml +++ b/tests/graphics/HwAccelerationTest/AndroidManifest.xml @@ -24,7 +24,7 @@ <uses-feature android:name="android.hardware.camera"/> <uses-feature android:name="android.hardware.camera.autofocus"/> - <uses-sdk android:minSdkVersion="21"/> + <uses-sdk android:minSdkVersion="21" /> <application android:label="HwUi" android:theme="@android:style/Theme.Material.Light"> @@ -409,6 +409,24 @@ </intent-filter> </activity> + <activity android:name="ScrollingZAboveSurfaceView" + android:label="SurfaceView/Z-Above scrolling" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + <category android:name="com.android.test.hwui.TEST"/> + </intent-filter> + </activity> + + <activity android:name="ScrollingZAboveScaledSurfaceView" + android:label="SurfaceView/Z-Above scrolling, scaled surface" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + <category android:name="com.android.test.hwui.TEST"/> + </intent-filter> + </activity> + <activity android:name="StretchySurfaceViewActivity" android:label="SurfaceView/Stretchy Movement" android:exported="true"> diff --git a/tests/graphics/HwAccelerationTest/res/layout/scrolling_zabove_surfaceview.xml b/tests/graphics/HwAccelerationTest/res/layout/scrolling_zabove_surfaceview.xml new file mode 100644 index 000000000000..31e5774dd1ad --- /dev/null +++ b/tests/graphics/HwAccelerationTest/res/layout/scrolling_zabove_surfaceview.xml @@ -0,0 +1,131 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2024 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + tools:context=".MainActivity"> + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="Above the ScrollView" + android:textColor="#FFFFFFFF" + android:background="#FF444444" + android:padding="32dp" /> + + <ScrollView + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="Scrolling Header" + android:background="#FFCCCCCC" + android:padding="32dp" /> + + <SurfaceView + android:layout_width="match_parent" + android:layout_height="500dp" + android:id="@+id/surfaceview" /> + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="Scrolling Item" + android:background="#FFCCCCCC" + android:padding="32dp" /> + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="Scrolling Item" + android:background="#FFCCCCCC" + android:padding="32dp" /> + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="Scrolling Item" + android:background="#FFCCCCCC" + android:padding="32dp" /> + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="Scrolling Item" + android:background="#FFCCCCCC" + android:padding="32dp" /> + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="Scrolling Item" + android:background="#FFCCCCCC" + android:padding="32dp" /> + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="Scrolling Item" + android:background="#FFCCCCCC" + android:padding="32dp" /> + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="Scrolling Item" + android:background="#FFCCCCCC" + android:padding="32dp" /> + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="Scrolling Item" + android:background="#FFCCCCCC" + android:padding="32dp" /> + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="Scrolling Item" + android:background="#FFCCCCCC" + android:padding="32dp" /> + + </LinearLayout> + + </ScrollView> + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:text="Below the ScrollView" + android:textColor="#FFFFFFFF" + android:background="#FF444444" + android:padding="32dp" /> + +</LinearLayout>
\ No newline at end of file diff --git a/tests/graphics/HwAccelerationTest/src/com/android/test/hwui/ScrollingZAboveScaledSurfaceView.kt b/tests/graphics/HwAccelerationTest/src/com/android/test/hwui/ScrollingZAboveScaledSurfaceView.kt new file mode 100644 index 000000000000..59ae885664db --- /dev/null +++ b/tests/graphics/HwAccelerationTest/src/com/android/test/hwui/ScrollingZAboveScaledSurfaceView.kt @@ -0,0 +1,58 @@ +/* + * 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.test.hwui + +import android.app.Activity +import android.graphics.Color +import android.graphics.Paint +import android.os.Bundle +import android.view.SurfaceHolder +import android.view.SurfaceView + +class ScrollingZAboveScaledSurfaceView : Activity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.scrolling_zabove_surfaceview) + + findViewById<SurfaceView>(R.id.surfaceview).apply { + setZOrderOnTop(true) + holder.setFixedSize(1000, 2000) + holder.addCallback(object : SurfaceHolder.Callback { + override fun surfaceCreated(p0: SurfaceHolder) { + + } + + override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { + holder.unlockCanvasAndPost(holder.lockCanvas().apply { + drawColor(Color.BLUE) + val paint = Paint() + paint.textSize = 16 * resources.displayMetrics.density + paint.textAlign = Paint.Align.CENTER + paint.color = Color.WHITE + drawText("I'm a setZOrderOnTop(true) SurfaceView!", + (width / 2).toFloat(), (height / 2).toFloat(), paint) + }) + } + + override fun surfaceDestroyed(p0: SurfaceHolder) { + + } + + }) + } + } +}
\ No newline at end of file diff --git a/tests/graphics/HwAccelerationTest/src/com/android/test/hwui/ScrollingZAboveSurfaceView.kt b/tests/graphics/HwAccelerationTest/src/com/android/test/hwui/ScrollingZAboveSurfaceView.kt new file mode 100644 index 000000000000..ccb71ec0ff2a --- /dev/null +++ b/tests/graphics/HwAccelerationTest/src/com/android/test/hwui/ScrollingZAboveSurfaceView.kt @@ -0,0 +1,57 @@ +/* + * 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.test.hwui + +import android.app.Activity +import android.graphics.Color +import android.graphics.Paint +import android.os.Bundle +import android.view.SurfaceHolder +import android.view.SurfaceView + +class ScrollingZAboveSurfaceView : Activity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.scrolling_zabove_surfaceview) + + findViewById<SurfaceView>(R.id.surfaceview).apply { + setZOrderOnTop(true) + holder.addCallback(object : SurfaceHolder.Callback { + override fun surfaceCreated(p0: SurfaceHolder) { + + } + + override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { + holder.unlockCanvasAndPost(holder.lockCanvas().apply { + drawColor(Color.BLUE) + val paint = Paint() + paint.textSize = 16 * resources.displayMetrics.density + paint.textAlign = Paint.Align.CENTER + paint.color = Color.WHITE + drawText("I'm a setZOrderOnTop(true) SurfaceView!", + (width / 2).toFloat(), (height / 2).toFloat(), paint) + }) + } + + override fun surfaceDestroyed(p0: SurfaceHolder) { + + } + + }) + } + } +}
\ No newline at end of file diff --git a/tests/utils/testutils/java/android/os/test/TestLooper.java b/tests/utils/testutils/java/android/os/test/TestLooper.java index a826646f69f3..e6eabd804294 100644 --- a/tests/utils/testutils/java/android/os/test/TestLooper.java +++ b/tests/utils/testutils/java/android/os/test/TestLooper.java @@ -90,17 +90,39 @@ public class TestLooper { * and call {@link #dispatchAll()}. */ public TestLooper(Clock clock) { + Field messageQueueUseConcurrentField = null; + boolean previousUseConcurrentValue = false; + try { + messageQueueUseConcurrentField = MessageQueue.class.getDeclaredField("sUseConcurrent"); + messageQueueUseConcurrentField.setAccessible(true); + previousUseConcurrentValue = messageQueueUseConcurrentField.getBoolean(null); + // If we are using CombinedMessageQueue, we need to disable concurrent mode for testing. + messageQueueUseConcurrentField.set(null, false); + } catch (NoSuchFieldException e) { + // Ignore - maybe this is not CombinedMessageQueue? + } catch (IllegalAccessException e) { + throw new RuntimeException("Reflection error constructing or accessing looper", e); + } + try { mLooper = LOOPER_CONSTRUCTOR.newInstance(false); - ThreadLocal<Looper> threadLocalLooper = (ThreadLocal<Looper>) THREAD_LOCAL_LOOPER_FIELD - .get(null); + ThreadLocal<Looper> threadLocalLooper = + (ThreadLocal<Looper>) THREAD_LOCAL_LOOPER_FIELD.get(null); threadLocalLooper.set(mLooper); } catch (IllegalAccessException | InstantiationException | InvocationTargetException e) { throw new RuntimeException("Reflection error constructing or accessing looper", e); } mClock = clock; + + if (messageQueueUseConcurrentField != null) { + try { + messageQueueUseConcurrentField.set(null, previousUseConcurrentValue); + } catch (IllegalAccessException e) { + throw new RuntimeException("Reflection error constructing or accessing looper", e); + } + } } public Looper getLooper() { diff --git a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java index 7e0bbc4b3e50..3828a71d7b28 100644 --- a/tests/vcn/java/com/android/server/VcnManagementServiceTest.java +++ b/tests/vcn/java/com/android/server/VcnManagementServiceTest.java @@ -70,6 +70,7 @@ import android.net.Network; import android.net.NetworkCapabilities; import android.net.NetworkRequest; import android.net.Uri; +import android.net.vcn.Flags; import android.net.vcn.IVcnStatusCallback; import android.net.vcn.IVcnUnderlyingNetworkPolicyListener; import android.net.vcn.VcnConfig; @@ -84,6 +85,7 @@ import android.os.Process; import android.os.UserHandle; import android.os.UserManager; import android.os.test.TestLooper; +import android.platform.test.flag.junit.SetFlagsRule; import android.telephony.SubscriptionInfo; import android.telephony.SubscriptionManager; import android.telephony.TelephonyManager; @@ -102,6 +104,7 @@ import com.android.server.vcn.util.PersistableBundleUtils; import com.android.server.vcn.util.PersistableBundleUtils.PersistableBundleWrapper; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -119,6 +122,8 @@ import java.util.UUID; @RunWith(AndroidJUnit4.class) @SmallTest public class VcnManagementServiceTest { + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + private static final String CONTEXT_ATTRIBUTION_TAG = "VCN"; private static final String TEST_PACKAGE_NAME = VcnManagementServiceTest.class.getPackage().getName(); @@ -288,6 +293,8 @@ public class VcnManagementServiceTest { doReturn(Collections.singleton(TRANSPORT_WIFI)) .when(mMockDeps) .getRestrictedTransports(any(), any(), any()); + + mSetFlagsRule.enableFlags(Flags.FLAG_FIX_CONFIG_GARBAGE_COLLECTION); } @@ -438,6 +445,14 @@ public class VcnManagementServiceTest { return subIds; }).when(snapshot).getAllSubIdsInGroup(any()); + doAnswer(invocation -> { + final Set<ParcelUuid> subGroups = new ArraySet<>(); + for (Entry<Integer, ParcelUuid> entry : subIdToGroupMap.entrySet()) { + subGroups.add(entry.getValue()); + } + return subGroups; + }).when(snapshot).getAllSubscriptionGroups(); + return snapshot; } @@ -1483,6 +1498,28 @@ public class VcnManagementServiceTest { } @Test + public void testGarbageCollectionKeepConfigUntilNewSnapshot() throws Exception { + setupActiveSubscription(TEST_UUID_2); + startAndGetVcnInstance(TEST_UUID_2); + + // Report loss of subscription from mSubMgr + doReturn(Collections.emptyList()).when(mSubMgr).getSubscriptionsInGroup(any()); + triggerSubscriptionTrackerCbAndGetSnapshot( + TEST_UUID_2, + Collections.singleton(TEST_UUID_2), + Collections.singletonMap(TEST_SUBSCRIPTION_ID, TEST_UUID_2)); + + assertTrue(mVcnMgmtSvc.getConfigs().containsKey(TEST_UUID_2)); + + // Report loss of subscription from snapshot + triggerSubscriptionTrackerCbAndGetSnapshot(null, Collections.emptySet()); + + mTestLooper.moveTimeForward(VcnManagementService.CARRIER_PRIVILEGES_LOST_TEARDOWN_DELAY_MS); + mTestLooper.dispatchAll(); + assertFalse(mVcnMgmtSvc.getConfigs().containsKey(TEST_UUID_2)); + } + + @Test public void testVcnCarrierConfigChangeUpdatesPolicyListener() throws Exception { setupActiveSubscription(TEST_UUID_2); diff --git a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java index e045f100e549..4c7b25aaa7c3 100644 --- a/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java +++ b/tests/vcn/java/com/android/server/vcn/VcnGatewayConnectionTestBase.java @@ -223,7 +223,6 @@ public class VcnGatewayConnectionTestBase { doReturn(mVcnNetworkProvider).when(mVcnContext).getVcnNetworkProvider(); doReturn(mFeatureFlags).when(mVcnContext).getFeatureFlags(); doReturn(true).when(mVcnContext).isFlagSafeModeTimeoutConfigEnabled(); - doReturn(true).when(mVcnContext).isFlagIpSecTransformStateEnabled(); doReturn(mUnderlyingNetworkController) .when(mDeps) diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java index bc7ff47d9a01..441b78035703 100644 --- a/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java +++ b/tests/vcn/java/com/android/server/vcn/routeselection/NetworkEvaluationTestBase.java @@ -20,7 +20,6 @@ import static com.android.server.vcn.VcnTestUtils.setupSystemService; import static com.android.server.vcn.routeselection.UnderlyingNetworkControllerTest.getLinkPropertiesWithName; import static org.mockito.Mockito.doNothing; -import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; @@ -127,8 +126,6 @@ public abstract class NetworkEvaluationTestBase { false /* isInTestMode */)); doNothing().when(mVcnContext).ensureRunningOnLooperThread(); - doReturn(true).when(mVcnContext).isFlagIpSecTransformStateEnabled(); - setupSystemService( mContext, mConnectivityManager, diff --git a/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java b/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java index 6f31d8db070f..e540932d0e1f 100644 --- a/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java +++ b/tests/vcn/java/com/android/server/vcn/routeselection/UnderlyingNetworkControllerTest.java @@ -226,7 +226,6 @@ public class UnderlyingNetworkControllerTest { private void resetVcnContext(VcnContext vcnContext) { reset(vcnContext); doNothing().when(vcnContext).ensureRunningOnLooperThread(); - doReturn(true).when(vcnContext).isFlagIpSecTransformStateEnabled(); } // Package private for use in NetworkPriorityClassifierTest diff --git a/tools/aapt2/cmd/Command.cpp b/tools/aapt2/cmd/Command.cpp index 514651e92c27..449d93dd8c0b 100644 --- a/tools/aapt2/cmd/Command.cpp +++ b/tools/aapt2/cmd/Command.cpp @@ -213,15 +213,28 @@ int Command::Execute(const std::vector<StringPiece>& args, std::ostream* out_err bool match = false; for (Flag& flag : flags_) { - if (arg == flag.name) { + // Allow both "--arg value" and "--arg=value" syntax. + if (arg.starts_with(flag.name) && + (arg.size() == flag.name.size() || (flag.num_args > 0 && arg[flag.name.size()] == '='))) { if (flag.num_args > 0) { - i++; - if (i >= args.size()) { - *out_error << flag.name << " missing argument.\n\n"; - Usage(out_error); - return false; + if (arg.size() == flag.name.size()) { + i++; + if (i >= args.size()) { + *out_error << flag.name << " missing argument.\n\n"; + Usage(out_error); + return 1; + } + arg = args[i]; + } else { + arg.remove_prefix(flag.name.size() + 1); + // Disallow empty arguments after '='. + if (arg.empty()) { + *out_error << flag.name << " has empty argument.\n\n"; + Usage(out_error); + return 1; + } } - flag.action(args[i]); + flag.action(arg); } else { flag.action({}); } diff --git a/tools/aapt2/cmd/Command_test.cpp b/tools/aapt2/cmd/Command_test.cpp index 7aa1aa017f7b..20d87e0025c3 100644 --- a/tools/aapt2/cmd/Command_test.cpp +++ b/tools/aapt2/cmd/Command_test.cpp @@ -19,6 +19,7 @@ #include "test/Test.h" using ::testing::Eq; +using namespace std::literals; namespace aapt { @@ -94,4 +95,27 @@ TEST(CommandTest, LongFullyQualifiedPathWindows) { } #endif +TEST(CommandTest, OptionsWithValues) { + TestCommand command; + std::string flag; + command.AddRequiredFlag("--flag", "", &flag); + + ASSERT_EQ(0, command.Execute({"--flag"s, "1"s}, &std::cerr)); + EXPECT_STREQ("1", flag.c_str()); + + ASSERT_EQ(0, command.Execute({"--flag=1"s}, &std::cerr)); + EXPECT_STREQ("1", flag.c_str()); + + ASSERT_EQ(0, command.Execute({"--flag"s, "=2"s}, &std::cerr)); + EXPECT_STREQ("=2", flag.c_str()); + + ASSERT_EQ(0, command.Execute({"--flag"s, "--flag"s}, &std::cerr)); + EXPECT_STREQ("--flag", flag.c_str()); + + EXPECT_NE(0, command.Execute({"--flag"s}, &std::cerr)); + EXPECT_NE(0, command.Execute({"--flag="s}, &std::cerr)); + EXPECT_NE(0, command.Execute({"--flag1=2"s}, &std::cerr)); + EXPECT_NE(0, command.Execute({"--flag1"s, "2"s}, &std::cerr)); +} + } // namespace aapt
\ No newline at end of file diff --git a/tools/aapt2/cmd/Compile.cpp b/tools/aapt2/cmd/Compile.cpp index 52372fa38525..a5e18d35a256 100644 --- a/tools/aapt2/cmd/Compile.cpp +++ b/tools/aapt2/cmd/Compile.cpp @@ -605,8 +605,9 @@ static bool CompilePng(IAaptContext* context, const CompileOptions& options, } // Write the crunched PNG. - if (!android::WritePng(image.get(), nine_patch.get(), &crunched_png_buffer_out, {}, - &source_diag, context->IsVerbose())) { + if (!android::WritePng(image.get(), nine_patch.get(), &crunched_png_buffer_out, + {.compression_level = options.png_compression_level_int}, &source_diag, + context->IsVerbose())) { return false; } @@ -924,6 +925,19 @@ int CompileCommand::Action(const std::vector<std::string>& args) { } } + if (!options_.png_compression_level) { + options_.png_compression_level_int = 9; + } else { + if (options_.png_compression_level->size() != 1 || + options_.png_compression_level->front() < '0' || + options_.png_compression_level->front() > '9') { + context.GetDiagnostics()->Error( + android::DiagMessage() << "PNG compression level should be a number in [0..9] range"); + return 1; + } + options_.png_compression_level_int = options_.png_compression_level->front() - '0'; + } + return Compile(&context, file_collection.get(), archive_writer.get(), options_); } diff --git a/tools/aapt2/cmd/Compile.h b/tools/aapt2/cmd/Compile.h index 70c8791524c8..e244546476b0 100644 --- a/tools/aapt2/cmd/Compile.h +++ b/tools/aapt2/cmd/Compile.h @@ -47,6 +47,8 @@ struct CompileOptions { bool verbose = false; std::optional<std::string> product_; FeatureFlagValues feature_flag_values; + std::optional<std::string> png_compression_level; + int png_compression_level_int = 9; }; /** Parses flags and compiles resources to be used in linking. */ @@ -65,6 +67,9 @@ class CompileCommand : public Command { AddOptionalSwitch("--pseudo-localize", "Generate resources for pseudo-locales " "(en-XA and ar-XB)", &options_.pseudolocalize); AddOptionalSwitch("--no-crunch", "Disables PNG processing", &options_.no_png_crunch); + AddOptionalFlag("--png-compression-level", + "Set the zlib compression level for crunched PNG images, [0-9], 9 by default.", + &options_.png_compression_level); AddOptionalSwitch("--legacy", "Treat errors that used to be valid in AAPT as warnings", &options_.legacy_mode); AddOptionalSwitch("--preserve-visibility-of-styleables", diff --git a/tools/protologtool/src/com/android/protolog/tool/ProtoLogTool.kt b/tools/protologtool/src/com/android/protolog/tool/ProtoLogTool.kt index 22858803b597..108942ee754a 100644 --- a/tools/protologtool/src/com/android/protolog/tool/ProtoLogTool.kt +++ b/tools/protologtool/src/com/android/protolog/tool/ProtoLogTool.kt @@ -368,7 +368,7 @@ object ProtoLogTool { } interface ProtologViewerConfigBuilder { - fun build(statements: Map<LogCall, Long>): ByteArray + fun build(groups: Collection<LogGroup>, statements: Map<LogCall, Long>): ByteArray } private fun viewerConf(command: CommandOptions) { @@ -416,7 +416,7 @@ object ProtoLogTool { } val outFile = injector.fileOutputStream(command.viewerConfigFileNameArg) - outFile.write(configBuilder.build(logCallRegistry.getStatements())) + outFile.write(configBuilder.build(groups.values, logCallRegistry.getStatements())) outFile.close() } diff --git a/tools/protologtool/src/com/android/protolog/tool/ViewerConfigJsonBuilder.kt b/tools/protologtool/src/com/android/protolog/tool/ViewerConfigJsonBuilder.kt index 7714db212c9f..16a3d7cdda21 100644 --- a/tools/protologtool/src/com/android/protolog/tool/ViewerConfigJsonBuilder.kt +++ b/tools/protologtool/src/com/android/protolog/tool/ViewerConfigJsonBuilder.kt @@ -21,8 +21,7 @@ import com.android.protolog.tool.Constants.VERSION import java.io.StringWriter class ViewerConfigJsonBuilder : ProtoLogTool.ProtologViewerConfigBuilder { - override fun build(statements: Map<ProtoLogTool.LogCall, Long>): ByteArray { - val groups = statements.map { it.key.logGroup }.toSet() + override fun build(groups: Collection<LogGroup>, statements: Map<ProtoLogTool.LogCall, Long>): ByteArray { val stringWriter = StringWriter() val writer = JsonWriter(stringWriter) writer.setIndent(" ") diff --git a/tools/protologtool/src/com/android/protolog/tool/ViewerConfigProtoBuilder.kt b/tools/protologtool/src/com/android/protolog/tool/ViewerConfigProtoBuilder.kt index 245e802df49b..de85411e4ffc 100644 --- a/tools/protologtool/src/com/android/protolog/tool/ViewerConfigProtoBuilder.kt +++ b/tools/protologtool/src/com/android/protolog/tool/ViewerConfigProtoBuilder.kt @@ -28,10 +28,14 @@ class ViewerConfigProtoBuilder : ProtoLogTool.ProtologViewerConfigBuilder { * @return a byte array of a ProtoLogViewerConfig proto message encoding all the viewer * configurations mapping protolog hashes to message information and log group information. */ - override fun build(statements: Map<ProtoLogTool.LogCall, Long>): ByteArray { + override fun build(groups: Collection<LogGroup>, statements: Map<ProtoLogTool.LogCall, Long>): ByteArray { val configBuilder = ProtoLogViewerConfig.newBuilder() - val groups = statements.map { it.key.logGroup }.toSet() + // TODO(b/373754057): We are passing all the groups now, because some groups might only be + // used by Kotlin code that is not processed, but for group that get enabled to log to + // logcat we try and load the viewer configurations for this group, so the group must exist + // in the viewer config. Once Kotlin is pre-processed or this logic changes we should only + // use the groups that are actually used as an optimization. val groupIds = mutableMapOf<LogGroup, Int>() groups.forEach { groupIds.putIfAbsent(it, groupIds.size + 1) diff --git a/tools/protologtool/tests/com/android/protolog/tool/ViewerConfigJsonBuilderTest.kt b/tools/protologtool/tests/com/android/protolog/tool/ViewerConfigJsonBuilderTest.kt index d27ae88fc488..1a20d4c5bad6 100644 --- a/tools/protologtool/tests/com/android/protolog/tool/ViewerConfigJsonBuilderTest.kt +++ b/tools/protologtool/tests/com/android/protolog/tool/ViewerConfigJsonBuilderTest.kt @@ -36,6 +36,8 @@ class ViewerConfigJsonBuilderTest { private val GROUP_DISABLED = LogGroup("DEBUG_GROUP", false, true, TAG2) private val GROUP_TEXT_DISABLED = LogGroup("DEBUG_GROUP", true, false, TAG2) private const val PATH = "/tmp/test.java" + + private val GROUPS = listOf(GROUP1, GROUP2, GROUP3) } private val configBuilder = ViewerConfigJsonBuilder() @@ -53,7 +55,7 @@ class ViewerConfigJsonBuilderTest { LogCall(TEST3.messageString, LogLevel.ERROR, GROUP3, PATH))) val parsedConfig = parseConfig( - configBuilder.build(logCallRegistry.getStatements()).toString(Charsets.UTF_8)) + configBuilder.build(GROUPS, logCallRegistry.getStatements()).toString(Charsets.UTF_8)) assertEquals(3, parsedConfig.size) assertEquals(TEST1, parsedConfig[CodeUtils.hash(PATH, TEST1.messageString, LogLevel.INFO, GROUP1)]) @@ -72,7 +74,7 @@ class ViewerConfigJsonBuilderTest { LogCall(TEST1.messageString, LogLevel.INFO, GROUP1, PATH))) val parsedConfig = parseConfig( - configBuilder.build(logCallRegistry.getStatements()).toString(Charsets.UTF_8)) + configBuilder.build(GROUPS, logCallRegistry.getStatements()).toString(Charsets.UTF_8)) assertEquals(1, parsedConfig.size) assertEquals(TEST1, parsedConfig[CodeUtils.hash(PATH, TEST1.messageString, LogLevel.INFO, GROUP1)]) diff --git a/tools/protologtool/tests/com/android/protolog/tool/ViewerConfigProtoBuilderTest.kt b/tools/protologtool/tests/com/android/protolog/tool/ViewerConfigProtoBuilderTest.kt new file mode 100644 index 000000000000..74a8de7f70c0 --- /dev/null +++ b/tools/protologtool/tests/com/android/protolog/tool/ViewerConfigProtoBuilderTest.kt @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.protolog.tool + +import com.android.internal.protolog.common.LogLevel +import com.android.protolog.tool.ProtoLogTool.LogCall +import com.google.common.truth.Truth +import org.junit.Test +import perfetto.protos.PerfettoTrace.ProtoLogViewerConfig + +class ViewerConfigProtoBuilderTest { + companion object { + private val TAG1 = "WM_TEST" + private val TAG2 = "WM_DEBUG" + + private val TEST1 = ViewerConfigParser.ConfigEntry("test1", LogLevel.INFO.name, + TAG1 + ) + private val TEST2 = ViewerConfigParser.ConfigEntry("test2", LogLevel.DEBUG.name, + TAG2 + ) + + private val GROUP1 = LogGroup("TEST_GROUP", true, true, TAG1) + private val GROUP2 = LogGroup("DEBUG_GROUP", true, true, TAG2) + private val GROUP3 = LogGroup("UNUSED_GROUP", true, true, TAG1) + + private val GROUPS = listOf( + GROUP1, + GROUP2, + GROUP3 + ) + + private const val PATH = "/tmp/test.java" + } + + @Test + fun includesUnusedProtoLogGroups() { + // Added because of b/373754057. This test might need to be removed in the future. + + val configBuilder = ViewerConfigProtoBuilder() + + val logCallRegistry = ProtoLogTool.LogCallRegistry() + logCallRegistry.addLogCalls(listOf( + LogCall(TEST1.messageString, LogLevel.INFO, GROUP1, PATH), + LogCall(TEST2.messageString, LogLevel.INFO, GROUP2, PATH), + )) + + val rawProto = configBuilder.build(GROUPS, logCallRegistry.getStatements()) + + val viewerConfig = ProtoLogViewerConfig.parseFrom(rawProto) + Truth.assertThat(viewerConfig.groupsCount).isEqualTo(GROUPS.size) + Truth.assertThat(viewerConfig.messagesCount).isLessThan(GROUPS.size) + } +}
\ No newline at end of file diff --git a/tools/systemfeatures/Android.bp b/tools/systemfeatures/Android.bp index 590f7190881a..e6d0a3d4149f 100644 --- a/tools/systemfeatures/Android.bp +++ b/tools/systemfeatures/Android.bp @@ -58,6 +58,7 @@ java_test_host { "junit", "objenesis", "mockito", + "systemfeatures-gen-lib", "truth", ], } diff --git a/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt b/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt index 196b5e7c02ab..1abe77fd3ceb 100644 --- a/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt +++ b/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt @@ -71,7 +71,7 @@ object SystemFeaturesGenerator { println("Usage: SystemFeaturesGenerator <outputClassName> [options]") println(" Options:") println(" --readonly=true|false Whether to encode features as build-time constants") - println(" --feature=\$NAME:\$VER A feature+version pair, where \$VER can be:") + println(" --feature=\$NAME:\$VER A feature+version pair, where \$VER can be:") println(" * blank/empty == undefined (variable API)") println(" * valid int == enabled (constant API)") println(" * UNAVAILABLE == disabled (constant API)") @@ -89,6 +89,17 @@ object SystemFeaturesGenerator { /** Main entrypoint for build-time system feature codegen. */ @JvmStatic fun main(args: Array<String>) { + generate(args, System.out) + } + + /** + * Simple API entrypoint for build-time system feature codegen. + * + * Note: Typically this would be implemented in terms of a proper Builder-type input argument, + * but it's primarily used for testing as opposed to direct production usage. + */ + @JvmStatic + fun generate(args: Array<String>, output: Appendable) { if (args.size < 1) { usage() return @@ -155,7 +166,7 @@ object SystemFeaturesGenerator { .addFileComment("This file is auto-generated. DO NOT MODIFY.\n") .addFileComment("Args: ${args.joinToString(" \\\n ")}") .build() - .writeTo(System.out) + .writeTo(output) } /* @@ -171,12 +182,27 @@ object SystemFeaturesGenerator { return when (featureArgs.getOrNull(1)) { null, "" -> FeatureInfo(name, null, readonly = false) "UNAVAILABLE" -> FeatureInfo(name, null, readonly = true) - else -> FeatureInfo(name, featureArgs[1].toIntOrNull(), readonly = true) + else -> { + val featureVersion = + featureArgs[1].toIntOrNull() + ?: throw IllegalArgumentException( + "Invalid feature version input for $name: ${featureArgs[1]}" + ) + FeatureInfo(name, featureArgs[1].toInt(), readonly = true) + } } } private fun parseFeatureName(name: String): String = - if (name.startsWith("FEATURE_")) name else "FEATURE_$name" + when { + name.startsWith("android") -> + throw IllegalArgumentException( + "Invalid feature name input: \"android\"-namespaced features must be " + + "provided as PackageManager.FEATURE_* suffixes, not raw feature strings." + ) + name.startsWith("FEATURE_") -> name + else -> "FEATURE_$name" + } /* * Adds per-feature query methods to the class with the form: diff --git a/tools/systemfeatures/tests/src/SystemFeaturesGeneratorApiTest.java b/tools/systemfeatures/tests/src/SystemFeaturesGeneratorApiTest.java new file mode 100644 index 000000000000..f8c585d60ef7 --- /dev/null +++ b/tools/systemfeatures/tests/src/SystemFeaturesGeneratorApiTest.java @@ -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.systemfeatures; + +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +import java.io.IOException; + +// Note: This is a very simple argument test to validate certain behaviors for +// invalid arguments. Correctness and validity is largely exercised by +// SystemFeaturesGeneratorTest. +@RunWith(JUnit4.class) +public class SystemFeaturesGeneratorApiTest { + + @Rule public final MockitoRule mockito = MockitoJUnit.rule(); + + @Mock private Appendable mOut; + + @Test + public void testEmpty() throws IOException { + final String[] args = new String[] {}; + // This should just print the commandline and return. + SystemFeaturesGenerator.generate(args, mOut); + verify(mOut, never()).append(any()); + } + + @Test + public void testBasic() throws IOException { + final String[] args = new String[] { + "com.foo.Features", + "--feature=TELEVISION:0", + }; + SystemFeaturesGenerator.generate(args, mOut); + verify(mOut, atLeastOnce()).append(any()); + } + + @Test(expected = IllegalArgumentException.class) + public void testInvalidFeatureVersion() throws IOException { + final String[] args = new String[] { + "com.foo.Features", + "--feature=TELEVISION:blarg", + }; + SystemFeaturesGenerator.generate(args, mOut); + verify(mOut, never()).append(any()); + } + + @Test(expected = IllegalArgumentException.class) + public void testInvalidFeatureNameFromAndroidNamespace() throws IOException { + final String[] args = new String[] { + "com.foo.Features", + "--feature=android.hardware.doesntexist:0", + }; + SystemFeaturesGenerator.generate(args, mOut); + verify(mOut, never()).append(any()); + } +} |